Skip to main content

sample_app/
sample_app.rs

1//! Sample app: a tour of `subms-arena-allocator` for per-tick / per-request
2//! scratch, base API first, then each optional feature. Run the base with
3//! `cargo run --example sample_app`; add `--all-features` (or a subset like
4//! `--features typed`) to light up the feature sections.
5//!
6//! * base       - per-tick order-book scratch, reset between ticks
7//! * typed      - TypedArena<Level>: slot handles, freed slots reused
8//! * growable   - a deep-book tick outgrows the chunk; the arena grows
9//! * stats      - lifetime counters to size the arena from real load
10//! * aligned    - cache-line-aligned scratch for a SIMD-style price scan
11
12use subms_arena_allocator::Bump;
13
14#[derive(Copy, Clone)]
15struct Level {
16    price_ticks: u64,
17    qty: u32,
18}
19
20fn main() {
21    base_per_tick_scratch();
22
23    #[cfg(feature = "typed")]
24    typed_snapshot();
25
26    #[cfg(feature = "growable")]
27    growable_deep_book();
28
29    #[cfg(feature = "stats")]
30    stats_sizing();
31
32    #[cfg(feature = "aligned")]
33    aligned_price_scan();
34}
35
36/// Base API: on every market-data tick the visible price levels land in one
37/// fixed scratch chunk as throwaway `Copy` structs; we fold each into the mid
38/// and the resting-quantity imbalance, then `reset()` for the next tick. The
39/// chunk is sized once, so the steady state pays no allocator round-trip and
40/// never touches the global malloc or the GC.
41fn base_per_tick_scratch() {
42    println!("== base: per-tick order-book scratch ==");
43    let mut scratch = Bump::with_capacity(4096);
44    let cap = scratch.capacity();
45
46    let ticks: [&[(u64, u32, bool)]; 2] = [
47        &[
48            (9998, 5, true),
49            (9997, 8, true),
50            (10002, 4, false),
51            (10003, 9, false),
52        ],
53        &[(9999, 3, true), (10001, 7, false)],
54    ];
55
56    for (t, updates) in ticks.iter().enumerate() {
57        let (mut best_bid, mut best_ask) = (0u64, u64::MAX);
58        let (mut bid_qty, mut ask_qty) = (0u64, 0u64);
59        for &(price, qty, is_bid) in updates.iter() {
60            let level = scratch.alloc_copy(Level {
61                price_ticks: price,
62                qty,
63            });
64            if is_bid {
65                best_bid = best_bid.max(level.price_ticks);
66                bid_qty += level.qty as u64;
67            } else {
68                best_ask = best_ask.min(level.price_ticks);
69                ask_qty += level.qty as u64;
70            }
71        }
72        let mid = (best_bid + best_ask) / 2;
73        let imbalance = bid_qty as i64 - ask_qty as i64;
74        println!(
75            "  tick {t}: {} levels, mid={mid} imbalance={imbalance:+} used={}B",
76            updates.len(),
77            scratch.used(),
78        );
79        assert!(scratch.used() > 0, "levels consumed scratch");
80        scratch.reset();
81        assert_eq!(scratch.used(), 0, "reset rewinds the cursor");
82        assert_eq!(scratch.capacity(), cap, "no reallocation between ticks");
83    }
84    println!("  -> steady-state chunk stays at {cap}B across all ticks");
85}
86
87/// `typed` feature: `TypedArena<Level>` when every scratch object is the same
88/// compile-time type. `alloc` hands back an opaque `Slot` that `get` reads,
89/// a cancelled level is `free`d back to the arena, and the next `alloc` takes
90/// that slot instead of consuming a fresh one - so an order cache that churns
91/// inside one tick stops advancing the high-water mark.
92#[cfg(feature = "typed")]
93fn typed_snapshot() {
94    use subms_arena_allocator::TypedArena;
95    println!("\n== typed: per-tick levels with slot reuse ==");
96    let mut book = TypedArena::<Level>::with_capacity(64);
97    let mut live = Vec::new();
98    for &(price, qty) in &[(9998u64, 5u32), (9997, 8), (10002, 4), (10003, 9)] {
99        live.push(book.alloc(Level {
100            price_ticks: price,
101            qty,
102        }));
103    }
104    let resting: u64 = live.iter().map(|s| book.get(s).qty as u64).sum();
105    let top = live.iter().map(|s| book.get(s).price_ticks).max().unwrap();
106    println!(
107        "  {} levels, {resting} resting, top price {top}",
108        book.len()
109    );
110    assert_eq!(book.len(), 4);
111    assert_eq!(resting, 26);
112
113    let cancelled = live.pop().expect("a level to cancel");
114    let freed_index = cancelled.index();
115    book.free(cancelled);
116    let replacement = book.alloc(Level {
117        price_ticks: 10_004,
118        qty: 2,
119    });
120    println!(
121        "  cancelled level {freed_index} reused by the replacement: {} ({} reuse hits)",
122        replacement.index() == freed_index,
123        book.reuse_hits(),
124    );
125    assert_eq!(replacement.index(), freed_index, "freed slot came back");
126    assert_eq!(book.reuse_hits(), 1);
127    assert_eq!(book.len(), 4, "cancel + replace is footprint-neutral");
128
129    book.reset();
130    assert!(book.is_empty(), "reset recycles the snapshot storage");
131}
132
133/// `growable` feature: a tick with an unusually deep book overflows the initial
134/// chunk. `GrowableBump` opens a fresh chunk instead of failing; `reset()` keeps
135/// only the largest chunk, so subsequent ticks run grow-free.
136#[cfg(feature = "growable")]
137fn growable_deep_book() {
138    use subms_arena_allocator::GrowableBump;
139    println!("\n== growable: a deep-book tick that outgrows the chunk ==");
140    let mut scratch = GrowableBump::with_capacity(256);
141    for i in 0..200u64 {
142        scratch.alloc_copy(Level {
143            price_ticks: 10_000 + i,
144            qty: 1,
145        });
146    }
147    let grown = scratch.chunk_count();
148    println!(
149        "  200 levels -> {grown} chunks, {}B retained",
150        scratch.total_capacity()
151    );
152    assert!(grown > 1, "deep book forced a grow");
153    scratch.reset();
154    assert_eq!(
155        scratch.chunk_count(),
156        1,
157        "reset keeps only the largest chunk"
158    );
159    let cap_after = scratch.total_capacity();
160    for i in 0..50u64 {
161        scratch.alloc_copy(Level {
162            price_ticks: 10_000 + i,
163            qty: 1,
164        });
165    }
166    assert_eq!(
167        scratch.total_capacity(),
168        cap_after,
169        "steady-state tick is grow-free"
170    );
171}
172
173/// `stats` feature: `StatsBump` keeps lifetime counters across resets, so a
174/// long-running feed handler can size the arena from observed `peak_bytes` and
175/// watch `bytes_wasted` for alignment-padding creep.
176#[cfg(feature = "stats")]
177fn stats_sizing() {
178    use subms_arena_allocator::StatsBump;
179    println!("\n== stats: size the arena from real load ==");
180    let mut scratch = StatsBump::with_capacity(4096);
181    for _tick in 0..1_000 {
182        for i in 0..8u64 {
183            scratch.alloc_copy(Level {
184                price_ticks: 10_000 + i,
185                qty: 1,
186            });
187        }
188        scratch.reset();
189    }
190    let s = scratch.stats();
191    println!(
192        "  {} allocs over 1000 ticks, peak {}B, wasted {}B",
193        s.allocations, s.peak_bytes, s.bytes_wasted,
194    );
195    assert_eq!(s.allocations, 8_000, "counters survive reset");
196    assert!(s.peak_bytes > 0, "peak recorded");
197}
198
199/// `aligned` feature: `AlignedBump` hands out cache-line-aligned scratch for a
200/// SIMD-style scan over a tick's prices. The backing buffer is 64-byte aligned,
201/// so the first cache-line request pays zero padding.
202#[cfg(feature = "aligned")]
203fn aligned_price_scan() {
204    use subms_arena_allocator::AlignedBump;
205    println!("\n== aligned: cache-line scratch for a price scan ==");
206    let mut scratch = AlignedBump::with_capacity(1024);
207    let region = scratch.alloc_aligned(64, 64);
208    assert_eq!(region.as_ptr() as usize % 64, 0, "cache-line aligned");
209    for (i, b) in region.iter_mut().enumerate() {
210        *b = i as u8;
211    }
212    let checksum: u32 = region.iter().map(|&b| b as u32).sum();
213    println!(
214        "  64B aligned scratch, checksum {checksum}, used {}B",
215        scratch.used()
216    );
217    assert_eq!(checksum, (0..64u32).sum::<u32>());
218    scratch.reset();
219    assert_eq!(scratch.used(), 0);
220}