Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion miles/dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ Views:
lane-selection grammar (`g:` / `rank:` / `node:` / `every:`) and outlier
quick-picks.
- **Rollouts** — per-step trajectory table and scatter, GRPO group degeneracy
(`zero_std`), average weight-version staleness, eval tab.
(`zero_std`), average weight-version staleness, eval tab. Opens on the newest
step that has samples, skipping one still being dumped; a step named in the
URL is always shown as asked.
- **sample view** — a `conversation` tab (role-tagged turns with thinking /
tool calls, from the trajectory sidecar) and a lazily-loaded `tokens` tab
(the whole sequence in one scrollable metric-colored strip, opened at the
Expand Down
8 changes: 4 additions & 4 deletions miles/dashboard/static/api.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const RETRIES_503 = 3;

async function fetchOk(path, params) {
async function fetchOk(path, params, { retry503 = true } = {}) {
const url = new URL(path, location.origin);
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null) url.searchParams.set(k, v);
}
for (let attempt = 0; ; attempt++) {
const res = await fetch(url);
if (res.status === 503 && attempt < RETRIES_503) {
if (res.status === 503 && retry503 && attempt < RETRIES_503) {
await new Promise((r) => setTimeout(r, 1500));
continue;
}
Expand All @@ -24,8 +24,8 @@ async function fetchOk(path, params) {
}
}

export async function api(path, params = {}) {
return (await fetchOk(path, params)).json();
export async function api(path, params = {}, options = {}) {
return (await fetchOk(path, params, options)).json();
}

// framed binary endpoints (/api/timeline/heatmap):
Expand Down
25 changes: 18 additions & 7 deletions miles/dashboard/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,14 @@ function parseRoute() {
return { view: "timeline", lanes: params.get("lanes") };
}
if (segments[0] === "rollout" && segments.length >= 2) {
const rolloutId = Number(segments[1]);
const evaluation = params.get("eval") === "1";
// "latest" is resolved against the dump at render time rather than baked
// into the link, because the newest step is often listed before it is
// readable; a typed step number is always honoured exactly
if (segments[1] === "latest") {
return { view: "rollout", rolloutId: null, evaluation };
}
const rolloutId = Number(segments[1]);
if (segments[2] === "sample" && segments.length === 4) {
return { view: "tokens", rolloutId, sampleIndex: Number(segments[3]), evaluation };
}
Expand All @@ -58,18 +64,23 @@ function parseRoute() {
}

function crumbs(route, meta) {
const nav = (label, href, active) => el("a", { class: `nav${active ? " active" : ""}`, href }, [label]);
const nav = (label, href, active, onclick = null) =>
el("a", { class: `nav${active ? " active" : ""}`, href, onclick }, [label]);
const parts = [nav("Metrics", "#/", route.view === "metrics")];
if (meta.capabilities.has_timeline) {
parts.push(nav("Compute Utilization", "#/timeline", route.view === "timeline"));
}
// the per-step data view is a top-level destination, not a hidden
// click-through from chart points; land on the newest train step
const latest = meta.rollout_ids.train.at(-1);
if (latest !== undefined) {
parts.push(nav("Rollouts", `#/rollout/${latest}`, route.view === "rollout" || route.view === "tokens"));
// click-through from chart points; land on the newest usable train step
if (meta.rollout_ids.train.length) {
const onRollout = route.view === "rollout" || route.view === "tokens";
const replaceInPlace = (event) => {
event.preventDefault();
location.replace("#/rollout/latest");
};
parts.push(nav("Rollouts", "#/rollout/latest", onRollout, onRollout ? replaceInPlace : null));
Comment on lines +77 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) The Rollouts breadcrumb's onclick calls event.preventDefault() unconditionally whenever the nav is active, so ctrl/cmd/middle-click to open the link in a new tab silently breaks (it navigates the current tab via location.replace instead), whereas on base this was a plain anchor that always honored modifier clicks. Fix: only preventDefault/replace for a plain left-click with no modifier keys (check event.button === 0 and !event.ctrlKey/!event.metaKey/!event.shiftKey), letting other clicks fall through to default anchor behavior.

Extended reasoning...

User is already on a rollout/tokens view (onRollout true), so nav("Rollouts", ...) gets the replaceInPlace onclick. Middle-clicking or ctrl+clicking the Rollouts crumb to open the latest step in a new tab still triggers event.preventDefault(); location.replace("#/rollout/latest") on the CURRENT tab, discarding the click's intent and no new tab opens - a capability present before this diff since the anchor had no onclick at all.

Verification: Severity: nit (minor UX regression from base, no crash/error page). app.js crumbs() now does, only when already on the rollout/tokens view (onRollout true): const replaceInPlace = (event) => { event.preventDefault(); location.replace("#/rollout/latest"); }; parts.push(nav("Rollouts", "#/rollout/latest", onRollout, onRollout ? replaceInPlace : null)); and el() installs this via… | nit.…

}
if (route.view === "rollout" || route.view === "tokens") {
if ((route.view === "rollout" || route.view === "tokens") && route.rolloutId !== null) {
const evalSuffix = route.evaluation ? "?eval=1" : "";
parts.push(
el("span", { class: "crumb" }, [
Expand Down
44 changes: 43 additions & 1 deletion miles/dashboard/static/views_rollout.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,50 @@ function sortableTable(rows, columns, { onRowClick, flagRow, sortState }) {
return wrap;
}

// How far back the "latest" landing will look for a step it can actually show.
// Anything deeper than this is not a fresh-dump race any more, so stopping lets
// the real error surface instead of silently walking the reader into old data.
const LANDING_LOOKBACK = 5;

// The newest step is listed as soon as its dump file exists, which is earlier
// than it can be read: the dump may still be mid-write (503), truncated, or
// recorded with no samples at all. Land on the newest step that actually has
// samples instead of on an error page.
async function resolveLatest(ids, evaluation) {
for (const id of ids.slice(-LANDING_LOOKBACK).reverse()) {
try {
const summary = await api(`/api/rollout/${id}/summary`, { eval: evaluation }, { retry503: false });
if (summary.rows.length) return id;
} catch {
/* mid-write, truncated, or already rotated away: try the step before it */
}
}
// nothing readable nearby: go to the newest anyway so the reader sees the
// real error rather than a silent redirect into stale data
return ids.at(-1);
}

export async function renderRollout(view, meta, route) {
const { rolloutId, evaluation } = route;
const { evaluation } = route;
let { rolloutId } = route;
if (rolloutId === null) {
const candidates = evaluation ? meta.rollout_ids.eval : meta.rollout_ids.train;
if (!candidates.length) {
const kind = evaluation ? "eval" : "rollout";
view.replaceChildren(el("p", { class: "muted" }, [`No ${kind} steps have been dumped yet.`]));
return;
}
view.replaceChildren(el("p", { class: "muted" }, ["finding the newest step with data…"]));
const entryHash = location.hash;
rolloutId = await resolveLatest(candidates, evaluation);
// the resolve spans several requests; if the user navigated away in the
// meantime, rewriting the URL now would drag them back into this view
if (location.hash !== entryHash) return;
// rewrite the URL to the step actually shown, so reloads, Prev/Next and
// the breadcrumb all work off a real id
location.replace(`#/rollout/${rolloutId}${evaluation ? "?eval=1" : ""}`);
Comment thread
Shi-Dong marked this conversation as resolved.
return;
}
const [summary, groups] = await Promise.all([
api(`/api/rollout/${rolloutId}/summary`, { eval: evaluation }),
api(`/api/rollout/${rolloutId}/groups`, { eval: evaluation }),
Expand Down
Loading