candle-graph 0.10.0

TensorFlow Profiler-style execution graphs for candle-rs (trace-only)
Documentation
//! Dependency-free interactive HTML visualizer for `candle-graph/viewer/6` evidence payloads.
//!
//! Embeds escaped JSON, CSS, and JS into one document. No CDN or network fetches.

use serde_json::Value;

use crate::evidence::EvidencePacket;

pub mod trace_view;

const CSS: &str = include_str!("viewer/style.css");
const TRACE_JS: &str = include_str!("viewer/app_trace.js");

/// Render application and GPU evidence in one standalone document.
pub fn render_evidence_html(evidence: &EvidencePacket) -> String {
    let projection = trace_view::project(evidence);
    render_trace_document(&projection)
}

fn render_trace_document(projection: &Value) -> String {
    let payload = embed_json(projection);
    let mut html = String::with_capacity(8192 + CSS.len() + TRACE_JS.len() + payload.len());
    html.push_str(
        "<!DOCTYPE html>\n<html lang=\"en\" data-viewer=\"candle-graph-evidence\">\n<head>\n",
    );
    html.push_str("<meta charset=\"utf-8\"/>\n");
    html.push_str("<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\n");
    html.push_str(r##"<meta name="theme-color" content="#ffffff"/>
<title>Candle graph</title>
<script>
(function () {
  var theme;
  try { theme = localStorage.getItem("cg-theme"); } catch (_) {}
  document.documentElement.dataset.theme = ["light", "dark"].includes(theme) ? theme : (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
})();
</script>
<style>"##);
    html.push_str(CSS);
    html.push_str(r##"</style>
</head>
<body>
<a class="skip" href="#profile-viewer">Skip to profile</a>
<header class="top">
  <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>
  <span class="header-divider" aria-hidden="true"></span>
  <div class="cov" data-coverage></div>
  <div class="top-actions">
    <span class="offline-label">Offline profile</span>
    <button type="button" class="btn" id="help-btn" aria-haspopup="dialog">Guide <kbd>?</kbd></button>
    <button type="button" class="btn" id="theme-btn" data-theme-toggle aria-label="Switch to dark theme">Dark theme</button>
  </div>
</header>
<nav class="view-nav" aria-label="Profile views">
  <div class="tabs" data-view-tabs role="tablist" aria-label="Profile evidence views"></div>
  <span class="nav-context" data-run-context></span>
</nav>
<div class="layout" id="app">
  <aside class="pane pane-sidebar" data-pane="sidebar" aria-label="Span hierarchy" hidden>
    <div id="trace-navigation" class="trace-navigation">
      <div class="pane-h"><h2>Span hierarchy</h2><button type="button" class="btn icon" id="close-hierarchy" aria-label="Close span hierarchy">×</button></div>
      <div class="search-box">
        <label for="span-search">Find a span, operation or tensor <kbd>/</kbd></label>
        <input id="span-search" name="span-search" data-span-search type="search" placeholder="e.g. attention…" autocomplete="off" spellcheck="false"/>
        <p id="span-search-status" class="field-hint" role="status"></p>
      </div>
      <div id="span-tree" class="scroll-area span-tree" data-span-tree role="tree" aria-label="Span hierarchy"></div>
      <p class="sidebar-note">Select a span to inspect its timings.<br>Arrow keys explore the hierarchy.</p>
    </div>
  </aside>
  <div class="resize-handle" data-side="left" role="separator" aria-label="Hierarchy width" aria-orientation="vertical" tabindex="0" hidden></div>
  <main class="pane pane-canvas" data-pane="canvas" id="profile-viewer" tabindex="-1" aria-label="Profile evidence">
    <div class="graph-toolbar" id="graph-toolbar" data-trace-only hidden>
      <div><h1 data-canvas-title>Execution graph</h1><p class="subtle" data-graph-stats></p></div>
      <div class="toolbar-actions">
        <button type="button" class="btn" id="hierarchy-btn" aria-controls="trace-navigation" aria-expanded="true">Hierarchy</button>
        <button type="button" class="btn" id="fit-btn">Fit graph</button>
        <button type="button" class="btn" id="reset-btn" aria-label="Reset zoom and pan">Reset</button>
        <button type="button" class="btn" id="export-btn" aria-label="Export trace graph as SVG">Export SVG</button>
      </div>
    </div>
    <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>
    <div class="canvas-wrap" id="view-panel-trace" data-view-panel="trace" role="tabpanel" aria-labelledby="view-tab-trace" tabindex="0" hidden>
      <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>
      <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>
      <div id="graph-tooltip" class="graph-tooltip" role="tooltip" hidden></div>
      <p class="canvas-hint">Drag to pan · Scroll to zoom · <kbd>F</kbd> to fit</p>
      <div class="legend-float" data-legend>
        <button type="button" class="legend-toggle" id="legend-toggle" aria-expanded="true" aria-controls="legend-body">Graph key</button>
        <div class="legend-body" id="legend-body">
          <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>
          <div class="heat-scale"><span>Low</span><span class="heat-ramp" aria-hidden="true"></span><span>High</span></div>
          <div class="edge-key"><span class="lg kind-op">Call</span><span class="lg kind-module">Tensor data</span></div>
        </div>
      </div>
      <div class="canvas-controls" role="group" aria-label="Zoom controls">
        <button type="button" class="btn icon" id="zoom-out" aria-label="Zoom out">−</button>
        <output class="zoom-label" id="zoom-label" aria-label="Zoom level">100%</output>
        <button type="button" class="btn icon" id="zoom-in" aria-label="Zoom in">+</button>
        <button type="button" class="btn" id="zoom-fit" aria-label="Fit graph to view">Fit</button>
      </div>
    </div>
    <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>
    <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>
    <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>
    <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>
  </main>
  <div class="resize-handle" data-side="right" role="separator" aria-label="Details width" aria-orientation="vertical" tabindex="0" hidden></div>
  <aside class="pane pane-inspector" data-pane="inspector" aria-label="Selection details" hidden>
    <div class="pane-h"><h2>Selection details</h2><button type="button" class="btn icon" id="close-inspector" aria-label="Close selection details">×</button></div>
    <div class="inspector-scroll scroll-area">
      <dl id="inspector" data-inspector>
        <div class="inspector-identity"><dt>Selected item</dt><dd data-field="label">—</dd></div>
        <div><dt>Kind</dt><dd data-field="kind">—</dd></div>
        <div><dt>Host self</dt><dd data-field="self_time">—</dd></div>
        <div><dt>Host total</dt><dd data-field="total_time">—</dd></div>
        <div><dt>Device busy</dt><dd data-field="device_time">—</dd></div>
        <div><dt>Shape</dt><dd data-field="shape">—</dd></div>
        <div><dt>Dtype</dt><dd data-field="dtype">—</dd></div>
        <div><dt>Dense footprint</dt><dd data-field="dense">—</dd></div>
        <div><dt>Logical peak</dt><dd data-field="peak_bytes">—</dd></div>
        <div><dt>Allocated</dt><dd data-field="bytes">—</dd></div>
      </dl>
      <p class="inspector-note">Self excludes child work. Total includes it. Host time does not measure asynchronous GPU completion.</p>
      <a class="btn inspector-link" id="selection-graph-link" href="#trace">Locate in graph →</a>
      <details class="inspector-peak"><summary>Run-wide peak allocations</summary><div id="peak-breakdown" class="peak-table" data-peak-breakdown></div></details>
    </div>
  </aside>
</div>
<dialog id="guide-dialog" aria-labelledby="guide-title">
  <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>
  <p>One captured invocation, from the run overview down to individual operations.</p>
  <dl class="guide-definitions">
    <div><dt>Overview</dt><dd>Start with capture health, the most expensive recorded work, and which evidence is available.</dd></div>
    <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>
    <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>
    <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>
    <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>
  </dl>
  <h3>Keyboard shortcuts</h3>
  <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>
</dialog>
<div id="viewer-status" class="sr" role="status" aria-live="polite"></div>
"##);

    html.push_str("<script id=\"cg-payload\" type=\"application/json\">");
    html.push_str(&payload);
    html.push_str("</script>\n<script>");
    html.push_str(include_str!("viewer/dagre.min.js"));
    html.push_str("</script>\n<script>");
    html.push_str(include_str!("viewer/layout.js"));
    html.push_str("</script>\n<script>");
    html.push_str(TRACE_JS);
    html.push_str("</script>\n</body>\n</html>\n");
    html
}

/// Serialize JSON and escape so embedded payload cannot close a surrounding `<script>` tag.
pub fn embed_json(value: &Value) -> String {
    escape_for_script(&value.to_string())
}

/// Escape text for safe inclusion inside HTML `<script>` content.
pub fn escape_for_script(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for ch in s.chars() {
        match ch {
            '<' => out.push_str("\\u003c"),
            '>' => out.push_str("\\u003e"),
            '\u{2028}' => out.push_str("\\u2028"),
            '\u{2029}' => out.push_str("\\u2029"),
            _ => out.push(ch),
        }
    }
    out
}