// The fabric map: switches in their tiers, adapters under their leaves, every
// cable between them, and the cluster's own adapters live.
//
// Plain SVG and no library, like every other chart here. The layout is the
// shape a fat tree already has — cores over spines over leaves over hosts —
// so it is computed rather than simulated, and it is the same every time the
// page is opened: a map that rearranges itself on each refresh is one nobody
// can learn.
(function () {
"use strict";
const root = document.getElementById("fabric-map");
if (!root) return;
const seed = document.getElementById("fabric-data");
let data = seed ? JSON.parse(seed.textContent) : null;
const url = root.dataset.src;
const svgNS = "http://www.w3.org/2000/svg";
const reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const state = { mode: location.hash === "#rails" ? "rails" : "speed", traffic: !reduced, problems: false, query: "", selected: null,
view: { x: 0, y: 0, k: 1 } };
const canvas = root.querySelector(".fm-canvas");
const tip = root.querySelector(".fm-tip");
const panel = root.querySelector(".fm-panel");
const stats = root.querySelector(".fm-stats");
// --- words -------------------------------------------------------------
// What the map says comes from the page, in the page's language: the
// server writes its catalogue's sentences into #fabric-i18n, with {0}, {1}
// where a figure goes. English is kept here for a page that has none, so
// the map still speaks rather than printing keys.
const EN = {
decimal: ".",
map_failed: "The map could not be made: {0}",
not_mapped: "Not mapped yet: the first scan runs within five minutes of a node on this fabric connecting.",
"tier.leaf": "leaf", "tier.spine": "spine", "tier.core": "core", "tier.supercore": "super-core", "tier.n": "tier {0}",
adapters_row: "adapters", ports_used: "{0}/{1} ports", hosts: "{0} hosts", cables_n: "{0} cables, ",
below_rate: "trained below the {0} Gb/s the rest of its kind run at",
adapters_n: "{0} adapters", degraded_n: "{0} degraded cable(s)", rail: "rail {0}", not_ours: "not one of this cluster's nodes",
"dt.tier": "tier", "dt.ports": "ports", "dt.adapters": "adapters", "dt.adapter": "adapter", "dt.rail": "rail",
"dt.leaf": "leaf", "dt.state": "state", "dt.rate": "rate", "dt.sending": "sending", "dt.receiving": "receiving",
cabled: "{0} of {1} cabled", degraded_cables: "Degraded cables", all_good: "Every cable at its expected rate.",
port_to: "port {0} → {1}: {2}", close: "close", open_node: "open {0} →", adapter: "adapter",
"stat.switches": "switches", "stat.adapters": "adapters", "stat.cables": "cables", "stat.degraded": "degraded",
"stat.rails": "rails", "stat.aligned": "aligned", "stat.crossed": "crossed", "stat.sent": "sent now",
};
const S = (() => {
const e = document.getElementById("fabric-i18n");
try { return e ? JSON.parse(e.textContent) : {}; } catch (_) { return {}; }
})();
const T = (k, ...a) => String(S[k] || EN[k] || k).replace(/\{(\d)\}/g, (_, i) => a[+i]);
const fixed = (v, n) => v.toFixed(n).replace(".", S.decimal || EN.decimal);
// --- colours ---------------------------------------------------------
const speedClass = (g) => g >= 800 ? "xdr" : g >= 400 ? "ndr" : g >= 200 ? "hdr" : g >= 100 ? "edr" : "slow";
const railPalette = ["#38bdf8", "#a78bfa", "#34d399", "#fbbf24", "#f472b6", "#fb7185", "#2dd4bf", "#c084fc"];
const fmtBps = (b) => {
const bits = b * 8;
if (bits >= 1e9) return fixed(bits / 1e9, 1) + " Gb/s";
if (bits >= 1e6) return fixed(bits / 1e6, 1) + " Mb/s";
if (bits >= 1e3) return fixed(bits / 1e3, 0) + " kb/s";
return fixed(bits, 0) + " b/s";
};
const el = (name, attrs, parent) => {
const e = document.createElementNS(svgNS, name);
for (const k in attrs) e.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(e);
return e;
};
const esc = (s) => String(s == null ? "" : s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
// --- layout ----------------------------------------------------------
function layout(map, width) {
const tiers = Math.max(1, map.tiers || 1);
// Rows are placed once the width is known; see below.
let gap = 150;
const rowY = (t) => 70 + (tiers - t) * gap;
const pos = {};
const byGUID = {};
map.switches.forEach((s) => (byGUID[s.guid] = s));
const leaves = map.switches.filter((s) => s.tier === 1).sort((a, b) => a.name.localeCompare(b.name));
// Hosts under their leaves, leaf by leaf, a little gap between groups.
const groups = new Map(leaves.map((l) => [l.guid, []]));
const loose = [];
map.hosts.forEach((h) => (groups.has(h.leaf) ? groups.get(h.leaf) : loose).push(h));
const hostGap = map.hosts.length > 120 ? 16 : map.hosts.length > 48 ? 24 : 34;
// The width decides the height: the drawing is fitted to the page, and a
// fabric four times wider than the page would be a thin strip at the
// height of a small one.
const hostCount = map.hosts.length;
const estWidth = Math.max(width, 60 + hostCount * hostGap + leaves.length * 90);
gap = Math.max(130, (estWidth * 0.42 - 180) / tiers);
const hostY = rowY(1) + gap;
// Room on the left for the tier names, at the size they will be drawn.
let x = 40 + 30 * Math.max(1, estWidth / width);
const leafX = {};
for (const l of leaves) {
const hs = groups.get(l.guid);
if (!hs.length) { leafX[l.guid] = x + 50; x += 130; continue; }
const start = x;
hs.forEach((h) => { pos[h.guid] = { x, y: hostY }; x += hostGap; });
leafX[l.guid] = (start + x - hostGap) / 2;
x += Math.max(40, 130 - hs.length * hostGap / 2);
}
loose.forEach((h) => { pos[h.guid] = { x, y: hostY }; x += hostGap; });
const span = Math.max(x + 20, width);
// Leaves too close for their boxes are spread, keeping their order.
let last = -Infinity;
for (const l of leaves) {
const want = Math.max(leafX[l.guid], last + 124);
pos[l.guid] = { x: want, y: rowY(1) };
last = want;
}
// Each tier above: ordered by where the switches they connect to sit,
// which keeps cables from crossing more than they must, then spread
// evenly across the width.
const adj = {};
map.links.forEach((k) => {
(adj[k.from] = adj[k.from] || []).push(k.to);
(adj[k.to] = adj[k.to] || []).push(k.from);
});
for (let t = 2; t <= tiers; t++) {
const row = map.switches.filter((s) => s.tier === t);
row.forEach((s) => {
const xs = (adj[s.guid] || []).map((g) => pos[g]).filter((p) => p && p.y > rowY(t)).map((p) => p.x);
s._bx = xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : span / 2;
});
row.sort((a, b) => a._bx - b._bx || a.name.localeCompare(b.name));
const lo = Math.min(...leaves.map((l) => pos[l.guid].x), span * 0.15);
const hi = Math.max(...leaves.map((l) => pos[l.guid].x), span * 0.85);
row.forEach((s, i) => {
pos[s.guid] = { x: row.length === 1 ? (lo + hi) / 2 : lo + (hi - lo) * i / (row.length - 1), y: rowY(t) };
});
}
map.switches.forEach((s) => { if (!pos[s.guid]) pos[s.guid] = { x: span / 2, y: rowY(s.tier || 1) }; });
// How much to enlarge switches and adapters so they read at the size
// they would on a page as wide as the fabric — as far as their spacing
// lets them without touching.
const fit = Math.max(1, Math.max(span, width) / width);
let leafGap = Infinity;
for (let i = 1; i < leaves.length; i++) leafGap = Math.min(leafGap, pos[leaves[i].guid].x - pos[leaves[i - 1].guid].x);
const swScale = Math.min(fit, leafGap / 118, 2.4);
const hostScale = Math.min(fit, hostGap / 13, 2.4);
return { pos, width: Math.max(span, width), height: hostY + 110 * Math.max(1, hostScale), rowY, tiers, byGUID,
gap, swScale: Math.max(1, swScale), hostScale: Math.max(1, hostScale), fit };
}
// --- drawing ---------------------------------------------------------
let svg, viewport, flowScale = 1;
function draw() {
canvas.textContent = "";
if (!data || !data.map) {
const msg = data && data.error ? T("map_failed", data.error) : T("not_mapped");
canvas.innerHTML = '
' + esc(msg) + "
";
fillStats();
return;
}
const map = data.map;
const width = canvas.clientWidth || 1000;
const L = layout(map, width);
// Fitted to the width to start with — the whole fabric at once — and
// zoomed from there for detail.
svg = el("svg", { class: "fm-svg mode-" + state.mode + (state.problems ? " only-problems" : ""),
viewBox: `0 0 ${L.width} ${L.height}`, preserveAspectRatio: "xMidYMin meet" });
svg.style.height = Math.max(380, Math.min(780, L.height * width / L.width)) + "px";
const defs = el("defs", {}, svg);
defs.innerHTML = `
`;
el("rect", { x: -5000, y: -5000, width: 20000, height: 20000, fill: "url(#fm-grid)" }, svg);
viewport = el("g", { class: "fm-viewport" }, svg);
// Tier labels down the left.
const names = { 1: T("tier.leaf"), 2: T("tier.spine"), 3: T("tier.core"), 4: T("tier.supercore") };
// Tier names down the left edge, turned on their side so they take no
// room from the leftmost switch.
const label = (y, text) => el("text", { class: "fm-tier",
transform: `translate(${14 * L.fit},${y}) rotate(-90) scale(${L.fit})` }, viewport).textContent = text;
for (let t = L.tiers; t >= 1; t--) label(L.rowY(t), names[t] || T("tier.n", t));
label(L.rowY(1) + L.gap, T("adapters_row"));
// Cables, parallel ones bundled.
const bundles = new Map();
for (const k of map.links) {
const key = k.from + "|" + k.to;
const b = bundles.get(key) || { from: k.from, to: k.to, links: [] };
b.links.push(k);
bundles.set(key, b);
}
const linkLayer = el("g", { class: "fm-links" }, viewport);
const flowLayer = el("g", { class: "fm-flows" }, viewport);
flowScale = L.hostScale;
const touches = {};
for (const b of bundles.values()) {
const a = L.pos[b.from], z = L.pos[b.to];
if (!a || !z) continue;
const my = (a.y + z.y) / 2;
const d = `M${a.x},${a.y + 17 * L.swScale} C${a.x},${my} ${z.x},${my} ${z.x},${z.y - (b.links[0].host ? 8 * L.hostScale : 17 * L.swScale)}`;
const bad = b.links.some((k) => k.degraded);
const g = Math.min(...b.links.map((k) => k.gbps || 0));
const host = b.links[0].host;
const hostInfo = host ? map.hosts.find((h) => h.guid === b.to) : null;
const path = el("path", { d, class: "fm-link sp-" + speedClass(g) + (bad ? " degraded" : "") + (host ? " host" : ""),
"stroke-width": host ? 1.4 : 1.2 + Math.log2(b.links.length) * 1.4 }, linkLayer);
if (host && hostInfo) path.style.setProperty("--rail", railPalette[hostInfo.rail % railPalette.length]);
path.dataset.ends = b.from + " " + b.to;
path.addEventListener("mouseenter", (e) => showTip(e, linkTip(b, map)));
path.addEventListener("mouseleave", hideTip);
(touches[b.from] = touches[b.from] || []).push(path);
(touches[b.to] = touches[b.to] || []).push(path);
if (bad) {
(touches[b.from].bad = (touches[b.from].bad || 0) + 1);
const mid = pointAt(a, z, my);
const w = el("g", { class: "fm-warn", transform: `translate(${mid.x},${mid.y}) scale(${L.swScale})` }, viewport);
el("circle", { r: 7 }, w);
el("text", { y: 3.5 }, w).textContent = "!";
w.addEventListener("mouseenter", (e) => showTip(e, linkTip(b, map)));
w.addEventListener("mouseleave", hideTip);
}
// Traffic: particles that run as fast as the adapter is busy — up
// the cable for what it sends, down for what it receives.
if (host && state.traffic && data.live && data.live[b.to]) {
const live = data.live[b.to];
flow(flowLayer, d, live.tx_bps, "up", live.gbps);
flow(flowLayer, d, live.rx_bps, "down", live.gbps);
}
}
// Switches.
const nodeLayer = el("g", { class: "fm-nodes" }, viewport);
for (const s of map.switches) {
const p = L.pos[s.guid];
const tierClass = s.tier >= 3 ? "core" : s.tier === 2 ? "spine" : "leaf";
const g = el("g", { class: "fm-switch t-" + tierClass + (touches[s.guid] && touches[s.guid].bad ? " has-bad" : ""),
transform: `translate(${p.x},${p.y}) scale(${L.swScale})`, tabindex: 0 }, nodeLayer);
el("rect", { x: -56, y: -17, width: 112, height: 34, rx: 10, fill: `url(#fm-g-${tierClass})` }, g);
el("text", { class: "fm-name", y: -1 }, g).textContent = s.name.length > 16 ? s.name.slice(0, 15) + "…" : s.name;
el("text", { class: "fm-sub", y: 11 }, g).textContent = T("ports_used", s.used, s.ports) + (s.hosts ? " · " + T("hosts", s.hosts) : "");
// Port use as an arc along the bottom edge.
const frac = s.ports ? Math.min(1, s.used / s.ports) : 0;
el("line", { x1: -46, y1: 15.5, x2: -46 + 92 * frac, y2: 15.5, class: "fm-use" }, g);
if (touches[s.guid] && touches[s.guid].bad) {
const bdg = el("g", { class: "fm-badge", transform: "translate(52,-15)" }, g);
el("circle", { r: 8 }, bdg);
el("text", { y: 3.5 }, bdg).textContent = touches[s.guid].bad;
}
wire(g, s.guid, () => switchTip(s, map, touches), () => switchPanel(s, map));
}
// Adapters.
const showLabels = map.hosts.length <= 96;
for (const h of map.hosts) {
const p = L.pos[h.guid];
if (!p) continue;
const live = data.live && data.live[h.guid];
const down = live && live.state && live.state.toUpperCase() !== "ACTIVE";
const errs = live && live.errors && Object.keys(live.errors).length;
const cls = "fm-host" + (h.node ? " known" : "") + (down ? " down" : "") + (errs ? " errs" : "") +
(state.query && matches(h) ? " found" : "");
const g = el("g", { class: cls, transform: `translate(${p.x},${p.y}) scale(${L.hostScale})`, tabindex: 0 }, nodeLayer);
g.style.setProperty("--rail", railPalette[h.rail % railPalette.length]);
el("circle", { r: 7.5, class: "fm-halo" }, g);
el("circle", { r: 5.5 }, g);
if (showLabels) {
const label = (h.node || (h.desc || "").split(" ")[0] || h.guid.slice(-6)) + (h.hca ? " " + h.hca.replace("mlx5_", "#") : "");
el("text", { class: "fm-hlabel", transform: "translate(3,14) rotate(55)" }, g).textContent = label;
}
wire(g, h.guid, () => hostTip(h, live), () => hostPanel(h, live, map));
}
canvas.appendChild(svg);
applyView();
enablePanZoom(svg, L);
if (state.selected) highlight(state.selected, touches);
svg._touches = touches;
fillStats();
}
function pointAt(a, z, my) {
// The middle of the cubic: its two control points share a height.
return { x: (a.x + z.x) / 2, y: my };
}
function flow(layer, d, bps, dir, gbps) {
if (!bps || bps < 1e3) return;
const cap = (gbps || 100) * 1e9 / 8;
const load = Math.min(1, bps / cap);
const count = 1 + Math.round(load * 4);
const dur = Math.max(0.7, 4.5 - Math.log10(bps) * 0.38);
for (let i = 0; i < count; i++) {
const c = el("circle", { r: 2.4 * flowScale, class: "fm-particle " + dir }, layer);
const m = el("animateMotion", { dur: dur + "s", repeatCount: "indefinite", path: d,
begin: (-(dur / count) * i).toFixed(2) + "s" }, c);
if (dir === "up") { m.setAttribute("keyPoints", "1;0"); m.setAttribute("keyTimes", "0;1"); m.setAttribute("calcMode", "linear"); }
}
}
// --- interaction ------------------------------------------------------
function wire(g, guid, tipFn, panelFn) {
g._guid = guid;
g.addEventListener("mouseenter", (e) => { highlight(guid, svg._touches); showTip(e, tipFn()); });
g.addEventListener("mousemove", moveTip);
g.addEventListener("mouseleave", () => { hideTip(); if (!state.selected) unhighlight(); else highlight(state.selected, svg._touches); });
const open = (e) => { e.stopPropagation(); state.selected = guid; highlight(guid, svg._touches); panel.innerHTML = panelFn(); panel.hidden = false; };
g.addEventListener("click", open);
g.addEventListener("keydown", (e) => { if (e.key === "Enter") open(e); });
}
function highlight(guid, touches) {
if (!svg) return;
svg.classList.add("focus");
svg.querySelectorAll(".hot").forEach((n) => n.classList.remove("hot"));
(touches[guid] || []).forEach((p) => {
p.classList.add("hot");
p.dataset.ends.split(" ").forEach((g) => {
svg.querySelectorAll(".fm-nodes > g").forEach((n) => { if (n._guid === g) n.classList.add("hot"); });
});
});
svg.querySelectorAll(".fm-nodes > g").forEach((n) => { if (n._guid === guid) n.classList.add("hot"); });
}
function unhighlight() {
if (!svg) return;
svg.classList.remove("focus");
svg.querySelectorAll(".hot").forEach((n) => n.classList.remove("hot"));
}
function showTip(e, html) { tip.innerHTML = html; tip.hidden = false; moveTip(e); }
function moveTip(e) {
const r = root.getBoundingClientRect();
tip.style.left = Math.min(e.clientX - r.left + 14, r.width - 260) + "px";
tip.style.top = (e.clientY - r.top + 14) + "px";
}
function hideTip() { tip.hidden = true; }
function linkTip(b, map) {
const n = (g) => { const s = map.switches.find((x) => x.guid === g); const h = map.hosts.find((x) => x.guid === g);
return s ? s.name : h ? (h.node ? h.node + " " + (h.hca || "") : h.desc) : g; };
const k = b.links[0];
const exp = k.host ? map.expected_host_gbps : map.expected_switch_gbps;
return `${esc(n(b.from))} ↔ ${esc(n(b.to))} ${b.links.length > 1 ? esc(T("cables_n", b.links.length)) : ""}${esc(k.rate)} · ${k.gbps} Gb/s` +
(b.links.some((x) => x.degraded) ? ` ${esc(T("below_rate", exp))}` : "");
}
const tierName = (s) => s.tier >= 3 ? T("tier.core") : s.tier === 2 ? T("tier.spine") : T("tier.leaf");
function switchTip(s, map, touches) {
const bad = touches[s.guid] && touches[s.guid].bad;
return `${esc(s.name)} ${esc(tierName(s))} · ${esc(T("ports_used", s.used, s.ports))}` +
(s.hosts ? " · " + esc(T("adapters_n", s.hosts)) : "") + (s.lid ? ` · LID ${s.lid}` : "") +
(bad ? ` ${esc(T("degraded_n", bad))}` : "");
}
function hostTip(h, live) {
let t = `${esc(h.node || h.desc || h.guid)}${h.hca ? " · " + esc(h.hca) : ""} ${esc(T("rail", h.rail))}`;
if (live) {
t += ` · ${esc(live.state)}${live.gbps ? " · " + live.gbps + " Gb/s" : ""}`;
if (live.tx_bps || live.rx_bps) t += ` ↑ ${fmtBps(live.tx_bps)} · ↓ ${fmtBps(live.rx_bps)}`;
if (live.errors) t += ` ${Object.entries(live.errors).map(([k, v]) => esc(k) + " " + v).join(", ")}`;
} else if (!h.node) {
t += ` ${esc(T("not_ours"))}`;
}
return t;
}
function switchPanel(s, map) {
const cables = map.links.filter((k) => k.from === s.guid || k.to === s.guid);
const bad = cables.filter((k) => k.degraded);
const rows = bad.map((k) => { const o = k.from === s.guid ? k.to : k.from; const on = map.switches.find((x) => x.guid === o) || map.hosts.find((x) => x.guid === o);
return `
${esc(T("port_to", k.from === s.guid ? k.from_port : k.to_port, on ? (on.name || on.node || on.desc) : o, k.rate))}
`; }).join("");
return `
${esc(s.name)}
${esc(T("dt.tier"))}
${esc(tierName(s))}
GUID
${esc(s.guid)}
LID
${s.lid || "—"}
${esc(T("dt.ports"))}
${esc(T("cabled", s.used, s.ports))}
${esc(T("dt.adapters"))}
${s.hosts}
` +
(bad.length ? `
${esc(T("degraded_cables"))}
${rows}
` : `
${esc(T("all_good"))}
`);
}
function hostPanel(h, live, map) {
const leaf = map.switches.find((x) => x.guid === h.leaf);
let body = `