1use std::ffi::OsString;
8use std::path::PathBuf;
9
10use anyhow::Result;
11use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
12
13use super::trace_cli::{self, QueryLabelFilter, TraceQueryKind};
14
15#[derive(Parser, Debug)]
17#[command(
18 name = "candle-graph",
19 version,
20 about = "Import and visualize Candle execution traces",
21 long_about = "Capability-qualified evidence and atomic bundles from candle-graph/trace/10 runs."
22)]
23pub struct Cli {
24 #[command(subcommand)]
25 command: Command,
26}
27
28impl Cli {
29 pub fn run(self) -> Result<()> {
31 match self.command {
32 Command::Import(import) => {
33 trace_cli::run_import(&import.trace, import.output.as_deref())
34 }
35 #[cfg(feature = "visualizer")]
36 Command::View(view) => {
37 trace_cli::run_view(&view.trace, &view.output, view.nsight_dir.as_deref())
38 }
39 Command::Summary(summary) => trace_cli::run_summary(
40 &summary.input,
41 summary.output.as_deref(),
42 summary.require_valid,
43 ),
44 Command::Query(query) => {
45 let filter = query.filter();
46 trace_cli::run_query(
47 &query.input,
48 query.kind.into(),
49 filter.as_ref(),
50 query.output.as_deref(),
51 )
52 }
53 Command::Overview(overview) => {
54 trace_cli::run_overview(&overview.input, overview.output.as_deref())
55 }
56 Command::Compare(compare) => trace_cli::run_compare(
57 &compare.baseline,
58 &compare.candidate,
59 compare.unverified_traces,
60 compare.require_eligible,
61 compare.output.as_deref(),
62 ),
63 Command::Report(report) => trace_cli::run_report(
64 &report.trace,
65 report.nsight_dir.as_deref(),
66 &report.bundle,
67 report.output.as_deref(),
68 ),
69 Command::Verify(verify) => {
70 trace_cli::run_verify(&verify.bundle, verify.semantic, verify.output.as_deref())
71 }
72 Command::Protocol(protocol) => trace_cli::run_protocol(protocol.output.as_deref()),
73 Command::CampaignStatus(status) => {
74 trace_cli::run_campaign_status(&status.manifest, status.output.as_deref())
75 }
76 Command::Series(series) => trace_cli::run_series(
77 series.manifest.as_deref(),
78 &series.bundle,
79 series.label_prefix.as_deref(),
80 series.output.as_deref(),
81 ),
82 }
83 }
84
85 pub fn parse_as_cargo_subcommand() -> Self {
93 let mut argv: Vec<OsString> = std::env::args_os().collect();
94 if argv
95 .get(1)
96 .is_some_and(|argument| argument == "candle-graph")
97 {
98 argv.remove(1);
99 }
100 let command = Self::command()
101 .name("cargo-candle-graph")
102 .bin_name("cargo candle-graph")
103 .about("Import and analyze candle-graph execution trace files");
104 let matches = command.get_matches_from(argv);
105 match Self::from_arg_matches(&matches) {
106 Ok(cli) => cli,
107 Err(error) => error.exit(),
108 }
109 }
110}
111
112pub fn command_catalog() -> Vec<serde_json::Value> {
114 Cli::command()
115 .get_subcommands()
116 .map(|subcommand| {
117 serde_json::json!({
118 "name": subcommand.get_name(),
119 "about": subcommand.get_about().map(|about| about.to_string()),
120 })
121 })
122 .collect()
123}
124
125#[derive(Subcommand, Debug)]
126enum Command {
127 Import(ImportArgs),
129 #[cfg(feature = "visualizer")]
131 View(ViewArgs),
132 Summary(SummaryArgs),
134 Query(QueryArgs),
136 Overview(OverviewArgs),
138 Compare(CompareArgs),
140 Report(ReportArgs),
142 Verify(VerifyArgs),
144 Protocol(ProtocolArgs),
146 CampaignStatus(CampaignStatusArgs),
148 Series(SeriesArgs),
150}
151
152#[derive(Args, Debug)]
153struct ImportArgs {
154 #[arg(value_name = "INPUT")]
156 trace: PathBuf,
157 #[arg(long, short, value_name = "FILE")]
158 output: Option<PathBuf>,
159}
160
161#[cfg(feature = "visualizer")]
162#[derive(Args, Debug)]
163struct ViewArgs {
164 #[arg(value_name = "TRACE")]
166 trace: PathBuf,
167 #[arg(long, value_name = "FILE")]
168 output: PathBuf,
169 #[arg(long, value_name = "DIR")]
171 nsight_dir: Option<PathBuf>,
172}
173
174#[derive(Args, Debug)]
175struct SummaryArgs {
176 #[arg(value_name = "INPUT")]
178 input: PathBuf,
179 #[arg(long)]
181 require_valid: bool,
182 #[arg(long, short, value_name = "FILE")]
183 output: Option<PathBuf>,
184}
185
186#[derive(Args, Debug)]
187struct QueryArgs {
188 #[arg(value_name = "INPUT")]
190 input: PathBuf,
191 #[arg(long, value_enum)]
192 kind: CliTraceQueryKind,
193 #[arg(long, value_name = "S", conflicts_with = "label_prefix")]
195 label: Option<String>,
196 #[arg(long, value_name = "S")]
198 label_prefix: Option<String>,
199 #[arg(long, short, value_name = "FILE")]
200 output: Option<PathBuf>,
201}
202
203impl QueryArgs {
204 fn filter(&self) -> Option<QueryLabelFilter> {
205 self.label
206 .clone()
207 .map(QueryLabelFilter::Exact)
208 .or_else(|| self.label_prefix.clone().map(QueryLabelFilter::Prefix))
209 }
210}
211
212#[derive(Args, Debug)]
213struct OverviewArgs {
214 #[arg(value_name = "INPUT")]
216 input: PathBuf,
217 #[arg(long, short, value_name = "FILE")]
218 output: Option<PathBuf>,
219}
220
221#[derive(Args, Debug)]
222struct CompareArgs {
223 #[arg(long, required = true, num_args = 1.., value_name = "BUNDLE")]
225 baseline: Vec<PathBuf>,
226 #[arg(long, required = true, num_args = 1.., value_name = "BUNDLE")]
228 candidate: Vec<PathBuf>,
229 #[arg(long)]
231 unverified_traces: bool,
232 #[arg(long)]
234 require_eligible: bool,
235 #[arg(long, short, value_name = "FILE")]
236 output: Option<PathBuf>,
237}
238
239#[derive(Args, Debug)]
240struct ReportArgs {
241 #[arg(value_name = "TRACE")]
243 trace: PathBuf,
244 #[arg(long, value_name = "DIR")]
245 nsight_dir: Option<PathBuf>,
246 #[arg(long, value_name = "DIR")]
247 bundle: PathBuf,
248 #[arg(long, short, value_name = "FILE")]
250 output: Option<PathBuf>,
251}
252
253#[derive(Args, Debug)]
254struct VerifyArgs {
255 #[arg(value_name = "BUNDLE")]
257 bundle: PathBuf,
258 #[arg(long)]
260 semantic: bool,
261 #[arg(long, short, value_name = "FILE")]
262 output: Option<PathBuf>,
263}
264
265#[derive(Args, Debug)]
266struct ProtocolArgs {
267 #[arg(long, short, value_name = "FILE")]
268 output: Option<PathBuf>,
269}
270
271#[derive(Args, Debug)]
272struct CampaignStatusArgs {
273 #[arg(long, value_name = "FILE")]
275 manifest: PathBuf,
276 #[arg(long, short, value_name = "FILE")]
277 output: Option<PathBuf>,
278}
279
280#[derive(Args, Debug)]
281struct SeriesArgs {
282 #[arg(
284 long,
285 value_name = "FILE",
286 conflicts_with = "bundle",
287 required_unless_present = "bundle"
288 )]
289 manifest: Option<PathBuf>,
290 #[arg(long, value_name = "DIR", num_args = 1..)]
292 bundle: Vec<PathBuf>,
293 #[arg(long, value_name = "P")]
295 label_prefix: Option<String>,
296 #[arg(long, short, value_name = "FILE")]
297 output: Option<PathBuf>,
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
301enum CliTraceQueryKind {
302 SlowestHost,
303 SlowestDevice,
304 Heaviest,
305 Memory,
306 Spans,
307 Tensors,
308 TensorStats,
309 Gradients,
310 Capabilities,
311 GpuStatus,
312 GpuCorrelation,
313 GpuPhases,
314 GpuKernels,
315 GpuAttributionGaps,
316}
317
318impl From<CliTraceQueryKind> for TraceQueryKind {
319 fn from(kind: CliTraceQueryKind) -> Self {
320 match kind {
321 CliTraceQueryKind::SlowestHost => Self::SlowestHost,
322 CliTraceQueryKind::SlowestDevice => Self::SlowestDevice,
323 CliTraceQueryKind::Heaviest => Self::Heaviest,
324 CliTraceQueryKind::Memory => Self::Memory,
325 CliTraceQueryKind::Spans => Self::Spans,
326 CliTraceQueryKind::Tensors => Self::Tensors,
327 CliTraceQueryKind::TensorStats => Self::TensorStats,
328 CliTraceQueryKind::Gradients => Self::Gradients,
329 CliTraceQueryKind::Capabilities => Self::Capabilities,
330 CliTraceQueryKind::GpuStatus => Self::GpuStatus,
331 CliTraceQueryKind::GpuCorrelation => Self::GpuCorrelation,
332 CliTraceQueryKind::GpuPhases => Self::GpuPhases,
333 CliTraceQueryKind::GpuKernels => Self::GpuKernels,
334 CliTraceQueryKind::GpuAttributionGaps => Self::GpuAttributionGaps,
335 }
336 }
337}