aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-03 03:30:19 +0100
committertv <tv@krebsco.de>2026-11-03 03:30:19 +0100
commitbdb76a07c824f3775c5eb92d2754f8f239ccdf70 (patch)
tree6cc9270c3ba0de7c7465b0ddf495f945ce6d0b14 /src
parent5b3d9cbd1bba986b06978bef0100d349b33e74c7 (diff)
dashboard: show eager links on the map
Diffstat (limited to 'src')
-rw-r--r--src/api/dashboard/clocks.rs2
-rw-r--r--src/api/dashboard/dashboard.html36
-rw-r--r--src/api/dashboard/mod.rs2
-rw-r--r--src/swarm/links.rs24
-rw-r--r--src/transport/mod.rs4
-rw-r--r--src/transport/wire.rs4
6 files changed, 65 insertions, 7 deletions
diff --git a/src/api/dashboard/clocks.rs b/src/api/dashboard/clocks.rs
index 9f069be..ac58676 100644
--- a/src/api/dashboard/clocks.rs
+++ b/src/api/dashboard/clocks.rs
@@ -99,6 +99,8 @@ mod tests {
down: false,
rtt_us: None,
dormant: false,
+ eager_to: false,
+ eager_from: false,
others: Vec::new(),
clock: Some(ClockOffset {
offset_ms,
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 9132275..1b442ae 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -161,6 +161,9 @@
.legend .dormant::before { border-top-width: 1.5px; --colour: var(--muted);
box-shadow: 0 0 0 2.5px color-mix(in srgb, var(--bad) 50%, transparent); }
.legend .silent::before { border-top-style: dotted; --colour: var(--muted); }
+ /* A chevron on the line, as on the map. */
+ .legend .eager::before { content: "\203A"; width: auto; height: auto; border: none;
+ font-weight: bold; font-size: 15px; line-height: 0; }
.legend .hint::before { display: none; }
/* A kind of message: a dot in its colour, as on the map. */
.legend .message::before { width: 8px; height: 8px; border: none; border-radius: 50%;
@@ -607,6 +610,7 @@ function renderLegend(measured) {
el("span", { class: "direct" }, "direct"),
el("span", { class: "relayed" }, "relayed"),
el("span", { class: "dormant" }, "dormant or down"),
+ el("span", { class: "eager" }, "eager (changes sent on at once, this way)"),
el("span", { class: "silent" }, "an end reports no links"));
document.getElementById("legend").replaceChildren(...items);
}
@@ -637,6 +641,18 @@ function edges() {
}
}
}
+ // Eager, which way, if either end says so: "a>b" where a sends b its changes as it
+ // applies them (a's eagerTo, or b's eagerFrom).
+ const eager = new Set();
+ for (const [a, report] of Object.entries(state.graph.measured)) {
+ for (const link of report.links) {
+ if (link.eagerTo) eager.add(a + ">" + link.peer);
+ if (link.eagerFrom) eager.add(link.peer + ">" + a);
+ }
+ }
+ for (const edge of measured.values()) {
+ edge.eager = [eager.has(edge.a + ">" + edge.b), eager.has(edge.b + ">" + edge.a)];
+ }
// Dormant, or down, if either end says so, whichever report is drawn.
for (const edge of measured.values()) {
if ((edge.dormant && !edge.link.dormant) || (edge.down && !edge.link.down)) {
@@ -954,7 +970,7 @@ function renderGraph() {
tooltip(hit(key, p, q, 1), name(a) + " – " + name(b) + ": intended (member list), not measured");
}
const ms = rtt => rtt < 10 ? rtt.toFixed(1) : String(Math.round(rtt));
- for (const [key, { a, b, link, secsAgo }] of measured) {
+ for (const [key, { a, b, link, secsAgo, eager }] of measured) {
const p = positions.get(a), q = positions.get(b);
if (!p || !q) continue;
// The transport in use thick, in the middle; the others thin, beside it.
@@ -1004,6 +1020,8 @@ function renderGraph() {
(line.transport ? " via " + line.transport : "") + (line.used ? " (in use)" : "") +
((line.used ? link.relayed : line.relayed) ? ", relayed" : "") + (link.dormant ? ", dormant" : "") +
(line.used && link.down ? ", down" : "") +
+ (line.used && eager[0] ? ", eager " + name(a) + " → " + name(b) : "") +
+ (line.used && eager[1] ? ", eager " + name(b) + " → " + name(a) : "") +
(silent ? ", one end reports no links" : "") +
(typeof line.rttMs === "number" ? ", " + line.rttMs.toFixed(1) + " ms" : "") +
", measured " + age(secsAgo + elapsed()) + " ago";
@@ -1027,6 +1045,22 @@ function renderGraph() {
}
});
children.push(...drawnLines.reverse());
+ // Eager, each way it is: a chevron on the line in use pointing that way, past the
+ // middle towards the end it goes to (both ways: one each side of it).
+ const ux = (q.x - p.x) / length, uy = (q.y - p.y) / length;
+ [[eager[0], 1], [eager[1], -1]].forEach(([on, way]) => {
+ if (!on) return;
+ const t = eager[0] && eager[1] ? .5 + way * .06 : .5;
+ const mx = p.x + (q.x - p.x) * t, my = p.y + (q.y - p.y) * t;
+ const dx = ux * way * 5, dy = uy * way * 5;
+ const d = "M " + (mx - dx + nx * 5) + " " + (my - dy + ny * 5) + " L " + (mx + dx) +
+ " " + (my + dy) + " L " + (mx - dx - nx * 5) + " " + (my - dy - ny * 5);
+ const shape = { d, fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" };
+ children.push(drawing("eager under " + key + " " + way, "path",
+ { ...shape, stroke: "var(--panel)", "stroke-width": 5 }));
+ children.push(drawing("eager " + key + " " + way, "path",
+ { ...shape, stroke: colourOf(link.transport), "stroke-width": 2 }));
+ });
tooltip(covering, descriptions.join("\n"));
}
// Above every line, so none crosses them.
diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs
index 8d954c0..14464d4 100644
--- a/src/api/dashboard/mod.rs
+++ b/src/api/dashboard/mod.rs
@@ -407,6 +407,8 @@ fn links_json(links: &[Link]) -> JsonValue {
"down": link.down,
"rttMs": link.rtt_us.map(|us| f64::from(us) / 1000.0),
"dormant": link.dormant,
+ "eagerTo": link.eager_to,
+ "eagerFrom": link.eager_from,
"clock": link.clock,
"others": link.others.iter().map(|other| json!({
"transport": other.transport,
diff --git a/src/swarm/links.rs b/src/swarm/links.rs
index c430d58..e0d483d 100644
--- a/src/swarm/links.rs
+++ b/src/swarm/links.rs
@@ -1,8 +1,9 @@
//! How members are linked, for a map of the swarm (the dashboard, `api::dashboard`): a
//! node asks a member with LINKS, and the member answers with a LINKS_REPLY listing its
//! neighbours and the peers it holds dormant (`own_links`), each with the transport its
-//! messages go over, whether only through a relay, the round trip and whether it is
-//! dormant: as the member measures them, which no other node can.
+//! messages go over, whether only through a relay, the round trip, whether it is
+//! dormant, and which way the link is eager (`swarm::relay`): as the member measures
+//! them, which no other node can.
//!
//! Every node answers, whatever its build: one with a dashboard asks any member, which
//! may have none. Only members (in the member list) are answered, as the links name
@@ -10,8 +11,8 @@
//! is written to the document: asked for while someone looks, forgotten after.
//!
//! A LINKS may subscribe (`subscribe_secs`): the member then sends a LINKS_REPLY again
-//! whenever its links change (`changed`: a peer, a transport, relayed, dormant, or a
-//! round trip that moved by a fifth), looked at every `WATCH_EVERY` while anyone
+//! whenever its links change (`changed`: a peer, a transport, relayed, dormant, eager,
+//! or a round trip that moved by a fifth), looked at every `WATCH_EVERY` while anyone
//! subscribes (round trips change without telling), until the subscription runs out
//! (the asker renews it with its next LINKS, so one that is gone stops it by not
//! renewing) or a LINKS with 0 ends it.
@@ -231,7 +232,14 @@ impl Links {
pub(crate) async fn own_links(state: &AppState) -> Vec<Link> {
let mut links = Vec::new();
for (peer, peer_state) in state.peers_snapshot().await {
- let dormant = peer_state.lock().await.liveness.is_dormant();
+ let (dormant, eager_to, eager_from) = {
+ let peer_state = peer_state.lock().await;
+ (
+ peer_state.liveness.is_dormant(),
+ peer_state.eager_to,
+ peer_state.eager_from,
+ )
+ };
if !dormant && !topology::is_neighbour(state, &peer) {
continue;
}
@@ -256,6 +264,8 @@ pub(crate) async fn own_links(state: &AppState) -> Vec<Link> {
.rtt(&peer)
.map(|rtt| u32::try_from(rtt.as_micros()).unwrap_or(u32::MAX)),
dormant,
+ eager_to,
+ eager_from,
clock: state.transport.clock_offset(&peer),
peer,
});
@@ -1010,6 +1020,8 @@ fn changed(old: &[Link], new: &[Link]) -> bool {
|| a.relayed != b.relayed
|| a.down != b.down
|| a.dormant != b.dormant
+ || a.eager_to != b.eager_to
+ || a.eager_from != b.eager_from
|| moved(a.rtt_us, b.rtt_us)
|| clock_moved(a.clock, b.clock)
|| a.others.len() != b.others.len()
@@ -1062,6 +1074,8 @@ mod tests {
down: false,
rtt_us,
dormant: false,
+ eager_to: false,
+ eager_from: false,
others: Vec::new(),
clock: None,
}
diff --git a/src/transport/mod.rs b/src/transport/mod.rs
index 7f1aa98..22782ef 100644
--- a/src/transport/mod.rs
+++ b/src/transport/mod.rs
@@ -469,6 +469,10 @@ pub(crate) struct Link {
pub(crate) rtt_us: Option<u32>,
/// Silent for long (`peers.md`, dormancy).
pub(crate) dormant: bool,
+ /// Eager links (`swarm::relay`): the member sends the peer its changes as it applies
+ /// them (the peer asked), and the peer the member (the member asked).
+ pub(crate) eager_to: bool,
+ pub(crate) eager_from: bool,
/// The peer's other transports (`swarm::links::MAX_OTHERS` at most): where else
/// it proved itself, those paths up.
pub(crate) others: Vec<OtherPath>,
diff --git a/src/transport/wire.rs b/src/transport/wire.rs
index 40976ee..fe690a2 100644
--- a/src/transport/wire.rs
+++ b/src/transport/wire.rs
@@ -14,7 +14,7 @@ use super::{EncodedMessage, MessageKind, ParsedMessage};
/// The wire format's version, the first byte of every payload. Bump it with every change
/// to a message type or a codec (`golden_bytes` fails then).
-pub(crate) const VERSION: u8 = 17;
+pub(crate) const VERSION: u8 = 18;
/// What the second byte of a payload says.
fn tag(kind: MessageKind) -> u8 {
@@ -445,6 +445,8 @@ pub(crate) fn samples() -> Vec<ParsedMessage> {
down: false,
rtt_us: Some(1500),
dormant: false,
+ eager_to: true,
+ eager_from: false,
others: vec![crate::transport::OtherPath {
transport: "mycelium".into(),
rtt_us: None,