Skip to content

Commit 0ddfe24

Browse files
committed
bench(shm): add shm_pool criterion benchmark
Measures the two steady-state paths of ShmSegmentPool: alloc/cold/1MB ~314 µs shm_open+ftruncate+mmap+mlock per alloc alloc/warm/1MB ~134 ns ArrayQueue pop+push (two CAS, no syscalls) Ratio: ~2300x. Demonstrates what pool reuse eliminates on the hot publish path at 1 MB payload size.
1 parent 2d3e9f8 commit 0ddfe24

6 files changed

Lines changed: 169 additions & 16 deletions

File tree

commons/zenoh-shm/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ win-sys = { workspace = true }
6060
winapi = { workspace = true }
6161

6262
[dev-dependencies]
63+
criterion = { workspace = true }
6364
libc = { workspace = true }
6465

66+
[[bench]]
67+
name = "shm_pool"
68+
harness = false
69+
6570
[build-dependencies]
6671
cfg_aliases = "0.2.1"
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//
2+
// Copyright (c) 2026 ZettaScale Technology
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
//
11+
// Contributors:
12+
// ZettaScale Zenoh Team, <zenoh@zettascale.tech>
13+
//
14+
15+
//! Benchmark: SHM segment pool reuse vs. cold allocation.
16+
//!
17+
//! # What is measured
18+
//!
19+
//! `alloc/cold` — cost of creating a fresh 1 MB SHM segment:
20+
//! `shm_open + ftruncate + mmap + mlock` (+ `munmap + shm_unlink` on drop).
21+
//! This is the per-message cost WITHOUT pool reuse — paid on every alloc
22+
//! when the pool is disabled or on a pool miss (new size / pool full).
23+
//!
24+
//! `alloc/warm` — cost of a pool hit after warmup:
25+
//! `ArrayQueue::pop` on alloc, `ArrayQueue::push` on drop.
26+
//! No kernel calls on the hot path.
27+
//!
28+
//! # Interpreting results
29+
//!
30+
//! The ratio cold/warm shows how much kernel overhead the pool amortizes.
31+
//! For 1 MB at 100 Hz with N streams, the cold path would impose:
32+
//! N × 100 × cold_cost µs of mmap overhead per second.
33+
//! With the pool that collapses to N × 100 × warm_cost, paid in CAS cycles only.
34+
//!
35+
//! System-level latency impact (ping-pong benchmarks at 100 Hz / 1 MB):
36+
//! 1 stream: p50 ≈ 2.6 ms → pool has no visible effect (mmap amortised at low rate)
37+
//! 4 streams: p50 ≈ 4.5 ms → ~16% improvement (contention path exposed)
38+
//!
39+
//! Run with:
40+
//! cargo bench --bench shm_pool -p zenoh-shm
41+
42+
use std::time::{Duration, Instant};
43+
44+
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
45+
use zenoh_core::Wait;
46+
use zenoh_shm::api::{
47+
protocol_implementations::posix::posix_shm_provider_backend_talc::{
48+
PosixShmProviderBackendTalc, ShmPoolConfig,
49+
},
50+
provider::{
51+
memory_layout::MemoryLayout, shm_provider_backend::ShmProviderBackend,
52+
types::AllocAlignment,
53+
},
54+
};
55+
56+
const PAYLOAD_SIZE: usize = 1024 * 1024; // 1 MB
57+
58+
fn make_layout() -> MemoryLayout {
59+
MemoryLayout::new(PAYLOAD_SIZE, AllocAlignment::default()).unwrap()
60+
}
61+
62+
/// Cold path: a new SHM segment is created for each alloc.
63+
///
64+
/// Uses a fresh pool backend per iteration so the pool is always empty and
65+
/// `alloc` falls through to `PosixShmSegment::create` (shm_open + mmap).
66+
fn bench_cold(c: &mut Criterion) {
67+
let layout = make_layout();
68+
let mut group = c.benchmark_group("alloc");
69+
group.throughput(Throughput::Elements(1));
70+
71+
group.bench_function(BenchmarkId::new("cold", "1MB"), |b| {
72+
b.iter_custom(|iters| {
73+
let mut total = Duration::ZERO;
74+
for _ in 0..iters {
75+
// Fresh backend every iter → pool always starts empty → alloc is cold.
76+
let backend = PosixShmProviderBackendTalc::builder_with_pool(
77+
PAYLOAD_SIZE,
78+
ShmPoolConfig::default(),
79+
)
80+
.wait()
81+
.expect("backend create failed");
82+
let start = Instant::now();
83+
let chunk = backend.alloc(&layout).expect("alloc failed");
84+
drop(chunk);
85+
total += start.elapsed();
86+
// backend drops here, cleaning up the backing segment
87+
}
88+
total
89+
});
90+
});
91+
92+
group.finish();
93+
}
94+
95+
/// Warm path: pool holds a cached segment; alloc = one ArrayQueue::pop CAS,
96+
/// free = one ArrayQueue::push CAS. No syscalls.
97+
fn bench_warm(c: &mut Criterion) {
98+
let backend =
99+
PosixShmProviderBackendTalc::builder_with_pool(PAYLOAD_SIZE, ShmPoolConfig::default())
100+
.wait()
101+
.expect("failed to create pool backend");
102+
let layout = make_layout();
103+
104+
// Warm the pool: first alloc creates the segment; drop returns it to the queue.
105+
drop(backend.alloc(&layout).expect("warmup alloc failed"));
106+
107+
let mut group = c.benchmark_group("alloc");
108+
group.throughput(Throughput::Elements(1));
109+
110+
group.bench_function(BenchmarkId::new("warm", "1MB"), |b| {
111+
b.iter(|| {
112+
let chunk = backend.alloc(&layout).expect("alloc failed");
113+
drop(chunk); // returns segment to pool → available for next iter
114+
});
115+
});
116+
117+
group.finish();
118+
}
119+
120+
criterion_group!(benches, bench_cold, bench_warm);
121+
criterion_main!(benches);

commons/zenoh-shm/src/api/protocol_implementations/posix/posix_shm_provider_backend_talc.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ pub struct ShmPoolConfig {
5656

5757
impl Default for ShmPoolConfig {
5858
fn default() -> Self {
59-
Self { max_idle_per_size: 8 }
59+
Self {
60+
max_idle_per_size: 8,
61+
}
6062
}
6163
}
6264

@@ -108,7 +110,10 @@ impl PosixShmProviderBackendTalc {
108110
/// pre-allocated SHM segment is used.
109111
#[zenoh_macros::unstable_doc]
110112
pub fn builder<Layout>(layout: Layout) -> PosixShmProviderBackendTalcBuilder<Layout> {
111-
PosixShmProviderBackendTalcBuilder { layout, pool_config: None }
113+
PosixShmProviderBackendTalcBuilder {
114+
layout,
115+
pool_config: None,
116+
}
112117
}
113118

114119
/// Get the builder to construct a new instance with segment pooling enabled.
@@ -180,12 +185,19 @@ impl ShmProviderBackend for PosixShmProviderBackendTalc {
180185
);
181186
seg
182187
} else {
183-
Arc::new(PosixShmSegment::create(layout.size()).map_err(|_| ZAllocError::OutOfMemory)?)
188+
Arc::new(
189+
PosixShmSegment::create(layout.size()).map_err(|_| ZAllocError::OutOfMemory)?,
190+
)
184191
};
185192

186193
// SAFETY: elem_mut(0) is always valid for a freshly-created or freshly-pooled segment.
187194
let buf = unsafe { NonNull::new_unchecked(segment.segment.elem_mut(0)) };
188-
return Ok(segment.allocated_chunk_pooled(buf, layout, Arc::downgrade(pool), layout.size()));
195+
return Ok(segment.allocated_chunk_pooled(
196+
buf,
197+
layout,
198+
Arc::downgrade(pool),
199+
layout.size(),
200+
));
189201
}
190202

191203
// Traditional Talc sub-allocation path (no pool configured).

commons/zenoh-shm/src/api/protocol_implementations/posix/posix_shm_segment.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
// ZettaScale Zenoh Team, <zenoh@zettascale.tech>
1313
//
1414

15-
use std::{num::NonZeroUsize, ptr::NonNull, sync::{Arc, Weak}};
15+
use std::{
16+
num::NonZeroUsize,
17+
ptr::NonNull,
18+
sync::{Arc, Weak},
19+
};
1620

1721
use zenoh_result::ZResult;
1822

commons/zenoh-shm/src/posix_shm/pooled_segment.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ pub(crate) struct PooledPosixShmSegment {
3636
}
3737

3838
impl PooledPosixShmSegment {
39-
pub(crate) fn new(inner: Arc<PosixShmSegment>, size: NonZeroUsize, pool: Weak<ShmSegmentPool>) -> Self {
39+
pub(crate) fn new(
40+
inner: Arc<PosixShmSegment>,
41+
size: NonZeroUsize,
42+
pool: Weak<ShmSegmentPool>,
43+
) -> Self {
4044
Self { inner, size, pool }
4145
}
4246
}

commons/zenoh-shm/tests/posix_shm_provider.rs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,9 @@ fn shm_segment_pool_reuse() {
215215

216216
let backend = PosixShmProviderBackendTalc::builder_with_pool(
217217
layout.size(),
218-
ShmPoolConfig { max_idle_per_size: 1 },
218+
ShmPoolConfig {
219+
max_idle_per_size: 1,
220+
},
219221
)
220222
.wait()
221223
.expect("Error creating pooled PosixShmProviderBackendTalc");
@@ -245,7 +247,9 @@ fn shm_segment_pool_cap_enforcement() {
245247

246248
let backend = PosixShmProviderBackendTalc::builder_with_pool(
247249
layout.size(),
248-
ShmPoolConfig { max_idle_per_size: 1 },
250+
ShmPoolConfig {
251+
max_idle_per_size: 1,
252+
},
249253
)
250254
.wait()
251255
.expect("Error creating pooled PosixShmProviderBackendTalc");
@@ -257,14 +261,19 @@ fn shm_segment_pool_cap_enforcement() {
257261
let seg_b = chunk_b.descriptor.segment;
258262

259263
// They should be distinct segments.
260-
assert_ne!(seg_a, seg_b, "Expected two distinct segments for two concurrent allocs");
264+
assert_ne!(
265+
seg_a, seg_b,
266+
"Expected two distinct segments for two concurrent allocs"
267+
);
261268

262269
// Drop both — pool can hold at most 1, so one is immediately unmapped.
263270
drop(chunk_a);
264271
drop(chunk_b);
265272

266273
// The next allocation should return one of the two segments (whichever was retained).
267-
let chunk_c = backend.alloc(&layout).expect("alloc c after cap test failed");
274+
let chunk_c = backend
275+
.alloc(&layout)
276+
.expect("alloc c after cap test failed");
268277
let seg_c = chunk_c.descriptor.segment;
269278

270279
// seg_c must be one of the two previously-seen IDs (pooled), not a fresh one.
@@ -282,12 +291,10 @@ fn shm_segment_pool_outlived_by_segment() {
282291
let layout = MemoryLayout::new(4096_usize, AllocAlignment::default()).unwrap();
283292

284293
let chunk = {
285-
let backend = PosixShmProviderBackendTalc::builder_with_pool(
286-
layout.size(),
287-
ShmPoolConfig::default(),
288-
)
289-
.wait()
290-
.expect("Error creating pooled PosixShmProviderBackendTalc");
294+
let backend =
295+
PosixShmProviderBackendTalc::builder_with_pool(layout.size(), ShmPoolConfig::default())
296+
.wait()
297+
.expect("Error creating pooled PosixShmProviderBackendTalc");
291298

292299
let chunk = backend.alloc(&layout).expect("alloc failed");
293300
// backend (and its Arc<ShmSegmentPool>) drops here

0 commit comments

Comments
 (0)