From ed967a2aa8aef527c73df7ee70d9564045eed9db Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Thu, 10 Sep 2026 00:35:44 +0200 Subject: [PATCH] bench(profiling): measure allocator thread CPU time Wall-clock allocator benchmarks charged host descheduling to nanosecond-scale sampler paths. The no-profiler case also used System, so libc allocator variance obscured the semaphore gate it was intended to protect. Measure scheduled thread CPU time, retire the old wall-time series IDs, and use the no-op allocator for the short-circuit regression. On Linux ARM64 with the CI sampling settings, five fresh-process medians spanned 6.2% for system allocation and 9.7% for the no-op fast path. The short-circuit case reported 2 ns in every run. The forced-sample path still spanned 12.2%. Benchmark-only; no production code changes. --- Cargo.lock | 1 + libdd-profiling-heap-allocator/Cargo.toml | 3 + .../benches/sampler_overhead.rs | 109 ++++++++++++------ 3 files changed, 77 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94c8432f00..c5b545465b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3375,6 +3375,7 @@ name = "libdd-profiling-heap-allocator" version = "1.0.0" dependencies = [ "criterion", + "libc", "libdd-profiling-heap-sampler", ] diff --git a/libdd-profiling-heap-allocator/Cargo.toml b/libdd-profiling-heap-allocator/Cargo.toml index 9708a0d28c..b5b652a3b2 100644 --- a/libdd-profiling-heap-allocator/Cargo.toml +++ b/libdd-profiling-heap-allocator/Cargo.toml @@ -26,6 +26,9 @@ libdd-profiling-heap-sampler = { version = "1.0.0", path = "../libdd-profiling-h [dev-dependencies] criterion.workspace = true +[target.'cfg(target_os = "linux")'.dev-dependencies] +libc.workspace = true + [[bench]] name = "sampler_overhead" harness = false diff --git a/libdd-profiling-heap-allocator/benches/sampler_overhead.rs b/libdd-profiling-heap-allocator/benches/sampler_overhead.rs index 924ee4a191..3c77bba26f 100644 --- a/libdd-profiling-heap-allocator/benches/sampler_overhead.rs +++ b/libdd-profiling-heap-allocator/benches/sampler_overhead.rs @@ -18,7 +18,9 @@ mod linux_bench { use libdd_profiling_heap_sampler::{dd_test_set_profiler_active, dd_tl_state_get_or_init}; use std::alloc::{GlobalAlloc, Layout, System}; use std::hint::black_box; + use std::mem::MaybeUninit; use std::ptr; + use std::time::Duration; const SIZES: &[usize] = &[16, 64, 256, 4096, 65_536]; const ALIGN: usize = 8; @@ -28,6 +30,30 @@ mod linux_bench { static mut NOOP_BUFFER: AlignedBuffer = AlignedBuffer([0; 128 * 1024]); + // Do not attribute time when this benchmark thread is descheduled to the sampler. + fn thread_cpu_time() -> Duration { + let mut time = MaybeUninit::::uninit(); + let result = + unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, time.as_mut_ptr()) }; + assert_eq!( + result, + 0, + "read thread CPU time: {}", + std::io::Error::last_os_error() + ); + let time = unsafe { time.assume_init() }; + Duration::new(time.tv_sec as u64, time.tv_nsec as u32) + } + + #[inline] + fn measure_thread_cpu_time(iterations: u64, mut routine: impl FnMut()) -> Duration { + let start = thread_cpu_time(); + for _ in 0..iterations { + routine(); + } + thread_cpu_time() - start + } + struct NoopAllocator; unsafe impl GlobalAlloc for NoopAllocator { @@ -91,14 +117,16 @@ mod linux_bench { // Pure system allocator cost with no sampler in the picture. fn bench_system_alloc_free(c: &mut Criterion) { - let mut group = c.benchmark_group("alloc_free/system"); + let mut group = c.benchmark_group("thread_cpu/alloc_free/system"); for &size in SIZES { let layout = Layout::from_size_align(size, ALIGN).unwrap(); group.bench_with_input(BenchmarkId::from_parameter(size), &layout, |b, &layout| { - b.iter(|| unsafe { - let ptr = System.alloc(layout); - black_box(ptr); - System.dealloc(ptr, layout); + b.iter_custom(|iterations| { + measure_thread_cpu_time(iterations, || unsafe { + let ptr = System.alloc(layout); + black_box(ptr); + System.dealloc(ptr, layout); + }) }); }); } @@ -119,16 +147,18 @@ mod linux_bench { fn bench_fast_path_system(c: &mut Criterion) { let alloc = SampledAllocator::new(System); - let mut group = c.benchmark_group("profiler_attached/fast_path_system"); + let mut group = c.benchmark_group("thread_cpu/profiler_attached/fast_path_system"); unsafe { dd_test_set_profiler_active(true) }; for &size in SIZES { let layout = Layout::from_size_align(size, ALIGN).unwrap(); group.bench_with_input(BenchmarkId::from_parameter(size), &layout, |b, &layout| { unsafe { pin_sampler_to_fast_path() }; - b.iter(|| unsafe { - let ptr = alloc.alloc(layout); - black_box(ptr); - alloc.dealloc(ptr, layout); + b.iter_custom(|iterations| { + measure_thread_cpu_time(iterations, || unsafe { + let ptr = alloc.alloc(layout); + black_box(ptr); + alloc.dealloc(ptr, layout); + }) }); }); } @@ -138,16 +168,18 @@ mod linux_bench { fn bench_fast_path_noop(c: &mut Criterion) { let alloc = SampledAllocator::new(NoopAllocator); - let mut group = c.benchmark_group("profiler_attached/fast_path_noop"); + let mut group = c.benchmark_group("thread_cpu/profiler_attached/fast_path_noop"); unsafe { dd_test_set_profiler_active(true) }; for &size in SIZES { let layout = Layout::from_size_align(size, ALIGN).unwrap(); group.bench_with_input(BenchmarkId::from_parameter(size), &layout, |b, &layout| { unsafe { pin_sampler_to_fast_path() }; - b.iter(|| unsafe { - let ptr = alloc.alloc(layout); - black_box(ptr); - alloc.dealloc(ptr, layout); + b.iter_custom(|iterations| { + measure_thread_cpu_time(iterations, || unsafe { + let ptr = alloc.alloc(layout); + black_box(ptr); + alloc.dealloc(ptr, layout); + }) }); }); } @@ -157,16 +189,18 @@ mod linux_bench { fn bench_slow_path_system(c: &mut Criterion) { let alloc = SampledAllocator::new(System); - let mut group = c.benchmark_group("profiler_attached/slow_path_system"); + let mut group = c.benchmark_group("thread_cpu/profiler_attached/slow_path_system"); unsafe { dd_test_set_profiler_active(true) }; for &size in SIZES { let layout = Layout::from_size_align(size, ALIGN).unwrap(); group.bench_with_input(BenchmarkId::from_parameter(size), &layout, |b, &layout| { - b.iter(|| unsafe { - force_next_allocation_to_sample(); - let ptr = alloc.alloc(layout); - black_box(ptr); - alloc.dealloc(ptr, layout); + b.iter_custom(|iterations| { + measure_thread_cpu_time(iterations, || unsafe { + force_next_allocation_to_sample(); + let ptr = alloc.alloc(layout); + black_box(ptr); + alloc.dealloc(ptr, layout); + }) }); }); } @@ -176,16 +210,18 @@ mod linux_bench { fn bench_slow_path_noop(c: &mut Criterion) { let alloc = SampledAllocator::new(NoopAllocator); - let mut group = c.benchmark_group("profiler_attached/slow_path_noop"); + let mut group = c.benchmark_group("thread_cpu/profiler_attached/slow_path_noop"); unsafe { dd_test_set_profiler_active(true) }; for &size in SIZES { let layout = Layout::from_size_align(size, ALIGN).unwrap(); group.bench_with_input(BenchmarkId::from_parameter(size), &layout, |b, &layout| { - b.iter(|| unsafe { - force_next_allocation_to_sample(); - let ptr = alloc.alloc(layout); - black_box(ptr); - alloc.dealloc(ptr, layout); + b.iter_custom(|iterations| { + measure_thread_cpu_time(iterations, || unsafe { + force_next_allocation_to_sample(); + let ptr = alloc.alloc(layout); + black_box(ptr); + alloc.dealloc(ptr, layout); + }) }); }); } @@ -195,21 +231,22 @@ mod linux_bench { // ── Short-circuit regression (semaphore OFF) ───────────────────────── // Single benchmark with the semaphore off (no profiler attached). - // The semaphore check in dd_allocation_requested short-circuits before - // any TLS access or sampling logic. This validates that the - // short-circuit path stays near-zero cost. + // The no-op allocator keeps the system allocator from obscuring the + // semaphore check this benchmark is intended to validate. fn bench_short_circuit(c: &mut Criterion) { - let alloc = SampledAllocator::new(System); - let mut group = c.benchmark_group("no_profiler/short_circuit"); + let alloc = SampledAllocator::new(NoopAllocator); + let mut group = c.benchmark_group("thread_cpu/no_profiler/short_circuit_noop"); // Semaphore is off by default - don't flip it on. for &size in SIZES { let layout = Layout::from_size_align(size, ALIGN).unwrap(); group.bench_with_input(BenchmarkId::from_parameter(size), &layout, |b, &layout| { - b.iter(|| unsafe { - let ptr = alloc.alloc(layout); - black_box(ptr); - alloc.dealloc(ptr, layout); + b.iter_custom(|iterations| { + measure_thread_cpu_time(iterations, || unsafe { + let ptr = alloc.alloc(layout); + black_box(ptr); + alloc.dealloc(ptr, layout); + }) }); }); }