Skip to main content

candle_graph/cli/
trace_cli.rs

1//! Evidence CLI engine (`import`, `view`, `summary`, `query`, `compare`, `report`).
2
3use anyhow::{Context, Result};
4use std::path::Path;
5
6use crate::evidence::{build_evidence, compare_documents, EvidencePacket};
7use crate::graph::{ExecutionGraph, GraphNode};
8use crate::trace::parse_trace;
9
10/// Bounded query kinds for trace-derived execution graphs.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TraceQueryKind {
13    Slowest,
14    Heaviest,
15    Memory,
16    Efficiency,
17    Spans,
18    Tensors,
19    Gradients,
20}
21
22/// Parse a trace and build its bounded application evidence.
23pub fn load_evidence(trace_path: &Path) -> Result<EvidencePacket> {
24    build_evidence(trace_path, None, None)
25}
26
27/// `import` — emit the full evidence packet JSON.
28pub fn run_import(trace_path: &Path, output: Option<&Path>) -> Result<()> {
29    let evidence = load_evidence(trace_path)?;
30    let rendered = serde_json::to_string_pretty(&evidence)? + "\n";
31    super::write_output(output, rendered.as_bytes())
32}
33
34/// `summary` — emit provenance, health, gaps, and graph summary JSON.
35pub fn run_summary(trace_path: &Path, output: Option<&Path>) -> Result<()> {
36    let evidence = load_evidence(trace_path)?;
37    let rendered = serde_json::to_string_pretty(&serde_json::json!({
38        "schema": "candle-graph/summary/1",
39        "provenance": evidence.provenance,
40        "health": evidence.health,
41        "findings": evidence.findings,
42        "gaps": evidence.gaps,
43        "summary": evidence.graph.summary,
44    }))? + "\n";
45    super::write_output(output, rendered.as_bytes())
46}
47
48/// `query` — emit a bounded slice of graph facts.
49pub fn run_query(trace_path: &Path, kind: TraceQueryKind, output: Option<&Path>) -> Result<()> {
50    let evidence = load_evidence(trace_path)?;
51    let graph = &evidence.graph;
52    let payload = match kind {
53        TraceQueryKind::Slowest => query_slowest(graph),
54        TraceQueryKind::Heaviest => query_heaviest(graph),
55        TraceQueryKind::Memory => query_memory(graph),
56        TraceQueryKind::Efficiency => query_efficiency(graph),
57        TraceQueryKind::Spans => query_spans(graph),
58        TraceQueryKind::Tensors => query_tensors(graph),
59        TraceQueryKind::Gradients => query_gradients(graph),
60    };
61    let payload = serde_json::json!({
62        "health": evidence.health,
63        "gaps": evidence.gaps,
64        "result": payload,
65    });
66    let rendered = serde_json::to_string_pretty(&payload)? + "\n";
67    super::write_output(output, rendered.as_bytes())
68}
69
70/// `view` — render standalone HTML from a trace (requires `visualizer` feature).
71#[cfg(feature = "visualizer")]
72pub fn run_view(
73    trace_path: &Path,
74    output: &Path,
75    baseline: Option<&Path>,
76    nsight_dir: Option<&Path>,
77) -> Result<()> {
78    let evidence = build_evidence(trace_path, baseline, nsight_dir)?;
79    let html = crate::viewer::render_evidence_html(&evidence);
80    super::write_output(Some(output), html.as_bytes())
81}
82
83/// `compare` — aggregate repeated semantic spans and compare candidate to baseline.
84pub fn run_compare(baseline: &Path, candidate: &Path, output: Option<&Path>) -> Result<()> {
85    let baseline_doc =
86        parse_trace(baseline).with_context(|| format!("parse baseline {}", baseline.display()))?;
87    let candidate_doc = parse_trace(candidate)
88        .with_context(|| format!("parse candidate {}", candidate.display()))?;
89    let comparison = compare_documents(&baseline_doc, &candidate_doc);
90    let rendered = serde_json::to_string_pretty(&comparison)? + "\n";
91    super::write_output(output, rendered.as_bytes())
92}
93
94/// `report` — publish durable JSON and concise Markdown from one profile run.
95pub fn run_report(
96    trace: &Path,
97    baseline: Option<&Path>,
98    nsight_dir: Option<&Path>,
99    json_output: &Path,
100    markdown_output: &Path,
101) -> Result<()> {
102    let evidence = build_evidence(trace, baseline, nsight_dir)?;
103    let json = serde_json::to_string_pretty(&evidence)? + "\n";
104    super::write_output(Some(json_output), json.as_bytes())?;
105    super::write_output(Some(markdown_output), evidence.markdown().as_bytes())
106}
107
108fn query_slowest(graph: &ExecutionGraph) -> serde_json::Value {
109    let mut ops: Vec<&GraphNode> = graph
110        .spans
111        .iter()
112        .filter(|node| matches!(node.kind, crate::graph::GraphNodeKind::Op))
113        .collect();
114    ops.sort_by(|left, right| {
115        right
116            .self_time_ns
117            .cmp(&left.self_time_ns)
118            .then_with(|| left.id.cmp(&right.id))
119    });
120    ops.truncate(50);
121
122    serde_json::json!({
123        "schema": "candle-graph/trace-query/1",
124        "kind": "slowest",
125        "entrypoint": graph.summary.entrypoint,
126        "total_ms": graph.summary.total_ms,
127        "slowest_spans": graph.summary.slowest_spans,
128        "slowest_ops": ops,
129    })
130}
131
132fn query_heaviest(graph: &ExecutionGraph) -> serde_json::Value {
133    let mut ops: Vec<&GraphNode> = graph
134        .spans
135        .iter()
136        .filter(|node| matches!(node.kind, crate::graph::GraphNodeKind::Op))
137        .collect();
138    ops.sort_by(|left, right| {
139        right
140            .bytes
141            .cmp(&left.bytes)
142            .then_with(|| left.id.cmp(&right.id))
143    });
144    ops.truncate(50);
145
146    serde_json::json!({
147        "schema": "candle-graph/trace-query/1",
148        "kind": "heaviest",
149        "entrypoint": graph.summary.entrypoint,
150        "peak_bytes": graph.summary.memory.peak_bytes,
151        "heaviest_spans": graph.summary.heaviest_spans,
152        "heaviest_ops": ops,
153    })
154}
155
156fn query_efficiency(graph: &ExecutionGraph) -> serde_json::Value {
157    let mut ops: Vec<&GraphNode> = graph
158        .spans
159        .iter()
160        .filter(|node| matches!(node.kind, crate::graph::GraphNodeKind::Op))
161        .filter(|node| node.self_time_ns > 0 && node.bytes > 0)
162        .collect();
163    ops.sort_by(|left, right| {
164        let left_score = left.bytes as f64 / left.self_time_ns as f64;
165        let right_score = right.bytes as f64 / right.self_time_ns as f64;
166        right_score
167            .partial_cmp(&left_score)
168            .unwrap_or(std::cmp::Ordering::Equal)
169            .then_with(|| left.id.cmp(&right.id))
170    });
171    ops.truncate(50);
172
173    serde_json::json!({
174        "schema": "candle-graph/trace-query/1",
175        "kind": "efficiency",
176        "entrypoint": graph.summary.entrypoint,
177        "note": "bytes per nanosecond of self time — higher means more memory traffic per unit compute",
178        "ops": ops,
179    })
180}
181
182fn query_memory(graph: &ExecutionGraph) -> serde_json::Value {
183    serde_json::json!({
184        "schema": "candle-graph/trace-query/1",
185        "kind": "memory",
186        "entrypoint": graph.summary.entrypoint,
187        "summary": graph.summary.memory,
188        "timeline": graph.memory.timeline,
189        "peak_breakdown": graph.memory.peak_breakdown,
190        "by_device": graph.memory.by_device,
191    })
192}
193
194fn query_spans(graph: &ExecutionGraph) -> serde_json::Value {
195    serde_json::json!({
196        "schema": "candle-graph/trace-query/1",
197        "kind": "spans",
198        "entrypoint": graph.summary.entrypoint,
199        "spans": graph.spans,
200        "edges": graph.edges,
201    })
202}
203
204fn query_gradients(graph: &ExecutionGraph) -> serde_json::Value {
205    serde_json::json!({
206        "schema": "candle-graph/trace-query/1",
207        "kind": "gradients",
208        "entrypoint": graph.summary.entrypoint,
209        "gradients": graph.gradients,
210    })
211}
212
213fn query_tensors(graph: &ExecutionGraph) -> serde_json::Value {
214    serde_json::json!({
215        "schema": "candle-graph/trace-query/1",
216        "kind": "tensors",
217        "entrypoint": graph.summary.entrypoint,
218        "tensors": graph.tensors,
219    })
220}