1use 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#[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
22pub fn load_evidence(trace_path: &Path) -> Result<EvidencePacket> {
24 build_evidence(trace_path, None, None)
25}
26
27pub 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
34pub 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
48pub 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#[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
83pub 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
94pub 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}