From dea50163fe457ec58167cafa193979f502e116e4 Mon Sep 17 00:00:00 2001 From: floor-licker Date: Fri, 30 Jan 2026 18:01:44 -0500 Subject: [PATCH] test(no-alloc): guard hot paths against heap allocs --- tests/no_alloc_hot_paths.rs | 129 ++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/no_alloc_hot_paths.rs diff --git a/tests/no_alloc_hot_paths.rs b/tests/no_alloc_hot_paths.rs new file mode 100644 index 0000000..441b1f5 --- /dev/null +++ b/tests/no_alloc_hot_paths.rs @@ -0,0 +1,129 @@ +use std::alloc::{GlobalAlloc, Layout, System}; +use std::cell::Cell; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +use chrono::Utc; +use polyfill_rs::{OrderBookImpl, Side}; + +thread_local! { + static ALLOCATIONS: Cell = const { Cell::new(0) }; +} + +struct CountingAllocator; + +unsafe impl GlobalAlloc for CountingAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + ALLOCATIONS.with(|count| count.set(count.get() + 1)); + System.alloc(layout) + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + ALLOCATIONS.with(|count| count.set(count.get() + 1)); + System.alloc_zeroed(layout) + } + + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + ALLOCATIONS.with(|count| count.set(count.get() + 1)); + System.realloc(ptr, layout, new_size) + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + System.dealloc(ptr, layout) + } +} + +#[global_allocator] +static GLOBAL: CountingAllocator = CountingAllocator; + +fn allocation_count() -> usize { + ALLOCATIONS.with(|count| count.get()) +} + +struct NoAllocGuard { + before: usize, +} + +impl NoAllocGuard { + fn new() -> Self { + Self { + before: allocation_count(), + } + } + + fn assert_no_allocations(self) { + let after = allocation_count(); + assert_eq!( + after, + self.before, + "expected no heap allocations, but saw {} allocation(s)", + after - self.before + ); + } +} + +fn token_id_hash(token_id: &str) -> u64 { + let mut hasher = DefaultHasher::new(); + token_id.hash(&mut hasher); + hasher.finish() +} + +fn mk_delta( + token_id_hash: u64, + side: Side, + price_ticks: polyfill_rs::types::Price, + size_units: polyfill_rs::types::Qty, + sequence: u64, +) -> polyfill_rs::types::FastOrderDelta { + polyfill_rs::types::FastOrderDelta { + token_id_hash, + timestamp: chrono::DateTime::::from_timestamp(0, 0).unwrap(), + side, + price: price_ticks, + size: size_units, + sequence, + } +} + +#[test] +fn no_alloc_mid_and_spread_fast() { + let token_id = "test_token"; + let token_hash = token_id_hash(token_id); + let mut book = OrderBookImpl::new(token_id.to_string(), 100); + + // Allocate during setup: create initial price levels. + book.apply_delta_fast(mk_delta(token_hash, Side::BUY, 7500, 1_000_000, 1)) + .unwrap(); + book.apply_delta_fast(mk_delta(token_hash, Side::SELL, 7600, 1_000_000, 2)) + .unwrap(); + + // Warm up TLS access before measuring (defensive). + let _ = allocation_count(); + + let guard = NoAllocGuard::new(); + assert!(book.best_bid_fast().is_some()); + assert!(book.best_ask_fast().is_some()); + assert!(book.spread_fast().is_some()); + assert!(book.mid_price_fast().is_some()); + guard.assert_no_allocations(); +} + +#[test] +fn no_alloc_apply_delta_fast_existing_level_update() { + let token_id = "test_token"; + let token_hash = token_id_hash(token_id); + let mut book = OrderBookImpl::new(token_id.to_string(), 100); + + // Allocate during setup: create an initial level. + book.apply_delta_fast(mk_delta(token_hash, Side::BUY, 7500, 1_000_000, 1)) + .unwrap(); + + // Warm up TLS access before measuring (defensive). + let _ = allocation_count(); + + let guard = NoAllocGuard::new(); + // Updating an existing level should not require heap allocation. + book.apply_delta_fast(mk_delta(token_hash, Side::BUY, 7500, 2_000_000, 2)) + .unwrap(); + guard.assert_no_allocations(); +}