diff options
| author | tv <tv@krebsco.de> | 2026-10-30 18:11:57 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-30 18:11:57 +0100 |
| commit | 46a7d204c81b002f960a9bca97941df7a98c43d4 (patch) | |
| tree | dcf519abd3b5e48dd3c768648fc4d5dca03d6ce3 /src/api | |
| parent | f65ecfdf313658a3979a2d0add53e342ce4b97df (diff) | |
dashboard: show all links between peers
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 81 | ||||
| -rw-r--r-- | src/api/dashboard/mod.rs | 8 |
2 files changed, 61 insertions, 28 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index c87341f..a8b697e 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -53,6 +53,8 @@ .legend span::before { content: ""; display: inline-block; width: 18px; height: 0; vertical-align: middle; margin-right: 4px; border-top: 2px solid var(--colour, currentColor); } .legend .intended::before { border-top-style: dashed; --colour: var(--intended); } + .legend .used::before { border-top-width: 3px; --colour: var(--muted); } + .legend .other::before { border-top-width: 1px; --colour: var(--muted); } .legend .direct::before { --colour: var(--muted); } .legend .relayed::before { border-top-style: dashed; --colour: var(--muted); } .legend .dormant::before { border-top-style: dotted; --colour: var(--bad); } @@ -242,14 +244,19 @@ function colourOf(transport) { function renderLegend(measured) { const transports = new Set(); - for (const { link } of measured.values()) transports.add(link.transport || ""); + for (const { link } of measured.values()) { + transports.add(link.transport || ""); + for (const other of link.others || []) transports.add(other.transport); + } const items = [el("span", { class: "intended" }, "intended (member list)")]; for (const transport of [...transports].sort()) { const item = el("span", {}, transport || "unknown transport"); item.style.setProperty("--colour", colourOf(transport || null)); items.push(item); } - items.push(el("span", { class: "direct" }, "direct"), + items.push(el("span", { class: "used" }, "in use"), + el("span", { class: "other" }, "other transport"), + el("span", { class: "direct" }, "direct"), el("span", { class: "relayed" }, "relayed"), el("span", { class: "dormant" }, "dormant")); document.getElementById("legend").replaceChildren(...items); @@ -263,13 +270,16 @@ function edges() { intended.set(key, [a, b]); } } - // One measured edge per pair: the fresher report wins. + // One measured edge per pair: a report with an RTT, then the fresher. const measured = new Map(); for (const [a, report] of Object.entries(state.graph.measured)) { for (const link of report.links) { const key = a < link.peer ? a + " " + link.peer : link.peer + " " + a; const known = measured.get(key); - if (!known || report.secsAgo < known.secsAgo) { + const timed = typeof link.rttMs === "number"; + const knownTimed = known && typeof known.link.rttMs === "number"; + if (!known || (timed && !knownTimed) || + (timed === knownTimed && report.secsAgo < known.secsAgo)) { measured.set(key, { a, b: link.peer, link, secsAgo: report.secsAgo }); } } @@ -335,31 +345,50 @@ function renderGraph() { children.push(svg("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, stroke: "var(--intended)", "stroke-dasharray": "4 4", "stroke-width": 1 })); } + const ms = rtt => rtt < 10 ? rtt.toFixed(1) : String(Math.round(rtt)); for (const { a, b, link, secsAgo } of measured.values()) { const p = positions.get(a), q = positions.get(b); if (!p || !q) continue; - const line = svg("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, - stroke: link.dormant ? "var(--bad)" : colourOf(link.transport), "stroke-width": 2 }); - // Dotted where dormant, dashed where relayed, solid where direct. - if (link.dormant) { - line.setAttribute("stroke-dasharray", "1 4"); - line.setAttribute("stroke-linecap", "round"); - } else if (link.relayed) { - line.setAttribute("stroke-dasharray", "7 4"); - } - const title = svg("title"); - title.textContent = name(a) + " – " + name(b) + (link.transport ? " via " + link.transport : "") + - (link.relayed ? ", relayed" : "") + (link.dormant ? ", dormant" : "") + - (typeof link.rttMs === "number" ? ", " + link.rttMs.toFixed(1) + " ms" : "") + - ", measured " + age(secsAgo + elapsed()) + " ago"; - line.append(title); - children.push(line); - if (typeof link.rttMs === "number") { - const label = svg("text", { x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 - 3, - "text-anchor": "middle", "font-size": 10, fill: "var(--muted)" }); - label.textContent = link.rttMs < 10 ? link.rttMs.toFixed(1) : Math.round(link.rttMs); - children.push(label); - } + // The transport in use thick, in the middle; the others thin, beside it. + const lines = [{ transport: link.transport, rttMs: link.rttMs, used: true }]; + for (const other of link.others || []) lines.push({ ...other, used: false }); + const length = Math.max(Math.hypot(q.x - p.x, q.y - p.y), 1); + const nx = -(q.y - p.y) / length, ny = (q.x - p.x) / length; + lines.forEach((line, i) => { + // 0, then +1, -1, +2, -2 … steps of 6 px to the side. + const side = i === 0 ? 0 : (i % 2 ? 1 : -1) * Math.ceil(i / 2) * 6; + const ox = nx * side, oy = ny * side; + const drawn = svg("line", { x1: p.x + ox, y1: p.y + oy, x2: q.x + ox, y2: q.y + oy, + stroke: link.dormant ? "var(--bad)" : colourOf(line.transport), + "stroke-width": line.used ? 3 : 1.2 }); + // Dotted where dormant, dashed where relayed, solid where direct. + if (link.dormant) { + drawn.setAttribute("stroke-dasharray", line.used ? "1 5" : "1 3"); + drawn.setAttribute("stroke-linecap", "round"); + } else if (line.used && link.relayed) { + drawn.setAttribute("stroke-dasharray", "7 4"); + } + const title = svg("title"); + title.textContent = name(a) + " – " + name(b) + + (line.transport ? " via " + line.transport : "") + (line.used ? " (in use)" : "") + + (line.used && link.relayed ? ", relayed" : "") + (link.dormant ? ", dormant" : "") + + (typeof line.rttMs === "number" ? ", " + line.rttMs.toFixed(1) + " ms" : "") + + ", measured " + age(secsAgo + elapsed()) + " ago"; + drawn.append(title); + children.push(drawn); + if (typeof line.rttMs === "number") { + // The one in use at the middle, larger; the others further along, beside theirs. + const t = line.used ? .5 : .3; + const out = line.used ? -4 : Math.sign(side) * 9; + const label = svg("text", { + x: p.x + (q.x - p.x) * t + ox + nx * out, + y: p.y + (q.y - p.y) * t + oy + ny * out + (line.used ? 0 : 3), + "text-anchor": "middle", "font-size": line.used ? 11 : 9, + fill: line.used ? "var(--fg)" : "var(--muted)" }); + label.textContent = ms(line.rttMs); + children.push(label); + } + }); } for (const id of nodes) { const p = positions.get(id), member = state.graph.members[id]; diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs index adaed10..6c7f1c5 100644 --- a/src/api/dashboard/mod.rs +++ b/src/api/dashboard/mod.rs @@ -35,9 +35,9 @@ use crate::transport::Link; /// How often the snapshot is built and compared, while anyone looks. const TICK: Duration = Duration::from_secs(2); /// How often members are asked for their links, and the graph recomputed. -const LINKS_EVERY: Duration = Duration::from_secs(30); +const LINKS_EVERY: Duration = Duration::from_secs(10); /// A member's links older than this are left out (it didn't answer since). -const LINKS_KEPT_FOR: Duration = Duration::from_secs(90); +const LINKS_KEPT_FOR: Duration = Duration::from_secs(30); /// The producer runs on this long after the last request for the state. const LINGER: Duration = Duration::from_secs(60); /// Members asked for their links at most, each round. @@ -230,6 +230,10 @@ fn links_json(links: &[Link]) -> JsonValue { "relayed": link.relayed, "rttMs": link.rtt_us.map(|us| f64::from(us) / 1000.0), "dormant": link.dormant, + "others": link.others.iter().map(|other| json!({ + "transport": other.transport, + "rttMs": other.rtt_us.map(|us| f64::from(us) / 1000.0), + })).collect::<Vec<_>>(), }) }) .collect() |
