Skip to main content

growth_main/
growth_main.rs

1//! Storage-growth capture for the bump arena: run many alloc-then-`reset`
2//! sessions and confirm resident memory returns to the same steady level every
3//! round. A bump allocator has zero per-object overhead, so resident tracks the
4//! live allocations exactly (amplification ~1x), and `reset` reclaims all of it -
5//! the arena never accretes across sessions.
6//!
7//! Emits the stable subms growth JSON on stdout.
8//!
9//! ```sh
10//! cat <<EOF | cargo run --release --example growth_main --features harness
11//! rounds=50
12//! allocs_per_round=20000
13//! EOF
14//! ```
15
16use std::collections::BTreeMap;
17use std::io::{self, Read};
18use std::process::ExitCode;
19
20use subms::{SubMsGrowthClass, SubMsGrowthRecipe, grow, growth_to_json};
21use subms_arena_allocator::Bump;
22
23struct ArenaChurn {
24    arena: Bump,
25    rounds: usize,
26    allocs_per_round: usize,
27}
28
29impl SubMsGrowthRecipe for ArenaChurn {
30    fn name(&self) -> &str {
31        "subms-arena-allocator"
32    }
33    fn op_name(&self) -> &str {
34        "alloc"
35    }
36    fn rounds(&self) -> usize {
37        self.rounds
38    }
39    fn ops_per_round(&self) -> usize {
40        self.allocs_per_round
41    }
42    fn op(&mut self, _round: usize, i: usize) {
43        // Start each round's session fresh: reset reclaims the whole buffer, then
44        // we bump-allocate the round's objects into it.
45        if i == 0 {
46            self.arena.reset();
47        }
48        let _ = self.arena.alloc_copy(i as u64);
49    }
50    fn memory_bytes(&mut self) -> u64 {
51        // Resident = bytes currently handed out of the buffer (measured at the
52        // round's peak, before the next round's reset).
53        self.arena.used() as u64
54    }
55    fn live_bytes(&mut self) -> u64 {
56        // Every allocated byte is live until reset, so resident == live: a bump
57        // arena wastes nothing (amplification 1x).
58        self.arena.used() as u64
59    }
60    fn structures(&mut self) -> Vec<(String, u64)> {
61        vec![("live_allocs".to_string(), self.allocs_per_round as u64)]
62    }
63    fn expected(&self) -> (SubMsGrowthClass, f64) {
64        // Resident memory must return to the same steady level every round - it
65        // must not climb, which would mean reset is leaking the buffer.
66        (SubMsGrowthClass::PlateauBounded, 1.5)
67    }
68}
69
70fn parse_usize(map: &BTreeMap<String, String>, key: &str, default: usize) -> usize {
71    map.get(key)
72        .and_then(|v| v.trim().parse().ok())
73        .unwrap_or(default)
74}
75
76fn main() -> ExitCode {
77    let mut raw = String::new();
78    if io::stdin().read_to_string(&mut raw).is_err() {
79        eprintln!("growth_main: failed to read stdin");
80        return ExitCode::FAILURE;
81    }
82    let mut map = BTreeMap::new();
83    for line in raw.lines() {
84        let line = line.trim();
85        if line.is_empty() || line.starts_with('#') {
86            continue;
87        }
88        if let Some((k, v)) = line.split_once('=') {
89            map.insert(k.trim().to_string(), v.trim().to_string());
90        }
91    }
92    let rounds = parse_usize(&map, "rounds", 50);
93    let allocs_per_round = parse_usize(&map, "allocs_per_round", 20_000);
94
95    // Buffer sized to hold one round's allocations (u64 + alignment) with headroom
96    // - the base Bump is fixed-capacity and panics rather than growing.
97    let capacity = allocs_per_round * 16 + 4096;
98    let mut recipe = ArenaChurn {
99        arena: Bump::with_capacity(capacity),
100        rounds,
101        allocs_per_round,
102    };
103    let report = grow(&mut recipe, "rust");
104
105    if growth_to_json(&report, &mut io::stdout().lock()).is_err() {
106        eprintln!("growth_main: failed to write json");
107        return ExitCode::FAILURE;
108    }
109    ExitCode::SUCCESS
110}