From c9065ee56c6e857f6ceddb78c4663bf9d82f0ac8 Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 5 Nov 2026 20:26:24 +0100 Subject: dashboard: snapshot lazy --- src/api/dashboard/mod.rs | 65 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 18 deletions(-) (limited to 'src/api/dashboard') diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs index aae514a..4ba25c7 100644 --- a/src/api/dashboard/mod.rs +++ b/src/api/dashboard/mod.rs @@ -56,10 +56,40 @@ const LINGER: Duration = Duration::from_secs(60); /// Members asked for their links at most, each round. const MAX_ASKED: usize = 512; -/// A snapshot or a patch as published: its number, and its JSON. +/// A snapshot or a patch as published: its number, and its JSON. A snapshot's text is +/// made when first asked for (a client connecting, or one that fell behind), not every +/// time the state changes: the whole state may be large, and mostly nobody new comes. struct Published { seq: u64, - json: String, + value: Option>, + json: std::sync::OnceLock, +} + +impl Published { + fn patch(seq: u64, patch: &JsonValue) -> Self { + Self { + seq, + value: None, + json: std::sync::OnceLock::from(patch.to_string()), + } + } + + fn snapshot(seq: u64, snapshot: Arc) -> Self { + Self { + seq, + value: Some(snapshot), + json: std::sync::OnceLock::new(), + } + } + + fn json(&self) -> &str { + self.json.get_or_init(|| { + self.value + .as_ref() + .map(|value| value.to_string()) + .unwrap_or_default() + }) + } } /// The producer and what it published, shared by all clients. @@ -146,7 +176,9 @@ impl Hub { } async fn run(self: Arc) { - let mut last: Option = None; + // The latest snapshot, and it without its ages (`without_ages`): what the next + // is told apart from. + let mut last: Option<(Arc, JsonValue)> = None; let mut seq = 0u64; let mut graph = Graph::default(); let mut graph_due = Instant::now(); @@ -162,24 +194,21 @@ impl Hub { graph_due = Instant::now() + LINKS_EVERY; } let snapshot = build(&self.state, &graph, live).await; - let changed = last - .as_ref() - .is_none_or(|old| without_ages(old) != without_ages(&snapshot)); + let stripped = without_ages(&snapshot); + let changed = last.as_ref().is_none_or(|(_, old)| *old != stripped); if changed { seq += 1; - if let Some(old) = &last + if let Some((old, _)) = &last && let Some(patch) = merge_patch(old, &snapshot) { - let _ = self.patches.send(Arc::new(Published { - seq, - json: patch.to_string(), - })); + let _ = self.patches.send(Arc::new(Published::patch(seq, &patch))); } - self.latest.send_replace(Some(Arc::new(Published { + let snapshot = Arc::new(snapshot); + self.latest.send_replace(Some(Arc::new(Published::snapshot( seq, - json: snapshot.to_string(), - }))); - last = Some(snapshot); + Arc::clone(&snapshot), + )))); + last = Some((snapshot, stripped)); } let poked = tokio::select! { () = tokio::time::sleep(TICK) => false, @@ -632,7 +661,7 @@ async fn current_state(Extension(hub): Extension>) -> Response { }; ( [(CONTENT_TYPE, HeaderValue::from_static("application/json"))], - current.json.clone(), + current.json().to_owned(), ) .into_response() } @@ -694,7 +723,7 @@ async fn events(Extension(hub): Extension>) -> impl IntoResponse { yield Ok(Event::default() .event("patch") .id(patch.seq.to_string()) - .data(patch.json.clone())); + .data(patch.json())); } Err(broadcast::error::RecvError::Lagged(_)) => { let Some(current) = hub.current().await else { @@ -724,7 +753,7 @@ fn snapshot_event(published: &Published) -> Event { Event::default() .event("snapshot") .id(published.seq.to_string()) - .data(published.json.clone()) + .data(published.json()) } #[cfg(test)] -- cgit v1.3.1