Skip to content

feat(nvtx): tolerant NVTX model and analyzer - #473

Merged
rapids-bot[bot] merged 2 commits into
rapidsai:mainfrom
9prady9:nvtx-phase-2-pr
Aug 6, 2026
Merged

feat(nvtx): tolerant NVTX model and analyzer#473
rapids-bot[bot] merged 2 commits into
rapidsai:mainfrom
9prady9:nvtx-phase-2-pr

Conversation

@9prady9

@9prady9 9prady9 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #372. Adds integrations/nvtx/analyzer — a framework-free reconstruction core turning a captured Event<NvtxEventEntity> stream into a labeled NvtxModel. No dependency on any legacy macros.

Two passes. The first orders by timestamp and learns every handle registration; the second replays against those tables. By then neither arrival order nor forward references matter.

Four match keys, each chosen because the alternatives fail silently rather than loudly:

Construct Key Why not coarser
RangeStart/RangeEnd range_id alone Ids come from one process-global counter, so they match across threads and domains; the domain on an end is redundant
RangePush/RangePop stack per (thread_id, domain) A global stack closes another thread's push and reconstructs believable-but-wrong nesting
Resource create/destroy bare handle The destroy carries no domain; keying on the pair makes every resource reconstruct as a leak
Registered strings, category names per domain Handle values and small category ids collide across domains

Kind-conditional fields live in SpanKind variant data — thread and parent on PushPop, identifier type on Resource — so a start/end range with a thread is unrepresentable rather than merely undocumented.

Tolerance. Nothing is substituted for what the stream never said. An open with no close keeps everything the open stated and ends None, as does one whose key was reused while still open. A close with no open yields no span — the closing events carry only a correlation key, with no name or attributes to build one from. Both that and the reuse are counted in ReconstructionAnomalies. Drains sort for determinism, arithmetic is saturating or checked throughout, and unresolved handles get placeholders that are pure functions of the raw id, so they cannot pass as real names.

Statistics. RangeStats per (name, domain, category) — count and total/avg/min/max duration. Marks have no duration and resource lifespans measure existence rather than work, so neither participates; a span whose close was never captured raises count without raising observed_count.

Test plan

  • cargo test -p nvtx-analyzer — 33 tests, hermetic
  • pixi run cargo test -p nvtx-analyzer --features real-capture-tests — adds the roundtrip over a real in-process capture (needs pixi for the nvtx-c headers)
  • pixi run cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
  • pixi run cargo fmt --all -- --check

Generated with Claude Code

@9prady9
9prady9 force-pushed the nvtx-phase-2-pr branch 2 times, most recently from 8324a98 to b4f3b93 Compare July 28, 2026 10:44
rapids-bot Bot pushed a commit that referenced this pull request Jul 29, 2026
…d in callbacks (#474)

Prerequisite for the NVTX tolerant analyzer (#473 ).

Adds `thread_id: u32` to the `RangePush` and `RangePop` variants in `nvtx-events` so the analyzer can reconstruct per-thread nested stacks. Without a stamped thread id, Push/Pop reconstruction would require a global (incorrect) stack or best-effort heuristics.

Changes:
- `nvtx-events`: `RangePush` and `RangePop` gain a `thread_id: u32` field
- `nvtx-injection`: `on_range_push` / `on_range_pop` callbacks stamp the OS thread id via `gettid` (Linux); non-Linux falls back to a hash of `std::thread::current().id()` truncated to `u32`
- `nvtx-injection`: `on_domain_range_push_ex` / `on_domain_range_pop` (CORE2 path) updated identically
- `nvtx-example`: new `tests/thread_id.rs` — captures a two-thread interleaved Push/Pop session and asserts each event carries the expected thread id

Authors:
  - Pradeep Garigipati (https://github.com/9prady9)

Approvers:
  - Johan Peltenburg (https://github.com/johanpel)

URL: #474
@9prady9
9prady9 force-pushed the nvtx-phase-2-pr branch 2 times, most recently from ff92c45 to f87f367 Compare July 30, 2026 12:19
@9prady9
9prady9 marked this pull request as ready for review August 3, 2026 08:34
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: a1bae334-e749-4746-83b7-353746e34e0e

📥 Commits

Reviewing files that changed from the base of the PR and between 52f4403 and f288515.

📒 Files selected for processing (2)
  • integrations/nvtx/analyzer/src/model.rs
  • integrations/nvtx/analyzer/src/span.rs
💤 Files with no reviewable changes (1)
  • integrations/nvtx/analyzer/src/model.rs

📝 Walkthrough

Walkthrough

Added the nvtx-analyzer workspace crate. It resolves NVTX labels, reconstructs ranges and resources from tolerant event replay, computes range statistics, and adds validation tests.

Changes

NVTX analyzer

Layer / File(s) Summary
Analyzer package and resolution contracts
Cargo.toml, integrations/nvtx/analyzer/Cargo.toml, integrations/nvtx/analyzer/src/{lib.rs,span.rs,tables.rs}, integrations/nvtx/analyzer/tests/{fixtures,resolution}.rs
The workspace now includes the analyzer crate. The crate exposes NVTX model types and performs deterministic two-pass resolution for domains, strings, categories, and threads.
Event replay and lifecycle reconstruction
integrations/nvtx/analyzer/src/{anomalies.rs,model.rs,ranges.rs,resource.rs}, integrations/nvtx/analyzer/tests/{pushpop,reconstruction,resource}.rs
The model builder replays ordered events and reconstructs start/end ranges, nested push/pop ranges, resources, marks, and metadata. It records anomalies, skips unmatched closes, and preserves open entities without end timestamps.
Range statistics aggregation
integrations/nvtx/analyzer/src/stats.rs, integrations/nvtx/analyzer/tests/stats.rs
Range statistics group eligible spans by name, domain, and category. The aggregation reports counts and duration metrics, supports filtering, excludes marks and resources, and handles unobserved or zero-duration ranges.
Captured-event roundtrip validation
integrations/nvtx/analyzer/tests/roundtrip.rs
A feature-gated test captures events from nvtx_example, builds an NvtxModel, and validates thread naming, marks, range reconstruction, closure state, timing, and statistics.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: feature request, non-breaking

Suggested reviewers: dhruv9vats, mbrobbel, joosthooz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the new tolerant NVTX model and analyzer, which are the primary changes.
Description check ✅ Passed The description explains the implementation, links issue #372, and documents the completed test plan; missing template headings are non-critical.
Linked Issues check ✅ Passed The implementation addresses issue #372 requirements for reconstruction, name resolution, malformed input tolerance, statistics, and model entities.
Out of Scope Changes check ✅ Passed The changes remain within issue #372 scope and add only the NVTX analyzer, model, reconstruction logic, and related tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (2)
integrations/nvtx/analyzer/tests/roundtrip.rs-54-57 (1)

54-57: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Arc::try_unwrap assumes the capture layer drops the callback.

sink holds a second Arc clone inside its closure. Arc::try_unwrap succeeds only if run_capture drops that callback before it returns. The comment on lines 50-51 states that injection is process-global and one-shot, which suggests the registry can retain the callback for the life of the process. If it does, this line panics with "sink outlived the capture" and the test fails for a reason unrelated to reconstruction.

Read the events through the Arc instead. This removes the ownership assumption.

🛡️ Proposed fix
-    let events = Arc::try_unwrap(collected)
-        .expect("sink outlived the capture")
-        .into_inner()
-        .expect("collector poisoned");
+    let events: Vec<Event<NvtxEventEntity>> = std::mem::take(
+        &mut *collected.lock().expect("collector poisoned"),
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/tests/roundtrip.rs` around lines 54 - 57, Update
the event-reading logic after run_capture to avoid Arc::try_unwrap on collected,
since the capture callback may retain another Arc clone; read the collected
events through the shared Arc while preserving the existing poisoned-lock
handling and reconstruction flow.
integrations/nvtx/analyzer/src/ranges.rs-85-96 (1)

85-96: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Overwriting an open entry silently drops a span in both reconstructions. Both modules register an open interval with HashMap::insert and treat the returned Some(previous) as a log line only. The previous open entry is discarded, so an observed start or create produces no span at all. This contradicts the stated contract that an open which is never closed is closed at trace end and flagged synthetic. Return the displaced entry as a synthetically closed span instead of dropping it.

  • integrations/nvtx/analyzer/src/ranges.rs#L85-L96: change StartEndRanges::start to return Option<NvtxSpan>, close the displaced OpenStartRange at the restart timestamp with synthetic_end = true, and push the returned span into slots at the RangeStart arm in src/model.rs.
  • integrations/nvtx/analyzer/src/resource.rs#L126-L146: change Resources::create to return Option<NvtxSpan>, close the displaced OpenResource at the recreate timestamp with synthetic_end = true, and push the returned span into slots at the ResourceCreate arm in src/model.rs.

Add a test for each path. New Rust components must include accompanying tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/src/ranges.rs` around lines 85 - 96, The
open-entry overwrite handling must preserve displaced spans in both modules. In
integrations/nvtx/analyzer/src/ranges.rs:85-96, update StartEndRanges::start to
return Option<NvtxSpan>, synthetically close any displaced OpenStartRange at the
restart timestamp, and append the returned span in the RangeStart arm of
src/model.rs; add a test. In integrations/nvtx/analyzer/src/resource.rs:126-146,
apply the same behavior to Resources::create and append its result in the
ResourceCreate arm of src/model.rs; add a test.

Source: Coding guidelines

🧹 Nitpick comments (4)
integrations/nvtx/analyzer/src/resource.rs (1)

148-159: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Lower the log level for the orphan destroy.

The doc at Lines 150-152 states that an orphan destroy is the normal case for a resource created before capture attached. The code logs it at warn!. A routine condition at warn level produces noise and reduces the signal of the genuine anomalies logged elsewhere in this crate. Use debug! here and keep warn! for the unexpected paths.

♻️ Proposed change
-use tracing::warn;
+use tracing::{debug, warn};
         let Some(open) = self.open.remove(&handle) else {
-            warn!(
-                "orphan nvtx resource destroy for handle 0x{handle:X} with no open create; skipping"
-            );
+            debug!(
+                "orphan nvtx resource destroy for handle 0x{handle:X} with no open create; \
+                 the create predates capture attach"
+            );
             return None;
         };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/src/resource.rs` around lines 148 - 159, In
Resource::destroy, change the orphan-destroy log from warn! to debug! while
preserving its message and early None return; leave warn! logging for genuinely
unexpected paths unchanged.
integrations/nvtx/analyzer/src/stats.rs (1)

109-121: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Avoid the per-span StatsKey allocation on the hot fold.

Each iteration clones span.name even when the group already exists. For large traces this allocates one String per span. Look up first, then build the key only on a miss.

♻️ Proposed refactor
     for span in spans
         .iter()
         .filter(|span| matches!(span.kind, SpanKind::PushPop | SpanKind::StartEnd))
     {
-        grouped
-            .entry(StatsKey {
-                name: span.name.clone(),
-                domain: span.domain,
-                category: span.category,
-            })
-            .or_default()
-            .accumulate(span);
+        let key = StatsKey {
+            name: span.name.clone(),
+            domain: span.domain,
+            category: span.category,
+        };
+        match grouped.get_mut(&key) {
+            Some(stats) => stats.accumulate(span),
+            None => grouped.entry(key).or_default().accumulate(span),
+        }
     }

The clone still happens on a miss only if you build the key lazily; a borrowed lookup key type (for example a (&str, u64, Option<u32>) index map) removes it entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/src/stats.rs` around lines 109 - 121, Update the
grouping fold around grouped and StatsKey to avoid cloning span.name on every
iteration: first perform a borrowed lookup using span.name, domain, and
category, accumulating into the existing group when found; only construct an
owned StatsKey, including the name clone, when inserting a missing group.
integrations/nvtx/analyzer/src/model.rs (2)

165-170: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Make the "no empty slot" invariant enforced, not documented.

flatten() drops any remaining None and shifts every later index. SpanId values were handed out against the pre-flatten indices, so a single unfilled slot silently repoints every parent in the model. The invariant holds today, because a push is filled by its pop or by close_at_trace_end. It is not enforced by any type or check. Add a debug assertion so a future change fails loudly.

🛡️ Proposed guard
+        debug_assert!(
+            slots.iter().all(Option::is_some),
+            "an unfilled span slot would shift every SpanId assigned after it"
+        );
         let spans: Vec<NvtxSpan> = slots.into_iter().flatten().collect();

Also applies to: 258-261

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/src/model.rs` around lines 165 - 170, Add a debug
assertion in flatten() before removing or compacting slots to verify every entry
in the slots Vec<Option<NvtxSpan>> is Some, preserving SpanId indices and
failing loudly if an unfilled slot is introduced; apply the same guard at the
corresponding trace-end drain path around the other slots declaration.

240-243: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the catch-all arm with explicit variants.

Match DomainCreate, DomainDestroy, RegisterString, NameCategory, and NameThread explicitly. This preserves the pass 1 behavior and makes new NvtxEvent variants fail compilation instead of being discarded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/src/model.rs` around lines 240 - 243, Update the
match containing the catch-all `_ => {}` arm to explicitly handle NvtxEvent
variants DomainCreate, DomainDestroy, RegisterString, NameCategory, and
NameThread with the existing no-op behavior. Remove the wildcard arm so any
newly added NvtxEvent variant causes a compile-time exhaustiveness error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@integrations/nvtx/analyzer/src/model.rs`:
- Around line 145-147: Update the NvtxModelBuilder::build method to return
NvtxModel directly instead of NvtxModelResult<NvtxModel>, since its
implementation only constructs and returns a successful value. Change the
corresponding Ok(...) return and remove unnecessary error handling from its
callers and tests, including the .expect("build") usage.

In `@integrations/nvtx/analyzer/tests/reconstruction.rs`:
- Around line 12-23: The span lookup helper is duplicated across both test
targets; move it into shared fixtures and import it at each use site. In
integrations/nvtx/analyzer/tests/fixtures.rs, add the helper as pub fn span; in
integrations/nvtx/analyzer/tests/reconstruction.rs lines 12-23 and
integrations/nvtx/analyzer/tests/pushpop.rs lines 18-29, remove the local
definitions and add span to each fixtures import. Remove NvtxModel and NvtxSpan
imports where no longer needed, retaining them in pushpop.rs only if span_id
still uses them.

In `@integrations/nvtx/analyzer/tests/resource.rs`:
- Around line 17-20: Update the shared fixtures module usage in this integration
test so unused public helpers do not trigger dead_code warnings under -D
warnings. In the test setup around `mod fixtures` and the
`fixtures::{range_push, resource_create, resource_destroy}` imports, limit
compilation or exposure to the helpers used by this binary while preserving the
separate helpers required by `tests/stats.rs`.

In `@integrations/nvtx/analyzer/tests/stats.rs`:
- Around line 14-18: Update the shared `fixtures` module declaration used by the
test binaries to allow unused fixture helpers under `-D warnings`, so helpers
imported only by other binaries do not trigger `dead_code`; preserve the
existing imports in `stats.rs` and `tests/resource.rs`.

---

Other comments:
In `@integrations/nvtx/analyzer/src/ranges.rs`:
- Around line 85-96: The open-entry overwrite handling must preserve displaced
spans in both modules. In integrations/nvtx/analyzer/src/ranges.rs:85-96, update
StartEndRanges::start to return Option<NvtxSpan>, synthetically close any
displaced OpenStartRange at the restart timestamp, and append the returned span
in the RangeStart arm of src/model.rs; add a test. In
integrations/nvtx/analyzer/src/resource.rs:126-146, apply the same behavior to
Resources::create and append its result in the ResourceCreate arm of
src/model.rs; add a test.

In `@integrations/nvtx/analyzer/tests/roundtrip.rs`:
- Around line 54-57: Update the event-reading logic after run_capture to avoid
Arc::try_unwrap on collected, since the capture callback may retain another Arc
clone; read the collected events through the shared Arc while preserving the
existing poisoned-lock handling and reconstruction flow.

---

Nitpick comments:
In `@integrations/nvtx/analyzer/src/model.rs`:
- Around line 165-170: Add a debug assertion in flatten() before removing or
compacting slots to verify every entry in the slots Vec<Option<NvtxSpan>> is
Some, preserving SpanId indices and failing loudly if an unfilled slot is
introduced; apply the same guard at the corresponding trace-end drain path
around the other slots declaration.
- Around line 240-243: Update the match containing the catch-all `_ => {}` arm
to explicitly handle NvtxEvent variants DomainCreate, DomainDestroy,
RegisterString, NameCategory, and NameThread with the existing no-op behavior.
Remove the wildcard arm so any newly added NvtxEvent variant causes a
compile-time exhaustiveness error.

In `@integrations/nvtx/analyzer/src/resource.rs`:
- Around line 148-159: In Resource::destroy, change the orphan-destroy log from
warn! to debug! while preserving its message and early None return; leave warn!
logging for genuinely unexpected paths unchanged.

In `@integrations/nvtx/analyzer/src/stats.rs`:
- Around line 109-121: Update the grouping fold around grouped and StatsKey to
avoid cloning span.name on every iteration: first perform a borrowed lookup
using span.name, domain, and category, accumulating into the existing group when
found; only construct an owned StatsKey, including the name clone, when
inserting a missing group.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 05b9dd7b-a823-4337-8ee8-1a8dc4ee43a0

📥 Commits

Reviewing files that changed from the base of the PR and between 61545c6 and f87f367.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
📒 Files selected for processing (17)
  • Cargo.toml
  • integrations/nvtx/analyzer/Cargo.toml
  • integrations/nvtx/analyzer/src/error.rs
  • integrations/nvtx/analyzer/src/lib.rs
  • integrations/nvtx/analyzer/src/model.rs
  • integrations/nvtx/analyzer/src/ranges.rs
  • integrations/nvtx/analyzer/src/resource.rs
  • integrations/nvtx/analyzer/src/span.rs
  • integrations/nvtx/analyzer/src/stats.rs
  • integrations/nvtx/analyzer/src/tables.rs
  • integrations/nvtx/analyzer/tests/fixtures.rs
  • integrations/nvtx/analyzer/tests/pushpop.rs
  • integrations/nvtx/analyzer/tests/reconstruction.rs
  • integrations/nvtx/analyzer/tests/resolution.rs
  • integrations/nvtx/analyzer/tests/resource.rs
  • integrations/nvtx/analyzer/tests/roundtrip.rs
  • integrations/nvtx/analyzer/tests/stats.rs

Comment thread integrations/nvtx/analyzer/src/model.rs Outdated
Comment thread integrations/nvtx/analyzer/tests/reconstruction.rs Outdated
Comment thread integrations/nvtx/analyzer/tests/resource.rs
Comment thread integrations/nvtx/analyzer/tests/stats.rs
@9prady9 9prady9 changed the title feat(nvtx): tolerant NVTX model and analyzer — phases 1 & 2 feat(nvtx): tolerant NVTX model and analyzer Aug 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
integrations/nvtx/analyzer/tests/fixtures.rs (1)

1-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move shared fixtures out of tests/ to avoid a spurious test binary.

Cargo auto-registers every .rs file placed directly under tests/ as its own integration-test binary. fixtures.rs has no #[test] functions, so cargo test compiles and runs an extra binary that reports zero tests, on top of the separate compilation triggered by each mod fixtures; declaration in reconstruction.rs, resource.rs, and stats.rs.

Move the file to a subdirectory, for example tests/common/mod.rs, and reference it from each consumer with #[path = "common/mod.rs"] mod fixtures; (or mod common;). Cargo only auto-discovers .rs files directly under tests/, not files nested in subdirectories, so this removes the extra binary target.

Please confirm current Cargo test-target discovery behavior for files nested under tests/<subdir>/, since this affects whether the proposed restructuring avoids the extra binary target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/tests/fixtures.rs` around lines 1 - 18, Move the
shared fixtures module from the top-level tests directory into a nested common
subdirectory so Cargo does not register it as a standalone integration-test
target; files nested under tests subdirectories are not auto-discovered as test
binaries. Update the fixture imports in reconstruction, resource, and stats to
reference the relocated module via an explicit path or shared common module, and
confirm cargo test no longer reports the zero-test fixtures binary.
integrations/nvtx/analyzer/src/ranges.rs (2)

131-144: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unbounded warn! volume at trace end in integrations/nvtx/analyzer/src/ranges.rs and integrations/nvtx/analyzer/src/resource.rs. Both trace-end cleanup paths log one warn! per still-open entity, so log output scales with the number of leaked ranges and resources. Replace the per-item warning with one aggregated warning and keep the per-item detail at debug!.

  • integrations/nvtx/analyzer/src/ranges.rs#L131-L144: log a single warn! with the leaked range count in StartEndRanges::close_at_trace_end, and move the per-range message to debug!.
  • integrations/nvtx/analyzer/src/resource.rs#L144-L158: log a single warn! with the leaked resource count in Resources::close_at_trace_end, and move the per-handle message to debug!.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/src/ranges.rs` around lines 131 - 144, In
integrations/nvtx/analyzer/src/ranges.rs:131-144, update
StartEndRanges::close_at_trace_end to emit one warn! containing the leaked range
count, and change each per-range warning to debug!. Apply the same change in
integrations/nvtx/analyzer/src/resource.rs:144-158 within
Resources::close_at_trace_end: aggregate the leaked resource count in a single
warn! and move per-handle details to debug!.

119-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Lower the orphan RangeEnd log level to debug!.

An orphan RangeEnd is routine, not anomalous. If capture attaches after a range starts, the end arrives with no recorded start. Resources::destroy in integrations/nvtx/analyzer/src/resource.rs (Lines 128-136) already treats the identical situation as routine and uses debug!, with the reason stated in the comment. Use the same level here so genuine anomalies stay visible.

♻️ Proposed change
-    pub(crate) fn end(&mut self, range_id: u64, end: TimeUnixNanoSec) -> Option<NvtxSpan> {
-        let Some(open) = self.open.remove(&range_id) else {
-            warn!("orphan nvtx range end for id 0x{range_id:X} with no open start; skipping");
-            return None;
-        };
+    pub(crate) fn end(&mut self, range_id: u64, end: TimeUnixNanoSec) -> Option<NvtxSpan> {
+        let Some(open) = self.open.remove(&range_id) else {
+            // `debug!`, not `warn!`: a range started before capture attached
+            // always ends without a recorded start.
+            debug!("orphan nvtx range end for id 0x{range_id:X} with no open start; skipping");
+            return None;
+        };

Add debug to the tracing import at Line 20.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/src/ranges.rs` around lines 119 - 125, In the end
method, lower the orphan NVTX range-end message from warn! to debug!, and add
the corresponding debug import from tracing while preserving the existing skip
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@integrations/nvtx/analyzer/tests/roundtrip.rs`:
- Around line 1-128: Before merging, obtain and record Quent maintainer approval
for Issue `#372`, add a valid Signed-off-by line to commit
26e3c91f603a1fed12befa91e39ad2a5b29d412f, and wait for the Rust, quent-open
compatibility, and UI E2E checks to complete successfully.

---

Nitpick comments:
In `@integrations/nvtx/analyzer/src/ranges.rs`:
- Around line 131-144: In integrations/nvtx/analyzer/src/ranges.rs:131-144,
update StartEndRanges::close_at_trace_end to emit one warn! containing the
leaked range count, and change each per-range warning to debug!. Apply the same
change in integrations/nvtx/analyzer/src/resource.rs:144-158 within
Resources::close_at_trace_end: aggregate the leaked resource count in a single
warn! and move per-handle details to debug!.
- Around line 119-125: In the end method, lower the orphan NVTX range-end
message from warn! to debug!, and add the corresponding debug import from
tracing while preserving the existing skip behavior.

In `@integrations/nvtx/analyzer/tests/fixtures.rs`:
- Around line 1-18: Move the shared fixtures module from the top-level tests
directory into a nested common subdirectory so Cargo does not register it as a
standalone integration-test target; files nested under tests subdirectories are
not auto-discovered as test binaries. Update the fixture imports in
reconstruction, resource, and stats to reference the relocated module via an
explicit path or shared common module, and confirm cargo test no longer reports
the zero-test fixtures binary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 2044582d-774d-4ec6-bcbb-3e959b8bd6d2

📥 Commits

Reviewing files that changed from the base of the PR and between f87f367 and 26e3c91.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
📒 Files selected for processing (16)
  • Cargo.toml
  • integrations/nvtx/analyzer/Cargo.toml
  • integrations/nvtx/analyzer/src/lib.rs
  • integrations/nvtx/analyzer/src/model.rs
  • integrations/nvtx/analyzer/src/ranges.rs
  • integrations/nvtx/analyzer/src/resource.rs
  • integrations/nvtx/analyzer/src/span.rs
  • integrations/nvtx/analyzer/src/stats.rs
  • integrations/nvtx/analyzer/src/tables.rs
  • integrations/nvtx/analyzer/tests/fixtures.rs
  • integrations/nvtx/analyzer/tests/pushpop.rs
  • integrations/nvtx/analyzer/tests/reconstruction.rs
  • integrations/nvtx/analyzer/tests/resolution.rs
  • integrations/nvtx/analyzer/tests/resource.rs
  • integrations/nvtx/analyzer/tests/roundtrip.rs
  • integrations/nvtx/analyzer/tests/stats.rs

Comment thread integrations/nvtx/analyzer/tests/roundtrip.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
integrations/nvtx/analyzer/src/tables.rs (1)

80-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tighten the order-independence claim.

The doc states that every accumulation is "an idempotent insert or a min/max". domain_names, registered_strings, category_names, and thread_names use last-writer-wins inserts. If a stream registers the same key twice with different values, the result depends on scan order. The result stays deterministic only because build receives a timestamp-ordered slice from NvtxModelBuilder::build. State that dependency, so a future caller does not pass an unordered slice.

📝 Proposed doc change
-/// Built by a single order-independent scan: every accumulation here is either
-/// an idempotent insert or a `min`/`max`, so the tables do not depend on arrival
-/// order.
+/// Built by a single scan whose result does not depend on where a registration
+/// sits relative to the events using it: lifespans fold with `min`/`max`, and
+/// name lookups are filled before any of them is resolved.
+///
+/// Conflicting registrations of the same key resolve last-writer-wins, so
+/// [`Self::build`] must be given the timestamp-ordered slice produced by
+/// `NvtxModelBuilder::build` for the outcome to stay deterministic.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/src/tables.rs` around lines 80 - 84, Update the
documentation above the pass-1 tables to remove the unconditional
order-independence claim and explicitly state that deterministic results rely on
build receiving a timestamp-ordered slice from NvtxModelBuilder::build, since
several name tables use last-writer-wins updates.
integrations/nvtx/analyzer/tests/fixtures/mod.rs (1)

40-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make span_id assert uniqueness like span.

span_id returns the first matching index. span panics when two spans share a name. If a fixture stream ever produces two spans with the same name, a parent assertion can pass against the wrong span. Reuse the same uniqueness rule for both helpers.

♻️ Proposed refactor
 pub fn span_id(model: &NvtxModel, name: &str) -> SpanId {
-    let index = model
-        .spans()
-        .iter()
-        .position(|span| span.name == name)
-        .unwrap_or_else(|| panic!("no span named {name:?}"));
-    SpanId(index)
+    let target = span(model, name);
+    let index = model
+        .spans()
+        .iter()
+        .position(|candidate| std::ptr::eq(candidate, target))
+        .expect("the span came from this model");
+    SpanId(index)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/tests/fixtures/mod.rs` around lines 40 - 47,
Update span_id to enforce the same unique-name assertion as span: locate the
matching span entries, panic when none or more than one matches, and return the
SpanId only for the single match. Reuse the existing span helper’s uniqueness
rule or matching logic rather than retaining first-match behavior.
integrations/nvtx/analyzer/src/stats.rs (1)

98-101: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make a new SpanKind fail to compile here.

The matches! filter excludes any future SpanKind variant silently. NvtxModelBuilder::build in integrations/nvtx/analyzer/src/model.rs states the opposite convention: it lists ignored event variants explicitly so a new variant breaks the build. Use an exhaustive match for the same protection.

♻️ Proposed refactor
-    for span in spans
-        .iter()
-        .filter(|span| matches!(span.kind, SpanKind::PushPop | SpanKind::StartEnd))
-    {
+    for span in spans.iter().filter(|span| match span.kind {
+        SpanKind::PushPop | SpanKind::StartEnd => true,
+        // Marks are instants and resource lifespans are not work.
+        SpanKind::Resource => false,
+    }) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/src/stats.rs` around lines 98 - 101, Replace the
`matches!` predicate in the span iteration with an exhaustive `match` on
`SpanKind`, returning true for `PushPop` and `StartEnd` and explicitly false for
every currently ignored variant. Follow the explicit-ignored-variant convention
used by `NvtxModelBuilder::build` so adding a new `SpanKind` variant causes a
compile error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@integrations/nvtx/analyzer/src/stats.rs`:
- Around line 98-101: Replace the `matches!` predicate in the span iteration
with an exhaustive `match` on `SpanKind`, returning true for `PushPop` and
`StartEnd` and explicitly false for every currently ignored variant. Follow the
explicit-ignored-variant convention used by `NvtxModelBuilder::build` so adding
a new `SpanKind` variant causes a compile error.

In `@integrations/nvtx/analyzer/src/tables.rs`:
- Around line 80-84: Update the documentation above the pass-1 tables to remove
the unconditional order-independence claim and explicitly state that
deterministic results rely on build receiving a timestamp-ordered slice from
NvtxModelBuilder::build, since several name tables use last-writer-wins updates.

In `@integrations/nvtx/analyzer/tests/fixtures/mod.rs`:
- Around line 40-47: Update span_id to enforce the same unique-name assertion as
span: locate the matching span entries, panic when none or more than one
matches, and return the SpanId only for the single match. Reuse the existing
span helper’s uniqueness rule or matching logic rather than retaining
first-match behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 6b0cac9d-c25e-4a7e-b3b1-07a57ee926df

📥 Commits

Reviewing files that changed from the base of the PR and between 26e3c91 and 644ea7a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
📒 Files selected for processing (16)
  • Cargo.toml
  • integrations/nvtx/analyzer/Cargo.toml
  • integrations/nvtx/analyzer/src/lib.rs
  • integrations/nvtx/analyzer/src/model.rs
  • integrations/nvtx/analyzer/src/ranges.rs
  • integrations/nvtx/analyzer/src/resource.rs
  • integrations/nvtx/analyzer/src/span.rs
  • integrations/nvtx/analyzer/src/stats.rs
  • integrations/nvtx/analyzer/src/tables.rs
  • integrations/nvtx/analyzer/tests/fixtures/mod.rs
  • integrations/nvtx/analyzer/tests/pushpop.rs
  • integrations/nvtx/analyzer/tests/reconstruction.rs
  • integrations/nvtx/analyzer/tests/resolution.rs
  • integrations/nvtx/analyzer/tests/resource.rs
  • integrations/nvtx/analyzer/tests/roundtrip.rs
  • integrations/nvtx/analyzer/tests/stats.rs

@johanpel johanpel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @9prady9, this is shaping up nicely. Reviewed the non-test parts to begin with:

Comment thread integrations/nvtx/analyzer/src/lib.rs Outdated
Comment thread integrations/nvtx/analyzer/src/lib.rs Outdated
Comment thread integrations/nvtx/analyzer/src/model.rs Outdated
Comment thread integrations/nvtx/analyzer/src/model.rs
Comment on lines +177 to +178
// The domain on a `RangeEnd` is redundant: `range_id` is
// process-globally unique, so it alone identifies the range.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thought about process-global uniqueness. Quent is designed to work for distributed systems too. If multiple processes are using NVTX, we need to ensure all NVTX data is properly attributed to that scope. This could be solved at a higher layer than what NvtxModel will provide so this PR doesn't necessarily need changes right now, but it is food for thought.

For our immediate purpose of instrumenting query engines there's already a "worker" entity that is supposed to represent a "process". One thought here is to represent the entire injection library as an "entity" so it gets its own unique id, and it would have two events:

  • one once-event with an entity reference to the entity representing the process (e.g. in the query engine case the worker entity).
  • one multi-event to push nvtx events

This way in QE analysis we can construct one NvtxModel per "worker".

Opened this issue: #522 to address external sources referencing OS id-ed things in general

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, leaving as-is — one NvtxModel covers one process's id spaces, so a distributed capture builds one per worker. Thanks for filing #522.

"{} nvtx resource(s) were never destroyed; closing them at trace end ({trace_end})",
leaked.len()
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same — drain_unclosed, end: None. A handle recreated while still open now also ends None, which reads like any other missing close, so the reuse is counted in ReconstructionAnomalies::reused_resource_handles (and reused_range_ids for the range equivalent).

Comment thread integrations/nvtx/analyzer/src/span.rs
Comment thread integrations/nvtx/analyzer/src/span.rs Outdated
Comment thread integrations/nvtx/analyzer/src/stats.rs Outdated
Comment thread integrations/nvtx/analyzer/src/tables.rs Outdated
Reconstructs a captured NVTX event stream into an in-memory model of
spans, marks, domains, threads and categories. Off the shared analysis
framework, which is what keeps zero-duration and never-closed spans
representable.

Two passes: order by timestamp and learn every handle registration, then
replay against it. Arrival order and forward references stop mattering.

Correlation keys, each chosen because the alternative fails silently
rather than loudly: `range_id` alone for RangeStart/End, a per-(thread,
domain) stack for Push/Pop, the bare handle for resources since the
destroy carries no domain, per-domain keying for registered strings and
category names.

Incomplete pairs are represented, not guessed, and nothing is
substituted for what the stream never said. An open with no close keeps
everything the open stated and ends `None`, leaving it to the consuming
analysis to decide whether to bound it and at what. A close with no open
yields no span at all — the closing events carry only a correlation key,
with no name or attributes to build one from — and is counted in
`ReconstructionAnomalies`, along with any key reused while still open.
Range statistics therefore cover observed closes only.

Kind-conditional fields live in `SpanKind` variant data: the thread and
parent on `PushPop`, the identifier type on `Resource`. A start/end
range with a thread, or a resource with a parent, is unrepresentable
rather than merely undocumented.

Trace bounds are retained on the model, for a consumer that wants a
right edge for a span that has none. The largest span end is not a
substitute, since the last event may carry no span at all.

Tested against hand-built streams, plus a feature-gated roundtrip over a
real in-process capture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Pradeep Garigipati <pgarigipati@nvidia.com>
@9prady9
9prady9 marked this pull request as ready for review August 6, 2026 10:53
@9prady9
9prady9 requested a review from johanpel August 6, 2026 10:53
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
integrations/nvtx/analyzer/src/tables.rs (1)

225-235: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider including the domain in the unregistered-string placeholder.

unregistered_string_name renders only the handle. unnamed_category_name renders the domain and the id. Registered strings key on (domain, handle), as the module docs state at lines 15-17. Two different domains that reference the same unregistered handle therefore produce identical labels. Adding the domain keeps the placeholder policy consistent with the keying rule.

♻️ Proposed change
-/// Placeholder for a registered-string handle that was never registered.
-fn unregistered_string_name(handle: u64) -> String {
-    format!("<unregistered string 0x{handle:X}>")
-}
+/// Placeholder for a registered-string handle that was never registered.
+///
+/// The domain is in the label because registered strings key on
+/// `(domain, handle)`.
+fn unregistered_string_name(domain: u64, handle: u64) -> String {
+    format!("<unregistered string 0x{handle:X} @ domain 0x{domain:X}>")
+}
-                .unwrap_or_else(|| unregistered_string_name(*handle)),
+                .unwrap_or_else(|| unregistered_string_name(domain, *handle)),

Note: tests/resolution.rs line 198 asserts the current label, so update that assertion too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/nvtx/analyzer/src/tables.rs` around lines 225 - 235, Update
resolve_message for unregistered RegisteredHandle values to generate
placeholders containing both domain and handle, consistent with the (domain,
handle) lookup key and unnamed_category_name; update the affected
tests/resolution.rs assertion to expect the domain-inclusive label.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@integrations/nvtx/analyzer/src/tables.rs`:
- Around line 225-235: Update resolve_message for unregistered RegisteredHandle
values to generate placeholders containing both domain and handle, consistent
with the (domain, handle) lookup key and unnamed_category_name; update the
affected tests/resolution.rs assertion to expect the domain-inclusive label.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: fa377211-de16-4d5b-9046-fdd3ef8b3576

📥 Commits

Reviewing files that changed from the base of the PR and between 92778bb and 52f4403.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
📒 Files selected for processing (17)
  • Cargo.toml
  • integrations/nvtx/analyzer/Cargo.toml
  • integrations/nvtx/analyzer/src/anomalies.rs
  • integrations/nvtx/analyzer/src/lib.rs
  • integrations/nvtx/analyzer/src/model.rs
  • integrations/nvtx/analyzer/src/ranges.rs
  • integrations/nvtx/analyzer/src/resource.rs
  • integrations/nvtx/analyzer/src/span.rs
  • integrations/nvtx/analyzer/src/stats.rs
  • integrations/nvtx/analyzer/src/tables.rs
  • integrations/nvtx/analyzer/tests/fixtures/mod.rs
  • integrations/nvtx/analyzer/tests/pushpop.rs
  • integrations/nvtx/analyzer/tests/reconstruction.rs
  • integrations/nvtx/analyzer/tests/resolution.rs
  • integrations/nvtx/analyzer/tests/resource.rs
  • integrations/nvtx/analyzer/tests/roundtrip.rs
  • integrations/nvtx/analyzer/tests/stats.rs

@johanpel johanpel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look great, nice work @9prady9! Can't wait to see this in action.

`nvtxDomainMarkEx` reports no thread, so `NvtxMark::thread_id` was set to
`None` at its one construction site and read nowhere. An `Option` that is
structurally never `Some` invites a consumer to branch on it.

The same rule the span kinds now follow: a field that cannot be
populated does not exist, rather than existing and being documented as
empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Pradeep Garigipati <pgarigipati@nvidia.com>
@9prady9

9prady9 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

/merge

@rapids-bot
rapids-bot Bot merged commit 5db3659 into rapidsai:main Aug 6, 2026
20 checks passed
@9prady9
9prady9 deleted the nvtx-phase-2-pr branch August 6, 2026 11:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 2: Reconstruct NVTX Ranges (Model & Tolerant Analyzer)

2 participants