Skip to main content

growth_main/
growth_main.rs

1//! Storage-growth capture for the block cache: stream far more distinct keys
2//! through a fixed-capacity LRU than it can hold, and confirm the resident set
3//! stays pinned at the capacity. Eviction is what bounds memory here - this
4//! proves it, rather than the cache being a map that quietly grows.
5//!
6//! Emits the stable subms growth JSON on stdout.
7//!
8//! ```sh
9//! cat <<EOF | cargo run --release --example growth_main --features harness
10//! rounds=50
11//! capacity=1024
12//! inserts_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_block_cache::BlockCache;
22
23const VALUE_BYTES: usize = 256;
24// Rough per-entry heap cost: value + key (u64) + slot/index bookkeeping.
25const ENTRY_BYTES: u64 = (VALUE_BYTES + 8 + 32) as u64;
26
27struct CacheChurn {
28    cache: BlockCache<u64, Vec<u8>>,
29    capacity: usize,
30    rounds: usize,
31    inserts_per_round: usize,
32    value: Vec<u8>,
33    next: u64,
34}
35
36impl SubMsGrowthRecipe for CacheChurn {
37    fn name(&self) -> &str {
38        "subms-block-cache"
39    }
40    fn op_name(&self) -> &str {
41        "put"
42    }
43    fn rounds(&self) -> usize {
44        self.rounds
45    }
46    fn ops_per_round(&self) -> usize {
47        self.inserts_per_round
48    }
49    fn op(&mut self, _round: usize, _i: usize) {
50        // A fresh distinct key every op: nothing is ever re-hit, so a naive
51        // unbounded map would grow to millions - the LRU must evict to stay flat.
52        self.cache.put(self.next, self.value.clone());
53        self.next += 1;
54    }
55    fn memory_bytes(&mut self) -> u64 {
56        self.cache.len() as u64 * ENTRY_BYTES
57    }
58    fn live_bytes(&mut self) -> u64 {
59        // A cache holds only live entries, so resident == live: amplification 1x
60        // is the healthy shape (every resident byte is one the cache chose to keep).
61        self.cache.len() as u64 * ENTRY_BYTES
62    }
63    fn structures(&mut self) -> Vec<(String, u64)> {
64        vec![("entries".to_string(), self.cache.len() as u64)]
65    }
66    fn expected(&self) -> (SubMsGrowthClass, f64) {
67        // Resident memory must never exceed the capacity's worth, no matter how
68        // many distinct keys stream through. 5% slack for bookkeeping estimate.
69        (
70            SubMsGrowthClass::Bounded,
71            (self.capacity as u64 * ENTRY_BYTES) as f64 * 1.05,
72        )
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 capacity = parse_usize(&map, "capacity", 1024);
100    let inserts_per_round = parse_usize(&map, "inserts_per_round", 20_000);
101
102    let mut recipe = CacheChurn {
103        cache: BlockCache::with_capacity(capacity),
104        capacity,
105        rounds,
106        inserts_per_round,
107        value: vec![0u8; VALUE_BYTES],
108        next: 0,
109    };
110    let report = grow(&mut recipe, "rust");
111
112    if growth_to_json(&report, &mut io::stdout().lock()).is_err() {
113        eprintln!("growth_main: failed to write json");
114        return ExitCode::FAILURE;
115    }
116    ExitCode::SUCCESS
117}