Goal. Confirm that steady-state iterations of
Executor::run_n perform zero heap allocations on
any thread (WaitSet thread + pool worker threads).
“Steady-state” excludes the one-time setup that
dispatch_loop performs each run_n entry (WaitSet
construction, trigger attachment, iceoryx2 lazy init); the
harness isolates per-iteration allocations from setup
allocations via a differential measurement.
Fixture. Five executor configurations covering the
dispatch paths (chain, single, graph) plus the event/fd
resolve path through AttachmentMap:
Executor::builder().worker_threads(0).build() + add_chain([h, m, t]) —
TaskKind::Chain on the inline pool.
Executor::builder().worker_threads(2).build() + add_chain([h, m, t]) —
TaskKind::Chain on the threaded pool.
Executor::builder().worker_threads(0).build() + add(single_item) —
TaskKind::Single on the inline pool.
Executor::builder().worker_threads(2).build() + diamond add_graph —
TaskKind::Graph on the threaded pool (vertex
dispatch via per-vertex pre-built closures + SPSC
ring).
Executor::builder().worker_threads(0).dispatch_mode(Legacy).build() +
add(interval_item) — forces the interval to attach as a WaitSet Tick
guard so dispatch routes through AttachmentMap::resolve on the
positive branch (precomputed Tick id -> real task index, a binary-search
hit), exercising the O(log n) resolver’s hot path allocation-free on
every platform. Unlike the Grid default (Linux), where intervals divert
to the master-timer run_grid_cyclic_pass path and never touch the
map, this case pins Legacy so the resolve branch runs everywhere.
See No heap allocation in dispatch (REQ_0060) / ADR_0106.
Each item / vertex returns Ok(Continue) without
allocating.
Allocator instrumentation. A hand-rolled counting
#[global_allocator] (CountingAllocator) wraps
std::alloc::System. Two atomics — ALLOC_COUNT and
TRACKING — are flipped on / off around the measurement
window. Every thread (including pool workers) increments
ALLOC_COUNT on alloc / realloc / alloc_zeroed when
TRACKING is set. This covers paths that
thread-local-flag schemes (assert_no_alloc) cannot
reach.
Steps.
Build the executor; register the task / chain / graph.
per_iter_allocs(&mut exec):
Warm up with run_n(10) (untracked) to absorb any
one-shot lazy init (iceoryx2 service handles
first-touched on the WaitSet thread, etc.).
Bracket run_n(10) with the counting allocator
and record a_small.
Bracket run_n(100) with the counting allocator
and record a_big.
Return ceil((a_big - a_small) / (100 - 10)) —
the average steady-state allocations per dispatch
iteration, with setup-phase allocations subtracted
out via the differential.
Assert per_iter == 0.
Repeat for each of the five fixture configurations
above.
Expected outcome. All five assertions hold:
per_iter == 0. Test passes under cargo test
-p taktora-executor-tests --test no_alloc_dispatch --release.
Note
Platform scope of the worker-thread fixtures (issue #132). The
two worker_threads(2) fixtures (threaded-pool chain and diamond
graph) assert per_iter == 0 strictly on Linux only. The
process-wide CountingAllocator counts allocations on the pool
worker threads too, and on macOS the workers’ crossbeam_channel /
Condvar park-notify path occasionally allocates inside
libsystem_malloc; that single allocation is charged
non-deterministically to one of the two differential windows but not
the other, producing a spurious diff == 1 off-by-one. This is
allocator/scheduler noise, not a product regression — the dispatch
hot path is provably zero-alloc (a 10-iter and a 100-iter window
record identical counts). The three single-threaded
(worker_threads(0)) fixtures have no pool threads, are
deterministic on every platform, and keep the strict assertion
everywhere — so No heap allocation in dispatch (REQ_0060) remains enforced on macOS for the
single-threaded dispatch path and on Linux for all paths.
Negative case. harness_catches_deliberate_allocation
registers a task whose execute body does
vec![1, 2, 3] per iteration and asserts that the
counting allocator records ≥ 10 allocations across 10
iterations — guards against silent harness regressions
where the #[global_allocator] is not actually wired up.
Lives under
crates/taktora-executor-tests/tests/no_alloc_dispatch.rs.
|