Skip to main content

candle_graph/
viewer.rs

1//! Dependency-free interactive HTML visualizer for `candle-graph/viewer/6` evidence payloads.
2//!
3//! Embeds escaped JSON, CSS, and JS into one document. No CDN or network fetches.
4
5use serde_json::Value;
6
7use crate::evidence::EvidencePacket;
8
9pub mod trace_view;
10
11const CSS: &str = include_str!("viewer/style.css");
12const TRACE_JS: &str = include_str!("viewer/app_trace.js");
13
14/// Render application and GPU evidence in one standalone document.
15pub fn render_evidence_html(evidence: &EvidencePacket) -> String {
16    let projection = trace_view::project(evidence);
17    render_trace_document(&projection)
18}
19
20fn render_trace_document(projection: &Value) -> String {
21    let payload = embed_json(projection);
22    let mut html = String::with_capacity(8192 + CSS.len() + TRACE_JS.len() + payload.len());
23    html.push_str(
24        "<!DOCTYPE html>\n<html lang=\"en\" data-viewer=\"candle-graph-evidence\">\n<head>\n",
25    );
26    html.push_str("<meta charset=\"utf-8\"/>\n");
27    html.push_str("<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\n");
28    html.push_str(r##"<meta name="theme-color" content="#ffffff"/>
29<title>Candle graph</title>
30<script>
31(function () {
32  var theme;
33  try { theme = localStorage.getItem("cg-theme"); } catch (_) {}
34  document.documentElement.dataset.theme = ["light", "dark"].includes(theme) ? theme : (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
35})();
36</script>
37<style>"##);
38    html.push_str(CSS);
39    html.push_str(r##"</style>
40</head>
41<body>
42<a class="skip" href="#profile-viewer">Skip to profile</a>
43<header class="top">
44  <a class="brand" href="#evidence" aria-label="Candle graph overview"><svg class="brand-mark" viewBox="0 0 36 36" aria-hidden="true"><rect class="brand-tile" width="36" height="36" rx="10"/><path class="brand-flame" d="M18 5c.8 3.5 4 4.7 4 7.5a4 4 0 0 1-8 0c0-2.1 2.5-4.2 4-7.5Z"/><path class="brand-wax" d="M13 20h10v10H13zM18 17v3M10 30h16"/></svg><span>candle<span class="brand-light">graph</span></span></a>
45  <span class="header-divider" aria-hidden="true"></span>
46  <div class="cov" data-coverage></div>
47  <div class="top-actions">
48    <span class="offline-label">Offline profile</span>
49    <button type="button" class="btn" id="help-btn" aria-haspopup="dialog">Guide <kbd>?</kbd></button>
50    <button type="button" class="btn" id="theme-btn" data-theme-toggle aria-label="Switch to dark theme">Dark theme</button>
51  </div>
52</header>
53<nav class="view-nav" aria-label="Profile views">
54  <div class="tabs" data-view-tabs role="tablist" aria-label="Profile evidence views"></div>
55  <span class="nav-context" data-run-context></span>
56</nav>
57<div class="layout" id="app">
58  <aside class="pane pane-sidebar" data-pane="sidebar" aria-label="Span hierarchy" hidden>
59    <div id="trace-navigation" class="trace-navigation">
60      <div class="pane-h"><h2>Span hierarchy</h2><button type="button" class="btn icon" id="close-hierarchy" aria-label="Close span hierarchy">×</button></div>
61      <div class="search-box">
62        <label for="span-search">Find a span, operation or tensor <kbd>/</kbd></label>
63        <input id="span-search" name="span-search" data-span-search type="search" placeholder="e.g. attention…" autocomplete="off" spellcheck="false"/>
64        <p id="span-search-status" class="field-hint" role="status"></p>
65      </div>
66      <div id="span-tree" class="scroll-area span-tree" data-span-tree role="tree" aria-label="Span hierarchy"></div>
67      <p class="sidebar-note">Select a span to inspect its timings.<br>Arrow keys explore the hierarchy.</p>
68    </div>
69  </aside>
70  <div class="resize-handle" data-side="left" role="separator" aria-label="Hierarchy width" aria-orientation="vertical" tabindex="0" hidden></div>
71  <main class="pane pane-canvas" data-pane="canvas" id="profile-viewer" tabindex="-1" aria-label="Profile evidence">
72    <div class="graph-toolbar" id="graph-toolbar" data-trace-only hidden>
73      <div><h1 data-canvas-title>Execution graph</h1><p class="subtle" data-graph-stats></p></div>
74      <div class="toolbar-actions">
75        <button type="button" class="btn" id="hierarchy-btn" aria-controls="trace-navigation" aria-expanded="true">Hierarchy</button>
76        <button type="button" class="btn" id="fit-btn">Fit graph</button>
77        <button type="button" class="btn" id="reset-btn" aria-label="Reset zoom and pan">Reset</button>
78        <button type="button" class="btn" id="export-btn" aria-label="Export trace graph as SVG">Export SVG</button>
79      </div>
80    </div>
81    <section id="view-panel-evidence" class="view-panel scroll-area evidence-view" data-view-panel="evidence" role="tabpanel" aria-labelledby="view-tab-evidence" tabindex="0"></section>
82    <div class="canvas-wrap" id="view-panel-trace" data-view-panel="trace" role="tabpanel" aria-labelledby="view-tab-trace" tabindex="0" hidden>
83      <div id="empty-graph" class="empty" data-empty-state hidden><h2>No execution graph</h2><p>A graph requires a completed, structurally valid capture.</p><a href="#evidence">Review capture details</a></div>
84      <svg id="graph-canvas" data-canvas role="group" aria-label="Execution graph. Use arrow keys to pan, plus and minus to zoom, and F to fit." tabindex="0"></svg>
85      <div id="graph-tooltip" class="graph-tooltip" role="tooltip" hidden></div>
86      <p class="canvas-hint">Drag to pan · Scroll to zoom · <kbd>F</kbd> to fit</p>
87      <div class="legend-float" data-legend>
88        <button type="button" class="legend-toggle" id="legend-toggle" aria-expanded="true" aria-controls="legend-body">Graph key</button>
89        <div class="legend-body" id="legend-body">
90          <fieldset><legend>Color by</legend><label><input type="radio" name="heat-mode" value="time" checked> Host self / total</label><label><input type="radio" name="heat-mode" value="memory"> Logical memory</label></fieldset>
91          <div class="heat-scale"><span>Low</span><span class="heat-ramp" aria-hidden="true"></span><span>High</span></div>
92          <div class="edge-key"><span class="lg kind-op">Call</span><span class="lg kind-module">Tensor data</span></div>
93        </div>
94      </div>
95      <div class="canvas-controls" role="group" aria-label="Zoom controls">
96        <button type="button" class="btn icon" id="zoom-out" aria-label="Zoom out">−</button>
97        <output class="zoom-label" id="zoom-label" aria-label="Zoom level">100%</output>
98        <button type="button" class="btn icon" id="zoom-in" aria-label="Zoom in">+</button>
99        <button type="button" class="btn" id="zoom-fit" aria-label="Fit graph to view">Fit</button>
100      </div>
101    </div>
102    <section id="view-panel-span_costs" class="view-panel scroll-area" data-view-panel="span_costs" role="tabpanel" aria-labelledby="view-tab-span_costs" tabindex="0" hidden></section>
103    <section id="view-panel-measurements" class="view-panel scroll-area" data-view-panel="measurements" role="tabpanel" aria-labelledby="view-tab-measurements" tabindex="0" hidden></section>
104    <section id="view-panel-memory" class="view-panel scroll-area" data-view-panel="memory" role="tabpanel" aria-labelledby="view-tab-memory" tabindex="0" hidden></section>
105    <section id="view-panel-gpu" class="view-panel scroll-area gpu-view" data-view-panel="gpu" role="tabpanel" aria-labelledby="view-tab-gpu" tabindex="0" hidden></section>
106  </main>
107  <div class="resize-handle" data-side="right" role="separator" aria-label="Details width" aria-orientation="vertical" tabindex="0" hidden></div>
108  <aside class="pane pane-inspector" data-pane="inspector" aria-label="Selection details" hidden>
109    <div class="pane-h"><h2>Selection details</h2><button type="button" class="btn icon" id="close-inspector" aria-label="Close selection details">×</button></div>
110    <div class="inspector-scroll scroll-area">
111      <dl id="inspector" data-inspector>
112        <div class="inspector-identity"><dt>Selected item</dt><dd data-field="label">—</dd></div>
113        <div><dt>Kind</dt><dd data-field="kind">—</dd></div>
114        <div><dt>Host self</dt><dd data-field="self_time">—</dd></div>
115        <div><dt>Host total</dt><dd data-field="total_time">—</dd></div>
116        <div><dt>Device busy</dt><dd data-field="device_time">—</dd></div>
117        <div><dt>Shape</dt><dd data-field="shape">—</dd></div>
118        <div><dt>Dtype</dt><dd data-field="dtype">—</dd></div>
119        <div><dt>Dense footprint</dt><dd data-field="dense">—</dd></div>
120        <div><dt>Logical peak</dt><dd data-field="peak_bytes">—</dd></div>
121        <div><dt>Allocated</dt><dd data-field="bytes">—</dd></div>
122      </dl>
123      <p class="inspector-note">Self excludes child work. Total includes it. Host time does not measure asynchronous GPU completion.</p>
124      <a class="btn inspector-link" id="selection-graph-link" href="#trace">Locate in graph →</a>
125      <details class="inspector-peak"><summary>Run-wide peak allocations</summary><div id="peak-breakdown" class="peak-table" data-peak-breakdown></div></details>
126    </div>
127  </aside>
128</div>
129<dialog id="guide-dialog" aria-labelledby="guide-title">
130  <div class="dialog-header"><h2 id="guide-title">Read a profile</h2><button type="button" class="btn icon" id="close-guide" aria-label="Close guide">×</button></div>
131  <p>One captured invocation, from the run overview down to individual operations.</p>
132  <dl class="guide-definitions">
133    <div><dt>Overview</dt><dd>Start with capture health, the most expensive recorded work, and which evidence is available.</dd></div>
134    <div><dt>Measurements</dt><dd>Inspect recorded losses, tensor statistics, and gradient states. Repeated labels are separate observations. Non-finite records do not represent zero, and missing or zero gradients can be expected for inactive or data-conditional families.</dd></div>
135    <div><dt>Host self / total</dt><dd>Self excludes recorded child work; total includes it. Nested totals overlap, so do not add them together. Neither implies completed asynchronous GPU work.</dd></div>
136    <div><dt>Logical / physical memory</dt><dd>Logical memory tracks recorded storage lifetimes. Physical memory comes from independent device samples. Tensor shape describes a dense footprint, not an allocation.</dd></div>
137    <div><dt>Evidence coverage</dt><dd>Complete covers the declared scope. Partial supports limited conclusions. Unavailable means no supported conclusion; it does not mean zero. Invalid evidence cannot be trusted.</dd></div>
138  </dl>
139  <h3>Keyboard shortcuts</h3>
140  <dl class="shortcuts"><div><dt><kbd>?</kbd></dt><dd>Open this guide</dd></div><div><dt><kbd>/</kbd></dt><dd>Search the current graph or table</dd></div><div><dt><kbd>F</kbd></dt><dd>Fit the graph</dd></div><div><dt><kbd>+</kbd> <kbd>−</kbd></dt><dd>Zoom the graph</dd></div><div><dt><kbd>Esc</kbd></dt><dd>Close details or this guide</dd></div></dl>
141</dialog>
142<div id="viewer-status" class="sr" role="status" aria-live="polite"></div>
143"##);
144
145    html.push_str("<script id=\"cg-payload\" type=\"application/json\">");
146    html.push_str(&payload);
147    html.push_str("</script>\n<script>");
148    html.push_str(include_str!("viewer/dagre.min.js"));
149    html.push_str("</script>\n<script>");
150    html.push_str(include_str!("viewer/layout.js"));
151    html.push_str("</script>\n<script>");
152    html.push_str(TRACE_JS);
153    html.push_str("</script>\n</body>\n</html>\n");
154    html
155}
156
157/// Serialize JSON and escape so embedded payload cannot close a surrounding `<script>` tag.
158pub fn embed_json(value: &Value) -> String {
159    escape_for_script(&value.to_string())
160}
161
162/// Escape text for safe inclusion inside HTML `<script>` content.
163pub fn escape_for_script(s: &str) -> String {
164    let mut out = String::with_capacity(s.len());
165    for ch in s.chars() {
166        match ch {
167            '<' => out.push_str("\\u003c"),
168            '>' => out.push_str("\\u003e"),
169            '\u{2028}' => out.push_str("\\u2028"),
170            '\u{2029}' => out.push_str("\\u2029"),
171            _ => out.push(ch),
172        }
173    }
174    out
175}