tracing-calltree
Always-on hierarchical profiling for Rust tracing applications.
tracing-calltree turns tracing spans into a live performance call tree, keeping rolling statistics for recent invocations of every path.
handle_request avg=81ms p95=143ms n=100
├── authenticate avg=3ms p95=7ms n=100
└── load_context avg=61ms p95=119ms n=100
├── cache_lookup avg=2ms p95=4ms n=100
└── database avg=57ms p95=112ms n=100
├── acquire_connection avg=34ms p95=91ms n=100
└── query avg=21ms p95=39ms n=100
It is designed for long-running applications where you want to answer:
Where has this application been spending time recently?
No profiler process, collector, exporter, async runtime, or external service is required.
Quick start
use CallTree;
use *;
let calltree = builder
.window_size
.build;
registry
.with
.init;
// Run instrumented application code...
let snapshot = calltree.snapshot;
println!;
Use ordinary tracing spans to define meaningful operations:
async
async
async
The span ancestry becomes the call tree automatically:
handle_request
└── load_context
└── query_database
Rolling statistics
Each node retains measurements for its most recent completed invocations. The default rolling window is 100 samples and can be configured with CallTree::builder().
For each node, tracing-calltree reports:
- minimum duration
- maximum duration
- mean duration
- p95 duration
- number of retained samples
- lifetime call count
This makes the tree reflect recent application behavior while still tracking how often each operation has executed over the lifetime of the profiler.
Wall, active, and suspended time
Every invocation records three related timing views.
Wall time is elapsed time from the span's first entry until it closes. It includes execution, asynchronous waits, scheduling delays, and other periods during which the operation remains incomplete.
Active time is accumulated while the span is entered. For an instrumented async future, this includes the periods during which the future is being polled.
Suspended time is: wall time - active time
This can help distinguish an operation that spends most of its latency actively executing from one that spends most of its lifetime waiting or suspended.
Active time is not CPU time.
Why tracing-calltree?
Existing observability and profiling tools answer related but different questions.
tracing-timing is well suited to aggregating timing measurements into latency distributions. tracing-calltree instead preserves the parent/child structure of spans and maintains recent statistics at every path in that hierarchy.
tracing-tracy and full profilers provide substantially deeper execution analysis, timelines, sampling, and visualization. tracing-calltree is deliberately smaller and designed to remain available continuously inside the application.
Metrics systems are excellent for exporting counters, gauges, and histograms. tracing-calltree instead retains the semantic hierarchy:
request
└── context
└── database
└── query
rather than flattening those operations into independent metric names.
The tools are complementary: a call tree can identify which semantic operation has recently become expensive, while a full profiler can then investigate exactly why.
Async behavior
tracing-calltree follows normal tracing span semantics and does not depend on a particular async runtime.
Use #[tracing::instrument] for async functions or tracing::Instrument for local futures:
use Instrument;
async
// create a future and instrument it; do not `.await` at top level in examples
let fut = async .instrument;
Do not hold a synchronous Span::enter() guard across an .await; use normal tracing future instrumentation instead.
Snapshots
The profiler exposes its current state as an owned structured snapshot:
use CallTree;
let calltree = new;
let snapshot = calltree.snapshot;
Snapshots are independent of the profiler's internal synchronization and can be rendered, serialized, exposed through an application diagnostics interface, or processed however the caller chooses.
Serialization support is optional:
[]
= { = "0.1", = ["serde"] }
Scope
tracing-calltree deliberately stops at collection and structured snapshots.
It does not provide:
- an HTTP or gRPC server
- a telemetry collector
- a metrics exporter
- persistent storage
- CPU sampling
- execution timelines
- distributed tracing
- HDR histograms
Those capabilities can be layered on top of the snapshot API or provided by complementary observability tools.
Minimum supported Rust version
tracing-calltree requires Rust 1.85 or newer.
License
Licensed under either of:
- Apache License, Version 2.0
- MIT License
at your option.