TraceML
TraceML is a VS Code extension for browsing ML training runs. It finds runs on the filesystem and shows them in a panel inside VS Code: a run table with filtering and sorting, metric plots, a detail view for each run, and side-by-side comparison. The filesystem is the only source of truth. There is no server, no database, no HTTP endpoint and no browser app. Everything runs inside VS Code, including over Remote-SSH, where the extension reads the runs directly on the remote machine. TraceML is read-only. It never creates, modifies or deletes files, it makes no network calls, and it reads only inside the runs folders you configure. See Security and resource use. Quick start
InstallationTraceML is shipped as a
The packaged extension doesn't need Node, npm or any other process running. Commands
Settings
The UITraceML uses a dark-only application shell with its own design system, inside a single VS Code webview. It has a sidebar, a header and three views.
Design system. All colors, radii, spacing and type sizes are CSS tokens in
Writing runs: the TraceML run formatThis section is the authoritative specification for producing runs that TraceML can display. It's written so that a person, or an AI coding agent asked to "add TraceML logging to this training script", can implement it exactly. Everything here is 1. Directory layout
2. Write rules (important for live viewing)TraceML reads these files while training is still writing them. Follow these rules and the viewer never sees broken data:
3.
|
| Field | Type | Rules |
|---|---|---|
schema_version |
int | 1. |
id |
string | Same as the directory name. |
name |
string | Short human name, shown in bold in the table. |
group |
string or null | Free-form group (e.g. a sweep name). It can be used for grouping. |
tags |
string[] | Free-form labels. |
notes |
string | Free text. |
status |
string | running → completed, failed or killed. Never write stale; TraceML derives it. |
started_at / ended_at / heartbeat_at |
ISO-8601 UTC or null | ended_at is null while the run is running. |
duration_s |
number or null | Set when the run finishes. |
exit |
object or null | null while running. On finish it's {"code": 0, "signal": null}. For a failure, add "error_type", "error_message" and "traceback" (full text; TraceML shows the tail). |
command, cwd, host, git |
optional | Shown in the run inspector. host and git may contain any keys. |
Status lifecycle: create the file with running → update heartbeat_at periodically → write the final status once. After a crash without cleanup, the run stays running, and TraceML shows it as stale.
4. params.yaml
A flat mapping with dot-separated keys and scalar values: numbers, strings, booleans or null. Every key becomes a table column that can be sorted, filtered and grouped on, and that appears in the comparison view.
data.batch_size: 64
model.hidden: 128
optim.lr: 0.0003
optim.name: "adamw"
Nested YAML is accepted and flattened (optim: {lr: …} → optim.lr). Lists are shown as compact JSON strings. Use the same key names across runs of a project, otherwise the columns don't line up.
5. config.json
The full nested training configuration, as any JSON value. It's shown as a collapsible tree in the run inspector. This file is optional; params.yaml is what powers the table.
6. metrics.jsonl: the metric history
One JSON object per line, appended as training progresses:
{"step": 9, "epoch": 9, "time": 1790000000.123, "train/loss": 0.41, "lr": 0.00021}
{"step": 9, "epoch": 9, "time": 1790000004.551, "val/loss": 0.38, "val/auc": 0.87}
step(int, required): the epoch-level step and the primary x-axis. In TraceML V1,stepnormally equalsepoch. Don't log per batch; log once or twice per epoch.epoch(int): the epoch number.time(float): Unix seconds.- Every other key is a metric. Values must be numbers or
null. A null is drawn as a gap. Don't write strings, lists or objects as metric values. - Name metrics with a prefix so the metric picker can group them:
train/…,val/…,test/…,sys/…(e.g.sys/gpu_mem_gb);lror other names without a prefix go under other. - Rows may be sparse. Log train and val metrics in separate rows with the same
step. - Never write NaN or Infinity. Write
nullinstead. TraceML tolerates Python'sNaNtokens, but other JSON readers don't. - Partial or corrupt lines: a half-written final line is ignored until it's completed, and other corrupt lines are skipped and reported as a warning.
7. metrics.json: the derived summary
Rewrite it atomically after every logged row (or at least every epoch). The run table is built from this file, so TraceML never has to read the full history just to list runs.
{
"schema_version": 1,
"updated_at": "2026-09-29T14:10:00Z",
"step": 19,
"epoch": 19,
"last": {"train/loss": 0.21, "val/auc": 0.88, "lr": 0.000001},
"best": {"val/auc": {"value": 0.894, "step": 14, "epoch": 14, "mode": "max"}},
"summary": {"test/auc": 0.881}
}
step/epoch: the latest logged step.last: the most recently logged value of every metric inmetrics.jsonl(the value may be null).best: for each metric you care about, the best value so far, plus thestepandepochwhere it happened, andmode. The mode ismaxfor AUC or accuracy andminfor losses.summary: final or evaluation values that have no history, e.g.test/*after training. Numbers or strings.- Default columns: the table shows every
bestandsummarymetric as a column by default. The scatter and box plots can use anylast,bestorsummarynumber.
8. Checklist for an implementation
- [ ] A unique run directory
<runs folder>/<YYYYMMDD-HHMMSS>_<name>_<hex4>/is created before training starts. - [ ]
run.json(statusrunning),params.yaml,config.jsonand an emptymetrics.jsonare written atomically at start. - [ ] Each evaluation appends complete lines to
metrics.jsonlwithstep,epoch,timeand prefixed metric names, then flushes. - [ ]
metrics.json(last,best,step,epoch) is rewritten atomically after each log call. - [ ]
heartbeat_atis refreshed at least every 30 s, including during long epochs (use a background thread). - [ ] On exit (normal, exception or Ctrl+C),
status,ended_at,duration_sandexitare written, including the error type, message and traceback on failure.summarygets the test metrics. - [ ] No NaN/Infinity, no per-batch rows, no in-place rewrites of
metrics.jsonl.
9. Reference logger (Python standard library only)
This is a complete, tested implementation of the rules above. The same file is in the repository as scripts/traceml_logger.py. Copy it into your project, or give it to an agent as the specification.
"""Minimal reference writer for the TraceML run format (schema_version 1). Python stdlib only.
Copy this file into a training project (or use it as the specification for your own logger).
from traceml_logger import TraceMLRun
with TraceMLRun("runs", "baseline", params={"optim": {"lr": 3e-4}, "model.hidden": 128},
group="lr-sweep", tags=["cv"], best={"val/auc": "max", "val/loss": "min"}) as run:
for epoch in range(num_epochs):
run.log(epoch, {"train/loss": train_loss, "lr": lr})
run.log(epoch, {"val/loss": val_loss, "val/auc": val_auc})
run.summary["test/auc"] = test_auc
Leaving the `with` block normally marks the run `completed`; an exception marks it `failed`
(with error type, message and traceback); Ctrl+C marks it `killed`.
"""
from __future__ import annotations
import json
import math
import os
import secrets
import socket
import sys
import threading
import time
import traceback
from datetime import datetime, timezone
SCHEMA_VERSION = 1
def _iso(t: float) -> str:
return datetime.fromtimestamp(t, timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _atomic_write(path: str, text: str) -> None:
"""Write to a dot-prefixed temp file, then rename: readers never see a half-written file."""
tmp = os.path.join(os.path.dirname(path), f".{os.path.basename(path)}.tmp")
with open(tmp, "w", encoding="utf-8", newline="\n") as f:
f.write(text)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, path)
def _yaml_scalar(v) -> str:
if v is None:
return "null"
if isinstance(v, bool):
return "true" if v else "false"
if isinstance(v, (int, float)):
return repr(v)
return json.dumps(str(v))
def _flatten(d: dict, prefix: str = "") -> dict:
out: dict = {}
for k, v in d.items():
key = f"{prefix}{k}"
if isinstance(v, dict):
out.update(_flatten(v, key + "."))
elif isinstance(v, (list, tuple)):
out[key] = json.dumps(list(v))
else:
out[key] = v
return out
def _metric(v):
"""Numbers become floats; None / NaN / inf become null (a gap in the plot)."""
if v is None:
return None
v = float(v)
return v if math.isfinite(v) else None
class TraceMLRun:
def __init__(self, runs_dir: str, name: str, params: dict | None = None, config: dict | None = None,
group: str | None = None, tags=(), notes: str = "", best: dict | None = None,
heartbeat_s: float = 15.0):
self.t0 = time.time()
stamp = datetime.fromtimestamp(self.t0, timezone.utc).strftime("%Y%m%d-%H%M%S")
self.id = f"{stamp}_{name}_{secrets.token_hex(2)}"
self.dir = os.path.join(runs_dir, self.id)
for sub in ("logs", "checkpoints", "artifacts"):
os.makedirs(os.path.join(self.dir, sub), exist_ok=True)
self.best_modes = dict(best or {}) # metric -> "max" | "min"
self.last: dict = {}
self.best: dict = {}
self.summary: dict = {}
self.step = None
self.epoch = None
self._lock = threading.Lock()
self.meta = {
"schema_version": SCHEMA_VERSION,
"id": self.id,
"name": name,
"group": group,
"tags": list(tags),
"notes": notes,
"status": "running",
"started_at": _iso(self.t0),
"ended_at": None,
"heartbeat_at": _iso(self.t0),
"duration_s": None,
"exit": None,
"command": [sys.executable, *sys.argv],
"cwd": os.getcwd(),
"host": {"hostname": socket.gethostname(), "pid": os.getpid(), "python": sys.version.split()[0]},
}
flat = _flatten(params or {})
_atomic_write(os.path.join(self.dir, "params.yaml"), "".join(f"{k}: {_yaml_scalar(v)}\n" for k, v in flat.items()))
_atomic_write(os.path.join(self.dir, "config.json"), json.dumps(config if config is not None else (params or {}), indent=2, default=str) + "\n")
self._jsonl = open(os.path.join(self.dir, "metrics.jsonl"), "a", encoding="utf-8", newline="\n")
self._write_meta()
self._write_metrics_json()
self._stop = threading.Event()
threading.Thread(target=self._heartbeat, args=(heartbeat_s,), daemon=True).start()
# -- writing ---------------------------------------------------------------
def log(self, step: int, metrics: dict, epoch: int | None = None) -> None:
"""Append one row to metrics.jsonl (step = epoch-level step) and refresh metrics.json + heartbeat."""
row = {"step": int(step), "epoch": int(step if epoch is None else epoch), "time": round(time.time(), 3)}
for k, v in metrics.items():
row[k] = _metric(v)
with self._lock:
self._jsonl.write(json.dumps(row) + "\n") # one complete line per write
self._jsonl.flush()
self.step, self.epoch = row["step"], row["epoch"]
for k, v in row.items():
if k in ("step", "epoch", "time"):
continue
self.last[k] = v
mode = self.best_modes.get(k)
if mode and v is not None:
b = self.best.get(k)
if b is None or (v > b["value"] if mode == "max" else v < b["value"]):
self.best[k] = {"value": v, "step": row["step"], "epoch": row["epoch"], "mode": mode}
self._write_metrics_json()
self.meta["heartbeat_at"] = _iso(time.time())
self._write_meta()
def finish(self, status: str = "completed", exc: BaseException | None = None) -> None:
self._stop.set()
now = time.time()
with self._lock:
if not self._jsonl.closed:
self._jsonl.close()
exit_info: dict = {"code": 0 if status == "completed" else 1, "signal": "SIGINT" if status == "killed" else None}
if exc is not None and status == "failed":
exit_info.update(
error_type=type(exc).__name__,
error_message=str(exc),
traceback="".join(traceback.format_exception(type(exc), exc, exc.__traceback__)),
)
self.summary = {k: (_metric(v) if isinstance(v, (int, float)) or v is None else v) for k, v in self.summary.items()}
self._write_metrics_json()
self.meta.update(status=status, ended_at=_iso(now), heartbeat_at=_iso(now), duration_s=round(now - self.t0, 3), exit=exit_info)
self._write_meta()
def __enter__(self) -> "TraceMLRun":
return self
def __exit__(self, exc_type, exc, tb) -> bool:
if exc_type is None:
self.finish("completed")
elif issubclass(exc_type, KeyboardInterrupt):
self.finish("killed", exc)
else:
self.finish("failed", exc)
return False # never swallow the exception
# -- internals -------------------------------------------------------------
def _write_meta(self) -> None:
_atomic_write(os.path.join(self.dir, "run.json"), json.dumps(self.meta, indent=2) + "\n")
def _write_metrics_json(self) -> None:
_atomic_write(
os.path.join(self.dir, "metrics.json"),
json.dumps({
"schema_version": SCHEMA_VERSION,
"updated_at": _iso(time.time()),
"step": self.step,
"epoch": self.epoch,
"last": self.last,
"best": self.best,
"summary": self.summary,
}, indent=2) + "\n",
)
def _heartbeat(self, every: float) -> None:
# Keeps heartbeat_at fresh during long epochs so the run is not shown as stale.
while not self._stop.wait(every):
with self._lock:
if self._stop.is_set():
return
self.meta["heartbeat_at"] = _iso(time.time())
self._write_meta()
Status and stale
stale is derived by TraceML; it's never written to disk. A run is stale when status == "running" and now - heartbeat_at > traceml.staleAfterSeconds. A completed, failed or killed run is never stale. If a running run has no heartbeat_at, it's shown as running?, because its staleness can't be judged. TraceML never invents a heartbeat time.
Live refresh
- TraceML polls every
refreshIntervalSeconds, and only while the panel is open. Each poll works like this:- It lists the configured runs folders (one directory read each), so new runs appear and deleted runs disappear.
- It stats the core files of each run and re-parses only files whose mtime or size changed.
- It advances the
metrics.jsonlof the plotted runs by reading only the newly appended bytes. If the file shrank, it starts over from the beginning.
- Completed, failed and killed runs are re-checked every 6th poll instead of every poll.
- Only changed runs are sent to the panel. Sort order, filters, selection, zoom and column choices are all kept across updates.
- Stale runs change status automatically as their heartbeat ages.
- Polling stays the reliable baseline even if a filesystem watcher is added later.
Remote-SSH
With "extensionKind": ["workspace"], the extension host, and so all filesystem access, runs on the remote machine next to the runs. Only small JSON messages cross the connection: run summaries, and downsampled series for the runs you're plotting. Nothing needs to be installed on the remote beyond the extension itself. You don't need Node, npm or Python packages there. To browse runs outside the opened folder, add an absolute path to traceml.roots, e.g. "/scratch/me/project/runs".
Security and resource use
What the extension can and cannot do:
- Read-only. TraceML cannot create, modify, rename or delete files. (Your view settings are remembered in VS Code's internal per-workspace storage, through the VS Code API; nothing is written into your project or runs folders.)
- The extension source uses only
stat,readdir,realpath,readFileandopen(…, 'r'). A test suite (test/security.test.ts) fails the build if a write, process or network API is ever added. - Files opened from the UI (Files in the inspector) are shown through a read-only
traceml-readonly:file system, so they can't be edited or saved through TraceML.
- The extension source uses only
- Confined to your runs folders. TraceML reads only inside the folders listed in
traceml.roots, and it doesn't search the rest of the workspace.- Every read first resolves symlinks. A run directory or file whose real location is outside its runs folder is ignored and reported (
symlink points outside the run folder). - Open file only accepts a known run plus one of six fixed file names, so a request like
../../secretis rejected.
- Every read first resolves symlinks. A run directory or file whose real location is outside its runs folder is ignored and reported (
- Restricted Mode: because it never executes anything from the workspace, TraceML is declared safe for untrusted workspaces.
- No network, no processes. It makes no HTTP requests, contacts no service (W&B included), and never starts a child process or runs code from the run files.
- Locked-down webview. The UI runs in a VS Code webview under a strict Content-Security-Policy: scripts need a nonce, and there are no remote resources, CDNs or inline event handlers.
- Values from run files (names, params, tracebacks, config) are inserted as text or HTML-escaped. They're never executed.
- The webview can't touch the filesystem. It sends a fixed set of typed messages, and the extension validates each one.
- Untrusted run files are handled defensively.
- JSON, YAML and JSONL parsing never executes anything. YAML alias expansion is capped (billion-laughs protection).
run.json,params.yaml,metrics.jsonandconfig.jsonover 16 MB are not parsed.metrics.jsonlis read up totraceml.maxMetricsFileMB.- A corrupt file only produces a warning on that run.
- Filesystem load is bounded:
- Polling only runs while the TraceML panel is open and visible. Nothing is read when it's closed or in a background tab; it refreshes as soon as you switch back. The default is every 5 s, and finished runs are checked about every 30 s.
- Each poll reads each configured runs folder once, plus the core files of the runs in it. Unchanged files are skipped by modification time and size.
- On slow network filesystems (e.g. NFS on a cluster), raise
traceml.refreshIntervalSecondsand narrowtraceml.roots.
- Dependencies: at runtime only
yamlanduplot, bundled intodist/.npm auditreports 0 known vulnerabilities. - CPU: the extension host runs JavaScript on a single thread, so parsing never uses more than one CPU core. A thread limit wouldn't change anything. Big files are read in 8 MB chunks, and
traceml.maxMetricsFileMBcaps the total per run. - Supply chain: a
.vsixruns with the same permissions as VS Code itself, like any extension. Share it from a trusted place (e.g. your team's repository or releases). Before publishing to the Marketplace, set a realpublisherandrepositoryinpackage.json. - Dev-only scripts (
scripts/, not included in the.vsix):fake_runs.py --cleandeletes its output directory. It refuses when that directory contains anything that isn't a run.
Limitations
- Series are loaded only for selected runs, at most 30 at a time. A downsampled series is re-sent in full when it grows (the host sends at most
maxPointsPerSeriespoints per series). - If
metrics.jsonlis rewritten in place with exactly the same size, TraceML doesn't notice until it grows or shrinks. Loggers only append, so this doesn't happen in practice. - A final line with no trailing newline is treated as partial. It shows up once the newline is written.
- The logs viewer, artifact/checkpoint browser, git diff viewer, experiment grouping, filesystem watcher and adaptive polling are not part of V1.
- Only the configured runs folders are read. Runs nested deeper than one level, or symlinked from outside a runs folder, aren't shown.
Fake run generator
scripts/fake_runs.py needs only the Python standard library: no PyTorch, CUDA, W&B, DVC or network access. It writes the exact TraceML format using atomic dot-file-and-rename writes, just like a real logger.
python scripts/fake_runs.py # 24 runs into test-data/traceml/runs
python scripts/fake_runs.py --runs 100 --output test-data/traceml/runs
python scripts/fake_runs.py --runs 1000 --clean # table performance
python scripts/fake_runs.py --big-run-lines 200000 # adds a run with a 200k-line metrics.jsonl
python scripts/fake_runs.py --runs 0 --live # only a live run, until Ctrl+C
python scripts/fake_runs.py --live --live-interval 1 --live-epochs 50 # live run that completes
| Option | Default | Meaning |
|---|---|---|
--output DIR |
test-data/traceml/runs |
The runs directory to write into. |
--runs N |
24 |
How many historical runs to generate (0 means none). |
--seed N |
0 |
Random seed. |
--clean |
off | Deletes the output directory first. It refuses if the directory contains anything that isn't a run. |
--big-run-lines N |
0 |
Also writes one run with about N metrics.jsonl lines. |
--live |
off | After generating, starts a live run that keeps appending metrics. |
--live-interval S |
2 |
Seconds between live epochs. |
--live-epochs N |
0 |
Stops after N epochs and marks the run completed. 0 means run until terminated. When the live run is stopped with Ctrl+C, it's left as running, so it turns stale. |
--no-partial-writes |
off | By default the live run sometimes writes half a line, pauses, then finishes it, to exercise partial-line handling. This option turns that off. |
The historical runs always start with a fixed set of special cases, followed by random parameter variants:
baseline,high-lrandsmall-model(completed)failed-run(CUDA OOM) andnan-loss(failed)killed-run(killed)stale-run(running, but the heartbeat is old)legacy-run(norun.json, nested params)future-schema(schema_version: 2)malformed-lines(a broken line and a partial final line)
For the analysis views, the runs also vary in:
data.dataset(cifar10,cifar100,tiny-imagenet), which clearly shifts the metric distributions. Try Group bydata.datasetwith the box view.model.patch_size, which only exists forvit-tinyruns, so some runs have no value for it.- the
test/*summary values, which are occasionallynull. - the non-numeric summary field
best_checkpoint.
The live run appends to metrics.jsonl, rewrites metrics.json and updates heartbeat_at in run.json for every epoch.
Development
You need Node.js 20 LTS (or newer) and npm on your local machine. Nothing is built on the training server.
node --version && npm --version
npm install
python scripts/fake_runs.py --clean # local fixture data in test-data/traceml/runs
npm run build # esbuild -> dist/extension.js, dist/webview.{js,css}
npm test # vitest
npm run typecheck # tsc --noEmit (strict)
npm run check # typecheck + test + build
npm run package # -> traceml-<version>.vsix
- Debugging: open this folder in VS Code and press F5 (Run TraceML (fake runs)). This opens an Extension Development Host on
test-data/. Then run TraceML: Show Experiments. Startpython scripts/fake_runs.py --runs 0 --livein a terminal to watch a live run. - Previewing the webview without VS Code:
npm run build && npm run preview -- test-datawritestest-data/preview.html. Open it in a browser; it uses the real host-side parsing and a mocked VS Code API. Add#demoto the URL to pre-select a few runs. - Benchmarking the host pipeline:
npm run bench -- test-data/perf(afterpython scripts/fake_runs.py --runs 1000 --output test-data/perf/runs --big-run-lines 200000). On a typical Windows laptop, parsing 1,000 runs takes about 0.7 s, a no-change poll takes about 20 ms, loading a 200k-line history takes about 0.4 s, and re-polling it when nothing changed takes under 1 ms.
Project layout:
src/extension.ts activation, commands, webview panel, polling loop, message handling
src/discovery.ts resolves the configured runs folders and lists the runs inside them
src/fsGuard.ts read confinement (realpath + inside-folder checks)
src/readonlyFs.ts read-only file system used to open run files
src/runStore.ts parsed-run cache (mtime/size), summaries, on-demand histories and details
src/poller.ts non-overlapping interval poller
src/protocol.ts typed host <-> webview messages + validation of webview input
src/types.ts, paths.ts shared types; OS-independent path/glob helpers
src/parsers/* run.json, params.yaml, config.json, metrics.json, incremental metrics.jsonl
src/model/* status/summary, columns, sorting, filters, comparison, downsampling, EMA
src/model/analysis.ts grouping, scatter/box data extraction, quartiles, axis ticks (pure, tested)
src/webview/* vanilla-TS UI: shell.ts (sidebar/header/inspector host), ui.ts (icons, buttons,
chips, switches, popovers, empty states), filters.ts (runs toolbar), table.ts
(virtualized, grouped, keyed rows), plots.ts (metric toolbar + uPlot lines),
analysis.ts (Line|Scatter|Box, SVG scatter/box), detail.ts (inspector),
comparison.ts, overview.ts, styles/ (design tokens + CSS)
test/* vitest suites + fixtures
scripts/fake_runs.py fake run generator; scripts/traceml_logger.py reference logger;
scripts/preview.ts, bench.ts are dev-only helpers
Architecture decisions
- No server. There's no HTTP server, no localhost port and no background process. The UI is a VS Code webview.
- No database. Parsed state is an in-memory cache rebuilt from the files.
- Filesystem only. The run directories are the single source of truth.
- The webview is embedded in VS Code and is a pure view. It can't touch the filesystem. It asks the host for data through a typed message protocol (
src/protocol.ts), and the host validates every incoming message. File opening only accepts a known run plus a fixed list of file names, so the webview never has arbitrary filesystem access. - The extension host owns all filesystem access and parsing. With
extensionKind: ["workspace"], that happens on the Remote-SSH host. metrics.jsonlis the canonical history. It's read incrementally by byte offset, and only for runs being plotted.metrics.jsonis a derived fast summary. The table is built fromrun.json,params.yamlandmetrics.jsonwithout readingmetrics.jsonl. For runs withoutmetrics.json, TraceML reads only the first 64 KB ofmetrics.jsonlto learn the metric names.- No DVC or DVCLive dependency, and no MLflow.
- TraceML never accesses W&B. The training-side logger may send the same run to W&B on its own; TraceML reads only the local files.
staleis derived from the heartbeat. It's never stored.- Downsampling uses min/max-preserving buckets. The first and last points are always kept, and so are nulls inside a bucket, so gaps survive. It never takes every Nth point.
- Development happens locally with Node.js 20 LTS and npm. Runtime dependencies are only
yamlanduplot, bundled by esbuild intodist/. - Dark-only webview UI with a token-based design system. Everything is vanilla TypeScript and CSS: no UI framework, no CDN, and a strict CSP. Styles are split into
tokens.css,base.css,layout.cssandcomponents.cssundersrc/webview/styles/. - Scatter and box plots use run-level data only. They are computed in the webview from the run summaries it already has (
run.json,params.yamlandmetrics.json), so changing a visualization never touches the filesystem or readsmetrics.jsonl. They are drawn as plain SVG because uPlot is built for x-ordered time series; line plots still use uPlot. - Grouping is computed locally from run metadata. Nothing about grouping is stored on disk or on a server.
- Fake runs come first. Development and tests use
scripts/fake_runs.pydata before validating against real remote runs.
Decisions made where the spec was ambiguous:
exitfailure details: both the flat layout (exit.error_type,exit.error_message,exit.traceback) and a nestedexit.errorobject are accepted.- A running run without
heartbeat_atis shown asrunning?. - The run color is assigned when a run is first selected, using the least-used palette entry among the selected runs, and is kept after that.
- The metric picker lists the metrics of the selected runs, or of all runs when nothing is selected. Until you pick metrics yourself, it defaults to
train/loss,val/lossandval/aucwhere they exist. traceml.rootslists folders explicitly. There's no recursive or pattern-based search, so TraceML never reads anything outside those folders.- A plain number in a text column's filter is a substring match. In a numeric column it's an equality match.
- If a file becomes unreadable (corrupt), the last good parse is kept and a warning is shown.
- In grouping, a run's tags are joined into one label (e.g.
cv, sweep), so each run belongs to exactly one group. Missing values form the(none)group. - Group colors come from the order of the groups among the filtered runs, so they can shift when the filters change.
- A scatter point whose Size value is missing is still plotted, at the default size. Only a missing X or Y excludes it.
