Skip to main content

veloq_nsys_query/
lib.rs

1//! veloq-nsys-query — per-subcommand query implementations.
2//!
3//! Each subcommand owns one module here. Phase 0 ships `summary`;
4//! `stats`, `search`, `inspect`, `timeline`, `gaps`, `correlate` follow.
5
6pub mod column_map;
7pub mod concurrency;
8pub mod correlate;
9pub mod docgen;
10pub mod error;
11pub mod event_ref;
12pub mod gaps;
13pub mod graph_replays;
14pub mod hardware;
15pub mod inspect;
16pub mod kind_filter;
17pub mod kind_policy;
18pub mod kind_sql;
19pub mod metrics;
20pub mod ncu_command;
21pub mod nvtx_attribution;
22pub mod nvtx_parent;
23pub mod nvtx_projection;
24pub mod nvtx_reverse;
25mod query_sql;
26pub mod row_id;
27pub mod search;
28pub mod slices;
29pub mod stats;
30pub mod stats_by_size;
31pub mod summary;
32pub mod timeline;
33pub mod viz_timeline;
34
35pub use error::{NsysQueryError, NsysQueryResult, SqlPhase};
36pub use event_ref::{EventRef, NvtxContext};
37pub use kind_filter::KindFilter;
38pub use row_id::{EventKind, RowId};
39
40/// Reject `limit == 0` at the public-API boundary. The CLI also
41/// guards via `CommonFilters::limit_or`, but library callers can
42/// hand-build a request with `limit: 0`, which silently zeroes
43/// `total_matched` (the count comes off SQL rows that LIMIT 0
44/// suppressed). Call this at the top of every `run()`.
45pub fn check_limit(limit: usize) -> NsysQueryResult<()> {
46    veloq_core::LimitRef::new(limit)
47        .map(|_| ())
48        .map_err(|_| NsysQueryError::LimitTooSmall { limit })
49}
50
51/// Shared verb preamble: validate the limit, open the trace, and resolve
52/// the `--from/--to` window to an absolute `(start_ns, end_ns)`. Used by
53/// the verbs whose `run()` opens with exactly this sequence (`stats` /
54/// `search` / `stats_by_size`). Verbs that interleave other validation
55/// between these steps — `gaps`' `--min` check, `timeline`'s `--interval`
56/// check, `slices`' deferred window resolution — keep their own preamble
57/// so error precedence is unchanged.
58pub fn open_scoped(
59    path: &std::path::Path,
60    limit: usize,
61    window: Option<veloq_core::time::TimeWindow>,
62) -> NsysQueryResult<(veloq_nsys_data::Trace, Option<(i64, i64)>)> {
63    check_limit(limit)?;
64    let trace = veloq_nsys_data::Trace::open(path).map_err(NsysQueryError::trace_open)?;
65    let abs_window = trace
66        .resolve_window(window)
67        .map_err(NsysQueryError::time_window_resolve)?;
68    Ok((trace, abs_window))
69}
70
71/// NSys records modules as absolute paths
72/// (`/usr/lib/x86_64-linux-gnu/libc.so.6`) or Windows-style
73/// (`C:\Windows\system32\foo.dll`). For hotspot tables / callchains
74/// agents (and humans) want the basename — `libc.so.6` /
75/// `foo.dll`. Centralised here so the `metrics --type cpu-sampling`
76/// path and `inspect cpu_sample:N` agree on what "module name"
77/// means without two copies of the slice-on-`/` logic drifting.
78pub fn module_basename(path: &str) -> String {
79    path.rsplit(['/', '\\']).next().unwrap_or("").to_string()
80}
81
82/// Decode an nsys `globalTid` into `(pid, tid)`. NSys packs four
83/// fields into the 64-bit slot:
84///
85/// | bits 48-63 | bits 24-47 | bits 16-23      | bits 0-15  |
86/// | HW/Host ID | Native PID | Source Domain   | Native TID |
87///
88/// **TID is 16 bits, not 24.** The middle 8 bits carry the source-
89/// domain id (`0x00` = OSRT tracer, `0x3B` = CUDA driver, …); using
90/// `>> 24` for PID extraction (instead of `>> 16`) skips that byte
91/// so the same PID lands consistently whether you're reading
92/// `PROCESSES.globalPid` (OSRT) or `ThreadNames.globalTid` (CUDA).
93/// A naive `>> 16` would land a domain-shifted "pid" that disagrees
94/// across tables by a constant offset.
95///
96/// Centralised here so future call sites have one place to update if
97/// the layout ever shifts; both `metrics --type cpu-sampling` and
98/// `inspect cpu_sample:N` go through this helper.
99pub fn decode_global_tid(global_tid: i64) -> (i64, i64) {
100    let pid = (global_tid >> 24) & 0xFFFFFF;
101    let tid = global_tid & 0xFFFF;
102    (pid, tid)
103}
104
105/// Parse a CLI duration flag (`100us` / `1.2s` / `42ns` / …) into ns,
106/// rejecting non-positive results. Wraps
107/// [`veloq_core::time::parse_duration_ns`] with a flag-name aware
108/// typed error and a "must be positive" guard. Used by every
109/// command that accepts a bucket/interval-like duration flag.
110///
111/// `gaps::parse_min_duration` intentionally does *not* go through
112/// this — `--min-duration 0ns` (the default) means "no minimum",
113/// which is a meaningful filter even though it isn't positive.
114pub fn parse_positive_duration(s: &str, flag: &str) -> NsysQueryResult<i64> {
115    let ns = veloq_core::time::parse_duration_ns(s).map_err(|source| {
116        NsysQueryError::PositiveDurationInvalid {
117            flag: flag.to_string(),
118            value: s.to_string(),
119            source,
120        }
121    })?;
122    if ns <= 0 {
123        return Err(NsysQueryError::PositiveDurationTooSmall {
124            flag: flag.to_string(),
125            ns,
126        });
127    }
128    Ok(ns)
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use veloq_core::VeloqDiagnostic;
135
136    #[test]
137    fn check_limit_zero_returns_typed_error() -> anyhow::Result<()> {
138        let err = match check_limit(0) {
139            Ok(()) => anyhow::bail!("expected check_limit(0) to fail"),
140            Err(err) => err,
141        };
142
143        assert_eq!(err.code().as_str(), "nsys.query.limit-too-small");
144        assert!(matches!(err, NsysQueryError::LimitTooSmall { limit: 0 }));
145        Ok(())
146    }
147
148    #[test]
149    fn parse_positive_duration_invalid_literal_returns_typed_error() -> anyhow::Result<()> {
150        let err = match parse_positive_duration("bogus", "--bucket") {
151            Ok(ns) => anyhow::bail!("expected invalid duration to fail, got {ns} ns"),
152            Err(err) => err,
153        };
154
155        assert_eq!(err.code().as_str(), "nsys.query.invalid-positive-duration");
156        match err {
157            NsysQueryError::PositiveDurationInvalid { flag, value, .. } => {
158                assert_eq!(flag, "--bucket");
159                assert_eq!(value, "bogus");
160            }
161            other => anyhow::bail!("expected PositiveDurationInvalid, got {other:?}"),
162        }
163        Ok(())
164    }
165
166    #[test]
167    fn parse_positive_duration_zero_returns_typed_error() -> anyhow::Result<()> {
168        let err = match parse_positive_duration("0ns", "--interval") {
169            Ok(ns) => anyhow::bail!("expected zero duration to fail, got {ns} ns"),
170            Err(err) => err,
171        };
172
173        assert_eq!(
174            err.code().as_str(),
175            "nsys.query.positive-duration-too-small"
176        );
177        assert!(matches!(
178            err,
179            NsysQueryError::PositiveDurationTooSmall {
180                flag,
181                ns: 0
182            } if flag == "--interval"
183        ));
184        Ok(())
185    }
186}