Skip to main content

Crate tracing_calltree

Crate tracing_calltree 

Source
Expand description

§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 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:

#[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:

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 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:

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:

[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.

Structs§

CallTree
CallTreeBuilder
CallTreeLayer
CallTreeSnapshot
InternalStats
NodeSnapshot
TimingStats