# tracing-calltree
**Always-on hierarchical profiling for Rust [`tracing`](https://crates.io/crates/tracing) applications.**
`tracing-calltree` turns tracing spans into a live performance call tree, keeping rolling statistics for recent invocations of every path.
```text
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
```rust
use tracing_calltree::CallTree;
use tracing_subscriber::prelude::*;
let calltree = CallTree::builder()
.window_size(100)
.build();
tracing_subscriber::registry()
.with(calltree.layer())
.init();
// Run instrumented application code...
let snapshot = calltree.snapshot();
println!("{}", snapshot.display());
```
Use ordinary `tracing` spans to define meaningful operations:
```rust
#[tracing::instrument(skip_all)]
async fn handle_request() {
load_context().await;
}
#[tracing::instrument(skip_all)]
async fn load_context() {
query_database().await;
}
#[tracing::instrument(skip_all)]
async fn query_database() {
// ...
}
```
The span ancestry becomes the call tree automatically:
```text
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:
```text
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:
```rust,no_run
use tracing::Instrument;
async fn query_database() {}
// create a future and instrument it; do not `.await` at top level in examples
let fut = async {
query_database().await;
}.instrument(tracing::info_span!("database_query"));
```
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:
```rust
use tracing_calltree::CallTree;
let calltree = 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:
```toml
[dependencies]
tracing-calltree = { version = "0.1", features = ["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.