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 — 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);