Scroll the runs table, pick a range, and choose what a comparison plots against
Docs / docs (push) Successful in 35s
Playwright Tests / test-playwright (1, 2) (push) Failing after 3m14s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m44s
pre-commit / pre-commit (push) Failing after 3m54s
Test Backend / test-backend (push) Successful in 3m12s
Compose Smoke Test / test-compose (push) Successful in 32s
Playwright Tests / merge-reports (push) Failing after 1m28s

This commit is contained in:
2026-08-25 16:32:34 +02:00
parent 3c15964364
commit 4350916bd8
14 changed files with 498 additions and 151 deletions
+31 -6
View File
@@ -132,6 +132,9 @@ class MetricSeries(BaseModel):
class SeriesAnswer(BaseModel):
metric: str
#: What the x values are: "step", "time" (seconds since this run's first
#: reading), or the name of another metric this one was plotted against.
x: str = "step"
lines: list[MetricSeries] = Field(default_factory=list)
@@ -356,13 +359,38 @@ def read_metrics(
return rows
def _points(session: Session, run_id: str, metric: str, x: str) -> list[list[float]]:
"""One run's readings of ``metric``, against whichever x was asked for.
The step is the default because it is what every run has. Time answers
"which one got there sooner", and is measured from this run's own first
reading so that runs started hours apart still lie on top of each other.
Another metric answers "against what the loop was actually counting" — an
epoch, or samples seen — and is joined on the step the two share, which is
the only thing they have in common.
"""
rows = _series(session, run_id, metric)
if x == "time":
if not rows:
return []
start = min(row.ts for row in rows)
return [[row.ts - start, row.value] for row in rows]
if x and x != "step":
against = {row.step: row.value for row in _series(session, run_id, x)}
return [[against[row.step], row.value] for row in rows if row.step in against]
return [[float(row.step), row.value] for row in rows]
@router.get("/series/compare", response_model=SeriesAnswer)
def compare_metric(session: SessionDep, ids: str, metric: str) -> Any:
def compare_metric(session: SessionDep, ids: str, metric: str, x: str = "") -> Any:
"""One metric across several runs, as the chart widget's series shape.
This is the comparison view: it answers in the same shape a flow answers a
chart's query with, so putting three training curves beside each other is
a widget binding rather than a screen of its own.
``x`` names what to plot against — nothing or "step", "time", or another
metric of the same runs.
"""
run_ids = [part for part in ids.split(",") if part]
if not run_ids:
@@ -376,13 +404,10 @@ def compare_metric(session: SessionDep, ids: str, metric: str) -> Any:
run = runs.get(run_id)
if run is None:
continue
rows = _series(session, run_id, metric)
label = run_id
if run.seed is not None:
label = f"{run_id} (seed {run.seed})"
lines.append(
MetricSeries(
label=label, points=[[float(row.step), row.value] for row in rows]
)
MetricSeries(label=label, points=_points(session, run_id, metric, x))
)
return SeriesAnswer(metric=metric, lines=lines)
return SeriesAnswer(metric=metric, x=x or "step", lines=lines)