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 |