Skip to main content

growth_main/
growth_main.rs

1//! Storage-growth capture for the timer wheel: under continuous schedule+tick
2//! churn, the pending-timer index (`id_to_slot`) must reach a bounded steady
3//! state and stay there. If `tick` fired timers but forgot to evict their ids,
4//! the index would climb every round - a slow leak a percentile can't see. This
5//! measures the index size round over round and gates it flat.
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//! num_slots=256
13//! ops_per_round=20000
14//! EOF
15//! ```
16
17use std::collections::BTreeMap;
18use std::io::{self, Read};
19use std::process::ExitCode;
20
21use subms::{SubMsGrowthClass, SubMsGrowthRecipe, grow, growth_to_json};
22use subms_timer_wheel::TimerWheel;
23
24// Rough per-pending-entry heap cost: the id->slot map entry plus the slot's
25// Entry<u64> (id + rounds + value + flag).
26const ENTRY_BYTES: u64 = 48;
27
28struct WheelChurn {
29    wheel: TimerWheel<u64>,
30    num_slots: usize,
31    rounds: usize,
32    ops_per_round: usize,
33    seq: u64,
34}
35
36impl SubMsGrowthRecipe for WheelChurn {
37    fn name(&self) -> &str {
38        "subms-timer-wheel"
39    }
40    fn op_name(&self) -> &str {
41        "schedule"
42    }
43    fn rounds(&self) -> usize {
44        self.rounds
45    }
46    fn ops_per_round(&self) -> usize {
47        self.ops_per_round
48    }
49    fn op(&mut self, _round: usize, i: usize) {
50        // Schedule one timer somewhere in the next rotation, then advance the hand
51        // one tick (firing anything now due). Over many ops the in-flight set
52        // reaches a steady size; a leaking tick would let it grow without bound.
53        let delay = (i % (self.num_slots - 1)) + 1;
54        self.wheel.schedule(delay, self.seq);
55        self.seq += 1;
56        let _ = self.wheel.tick();
57    }
58    fn memory_bytes(&mut self) -> u64 {
59        self.wheel.pending() as u64 * ENTRY_BYTES
60    }
61    fn live_bytes(&mut self) -> u64 {
62        // The genuinely-in-flight timers are the live set; a correct wheel holds
63        // exactly those, so resident == live.
64        self.wheel.pending() as u64 * ENTRY_BYTES
65    }
66    fn structures(&mut self) -> Vec<(String, u64)> {
67        vec![("pending".to_string(), self.wheel.pending() as u64)]
68    }
69    fn expected(&self) -> (SubMsGrowthClass, f64) {
70        // The pending index must plateau at its steady size, not climb round over
71        // round - climbing would mean fired ids are never evicted.
72        (SubMsGrowthClass::PlateauBounded, 1.5)
73    }
74}
75
76fn parse_usize(map: &BTreeMap<String, String>, key: &str, default: usize) -> usize {
77    map.get(key)
78        .and_then(|v| v.trim().parse().ok())
79        .unwrap_or(default)
80}
81
82fn main() -> ExitCode {
83    let mut raw = String::new();
84    if io::stdin().read_to_string(&mut raw).is_err() {
85        eprintln!("growth_main: failed to read stdin");
86        return ExitCode::FAILURE;
87    }
88    let mut map = BTreeMap::new();
89    for line in raw.lines() {
90        let line = line.trim();
91        if line.is_empty() || line.starts_with('#') {
92            continue;
93        }
94        if let Some((k, v)) = line.split_once('=') {
95            map.insert(k.trim().to_string(), v.trim().to_string());
96        }
97    }
98    let rounds = parse_usize(&map, "rounds", 50);
99    let num_slots = parse_usize(&map, "num_slots", 256).max(4);
100    let ops_per_round = parse_usize(&map, "ops_per_round", 20_000);
101
102    let mut recipe = WheelChurn {
103        wheel: TimerWheel::new(num_slots),
104        num_slots,
105        rounds,
106        ops_per_round,
107        seq: 0,
108    };
109    let report = grow(&mut recipe, "rust");
110
111    if growth_to_json(&report, &mut io::stdout().lock()).is_err() {
112        eprintln!("growth_main: failed to write json");
113        return ExitCode::FAILURE;
114    }
115    ExitCode::SUCCESS
116}