summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/mod.rs1
-rw-r--r--src/api/peers.rs23
-rw-r--r--src/main.rs1
-rw-r--r--src/state.rs15
-rw-r--r--src/swarm/mod.rs1
-rw-r--r--src/swarm/peer.rs5
-rw-r--r--src/swarm/sync.rs19
-rw-r--r--src/swarm/throughput.rs168
8 files changed, 231 insertions, 2 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 95d63aa..85427b0 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -159,6 +159,7 @@ fn response_for_value(
crate::transport::traffic::ByKind,
crate::transport::traffic::Counter,
crate::state::Incidents,
+ peers::ThroughputInfo,
)
),
tags(
diff --git a/src/api/peers.rs b/src/api/peers.rs
index 5b7d576..0305647 100644
--- a/src/api/peers.rs
+++ b/src/api/peers.rs
@@ -70,6 +70,18 @@ pub(crate) struct PeerStatus {
/// since it was last listed).
traffic: PeerTraffic,
incidents: Incidents,
+ /// Smoothed sync throughput (`swarm::throughput`); absent before the first large
+ /// exchange.
+ sync_throughput: Option<ThroughputInfo>,
+}
+
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct ThroughputInfo {
+ bytes_per_sec: f64,
+ samples: u32,
+ /// Age of the last sample.
+ secs_ago: u64,
}
#[derive(Debug, Serialize, ToSchema)]
@@ -122,6 +134,16 @@ pub(crate) async fn list_peers(
.effective(&public_key, now, state.timing.rtt_fresh_for())
.map(|rtt| rtt.as_secs_f64() * 1000.0);
let traffic = state.traffic.peer(&public_key).unwrap_or_default();
+ let sync_throughput = state
+ .throughput
+ .lock()
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
+ .get(&public_key)
+ .map(|estimate| ThroughputInfo {
+ bytes_per_sec: estimate.bytes_per_sec.round(),
+ samples: estimate.samples,
+ secs_ago: now.saturating_duration_since(estimate.at).as_secs(),
+ });
let ps = peer_state_mutex.lock().await;
let (status, dormant_for_secs) = match ps.liveness {
@@ -149,6 +171,7 @@ pub(crate) async fn list_peers(
rtt_ms,
traffic,
incidents: ps.incidents,
+ sync_throughput,
});
}
diff --git a/src/main.rs b/src/main.rs
index 622f755..7261d6a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -204,6 +204,7 @@ async fn main() -> Result<()> {
neighbours: Default::default(),
pulling: Default::default(),
rtt: Default::default(),
+ throughput: Default::default(),
stability: Default::default(),
stale: Default::default(),
catch_up_source: Default::default(),
diff --git a/src/state.rs b/src/state.rs
index b8944ab..d8f0bbf 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -16,6 +16,7 @@ use crate::backoff::Backoff;
use crate::document::replica::{Epoch, Replica};
use crate::remake::{Host, PeerSummary, RemakeState};
use crate::swarm::sync::{ChangeSet, SyncEvent};
+use crate::swarm::throughput::{SyncBytes, ThroughputTable};
use crate::transport;
use crate::transport::firewall::Firewall;
@@ -204,6 +205,9 @@ pub(crate) struct SyncSession {
#[derive(Clone, Debug)]
pub(crate) struct SyncReplyWait {
pub(crate) deadline: Instant,
+ /// When the first of the unanswered messages was generated, and the sync bytes
+ /// exchanged with the peer by then (`swarm::throughput`).
+ pub(crate) started: (Instant, SyncBytes),
/// Changes first sent in the unanswered messages.
pub(crate) sent: Vec<automerge::ChangeHash>,
}
@@ -266,13 +270,20 @@ impl SyncSession {
}
/// A sync message went out; `sent` are the changes it carried for the first time.
- pub(crate) fn expect_reply(&mut self, sent: Vec<automerge::ChangeHash>, now: Instant) {
+ /// `bytes` are the sync bytes exchanged with the peer before it.
+ pub(crate) fn expect_reply(
+ &mut self,
+ sent: Vec<automerge::ChangeHash>,
+ now: Instant,
+ bytes: SyncBytes,
+ ) {
match &mut self.reply {
// Still waiting for an earlier one: keep its deadline, remember these too.
Some(wait) => wait.sent.extend(sent),
None => {
self.reply = Some(SyncReplyWait {
deadline: now + self.reply_backoff.current(),
+ started: (now, bytes),
sent,
});
}
@@ -586,6 +597,8 @@ pub(crate) struct AppState {
pub(crate) stability: Arc<std::sync::Mutex<crate::swarm::stability::Stability>>,
/// Round-trip times to peers (`swarm::rtt`).
pub(crate) rtt: Arc<std::sync::Mutex<crate::swarm::rtt::RttTable>>,
+ /// Sync throughput with peers (`swarm::throughput`).
+ pub(crate) throughput: Arc<std::sync::Mutex<ThroughputTable>>,
/// The neighbour we are pulling changes from, and since when (`swarm::relay`).
pub(crate) pulling: Arc<std::sync::Mutex<Option<(String, Instant)>>>,
/// Our neighbours, once known (`swarm::topology`).
diff --git a/src/swarm/mod.rs b/src/swarm/mod.rs
index 83e2d2d..6f194dc 100644
--- a/src/swarm/mod.rs
+++ b/src/swarm/mod.rs
@@ -12,4 +12,5 @@ pub(crate) mod source;
pub(crate) mod stability;
pub(crate) mod stats;
pub(crate) mod sync;
+pub(crate) mod throughput;
pub(crate) mod topology;
diff --git a/src/swarm/peer.rs b/src/swarm/peer.rs
index 7a3eed8..3b85539 100644
--- a/src/swarm/peer.rs
+++ b/src/swarm/peer.rs
@@ -496,6 +496,11 @@ pub(crate) async fn run_peer_worker(state: AppState) {
{
let known = state.peer_states.read().await;
state.traffic.retain(|peer| known.contains_key(peer));
+ state
+ .throughput
+ .lock()
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
+ .retain(|peer| known.contains_key(peer));
}
if let Some(change) = topology::refresh(&state).await {
apply_neighbour_change(&state, change).await;
diff --git a/src/swarm/sync.rs b/src/swarm/sync.rs
index 09e10f4..3284015 100644
--- a/src/swarm/sync.rs
+++ b/src/swarm/sync.rs
@@ -14,6 +14,7 @@ use tracing::{debug, info, trace, warn};
use crate::state::{AppState, Liveness, Sending, Standing};
use crate::swarm::peer::{peer_swarm, send_hello_once};
use crate::swarm::source;
+use crate::swarm::throughput::SyncBytes;
use crate::transport::{self, ParsedMessage};
#[derive(Debug)]
@@ -127,6 +128,8 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> {
let transport = Arc::clone(&state.transport);
let peer_owned = peer.to_owned();
let ready = state.is_ready().await;
+ // Before this message is counted.
+ let bytes_before = SyncBytes::of(&state.traffic, peer);
let generated =
tokio::task::spawn_blocking(move || -> Result<Option<transport::EncodedMessage>> {
@@ -157,7 +160,9 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> {
if in_step {
peer_state.sync.reply_settled();
} else if message.is_some() {
- peer_state.sync.expect_reply(sent.clone(), Instant::now());
+ peer_state
+ .sync
+ .expect_reply(sent.clone(), Instant::now(), bytes_before);
}
if message.is_some() {
// Marked under the same lock as the generation, so no second
@@ -255,6 +260,8 @@ pub(crate) async fn handle_sync(
envelope: transport::SyncEnvelope,
) -> Result<()> {
debug!(peer = %peer, "Processing sync envelope");
+ // This message is counted already (`transport::traffic`).
+ let arrived = (Instant::now(), SyncBytes::of(&state.traffic, peer));
let peer_state_mutex = state.get_or_create_peer_state(peer).await;
@@ -306,6 +313,10 @@ pub(crate) async fn handle_sync(
let doc = Arc::clone(&state.doc);
let peer_state_mutex = Arc::clone(&peer_state_mutex);
let stats = Arc::clone(&state.stats);
+ let throughput = Arc::clone(&state.throughput);
+ let rtt = state
+ .lock_rtt()
+ .effective(peer, Instant::now(), state.timing.rtt_fresh_for());
let check_caught_up = !state.initial_sync_done.load(Ordering::Acquire);
// While catching up, everything arrives late; that says nothing about propagation.
let measure_propagation = !check_caught_up;
@@ -334,6 +345,12 @@ pub(crate) async fn handle_sync(
.receive_sync_message(&mut peer_state.sync.state, message)
.context("Automerge synchronization failed")?;
peer_state.last_sync_rx = Some(Instant::now());
+ if let Some(wait) = &peer_state.sync.reply {
+ throughput
+ .lock()
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
+ .exchange(&peer, wait.started, arrived, rtt);
+ }
peer_state.sync.reply_settled();
resend_if_needed(&peer, &mut peer_state.sync.state);
diff --git a/src/swarm/throughput.rs b/src/swarm/throughput.rs
new file mode 100644
index 0000000..f1b9b14
--- /dev/null
+++ b/src/swarm/throughput.rs
@@ -0,0 +1,168 @@
+//! Sync throughput with peers, measured from sync exchanges we have anyway: from sending
+//! a sync message until the peer's answer arrives, the sync bytes that went either way
+//! with that peer. No transfers of our own.
+//!
+//! Only exchanges of at least `MIN_BYTES` count; smaller ones measure the round trip,
+//! not the throughput. The round trip (`swarm::rtt`) is taken off the elapsed time, but
+//! never more than half of it, so a stale round trip can't inflate a sample. What
+//! remains also includes the time the peer took to apply and answer: this is the rate a
+//! peer delivers at in practice, not the raw capacity of the path.
+//!
+//! Samples are smoothed (exponential moving average, weight 1/4). Large exchanges are
+//! rare (catching up, big writes), so estimates are kept however old; they come with
+//! their age.
+
+use std::collections::HashMap;
+use std::time::Duration;
+
+use tokio::time::Instant;
+
+use crate::transport::traffic::Traffic;
+
+/// Smallest exchange that counts: some dozens of Mycelium chunks.
+const MIN_BYTES: u64 = 32 * 1024;
+const WEIGHT: f64 = 0.25;
+
+/// Sync bytes exchanged with a peer so far (from the traffic counters).
+#[derive(Debug, Default, Clone, Copy)]
+pub(crate) struct SyncBytes {
+ sent: u64,
+ received: u64,
+}
+
+impl SyncBytes {
+ pub(crate) fn of(traffic: &Traffic, peer: &str) -> Self {
+ traffic
+ .peer(peer)
+ .map(|t| Self {
+ sent: t.sent.sync.bytes,
+ received: t.received.sync.bytes,
+ })
+ .unwrap_or_default()
+ }
+
+ /// Bytes either way since `earlier` (0 if the counters were dropped meanwhile).
+ fn since(&self, earlier: &SyncBytes) -> u64 {
+ self.sent.saturating_sub(earlier.sent) + self.received.saturating_sub(earlier.received)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub(crate) struct Estimate {
+ pub(crate) bytes_per_sec: f64,
+ pub(crate) samples: u32,
+ /// When the last sample was taken.
+ pub(crate) at: Instant,
+}
+
+#[derive(Debug, Default)]
+pub(crate) struct ThroughputTable {
+ peers: HashMap<String, Estimate>,
+}
+
+impl ThroughputTable {
+ /// A sync exchange with `peer` finished: it started at `started` with the counters
+ /// at `before`, and its answer arrived at `arrived` with the counters at `after`.
+ pub(crate) fn exchange(
+ &mut self,
+ peer: &str,
+ (started, before): (Instant, SyncBytes),
+ (arrived, after): (Instant, SyncBytes),
+ rtt: Option<Duration>,
+ ) {
+ let bytes = after.since(&before);
+ if bytes < MIN_BYTES {
+ return;
+ }
+ let elapsed = arrived.saturating_duration_since(started);
+ let transfer = elapsed
+ .saturating_sub(rtt.unwrap_or_default())
+ .max(elapsed / 2);
+ if transfer.is_zero() {
+ return;
+ }
+ let sample = bytes as f64 / transfer.as_secs_f64();
+ let estimate = self.peers.entry(peer.to_owned()).or_insert(Estimate {
+ bytes_per_sec: sample,
+ samples: 0,
+ at: arrived,
+ });
+ if estimate.samples > 0 {
+ estimate.bytes_per_sec += WEIGHT * (sample - estimate.bytes_per_sec);
+ }
+ estimate.samples += 1;
+ estimate.at = arrived;
+ }
+
+ pub(crate) fn get(&self, peer: &str) -> Option<Estimate> {
+ self.peers.get(peer).copied()
+ }
+
+ pub(crate) fn retain(&mut self, mut keep: impl FnMut(&str) -> bool) {
+ self.peers.retain(|peer, _| keep(peer));
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn bytes(sent: u64, received: u64) -> SyncBytes {
+ SyncBytes { sent, received }
+ }
+
+ #[test]
+ fn small_exchanges_dont_count() {
+ let mut table = ThroughputTable::default();
+ let t0 = Instant::now();
+ table.exchange(
+ "a",
+ (t0, bytes(0, 0)),
+ (t0 + Duration::from_millis(200), bytes(500, 10_000)),
+ None,
+ );
+ assert!(table.get("a").is_none());
+ }
+
+ #[test]
+ fn round_trip_is_taken_off_but_at_most_half() {
+ let mut table = ThroughputTable::default();
+ let t0 = Instant::now();
+ // 1 MB in 2 s, of which 1 s round trip: 1 MB/s.
+ table.exchange(
+ "a",
+ (t0, bytes(100, 0)),
+ (t0 + Duration::from_secs(2), bytes(200, 999_900)),
+ Some(Duration::from_secs(1)),
+ );
+ let a = table.get("a").unwrap();
+ assert!((a.bytes_per_sec - 1_000_000.0).abs() < 1.0);
+
+ // A round trip longer than the exchange takes off only half.
+ table.exchange(
+ "b",
+ (t0, bytes(0, 0)),
+ (t0 + Duration::from_secs(2), bytes(0, 1_000_000)),
+ Some(Duration::from_secs(5)),
+ );
+ assert!((table.get("b").unwrap().bytes_per_sec - 1_000_000.0).abs() < 1.0);
+ }
+
+ #[test]
+ fn smoothed() {
+ let mut table = ThroughputTable::default();
+ let t0 = Instant::now();
+ for received in [500_000u64, 1_000_000] {
+ table.exchange(
+ "a",
+ (t0, bytes(0, 0)),
+ (t0 + Duration::from_secs(1), bytes(0, received)),
+ None,
+ );
+ }
+ // Samples of 500 kB/s, then 1 MB/s: 500 kB/s + (1 MB/s - 500 kB/s) / 4.
+ let a = table.get("a").unwrap();
+ assert_eq!(a.samples, 2);
+ assert!((a.bytes_per_sec - 625_000.0).abs() < 1.0);
+ }
+}