Skip to content

8040: Allow impl FnMut() in taskdumps instead of just fn()#94

Open
martin-augment wants to merge 4 commits intomasterfrom
pr-8040-2026-04-13-20-16-46
Open

8040: Allow impl FnMut() in taskdumps instead of just fn()#94
martin-augment wants to merge 4 commits intomasterfrom
pr-8040-2026-04-13-20-16-46

Conversation

@martin-augment
Copy link
Copy Markdown
Owner

8040: To review by AI

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

Walkthrough

The changes refactor the task tracing system to support mutable closures in callbacks instead of static function pointers, and to pass trace data through function parameters rather than relying on thread-local storage. The trace_with public API now accepts a generic closure parameter alongside a leaf callback closure. Internally, the trace leaf callback storage was converted to use non-null pointers to dynamically-dispatched trait objects. The capture function was updated to create an in-memory trace and pass it directly to callbacks, eliminating the previous thread-local context management pattern. Test utilities were similarly adjusted to accept mutable trace accumulators via closure parameters.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-8040-2026-04-13-20-16-46

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the task tracing infrastructure to support FnMut closures instead of requiring function pointers. By updating trace_with and internal context management to handle dynamic closures via NonNull, the implementation eliminates the need for several thread-local variables in both the runtime and tests. Feedback indicates that the documentation and examples for trace_with should be updated to reflect this new capability and the more idiomatic usage of closures for state capture.

/// # }
/// ```
pub fn trace_with<F, R>(f: F, trace_leaf: fn(&TraceMeta)) -> R
pub fn trace_with<FN, FT, R>(f: FN, mut trace_leaf: FT) -> R
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The documentation and example for trace_with (lines 170-211) are now outdated. They still state that trace_leaf must be a function pointer (fn) and demonstrate using thread-locals for communication. Since the API now supports FnMut, the documentation should be updated to reflect that closures can be used to capture state directly, which is more idiomatic and simplifies the usage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

value:useful; category:documentation; feedback: The Gemini AI reviewer is correct! The way the function works has changed - it no more needs the two extra thread locals and its signature changed a bit. Its documentation should be updated to show how to use it now. Prevents confusion in the readers about its usage

@claude
Copy link
Copy Markdown

claude bot commented Apr 13, 2026

Code Review

This PR is a solid improvement — eliminating the auxiliary thread-locals (TRACE_CONTEXT, TRACE_WITH_LOG) makes the code meaningfully simpler and the trace_with API far more ergonomic. The core unsafe strategy (lifetime erasure + drop-guard cleanup) is correct. Here is my detailed feedback.


Stale documentation (required fix)

The docstring for trace_with in tokio/src/runtime/task/trace/mod.rs (lines 170–211) still describes the old API:

trace_leaf is a function pointer (fn) rather than a closure (Fn) because it must be stored in thread-local state via a Cell. Use thread-locals to communicate between the callback and calling code (see example below).

This is now wrong on two counts:

  1. The parameter is now impl FnMut, not fn.
  2. Thread-locals are no longer needed — that is the whole point of the PR.

The embedded example still shows the old thread-local pattern (LEAF_COUNT static, bare fn my_trace_leaf). It should be updated to show the closure-based approach, e.g.:

let mut count = 0u32;
Trace::root(std::future::poll_fn(|cx| {
    trace_with(|| { let _ = fut.as_mut().poll(cx); }, |_meta| count += 1);
    Poll::Ready(())
})).await;
assert!(count > 0);

Safety analysis of the transmute in trace_with

The soundness argument is:

  1. A mutable reference to the local trace_leaf is cast to *mut dyn FnMut with its real (short) lifetime, then transmuted to 'static.
  2. The _restore defer guard clears this pointer from the thread-local before trace_leaf is dropped (Rust drops locals in reverse declaration order: _restoretrace_leaf).

This is correct, but the proof depends critically on drop order. A comment naming the drop-order invariant would prevent future refactoring from accidentally breaking it. Consider adding:

// `_restore` must be declared after `trace_leaf_dyn` and before `trace_leaf`
// so that it drops (and clears the thread-local) before `trace_leaf` is freed.

Subtle behavioral change in is_tracing()

Now try_with_current_trace_leaf_fn temporarily sets the cell to None during the call. This means is_tracing() is slightly heavier than it needs to be — it acquires the thread-local context, does the cell swap, and creates a defer guard just to discard a (). A lighter alternative that avoids the temporary clear:

pub(crate) fn is_tracing() -> bool {
    // SAFETY: only reads the trace_leaf_fn cell, does not modify the frame list.
    unsafe {
        Self::try_with_current(|ctx| ctx.trace_leaf_fn.get().is_some())
            .unwrap_or(false)
    }
}

This matches the spirit of the old implementation and is a pure read with no side effects.


Clippy allow vs. type alias

#[allow(clippy::type_complexity)]
trace_leaf_fn: Cell<Option<NonNull<dyn FnMut(&TraceMeta)>>>,

A private type alias would be cleaner and avoids silencing the lint:

type TraceLeafFn = NonNull<dyn FnMut(&TraceMeta)>;
// ...
trace_leaf_fn: Cell<Option<TraceLeafFn>>,

Minor: dropped #[inline(never)] on trace_impl::trace_leaf

The old trace_impl::trace_leaf had #[inline(never)]. It is no longer used as a function pointer (the boundary address detection uses mod.rs::trace_leaf), so dropping the attribute is fine — just worth calling out intentionally so it is not questioned later.


Positive changes

  • Removing TRACE_CONTEXT and TRACE_WITH_LOG thread-locals is a clear win: less global state, no "action at a distance" between the call site and the callback.
  • The re-entrancy protection in try_with_current_trace_leaf_fn (storing None while calling, restoring afterward) is well-thought-out and correctly documented.
  • The test simplification (this.logs.lock().unwrap().extend(logs) vs the old TRACE_WITH_LOG drain) is much more readable.
  • Nested trace_with calls (save/restore of the old pointer) correctly handle the case where the outer closure is still alive when the inner one exits.
  • The drop-order-based safety of the transmute is correct as written.

Summary: The implementation is sound but the documentation must be updated before merging — the current docs actively mislead callers into thinking they need thread-locals. The is_tracing() and type-alias points are suggestions rather than blockers.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tokio/src/runtime/task/trace/mod.rs`:
- Around line 212-215: Update the rustdoc for trace_with to reflect the new
callback model: replace references to a stand-alone fn callback and any
thread-local communication workaround with a description that trace_with accepts
a closure (FT: FnMut(&TraceMeta)) which is invoked directly with a &TraceMeta
for the current trace, and that callers should use the provided TraceMeta
argument rather than thread-local state; ensure the doc comments around the
trace_with function mention the exact signature (trace_with and TraceMeta) and
explain the intended usage and ownership/borrowing semantics of the &TraceMeta
parameter.
- Around line 147-150: The current is_tracing() calls
try_with_current_trace_leaf_fn which clears the trace slot when invoking a
callback, causing is_tracing() to return false when called from inside an active
leaf callback; update is_tracing() so it checks presence without
consuming/clearing the slot (e.g., call a non-consuming accessor such as a
ref-based helper like try_with_current_trace_leaf_ref_fn or add a new
try_peek_current_trace_leaf() that returns Some(()) without clearing) so
Handle::is_tracing() correctly returns true during active callbacks; keep the
public is_tracing() signature but replace the call to
try_with_current_trace_leaf_fn with the non-consuming check.

In `@tokio/src/runtime/task/trace/trace_impl.rs`:
- Around line 27-42: The code currently always calls
trace.push_backtrace(frames) even when TraceMeta::root_addr is None, producing
an empty backtrace; fix by only recording the backtrace when meta.root_addr is
present — move the trace.push_backtrace(frames) call into the if let
Some(root_addr) = meta.root_addr { ... } block (the block that uses
backtrace::trace and builds frames based on meta.trace_leaf_addr and ptr::eq),
so you only push frames when a root_addr was used to collect them.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 27c934f4-c4c8-4246-b004-0e386d8e462e

📥 Commits

Reviewing files that changed from the base of the PR and between fe4f928 and c6dbad2.

📒 Files selected for processing (3)
  • tokio/src/runtime/task/trace/mod.rs
  • tokio/src/runtime/task/trace/trace_impl.rs
  • tokio/tests/task_trace_self.rs

Comment on lines 147 to 150
/// Produces `true` if the current task is being traced; otherwise false.
pub(crate) fn is_tracing() -> bool {
Self::try_with_current_trace_leaf_fn(|maybe_trace_leaf| maybe_trace_leaf.get().is_some())
.unwrap_or(false)
Self::try_with_current_trace_leaf_fn(|_| ()).is_some()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'Context::is_tracing\s*\(' --type rust

Repository: martin-augment/tokio

Length of output: 468


🏁 Script executed:

#!/bin/bash
# Search for all calls to Handle::is_tracing()
rg -n 'Handle::is_tracing\s*\(\)' --type rust

Repository: martin-augment/tokio

Length of output: 125


🏁 Script executed:

#!/bin/bash
# Search for all references to is_tracing in the codebase
rg -n 'is_tracing' --type rust | head -30

Repository: martin-augment/tokio

Length of output: 362


🏁 Script executed:

#!/bin/bash
# Examine the try_with_current_trace_leaf_fn implementation to confirm slot clearing
rg -n -A 10 'try_with_current_trace_leaf_fn' tokio/src/runtime/task/trace/mod.rs | head -40

Repository: martin-augment/tokio

Length of output: 1409


🏁 Script executed:

#!/bin/bash
# Check the context of the is_tracing() call in the test
sed -n '100,130p' tokio/tests/dump.rs

Repository: martin-augment/tokio

Length of output: 835


🏁 Script executed:

#!/bin/bash
# Confirm the trace_with implementation timing - when is the callback invoked relative to task polling
cat -n tokio/src/runtime/task/trace/mod.rs | sed -n '290,330p'

Repository: martin-augment/tokio

Length of output: 1799


Audit Context::is_tracing() behavior with active callbacks.

The slot-clearing in try_with_current_trace_leaf_fn means is_tracing() now returns false when called from within an active leaf callback. The test at tokio/tests/dump.rs:116 (complete_during_trace()) relies on Handle::is_tracing() returning true during tracing to avoid deadlocks; this test will now fail to detect active tracing and futures will remain pending instead of completing as designed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tokio/src/runtime/task/trace/mod.rs` around lines 147 - 150, The current
is_tracing() calls try_with_current_trace_leaf_fn which clears the trace slot
when invoking a callback, causing is_tracing() to return false when called from
inside an active leaf callback; update is_tracing() so it checks presence
without consuming/clearing the slot (e.g., call a non-consuming accessor such as
a ref-based helper like try_with_current_trace_leaf_ref_fn or add a new
try_peek_current_trace_leaf() that returns Some(()) without clearing) so
Handle::is_tracing() correctly returns true during active callbacks; keep the
public is_tracing() signature but replace the call to
try_with_current_trace_leaf_fn with the non-consuming check.

Comment on lines +212 to +215
pub fn trace_with<FN, FT, R>(f: FN, mut trace_leaf: FT) -> R
where
F: FnOnce() -> R,
FN: FnOnce() -> R,
FT: FnMut(&TraceMeta),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update the rustdoc for the new callback model.

The docs above trace_with still describe a fn callback plus thread-local communication. After this signature change, the public API docs are teaching the workaround this PR is removing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tokio/src/runtime/task/trace/mod.rs` around lines 212 - 215, Update the
rustdoc for trace_with to reflect the new callback model: replace references to
a stand-alone fn callback and any thread-local communication workaround with a
description that trace_with accepts a closure (FT: FnMut(&TraceMeta)) which is
invoked directly with a &TraceMeta for the current trace, and that callers
should use the provided TraceMeta argument rather than thread-local state;
ensure the doc comments around the trace_with function mention the exact
signature (trace_with and TraceMeta) and explain the intended usage and
ownership/borrowing semantics of the &TraceMeta parameter.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

value:useful; category:documentation; feedback: The CodeRabbit AI reviewer is correct! The way the function works has changed - it no more needs the two extra thread locals and its signature changed a bit. Its documentation should be updated to show how to use it now. Prevents confusion in the readers about its usage

Comment on lines +27 to +42
if let Some(root_addr) = meta.root_addr {
backtrace::trace(|frame| {
let below_root = !ptr::eq(frame.symbol_address(), root_addr);

if ptr::eq(frame.symbol_address(), meta.trace_leaf_addr) {
above_leaf = true;
}
if above_leaf && below_root {
frames.push(frame.to_owned().into());
}

below_root
});
if ptr::eq(frame.symbol_address(), meta.trace_leaf_addr) {
above_leaf = true;
}
collector.push_backtrace(frames);
state.collector.set(Some(collector));
}
});

below_root
});
}
trace.push_backtrace(frames);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Skip recording a trace when root_addr is missing.

TraceMeta::root_addr is optional, but this version always appends frames afterward. That turns “no trace captured” into “one empty backtrace captured”, which can surface phantom entries for callers that reach trace_leaf outside a Trace::root boundary.

Proposed fix
 pub(crate) fn trace_leaf(meta: &TraceMeta, trace: &mut Trace) {
     let mut frames: Vec<backtrace::BacktraceFrame> = vec![];
     let mut above_leaf = false;
 
     if let Some(root_addr) = meta.root_addr {
         backtrace::trace(|frame| {
             let below_root = !ptr::eq(frame.symbol_address(), root_addr);
 
             if above_leaf && below_root {
                 frames.push(frame.to_owned().into());
             }
 
             if ptr::eq(frame.symbol_address(), meta.trace_leaf_addr) {
                 above_leaf = true;
             }
 
             below_root
         });
+
+        trace.push_backtrace(frames);
     }
-    trace.push_backtrace(frames);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(root_addr) = meta.root_addr {
backtrace::trace(|frame| {
let below_root = !ptr::eq(frame.symbol_address(), root_addr);
if ptr::eq(frame.symbol_address(), meta.trace_leaf_addr) {
above_leaf = true;
}
if above_leaf && below_root {
frames.push(frame.to_owned().into());
}
below_root
});
if ptr::eq(frame.symbol_address(), meta.trace_leaf_addr) {
above_leaf = true;
}
collector.push_backtrace(frames);
state.collector.set(Some(collector));
}
});
below_root
});
}
trace.push_backtrace(frames);
pub(crate) fn trace_leaf(meta: &TraceMeta, trace: &mut Trace) {
let mut frames: Vec<backtrace::BacktraceFrame> = vec![];
let mut above_leaf = false;
if let Some(root_addr) = meta.root_addr {
backtrace::trace(|frame| {
let below_root = !ptr::eq(frame.symbol_address(), root_addr);
if above_leaf && below_root {
frames.push(frame.to_owned().into());
}
if ptr::eq(frame.symbol_address(), meta.trace_leaf_addr) {
above_leaf = true;
}
below_root
});
trace.push_backtrace(frames);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tokio/src/runtime/task/trace/trace_impl.rs` around lines 27 - 42, The code
currently always calls trace.push_backtrace(frames) even when
TraceMeta::root_addr is None, producing an empty backtrace; fix by only
recording the backtrace when meta.root_addr is present — move the
trace.push_backtrace(frames) call into the if let Some(root_addr) =
meta.root_addr { ... } block (the block that uses backtrace::trace and builds
frames based on meta.trace_leaf_addr and ptr::eq), so you only push frames when
a root_addr was used to collect them.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

value:useful; category:bug; feedback: The CodeRabbit AI reviewer is correct! There is no need to push the empty frames if there is no root_addr. Prevents useless allocation of the frames Vec

@augmentcode
Copy link
Copy Markdown

augmentcode bot commented Apr 13, 2026

🤖 Augment PR Summary

Summary: This PR expands taskdump tracing to accept FnMut callbacks (closures) instead of requiring a plain fn pointer.

Changes:

  • Store the trace-leaf callback in the runtime trace context as a `NonNull<dyn FnMut(&TraceMeta)>` rather than `fn(&TraceMeta)`.
  • Add a helper (`Context::try_with_current_trace_leaf_fn`) that temporarily clears the TLS slot to provide exclusive `&mut` access to the callback and prevent re-entrant borrows.
  • Update `trace_with` to accept any `FnMut(&TraceMeta)` and install/uninstall it around the provided closure using a deferred restore.
  • Adjust `trace_leaf` to call the new helper and return `Pending` only when tracing is active.
  • Refactor the default backtrace collector to pass `&mut Trace` through the callback instead of using a separate TLS collector.
  • Update taskdump self-tests to use closures capturing local state (no per-test TLS log).

Technical Notes: The new callback storage relies on temporarily extending the callback’s lifetime while it is installed in Tokio’s thread-local trace context, and on clearing the slot while invoking the callback to enforce exclusivity.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

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

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

/// # }
/// ```
pub fn trace_with<F, R>(f: F, trace_leaf: fn(&TraceMeta)) -> R
pub fn trace_with<FN, FT, R>(f: FN, mut trace_leaf: FT) -> R
Copy link
Copy Markdown

@augmentcode augmentcode bot Apr 13, 2026

Choose a reason for hiding this comment

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

The doc comment above trace_with still says the trace-leaf callback must be a fn stored in TLS via a Cell, but trace_with now accepts FnMut closures stored via a raw pointer; consider updating the docs/example to reflect the new API and its safety invariants.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

value:useful; category:documentation; feedback: The Augment AI reviewer is correct! The way the function works has changed - it no more needs the two extra thread locals and its signature changed a bit. Its documentation should be updated to show how to use it now. Prevents confusion in the readers about its usage

@martin-augment
Copy link
Copy Markdown
Owner Author

Stale documentation (required fix)

The docstring for trace_with in tokio/src/runtime/task/trace/mod.rs (lines 170–211) still describes the old API:

trace_leaf is a function pointer (fn) rather than a closure (Fn) because it must be stored in thread-local state via a Cell. Use thread-locals to communicate between the callback and calling code (see example below).

This is now wrong on two counts:

  1. The parameter is now impl FnMut, not fn.
  2. Thread-locals are no longer needed — that is the whole point of the PR.

The embedded example still shows the old thread-local pattern (LEAF_COUNT static, bare fn my_trace_leaf). It should be updated to show the closure-based approach, e.g.:

let mut count = 0u32;
Trace::root(std::future::poll_fn(|cx| {
    trace_with(|| { let _ = fut.as_mut().poll(cx); }, |_meta| count += 1);
    Poll::Ready(())
})).await;
assert!(count > 0);

value:useful; category:documentation; feedback: The Claude AI reviewer is correct! The way the function works has changed - it no more needs the two extra thread locals and its signature changed a bit. Its documentation should be updated to show how to use it now. Prevents confusion in the readers about its usage

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.

2 participants