OXYGENHUB

State cache: weigh a loaded state by what it really costs d2077a57b1f9

The hub on Earth held 8 GB with its state cache reporting 2.5 GB: the
cache weighed a state at its text three times over, but a loaded state's
line graphs cost about 384 bytes a line on top. Measured on 2026-10-17
(measures_a_real_state, an ignored test that loads real states and reads
the process's resident size): oxygen's states ~85 MiB against an estimate
of 18, oxygenhub's ~70 against 28. The weight is now five bytes per byte
of text, 384 per line and a kilobyte per change (88 and 75 MiB for those),
so the 3 GiB budget means about 3 GiB. Fewer states stay cached, so big
repositories may miss it more often: OXYHUB_STATE_CACHE_MB raises it.

Refs #246
Author
Andy McCall @andymccall <[email protected]>
via
agent Claude Code <[email protected]> · delegate for changes, until 2026-11-13 19:00Z · key ea5f78160088
When
· 2026-10-17 00:09 UTC
Signature
✔ signed by Claude Code
Hash
d2077a57b1f9a114ebdee7cf9e95e81f8932ebc82b59b4b126664a6b1941260b
Depends on
736ae87ef318 9ff27da517fe
Depended on by
nothing yet
In bookmarks
api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
In tags
v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
4 atoms, 0 tree operations, 1 file

Browse the tree at this change

modified crates/hub/src/repos/cache.rs +57 −14
…
28 unchanged lines
29 29
    weight: usize,
30 30
}
31 31
32 -
/// A loaded state's estimated size: its rendered text three times over (the
33 -
/// text, and the line graphs it was rendered from), plus a kilobyte per
34 -
/// change. Rough, but proportional: a repository of large files weighs
35 -
/// more than one of small ones.
32 +
/// A loaded state's estimated size (oxygenhub#246): five bytes per byte of
33 +
/// text and 384 per line (each file's line graph), and a kilobyte per change.
34 +
/// Measured on 2026-10-17 against what loading a state really adds to the
35 +
/// process (`measures_a_real_state`): oxygen's states ~85 MiB, oxygenhub's
36 +
/// ~70 MiB, which this puts at 88 and 75. The older "text three times over"
37 +
/// said 18 and 28, so a cache that thought it held 2.5 GB held 6-8 GB.
36 38
fn weight(l: &Loaded) -> usize {
37 -
    let text: usize = l
38 -
        .snapshot
39 -
        .files
40 -
        .values()
41 -
        .map(|f| match &f.content {
42 -
            oxy_core::state::FileContent::Text { bytes, .. } => bytes.len(),
43 -
            _ => 64,
44 -
        })
45 -
        .sum();
46 -
    text.saturating_mul(3).saturating_add(l.state.len().saturating_mul(1024))
39 +
    let (mut text, mut lines) = (0usize, 0usize);
40 +
    for f in l.snapshot.files.values() {
41 +
        match &f.content {
42 +
            oxy_core::state::FileContent::Text { bytes, .. } => {
43 +
                text += bytes.len();
44 +
                lines += bytes.iter().filter(|b| **b == b'\n').count() + 1;
45 +
            }
46 +
            _ => text += 64,
47 +
        }
48 +
    }
49 +
    text.saturating_mul(5).saturating_add(lines.saturating_mul(384)).saturating_add(l.state.len().saturating_mul(1024))
47 50
}
48 51
49 52
/// What the state cache may hold, in bytes: OXYHUB_STATE_CACHE_MB, else
…
134 unchanged lines
184 187
#[cfg(test)]
185 188
mod tests {
186 189
    use super::*;
190 +
191 +
    /// What a loaded state really costs against `weight` (oxygenhub#246):
192 +
    /// `OXYHUB_MEASURE_REPO=path cargo test -p oxygenhub --lib
193 +
    /// measures_a_real_state -- --ignored --nocapture`.
194 +
    #[test]
195 +
    #[ignore]
196 +
    fn measures_a_real_state() {
197 +
        fn rss_kib() -> usize {
198 +
            let s = std::fs::read_to_string("/proc/self/status").unwrap();
199 +
            s.lines().find(|l| l.starts_with("VmRSS:")).and_then(|l| l.split_whitespace().nth(1)).unwrap().parse().unwrap()
200 +
        }
201 +
        let path = std::env::var("OXYHUB_MEASURE_REPO").expect("OXYHUB_MEASURE_REPO");
202 +
        let repo = Repo::open(std::path::Path::new(&path)).unwrap();
203 +
        let view = repo.view().unwrap();
204 +
        // Kept alive, so each load's memory is new, not reused.
205 +
        let cache = StateCache::with_budget(64, usize::MAX);
206 +
        let mut held = Vec::new();
207 +
        for bookmark in view.bookmarks.keys().take(6).cloned().collect::<Vec<_>>() {
208 +
            let heads = repo.heads_of(&view, &bookmark).unwrap();
209 +
            let before = rss_kib();
210 +
            let loaded = cache.load(&repo, &heads).unwrap();
211 +
            let after = rss_kib();
212 +
            let files = loaded.snapshot.files.len();
213 +
            let (mut text, mut lines) = (0usize, 0usize);
214 +
            for f in loaded.snapshot.files.values() {
215 +
                if let oxy_core::state::FileContent::Text { bytes, .. } = &f.content {
216 +
                    text += bytes.len();
217 +
                    lines += bytes.iter().filter(|b| **b == b'\n').count();
218 +
                }
219 +
            }
220 +
            println!(
221 +
                "{bookmark}: {} changes, {files} files, {} KiB text, {lines} lines: weight {} MiB, resident +{} MiB",
222 +
                loaded.state.len(),
223 +
                text >> 10,
224 +
                weight(&loaded) >> 20,
225 +
                (after.saturating_sub(before)) >> 10
226 +
            );
227 +
            held.push(loaded);
228 +
        }
229 +
    }
187 230
188 231
    #[test]
189 232
    fn caches_by_heads_and_evicts_the_oldest() {
…
48 unchanged lines