Files
sglang/benchmark/agentic-rollout/viewer.js
T

484 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const DATA = __REAL_DATA__;
const $ = (s) => document.querySelector(s),
NS = "http://www.w3.org/2000/svg",
COLORS = ["var(--sampling)", "var(--tool)", "var(--s3)", "var(--s4)"];
let start = 0,
span = DATA.summary.duration,
selected = DATA.rows[0],
drag = null;
const total = span;
const lanes = [
...new Map(
DATA.rows.map((r) => [
JSON.stringify([r.worker, r.conversation]),
{ worker: r.worker, conversation: r.conversation },
]),
).values(),
].sort(
(a, b) =>
(a.worker ?? Infinity) - (b.worker ?? Infinity) ||
String(a.conversation).localeCompare(String(b.conversation), undefined, {
numeric: true,
}),
);
let metrics = Object.values(DATA.exporters)[0];
$("#subtitle").textContent =
`${DATA.summary.model} · ${DATA.summary.requests} requests · ${DATA.summary.conversations} conversations × ${DATA.summary.turns} turns · concurrency ${DATA.summary.concurrency} · page ${DATA.summary.page_size} · ${total.toFixed(1)}s · ${DATA.summary.errors} errors`;
function elem(tag, attrs, parent) {
const e = document.createElementNS(NS, tag);
Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
parent.append(e);
return e;
}
function text(parent, x, y, s, extra = {}) {
const e = elem(
"text",
{ x, y, fill: "var(--muted)", "font-size": 12, ...extra },
parent,
);
e.textContent = s;
return e;
}
function fmt(v) {
if (v === null || v === undefined) return "Unavailable";
return Math.abs(v) >= 1000
? (v / 1000).toFixed(1) + "k"
: Math.abs(v) >= 10
? v.toFixed(1)
: v.toFixed(2);
}
function details() {
if (!selected) {
$("#selected-title").textContent = "No request records";
return;
}
$("#request").value = String(DATA.rows.indexOf(selected));
$("#selected-title").textContent =
`Conversation ${selected.conversation} · Turn ${selected.turn} · Worker ${selected.worker ?? "unknown"}`;
$("#availability").textContent = [
selected.error ? "Failed: " + selected.error : "",
selected.unavailable.length
? "Phase timestamps unavailable: " + selected.unavailable.join(", ")
: "",
]
.filter(Boolean)
.join(" · ");
const fields = [
["Context", fmt(selected.context) + " tokens"],
["Output", fmt(selected.output) + " tokens"],
[
"Sampling",
fmt(selected.latency == null ? null : selected.latency * 1000) + " ms",
],
["TTFT", fmt(selected.ttft == null ? null : selected.ttft * 1000) + " ms"],
[
"Client wait",
fmt(selected.wait == null ? null : selected.wait * 1000) + " ms",
],
[
"GPU / CPU hits",
`${selected.hits.device ?? "N/A"} / ${selected.hits.host ?? "N/A"} tokens`,
],
];
$("#details").replaceChildren();
for (const [label, value] of fields) {
const field = document.createElement("div");
const name = document.createElement("span");
const content = document.createElement("strong");
name.textContent = label;
content.textContent = value;
field.append(name, content);
$("#details").append(field);
}
}
function drawTimeline() {
const svg = $("#timeline");
if ($("#timeline-panel").hidden) return;
let w = svg.parentElement.clientWidth,
L = w < 500 ? 70 : 112,
R = 12,
h =
102 +
lanes.length * 33 +
new Set(lanes.map((lane) => lane.worker)).size * 24;
const x = (t) => L + ((t - start) / span) * (w - L - R);
svg.replaceChildren();
svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
svg.style.width = "100%";
svg.style.height = h + "px";
let ticks = w < 500 ? 3 : 6;
for (let i = 0; i <= ticks; i++) {
let t = start + (span * i) / ticks,
px = x(t);
elem(
"line",
{ x1: px, x2: px, y1: 25, y2: h - 30, stroke: "var(--line)" },
svg,
);
text(svg, px, 15, t.toFixed(span < 3 ? 2 : 1) + "s", {
"text-anchor": i === 0 ? "start" : i === ticks ? "end" : "middle",
});
}
let groupIndex = -1;
for (let lane = 0; lane < lanes.length; lane++) {
let { conversation: c, worker } = lanes[lane];
const startsGroup = lane === 0 || lanes[lane - 1].worker !== worker;
if (startsGroup) groupIndex++;
let y = 66 + lane * 33 + groupIndex * 24;
if (startsGroup)
text(
svg,
0,
y - 5,
worker === null ? "Worker unknown" : "Worker " + worker,
{ "font-size": 10 },
);
text(svg, 4, y + 14, "Convo " + c);
for (const r of DATA.rows.filter(
(v) => v.conversation === c && v.worker === worker,
)) {
for (const p of r.phases) {
let a = Math.max(start, p.start),
b = Math.min(start + span, p.end);
if (b <= a) continue;
let fill =
p.type === "Sampling"
? "var(--sampling)"
: p.type === "Tool call"
? "var(--tool)"
: "var(--wait)";
const g = elem(
"g",
{
role: "button",
"aria-label": `Conversation ${c} turn ${r.turn} ${p.type}, ${((p.end - p.start) * 1000).toFixed(2)} ms`,
tabindex: 0,
},
svg,
);
const rect = elem(
"rect",
{
x: x(a),
y,
width: Math.max(0.4, x(b) - x(a)),
height: 22,
fill,
opacity: selected === r ? 1 : 0.66,
rx: 2,
},
g,
);
g.style.cursor = "pointer";
const choose = () => {
selected = r;
details();
drawTimeline();
};
g.addEventListener("click", () => {
if (!drag?.moved) choose();
});
g.addEventListener("keydown", (e) => {
if (e.key === "Enter") choose();
});
if (p.type === "Sampling" && x(b) - x(a) > 24)
text(g, (x(a) + x(b)) / 2, y + 15, "T" + r.turn, {
"text-anchor": "middle",
fill: "var(--bg)",
"pointer-events": "none",
});
g.addEventListener("pointermove", (e) => {
let tip = $("#tooltip"),
bounds = svg.getBoundingClientRect();
tip.hidden = false;
tip.textContent = `Convo ${c} · Turn ${r.turn}\n${p.type}: ${((p.end - p.start) * 1000).toFixed(2)} ms\n${p.start.toFixed(3)}${p.end.toFixed(3)}s`;
tip.style.left =
Math.max(0, Math.min(w - 270, e.clientX - bounds.left + 12)) + "px";
tip.style.top = e.clientY - bounds.top + 24 + "px";
});
g.addEventListener("pointerleave", () => ($("#tooltip").hidden = true));
}
}
}
text(svg, (L + w - R) / 2, h - 7, "Elapsed time (seconds)", {
"text-anchor": "middle",
});
svg.onpointerdown = (e) => {
let px = e.clientX - svg.getBoundingClientRect().left;
if (px < L) return;
drag = { px, t: start + ((px - L) / (w - L - R)) * span, moved: false };
};
svg.onpointermove = (e) => {
if (
drag &&
Math.abs(e.clientX - svg.getBoundingClientRect().left - drag.px) > 8
)
drag.moved = true;
};
svg.onpointerup = (e) => {
if (drag?.moved) {
let px = Math.max(
L,
Math.min(w - R, e.clientX - svg.getBoundingClientRect().left),
),
end = start + ((px - L) / (w - L - R)) * span;
start = Math.min(drag.t, end);
span = Math.max(0.05, Math.abs(end - drag.t));
$("#tooltip").hidden = true;
let o = $("#window option[data-custom]");
if (!o) {
o = document.createElement("option");
o.dataset.custom = "1";
$("#window").append(o);
}
o.value = span;
o.textContent = span.toFixed(2) + " seconds";
$("#window").value = String(span);
render();
}
setTimeout(() => (drag = null), 0);
};
}
function drawMetrics() {
if ($("#metrics-panel").hidden) return;
const grid = $("#metric-grid");
grid.replaceChildren();
let group = null;
for (const m of metrics) {
if ((m.group || "Topline Metrics") !== group) {
group = m.group || "Topline Metrics";
let heading = document.createElement("h2");
heading.textContent = group;
heading.style.cssText = "grid-column:1/-1;font-size:16px;margin:6px 0 0";
grid.append(heading);
}
drawMetricPanel(m, grid);
}
}
function drawMetricPanel(m, grid) {
const panel = document.createElement("div");
panel.className = "panel";
const heading = document.createElement("h3");
heading.textContent = m.title;
const svg = document.createElementNS(NS, "svg");
svg.setAttribute("role", "img");
svg.setAttribute("aria-label", m.title);
const legend = document.createElement("div");
legend.className = "metric-legend";
const source = document.createElement("p");
source.className = "source";
source.textContent = m.source;
panel.append(heading, svg, legend, source);
grid.append(panel);
let w = panel.clientWidth,
h = 160,
L = 64,
R = 12,
T = 20,
B = 35;
svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
svg.style.width = "100%";
svg.style.height = h + "px";
let vs = m.series.flatMap((s) =>
s.points.map((p) => p[1]).filter((v) => v !== null),
);
let max =
m.unit === "%"
? 100
: vs.length
? vs.reduce((maximum, value) => Math.max(maximum, value), 0) * 1.08
: 1;
if (max === 0) max = 1;
const x = (t) => L + ((t - start) / span) * (w - L - R),
y = (v) => T + (1 - v / max) * (h - T - B);
elem(
"rect",
{
x: L,
y: T,
width: w - L - R,
height: h - T - B,
fill: "none",
stroke: "var(--line)",
},
svg,
);
for (let f of [0, 0.5, 1]) {
let yy = y(max * f);
text(svg, L - 6, yy + 4, fmt(max * f), { "text-anchor": "end" });
if (f === 0.5)
elem(
"line",
{ x1: L, x2: w - R, y1: yy, y2: yy, stroke: "var(--line)" },
svg,
);
}
for (let f of [0, 0.5, 1])
text(svg, x(start + span * f), h - 18, (start + span * f).toFixed(1), {
"text-anchor": f === 0 ? "start" : f === 1 ? "end" : "middle",
});
text(svg, L, T - 6, m.unit);
text(svg, (L + w - R) / 2, h - 2, "Elapsed time (s)", {
"text-anchor": "middle",
"font-size": 11,
});
if (!vs.length)
text(svg, (L + w - R) / 2, 76, "Unavailable", {
"text-anchor": "middle",
});
const paths = [];
m.series.forEach((s, j) => {
let path = "",
pen = false;
let points = s.points.filter((p) => p[0] >= start && p[0] <= start + span);
for (let [t, v] of points) {
if (v === null) {
pen = false;
continue;
}
path += (pen ? "L" : "M") + x(t) + "," + y(v);
pen = true;
}
let g = elem("g", {}, svg);
elem(
"path",
{
d: path,
fill: "none",
stroke: COLORS[s.color ?? j % 4],
"stroke-width": 1.6,
"stroke-dasharray": s.dashed ? "5 4" : "",
},
g,
);
for (let [t, v] of points)
if (v !== null)
elem(
"circle",
{ cx: x(t), cy: y(v), r: 2, fill: COLORS[s.color ?? j % 4] },
g,
);
paths.push(g);
let b = document.createElement("button");
b.type = "button";
b.setAttribute("aria-pressed", "true");
let sw = document.createElement("i");
sw.style.background = COLORS[s.color ?? j % 4];
if (s.dashed) {
sw.style.background = "none";
sw.style.borderTop = "2px dashed " + COLORS[s.color ?? j % 4];
}
b.append(sw, document.createTextNode(s.name));
b.onclick = () => {
let on = b.getAttribute("aria-pressed") !== "true";
b.setAttribute("aria-pressed", on);
g.style.display = on ? "" : "none";
};
legend.append(b);
});
let guide = elem(
"line",
{ y1: T, y2: h - B, stroke: "var(--muted)", visibility: "hidden" },
svg,
);
svg.addEventListener("pointermove", (e) => {
if (!vs.length) return;
let px = Math.max(
L,
Math.min(w - R, e.clientX - svg.getBoundingClientRect().left),
),
t = start + ((px - L) / (w - L - R)) * span;
guide.setAttribute("x1", px);
guide.setAttribute("x2", px);
guide.setAttribute("visibility", "visible");
const vals = m.series
.map((s, j) => {
if (paths[j].style.display === "none") return null;
let pt = s.points.reduce(
(best, p) =>
!best || Math.abs(p[0] - t) < Math.abs(best[0] - t) ? p : best,
null,
);
return (
s.name +
": " +
(pt ? fmt(pt[1]) : "Unavailable") +
(pt ? " @ " + pt[0].toFixed(1) + "s" : "")
);
})
.filter(Boolean);
source.textContent = vals.join(" · ");
});
svg.addEventListener("pointerleave", () => {
guide.setAttribute("visibility", "hidden");
source.textContent = m.source;
});
}
function render() {
$("#range").textContent = `${start.toFixed(2)}${(start + span).toFixed(2)}s`;
$("#position").max = Math.max(0, total - span);
$("#position").value = start;
drawTimeline();
drawMetrics();
}
for (let b of document.querySelectorAll("[role=tab]"))
b.onclick = () => {
for (let tab of document.querySelectorAll("[role=tab]")) {
const on = b === tab;
tab.setAttribute("aria-selected", on);
$("#" + tab.getAttribute("aria-controls")).hidden = !on;
}
render();
};
$("#window").onchange = () => {
span = $("#window").value === "all" ? total : Number($("#window").value);
span = Math.min(span, total);
start = Math.min(start, total - span);
render();
};
$("#position").oninput = () => {
start = Number($("#position").value);
render();
};
$("#reset").onclick = () => {
start = 0;
span = total;
$("#window").value = "all";
render();
};
const endpoint = $("#exporter");
for (const url of Object.keys(DATA.exporters)) {
const option = document.createElement("option");
option.value = url;
option.textContent = url;
endpoint.append(option);
}
endpoint.onchange = () => {
metrics = DATA.exporters[endpoint.value];
render();
};
$(".badge").textContent =
DATA.summary.status.toUpperCase() +
" · " +
DATA.summary.errors +
" recorded errors";
if (DATA.summary.error) $(".badge").title = DATA.summary.error;
const requestPicker = $("#request");
DATA.rows.forEach((row, index) => {
const option = document.createElement("option");
option.value = index;
option.textContent = `Conversation ${row.conversation}, turn ${row.turn}${row.error ? " (failed)" : ""}`;
requestPicker.append(option);
});
requestPicker.onchange = () => {
selected = DATA.rows[Number(requestPicker.value)];
details();
drawTimeline();
};
details();
new ResizeObserver(render).observe(document.querySelector("main"));
render();