# Offline evidence visualizer
The default `visualizer` feature renders an `EvidencePacket` as one self-contained HTML document.
The file embeds `candle-graph/viewer/6` JSON, CSS, JavaScript, and dagre; it makes no CDN or network
requests.
```bash
cargo candle-graph view application.jsonl --output viewer.html
# Include normalized GPU evidence while building the page.
cargo candle-graph view application.jsonl \
--nsight-dir nsight \
--output viewer.html
```
A bundle published with the feature enabled includes the same viewer as `viewer.html`. Previously
generated HTML keeps its embedded UI; render a new file to use updated viewer code.
## Views
| Overview | Run identity, qualified headline measurements, largest recorded host self times, coverage, and expandable evidence details |
| Execution graph | Call/data graph, searchable spans/operations/tensors, pan and zoom, selection details, and standalone SVG export |
| Timings | Graph nodes initially ranked by host self time; sortable columns, name/value search, type filtering, and selection details |
| Measurements | Recorded scalar values, tensor statistics, and gradients; label/value search, state filtering, declared family expectations, and links to recording spans |
| Memory | Step chart of logical storage, labeled peak, searchable allocations and timeline, and independent physical observations |
| GPU | Nsight artifact provenance, report tables, semantic correlation, phase attribution, and diagnostics |
A failed or structurally invalid capture still opens on the Overview view, but graph-dependent
views have empty states because no `ExecutionGraph` was derived. Missing GPU evidence has an
explicit unavailable state rather than an empty-success table.
GPU summary tables are global. Phase GPU attribution is exact only when a projected NVTX row has
usable join identifiers, matches GPU timeline rows, and its declared operation count equals the
joined count. Projection-only rows remain a separate, qualified view. Candle, device-event, and
Nsight clocks are never drawn as one aligned clock.
## Investigation workflow
Start on Overview to check capture health and evidence coverage. The largest-self-time rows link
directly to their graph nodes. They rank full recorded node self times, including nodes outside the
measured region; their bars are relative to the largest row, not a partition of measured wall time.
Backend-qualified findings retain their separate measured-scope semantics.
Timings starts with host self time, which excludes recorded child work. Click a column heading to
change the sort, filter by name/value or node type, and select a row for details. “Locate in graph”
centers the selected node. The inspector omits inapplicable fields and labels its optional peak
allocation list as run-wide. Logical peak and allocated bytes remain separate columns.
Measurements exposes the tensor-statistics plane, including host-recorded losses and optimizer
values. Single-element rank-zero records appear under Scalar values; this shape convention does
not prove how the value was captured. Repeated labels retain every observation. Non-finite counts
remain visible, while the corresponding numeric summaries are withheld so serialized placeholder
zeros cannot be mistaken for observations. Finite values display six significant digits; hover
for the exact serialized number. The source-span link opens its graph node when a valid graph exists.
Gradients retain the original root (including pre-clip/post-clip identity), parameter, state, norm,
and exact-contract family/expectation when present. Filter by state or search a root, parameter,
or family. Missing and zero are observations, not automatic defects: inactive and data-conditional
families can legitimately have them. Unknown norms stay unknown. Failed or structurally invalid
captures retain diagnostic tensor statistics but withhold graph-derived gradient rows.
Tables search the entire embedded collection and show 25 rows per page. Column discovery includes
rows beyond the first page. Expanded sections, filters, sorting, and scroll positions remain in
place when switching views. Tabs and selected graph nodes support local fragment links such as
`#trace?node=span-id`; table filters and disclosure state stay in memory for this page visit.
Missing data stays unknown. A partial measurement carries its coverage badge and reasons; a missing
logical-memory value never becomes zero heat. GPU tables retain provenance and correlation limits
even when normalized report rows are present. Localized capture timestamps use the reader’s timezone.
## Visual design
The viewer follows the Candle / Hugging Face visual direction: white and neutral gray surfaces,
charcoal text, a yellow Candle mark and selected-tab underline, and restrained orange chart accents.
Dark mode uses neutral charcoal with warm accents. Links and keyboard focus use contrast-safe
colors; yellow primary buttons keep dark text. Status badges retain explicit labels, and graph
selection adds a dashed outline to distinguish it from the warm heat scale.
## Interaction and accessibility
- The view switcher uses ARIA tabs with arrow-key, Home, and End navigation.
- The graph hierarchy uses arrow keys; searching includes operations and tensors and retains
matching ancestors, including branches that were collapsed.
- Drag to pan, use the zoom buttons or scroll wheel to zoom, and use **F** to fit. Arrow keys pan
when the canvas is focused and move between nodes when a node is focused. **+** and **−** zoom.
Zoom and pan update the transform without rebuilding nodes or losing focus.
- **/** focuses search in the graph or first table of the current view. **?** opens the guide. **Escape** closes the guide or
selection details. Pane dividers can be resized with arrow keys, Home, and End.
- On narrow screens, hierarchy and selection details are dismissible panels. Locating a selection
dismisses the details panel to expose the graph. Data tables scroll within their own containers.
- Theme preference is applied before the page paints. The theme button names the destination theme;
unavailable local storage does not prevent viewing or switching themes.
- SVG export includes the entire graph, native SVG text, resolved colors, and arrow markers. It
preserves selection and dimming as shown, remains readable in separate SVG renderers, and does
not depend on the page’s CSS or current zoom.
- Fonts use local fallbacks; no fonts, icons, scripts, or data are fetched from the network.
## Verification
After building a fresh standalone viewer, the dependency-free browser regression suite can attach
to a **dedicated, isolated test Chromium** using its local DevTools port:
```bash
cargo test
cargo clippy --all-targets
node tests/viewer_browser.mjs /absolute/path/to/viewer.html <isolated-CDP-port>
```
The suite uses Node’s built-in WebSocket and fetch APIs (tested with Node 24). It creates temporary
fixture pages alongside the supplied viewer, tests them, and removes them when finished. It does
not launch a browser, choose a display, or close the caller’s browser. Follow the repository’s
headless-compositor isolation rules before starting the browser; never attach to a live user
session. Inspect affected states with compositor-scoped screenshots as well as running the suite.
Device and session isolation must apply before the initial compositor launch, including capability
probes. Environment flags alone cannot guarantee it: an installed compositor can ignore an
unsupported headless flag. The validated local setup used Sway with `WLR_BACKENDS=headless` and
`WLR_RENDERER=pixman` inside `bwrap`, with private `/dev`, hidden host `/run` and `/sys`, a private
runtime, and no inherited display/seat/session bus variables. It verified the sole `HEADLESS-1`
output and absence of physical device descriptors before launching Chromium on that private socket.
Disable core dumps for disposable test processes so a failed launch cannot trigger desktop crash
notifications. Track and reap the exact test processes and remove their private runtime afterward.
The design explorations are preserved locally on `prototype/human-viewer-2026-09-07`, with their
verdict in `src/viewer/PROTOTYPE.md` on that branch. The chosen direction and reusable tokens are
recorded in [the interface system](../.interface-design/system.md).
## Source map
| `src/viewer.rs` | HTML shell, embedded assets, and script-safe JSON escaping |
| `src/viewer/trace_view.rs` | `EvidencePacket` to viewer/6 projection |
| `src/viewer/app_trace.js` | View rendering and interactions |
| `src/viewer/style.css` | Responsive visual system |
| `src/viewer/layout.js` | Graph layout adapter |
| `src/viewer/dagre.min.js` | Embedded dagre dependency |
The visualizer is vanilla HTML/CSS/JavaScript. Changes should preserve the standalone, offline
artifact rather than introducing a build-time frontend framework.