322 lines
12 KiB
Rust
322 lines
12 KiB
Rust
use criterion::{criterion_group, criterion_main, Criterion};
|
|
use opentelemetry::{metrics::MeterProvider as _, Key, KeyValue};
|
|
use opentelemetry_sdk::metrics::{Instrument, ManualReader, SdkMeterProvider, Stream, Temporality};
|
|
|
|
// Run this benchmark with:
|
|
// cargo bench --bench bound_instruments --features metrics,experimental_metrics_custom_reader,experimental_metrics_bound_instruments,spec_unstable_metrics_views
|
|
//
|
|
// Apple M4 Max, 16 cores (12 performance + 4 efficiency), macOS 15.4
|
|
//
|
|
// Results (3 attributes: method, status, path):
|
|
// Counter_Unbound_Delta time: [50.20 ns]
|
|
// Counter_Bound_Delta time: [ 1.92 ns] ~26x faster
|
|
// Counter_Bound_With_View_Delta time: [ 1.82 ns] view filter applied at bind, not on hot path
|
|
// Counter_Bound_AtOverflow_Delta time: [ 1.82 ns] bind() at cardinality limit binds directly to the overflow
|
|
// tracker — perf parity with a normal bind, no per-call resolution
|
|
// UpDownCounter_Unbound_Cumulative time: [53.08 ns] UpDownCounter always uses Cumulative regardless of reader pref
|
|
// UpDownCounter_Bound_Cumulative time: [ 1.91 ns] ~28x faster
|
|
// Gauge_Unbound_Delta time: [52.82 ns]
|
|
// Gauge_Bound_Delta time: [ 1.27 ns] ~42x faster (LastValue store is cheaper than atomic add)
|
|
// Histogram_Unbound_Delta time: [60.15 ns]
|
|
// Histogram_Bound_Delta time: [ 6.61 ns] ~9.1x faster
|
|
// Histogram_Bound_AtOverflow_Delta time: [ 6.58 ns] perf parity with a normal bind
|
|
// Counter_Bound_Multithread/2 time: [21.59 µs] (100 adds/thread)
|
|
// Counter_Bound_Multithread/4 time: [37.21 µs] (100 adds/thread)
|
|
// Counter_Bound_Multithread/8 time: [71.70 µs] (100 adds/thread)
|
|
//
|
|
// Note: criterion does not fail CI on regression by itself. These numbers are
|
|
// reference values for human review; use `cargo criterion --baseline` locally
|
|
// if you need automated comparison against a saved baseline.
|
|
|
|
fn create_provider(temporality: Temporality) -> SdkMeterProvider {
|
|
let reader = ManualReader::builder()
|
|
.with_temporality(temporality)
|
|
.build();
|
|
SdkMeterProvider::builder().with_reader(reader).build()
|
|
}
|
|
|
|
fn bench_bound_instruments(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("BoundInstruments");
|
|
group.sample_size(100);
|
|
|
|
let attrs = [
|
|
KeyValue::new("method", "GET"),
|
|
KeyValue::new("status", "200"),
|
|
KeyValue::new("path", "/api/v1/users"),
|
|
];
|
|
|
|
// Counter: Unbound vs Bound (Delta)
|
|
{
|
|
let provider = create_provider(Temporality::Delta);
|
|
let meter = provider.meter("bench");
|
|
let counter = meter.u64_counter("unbound").build();
|
|
group.bench_function("Counter_Unbound_Delta", |b| {
|
|
b.iter(|| counter.add(1, &attrs));
|
|
});
|
|
}
|
|
|
|
{
|
|
let provider = create_provider(Temporality::Delta);
|
|
let meter = provider.meter("bench");
|
|
let counter = meter.u64_counter("bound").build();
|
|
let bound = counter.bind(&attrs);
|
|
group.bench_function("Counter_Bound_Delta", |b| {
|
|
b.iter(|| bound.add(1));
|
|
});
|
|
}
|
|
|
|
// Counter: Bound with a View filter — confirms the filter is applied at
|
|
// bind() time and the hot path stays free of attribute processing.
|
|
{
|
|
let view = |i: &opentelemetry_sdk::metrics::Instrument| {
|
|
if i.name() == "bound_with_view" {
|
|
Stream::builder()
|
|
.with_allowed_attribute_keys(vec![
|
|
Key::new("method"),
|
|
Key::new("status"),
|
|
Key::new("path"),
|
|
])
|
|
.build()
|
|
.ok()
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
let reader = ManualReader::builder()
|
|
.with_temporality(Temporality::Delta)
|
|
.build();
|
|
let provider = SdkMeterProvider::builder()
|
|
.with_reader(reader)
|
|
.with_view(view)
|
|
.build();
|
|
let meter = provider.meter("bench");
|
|
let counter = meter.u64_counter("bound_with_view").build();
|
|
let bound = counter.bind(&attrs);
|
|
group.bench_function("Counter_Bound_With_View_Delta", |b| {
|
|
b.iter(|| bound.add(1));
|
|
});
|
|
}
|
|
|
|
// Counter: Bound at overflow — confirms that binding when the cardinality
|
|
// limit is exhausted yields the same hot-path performance as a normal bind
|
|
// (writes go directly to the overflow tracker, no per-call resolution).
|
|
{
|
|
let cardinality_limit = 4;
|
|
let view = move |i: &Instrument| {
|
|
if i.name() == "bound_at_overflow" {
|
|
Stream::builder()
|
|
.with_cardinality_limit(cardinality_limit)
|
|
.build()
|
|
.ok()
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
let reader = ManualReader::builder()
|
|
.with_temporality(Temporality::Delta)
|
|
.build();
|
|
let provider = SdkMeterProvider::builder()
|
|
.with_reader(reader)
|
|
.with_view(view)
|
|
.build();
|
|
let meter = provider.meter("bench");
|
|
let counter = meter.u64_counter("bound_at_overflow").build();
|
|
// Saturate cardinality with unbound calls so bind() lands in overflow.
|
|
for i in 0..cardinality_limit {
|
|
counter.add(1, &[KeyValue::new("filler", i as i64)]);
|
|
}
|
|
let bound = counter.bind(&attrs);
|
|
group.bench_function("Counter_Bound_AtOverflow_Delta", |b| {
|
|
b.iter(|| bound.add(1));
|
|
});
|
|
}
|
|
|
|
// UpDownCounter: Unbound vs Bound (always Cumulative regardless of reader pref)
|
|
{
|
|
let provider = create_provider(Temporality::Cumulative);
|
|
let meter = provider.meter("bench");
|
|
let updown = meter.i64_up_down_counter("unbound_updown").build();
|
|
group.bench_function("UpDownCounter_Unbound_Cumulative", |b| {
|
|
b.iter(|| updown.add(1, &attrs));
|
|
});
|
|
}
|
|
|
|
{
|
|
let provider = create_provider(Temporality::Cumulative);
|
|
let meter = provider.meter("bench");
|
|
let updown = meter.i64_up_down_counter("bound_updown").build();
|
|
let bound = updown.bind(&attrs);
|
|
group.bench_function("UpDownCounter_Bound_Cumulative", |b| {
|
|
b.iter(|| bound.add(1));
|
|
});
|
|
}
|
|
|
|
// Gauge: Unbound vs Bound (Delta)
|
|
{
|
|
let provider = create_provider(Temporality::Delta);
|
|
let meter = provider.meter("bench");
|
|
let gauge = meter.u64_gauge("unbound_gauge").build();
|
|
group.bench_function("Gauge_Unbound_Delta", |b| {
|
|
b.iter(|| gauge.record(1, &attrs));
|
|
});
|
|
}
|
|
|
|
{
|
|
let provider = create_provider(Temporality::Delta);
|
|
let meter = provider.meter("bench");
|
|
let gauge = meter.u64_gauge("bound_gauge").build();
|
|
let bound = gauge.bind(&attrs);
|
|
group.bench_function("Gauge_Bound_Delta", |b| {
|
|
b.iter(|| bound.record(1));
|
|
});
|
|
}
|
|
|
|
// Histogram: Unbound vs Bound (Delta)
|
|
{
|
|
let provider = create_provider(Temporality::Delta);
|
|
let meter = provider.meter("bench");
|
|
let histogram = meter.f64_histogram("unbound_hist").build();
|
|
group.bench_function("Histogram_Unbound_Delta", |b| {
|
|
b.iter(|| histogram.record(1.5, &attrs));
|
|
});
|
|
}
|
|
|
|
{
|
|
let provider = create_provider(Temporality::Delta);
|
|
let meter = provider.meter("bench");
|
|
let histogram = meter.f64_histogram("bound_hist").build();
|
|
let bound = histogram.bind(&attrs);
|
|
group.bench_function("Histogram_Bound_Delta", |b| {
|
|
b.iter(|| bound.record(1.5));
|
|
});
|
|
}
|
|
|
|
// Histogram: Bound at overflow — same property as the counter version.
|
|
{
|
|
let cardinality_limit = 4;
|
|
let view = move |i: &Instrument| {
|
|
if i.name() == "bound_hist_at_overflow" {
|
|
Stream::builder()
|
|
.with_cardinality_limit(cardinality_limit)
|
|
.build()
|
|
.ok()
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
let reader = ManualReader::builder()
|
|
.with_temporality(Temporality::Delta)
|
|
.build();
|
|
let provider = SdkMeterProvider::builder()
|
|
.with_reader(reader)
|
|
.with_view(view)
|
|
.build();
|
|
let meter = provider.meter("bench");
|
|
let histogram = meter.f64_histogram("bound_hist_at_overflow").build();
|
|
for i in 0..cardinality_limit {
|
|
histogram.record(1.5, &[KeyValue::new("filler", i as i64)]);
|
|
}
|
|
let bound = histogram.bind(&attrs);
|
|
group.bench_function("Histogram_Bound_AtOverflow_Delta", |b| {
|
|
b.iter(|| bound.record(1.5));
|
|
});
|
|
}
|
|
|
|
// Multi-threaded bound counter
|
|
for num_threads in [2, 4, 8] {
|
|
let provider = create_provider(Temporality::Delta);
|
|
let meter = provider.meter("bench");
|
|
let counter = meter.u64_counter("mt_bound").build();
|
|
let bound = counter.bind(&attrs);
|
|
|
|
group.bench_function(format!("Counter_Bound_Multithread/{num_threads}"), |b| {
|
|
b.iter(|| {
|
|
std::thread::scope(|s| {
|
|
for _ in 0..num_threads {
|
|
s.spawn(|| {
|
|
for _ in 0..100 {
|
|
bound.add(1);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// Multi-threaded bound up-down counter (atomic add on i64)
|
|
for num_threads in [2, 4, 8] {
|
|
let provider = create_provider(Temporality::Cumulative);
|
|
let meter = provider.meter("bench");
|
|
let updown = meter.i64_up_down_counter("mt_bound_updown").build();
|
|
let bound = updown.bind(&attrs);
|
|
|
|
group.bench_function(
|
|
format!("UpDownCounter_Bound_Multithread/{num_threads}"),
|
|
|b| {
|
|
b.iter(|| {
|
|
std::thread::scope(|s| {
|
|
for _ in 0..num_threads {
|
|
s.spawn(|| {
|
|
for _ in 0..100 {
|
|
bound.add(1);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
// Multi-threaded bound histogram (Mutex<Buckets> — expected to show
|
|
// contention quickly relative to atomic-based instruments).
|
|
for num_threads in [2, 4, 8] {
|
|
let provider = create_provider(Temporality::Delta);
|
|
let meter = provider.meter("bench");
|
|
let histogram = meter.f64_histogram("mt_bound_hist").build();
|
|
let bound = histogram.bind(&attrs);
|
|
|
|
group.bench_function(format!("Histogram_Bound_Multithread/{num_threads}"), |b| {
|
|
b.iter(|| {
|
|
std::thread::scope(|s| {
|
|
for _ in 0..num_threads {
|
|
s.spawn(|| {
|
|
for _ in 0..100 {
|
|
bound.record(1.5);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// Multi-threaded bound gauge (LastValue: atomic store, no read-modify-write)
|
|
for num_threads in [2, 4, 8] {
|
|
let provider = create_provider(Temporality::Delta);
|
|
let meter = provider.meter("bench");
|
|
let gauge = meter.u64_gauge("mt_bound_gauge").build();
|
|
let bound = gauge.bind(&attrs);
|
|
|
|
group.bench_function(format!("Gauge_Bound_Multithread/{num_threads}"), |b| {
|
|
b.iter(|| {
|
|
std::thread::scope(|s| {
|
|
for _ in 0..num_threads {
|
|
s.spawn(|| {
|
|
for _ in 0..100 {
|
|
bound.record(1);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
criterion_group!(benches, bench_bound_instruments);
|
|
criterion_main!(benches);
|