aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-05 20:26:24 +0100
committertv <tv@krebsco.de>2026-11-05 20:26:24 +0100
commitc9065ee56c6e857f6ceddb78c4663bf9d82f0ac8 (patch)
tree1ccba51d24d0c4b7cc74dcd8a06b5a0ffab2f2ff /src
parent1e579cba7e84ac26c00c6f51683bc18118712143 (diff)
dashboard: snapshot lazy
Diffstat (limited to 'src')
-rw-r--r--src/api/dashboard/mod.rs65
1 files changed, 47 insertions, 18 deletions
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<Arc<JsonValue>>,
+ json: std::sync::OnceLock<String>,
+}
+
+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<JsonValue>) -> 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<Self>) {
- let mut last: Option<JsonValue> = None;
+ // The latest snapshot, and it without its ages (`without_ages`): what the next
+ // is told apart from.
+ let mut last: Option<(Arc<JsonValue>, 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<Arc<Hub>>) -> 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<Arc<Hub>>) -> 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)]