tracing-calltree 0.1.2

Always-on hierarchical profiling for Rust tracing spans with rolling latency statistics.
Documentation
# 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.