Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 70 additions & 22 deletions tokio/src/runtime/task/trace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ pub(crate) struct Context {
/// The function that is invoked at each leaf future inside of Tokio
///
/// For example, within tokio::time:sleep, sockets. etc.
trace_leaf_fn: Cell<Option<fn(&TraceMeta)>>,
#[allow(clippy::type_complexity)]
trace_leaf_fn: Cell<Option<NonNull<dyn FnMut(&TraceMeta)>>>,
}

/// A [`Frame`] in an intrusive, doubly-linked tree of [`Frame`]s.
Expand Down Expand Up @@ -114,19 +115,38 @@ impl Context {
}
}

/// Calls the provided closure if we are being traced.
fn try_with_current_trace_leaf_fn<F, R>(f: F) -> Option<R>
where
F: FnOnce(&Cell<Option<fn(&TraceMeta)>>) -> R,
F: for<'a> FnOnce(&'a mut dyn FnMut(&TraceMeta)) -> R,
{
let mut ret = None;

let inner = |context: &Context| {
if let Some(mut trace_leaf_fn) = context.trace_leaf_fn.get() {
context.trace_leaf_fn.set(None);
let _restore = defer(move || {
context.trace_leaf_fn.set(Some(trace_leaf_fn));
});

// SAFETY: The trace leaf fn is valid for the duration in which it's stored in the
// context. Furthermore, re-entrant calls are not possible since we store `None` for
// the duration in which we hold a mutable reference, so access is exclusive for that
// duration.
ret = Some(f(unsafe { trace_leaf_fn.as_mut() }));
}
};

// SAFETY: This call can only access the trace_leaf_fn field, so it cannot
// break the trace frame linked list.
unsafe { Self::try_with_current(|context| f(&context.trace_leaf_fn)) }
unsafe { Self::try_with_current(inner) };

ret
}

/// 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()
}
Comment on lines 147 to 150
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.

}

Expand Down Expand Up @@ -189,20 +209,46 @@ pub struct TraceMeta {
/// assert!(count > 0);
/// # }
/// ```
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

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

where
F: FnOnce() -> R,
FN: FnOnce() -> R,
FT: FnMut(&TraceMeta),
Comment on lines +212 to +215
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

{
// store our new trace_leaf function
let previous =
Context::try_with_current_trace_leaf_fn(|current| current.replace(Some(trace_leaf)));
let trace_leaf_dyn = (&mut trace_leaf) as &mut (dyn FnMut(&TraceMeta) + '_);
// SAFETY: The raw pointer is removed from the thread local before `trace_leaf` is dropped, so
// this transmute cannot lead to the violation of any lifetime requirements.
let trace_leaf_dyn = unsafe {
std::mem::transmute::<
*mut (dyn FnMut(&TraceMeta) + '_),
*mut (dyn FnMut(&TraceMeta) + 'static),
>(trace_leaf_dyn)
};
// SAFETY: Pointer comes from reference, so not null.
let trace_leaf_dyn = unsafe { NonNull::new_unchecked(trace_leaf_dyn) };

let mut old_trace_leaf_fn = None;

// Even if this access fails, that's okay. In that case, we still call the closure without
// actually performing any tracing.
//
// SAFETY: This call can only access the trace_leaf_fn field, so it cannot
// break the trace frame linked list.
unsafe {
Context::try_with_current(|ctx| {
old_trace_leaf_fn = ctx.trace_leaf_fn.replace(Some(trace_leaf_dyn));
})
};

// restore previous on drop. This is ensures state remains consistent
// even if the trace_leaf function panics
let _restore = defer(move || {
if let Some(previous) = previous {
Context::try_with_current_trace_leaf_fn(|current| current.set(previous));
}
// This ensures that `trace_leaf_fn` cannot be accessed after this call returns.
//
// SAFETY: This call can only access the trace_leaf_fn field, so it cannot
// break the trace frame linked list.
unsafe {
Context::try_with_current(|ctx| {
ctx.trace_leaf_fn.set(old_trace_leaf_fn);
})
};
});

f()
Expand Down Expand Up @@ -249,13 +295,14 @@ impl Trace {
// internal implementation details of this crate).
#[inline(never)]
pub(crate) fn trace_leaf(cx: &mut task::Context<'_>) -> Poll<()> {
let trace_leaf_fn = Context::try_with_current_trace_leaf_fn(|cell| cell.get()).flatten();
if let Some(trace_leaf_fn) = trace_leaf_fn {
let root_addr = Context::current_frame_addr();

let ret = Context::try_with_current_trace_leaf_fn(|leaf_fn| {
let meta = TraceMeta {
root_addr: Context::current_frame_addr(),
root_addr,
trace_leaf_addr: trace_leaf as *const c_void,
};
trace_leaf_fn(&meta);
leaf_fn(&meta);

// Use the same logic that `yield_now` uses to send out wakeups after
// the task yields.
Expand All @@ -268,10 +315,11 @@ pub(crate) fn trace_leaf(cx: &mut task::Context<'_>) -> Poll<()> {
}
}
});
});

Poll::Pending
} else {
Poll::Ready(())
match ret {
Some(()) => Poll::Pending,
None => Poll::Ready(()),
}
}

Expand Down
73 changes: 21 additions & 52 deletions tokio/src/runtime/task/trace/trace_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,42 @@
//!
//! This implementation may eventually be extracted into a separate `tokio-taskdump` crate.

use std::{cell::Cell, ptr};
use std::ptr;

use crate::runtime::task::trace::{trace_with, Trace, TraceMeta};

use super::defer;

/// Thread local state used to communicate between calling the trace and the interior `trace_leaf` function
struct TraceContext {
collector: Cell<Option<Trace>>,
}

thread_local! {
static TRACE_CONTEXT: TraceContext = const {
TraceContext {
collector: Cell::new(None),
}
};
}

/// Capture using the default `backtrace::trace`-based implementation.
#[inline(never)]
pub(super) fn capture<F, R>(f: F) -> (R, Trace)
where
F: FnOnce() -> R,
{
let collector = Trace::empty();

let previous = TRACE_CONTEXT.with(|state| state.collector.replace(Some(collector)));
let mut trace = Trace::empty();

// restore previous collector on drop even if the callback panics
let _restore = defer(move || {
TRACE_CONTEXT.with(|state| state.collector.set(previous));
});
let result = trace_with(f, |meta| trace_leaf(meta, &mut trace));

let result = trace_with(f, trace_leaf);

// take the collector before _restore runs
let collector = TRACE_CONTEXT.with(|state| state.collector.take()).unwrap();

(result, collector)
(result, trace)
}

/// Capture a backtrace via `backtrace::trace` and collect it into `STATE`
#[inline(never)]
pub(crate) fn trace_leaf(meta: &TraceMeta) {
TRACE_CONTEXT.with(|state| {
if let Some(mut collector) = state.collector.take() {
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);
/// Capture a backtrace via `backtrace::trace` and collect it into `trace`.
pub(crate) fn trace_leaf(meta: &TraceMeta, trace: &mut Trace) {
let mut frames: Vec<backtrace::BacktraceFrame> = vec![];
let mut above_leaf = false;

if above_leaf && below_root {
frames.push(frame.to_owned().into());
}
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);
Comment on lines +27 to +42
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

}
73 changes: 33 additions & 40 deletions tokio/tests/task_trace_self.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,48 +109,41 @@ async fn task_trace_self() {

/// Collect frames between `trace_leaf_for_test` and `root_addr` using
/// `backtrace::trace`, resolve them, and store pretty-printed symbol names
/// (with compiler hashes stripped) into `TRACE_WITH_LOG`.
/// (with compiler hashes stripped) into `logs`.
#[inline(never)]
fn trace_leaf_for_test(meta: &TraceMeta) {
TRACE_WITH_LOG.with(|log| {
let mut frames: Vec<backtrace::BacktraceFrame> = vec![];
let mut above_leaf = false;
fn trace_leaf_for_test(meta: &TraceMeta, log: &mut Vec<Vec<String>>) {
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 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 above_leaf && below_root {
frames.push(frame.to_owned().into());
}

if ptr::eq(frame.symbol_address(), meta.trace_leaf_addr) {
above_leaf = true;
}
if ptr::eq(frame.symbol_address(), meta.trace_leaf_addr) {
above_leaf = true;
}

below_root
});
}
below_root
});
}

// Resolve frames into human-readable symbol names with hashes stripped.
let mut bt = backtrace::Backtrace::from(frames);
bt.resolve();
let mut names = vec![];
for frame in bt.frames() {
for symbol in frame.symbols() {
if let Some(name) = symbol.name() {
names.push(strip_symbol_hash(&format!("{name}")).to_owned());
}
// Resolve frames into human-readable symbol names with hashes stripped.
let mut bt = backtrace::Backtrace::from(frames);
bt.resolve();
let mut names = vec![];
for frame in bt.frames() {
for symbol in frame.symbols() {
if let Some(name) = symbol.name() {
names.push(strip_symbol_hash(&format!("{name}")).to_owned());
}
}
}

log.borrow_mut().push(names);
});
}

thread_local! {
static TRACE_WITH_LOG: std::cell::RefCell<Vec<Vec<String>>> =
const { std::cell::RefCell::new(vec![]) };
log.push(names);
}

/// Strip the trailing `::h<hex>` hash that rustc appends to symbol names.
Expand Down Expand Up @@ -197,25 +190,25 @@ impl<F: Future> Future for TaskDump<F> {
Poll::Pending => {}
};

let mut logs = Vec::new();

// Tracing poll with a noop waker. If the future is at a yield
// point, trace_leaf fires our callback and returns Pending. We discard
// the result — this poll is purely for capturing the backtrace.
let noop = futures::task::noop_waker();
let mut noop_cx = Context::from_waker(&noop);
let logs = this.logs.clone();
let trace_poll = trace_with(|| this.f.as_mut().poll(&mut noop_cx), trace_leaf_for_test);
let trace_poll = trace_with(
|| this.f.as_mut().poll(&mut noop_cx),
|meta| trace_leaf_for_test(meta, &mut logs),
);
// trace should always produce poll pending
assert!(
matches!(trace_poll, Poll::Pending),
"expected trace to produce Poll::Pending but it was ready"
);

// Drain any frames captured by trace_leaf_for_test into our log.
TRACE_WITH_LOG.with(|tl| {
let mut tl = tl.borrow_mut();
let mut dest = logs.lock().unwrap();
dest.append(&mut tl);
});
this.logs.lock().unwrap().extend(logs);
Poll::Pending
}
}
Expand Down
Loading