Low-cost generation of performance percentiles (p50, p90, p99, p99.9, etc.).
- Introduction
- How It Works
- Performance & Trade-offs
- Installation
- Components
- Examples
- Project Information
p99 is a lightweight, low-overhead library designed for generating real-time performance percentiles in high-frequency or latency-sensitive environments.
p99.Rust is the Rust implementation.
Histogram is a low-overhead, zero-allocation, fixed-size structure designed to track event durations (typically in nanoseconds) using 64 logarithmic buckets.
- Logarithmic Bucketing: The bucket boundaries are spaced as powers of two:
- Bucket
0represents[0, 1]nanoseconds; - Bucket
1represents[2, 3]nanoseconds; - Bucket
2represents[4, 7]nanoseconds; - Bucket
irepresents[2^i, 2^(i+1) - 1]nanoseconds.
- Bucket
- Branchless Indexing: Finding the correct bucket index for an incoming duration is extremely fast and branchless. It is computed in a few CPU instructions using the CPU's leading-zeros count intrinsic (
u64::leading_zeros). - Linear Interpolation: Percentile queries iterate through the buckets to find the target rank and perform linear interpolation within the matching bucket to approximate the exact percentile duration.
- Zero Allocation:
Histogramdoes not allocate memory on the heap during creation, event insertion, or percentile queries. It is a compact (~576-byte) structure that can reside entirely on the stack or be embedded in other structures. - Ultra-Low Latency Insertion: Recording a latency measurement (
push_event_time_ns) takes approximately 11 nanoseconds (about 35 CPU cycles on modern hardware). - Blazing-Fast Queries: Querying percentiles (such as
value_at_p99()) takes only 11 to 17 nanoseconds, depending on the distribution of events across the buckets. - Instruction-Cache Friendly: The query methods are designed with a "thin caller / heavy worker" pattern to prevent instruction-cache bloat and maintain high CPU cache locality under real-world workloads.
-
Logarithmic Precision: To achieve zero allocation and constant-time operations,
Histogramsacrifices exact precision. It does not store individual event times. Instead, values are grouped into logarithmic buckets. - Approximation: Percentile values are approximated using linear interpolation within the bucket boundaries. For very large values, the bucket width is wider, which leads to a wider approximation range. However, for low-latency performance measurements where precision is needed most (the lower nanosecond ranges), the buckets are extremely narrow (e.g., 1ns, 2ns, 4ns wide), providing exceptional resolution.
-
binary-scalingAccuracy: When thebinary-scalingfeature is enabled, the percentile target rank is computed using a$2^{32}$ fixed-point approximation. The pre-encoded multiplier for each percentile (e.g.,3_865_470_566 >> 32≈0.9000for p90) differs from the true decimal value by less than$10^{-9}$ , which is far below the approximation error introduced by the logarithmic bucketing itself. In practice this has no measurable impact on percentile accuracy.
Reference in Cargo.toml in the usual way:
p99 = { version = "0" }To enable the optional binary-scaling optimization:
p99 = { version = "0", features = ["binary-scaling"] }No public constants are defined at this time.
No public enumerations are defined at this time.
The following crate features are available:
-
binary-scaling(opt-in): Replaces integer division in the integer-based percentile methods (value_at_p90(),value_at_p95(),value_at_p99(), etc.) with$2^{32}$ fixed-point binary scaling. Each percentile multiplier (e.g.,0.90for p90) is pre-encoded as au32constant and the target rank is computed via a single multiplication and a 32-bit right-shift, avoiding the cost of integer division entirely. This yields a ~1.5x to 2x speedup for percentile queries with a negligible loss of accuracy (the scaled multiplier differs from the true value by less than$10^{-9}$ ). The genericvalue_at_percentile(f64)method is unaffected by this feature. -
null-feature(opt-in): A no-op feature that has no effect on the compiled library. It exists to simplify driver scripts and CI pipelines that conditionally pass--featuresflags, allowing a feature list to always be present even when no real features are needed.
Add the feature in your Cargo.toml:
[dependencies]
p99 = { version = "0", features = ["binary-scaling"] }Or, when building from the command line:
# Default (standard integer division)
cargo run --example build_histogram
# With binary scaling enabled
cargo run --example build_histogram --features binary-scalingMeasured with criterion on 100k events (Apple M-series, release profile). Only the integer-based percentile methods are affected; the generic value_at_percentile(f64) method is unchanged.
| Method | Default | binary-scaling |
Improvement |
|---|---|---|---|
value_at_p90() |
23.25 ns | 21.63 ns | -7.0% |
value_at_p99() (dense) |
16.52 ns | 14.88 ns | -10.4% |
value_at_p99() (wide) |
23.39 ns | 21.55 ns | -7.9% |
value_at_p99_99() |
23.32 ns | 21.64 ns | -7.2% |
Methods using simple fractional multipliers (p50 = 1/2, p75 = 3/4) already compile to bit-shifts without this feature, so they show no change.
No public functions are defined at this time.
No public macros are defined at this time.
The following public structures are defined in the current version:
A low-cost, zero-allocation, 64-bucket logarithmic histogram designed for recording event durations in nanoseconds and querying high-resolution percentiles.
pub struct Histogram {
// ... private fields ...
}Here is a simple example of how to initialize a Histogram, record event times, and query percentiles:
use p99::Histogram;
use std::time::Duration;
fn main() {
// 1. Initialize a default histogram (zero-allocated, ~576 bytes on the stack)
let mut histogram = Histogram::default();
// 2. Record event times using various units
histogram.push_event_time_ns(150); // 150 ns
histogram.push_event_time_us(5); // 5 microseconds (5,000 ns)
histogram.push_event_time_ms(10); // 10 milliseconds (10,000,000 ns)
histogram.push_event_duration(Duration::from_nanos(250));
// 3. Retrieve basic statistics
assert_eq!(histogram.event_count(), 4);
assert_eq!(histogram.min_event_time(), Some(150));
assert_eq!(histogram.max_event_time(), Some(10_000_000));
assert_eq!(histogram.event_time_total(), Some(10_005_400));
// 4. Query percentiles
// Generic floating-point query (e.g., 90th percentile)
if let Some(p90) = histogram.value_at_percentile(90.0) {
println!("p90: {} ns", p90);
}
// Fast, optimized integer-based percentile wrappers
if let Some(p50) = histogram.value_at_p50() {
println!("p50 (median): {} ns", p50);
}
if let Some(p99) = histogram.value_at_p99() {
println!("p99: {} ns", p99);
}
}No public traits are defined at this time.
An example program showing Histogram usage is provided in examples/build_histogram.rs.
It simulates a histogram of event times generated by std::thread::sleep delays under a custom PRNG.
The number of iterations can be configured via the P99_TRIES environment variable:
# Run with the default of 100 tries
cargo run --example build_histogram
# Run with 1000 tries
P99_TRIES=1000 cargo run --example build_histogram
# Run with binary-scaling enabled (faster percentile queries)
cargo run --example build_histogram --features binary-scalingDefect reports, feature requests, and pull requests are welcome on https://github.com/synesissoftware/p99.Rust.
p99.Rust has no (non-development) dependencies.
Crates upon which p99.Rust has development dependencies:
p99.Rust is released under the 3-clause BSD license. See LICENSE for details.