From ea7b213b47b3fe6b3c052a721c9d2334dea42112 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 00:57:04 +0000 Subject: [PATCH 1/3] test(drf-w8): eviction under a resident budget with live readers; cyclic churn Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg --- .../tests/integration/budget.rs | 131 ++++++++++++++++++ .../drf-focus-probe/tests/integration/main.rs | 1 + 2 files changed, 132 insertions(+) create mode 100644 crates/drf-focus-probe/tests/integration/budget.rs diff --git a/crates/drf-focus-probe/tests/integration/budget.rs b/crates/drf-focus-probe/tests/integration/budget.rs new file mode 100644 index 000000000..2ba82d6af --- /dev/null +++ b/crates/drf-focus-probe/tests/integration/budget.rs @@ -0,0 +1,131 @@ +//! D-DRF W8 + focus churn: eviction under a resident-byte budget while +//! readers still hold foci. +use drf_focus_probe::*; +use std::sync::Arc; + +const NROWS: usize = 1000; +const OFF: usize = 16; + +fn rows() -> Vec { + (0..NROWS) + .map(|i| { + let mut r = [0u8; ROW]; + r[OFF..OFF + 4].copy_from_slice(&((i as i32) * 3 - 700).to_le_bytes()); + r + }) + .collect() +} +fn soa(r: &[Row]) -> Soa<'_> { + Soa { + rows: r, + id: SourceId { + dataset: 1, + branch: 1, + version: 1, + index_seal: 9, + }, + } +} +fn map(id: u64) -> Arc { + let mut o = Box::new([0u16; SLOTS]); + for (s, v) in o.iter_mut().enumerate() { + *v = ((s * (id as usize + 1)) % NROWS) as u16; + } + Arc::new(SealedMap::new(MapId(id), 9, o)) +} +fn facet() -> Facet { + Facet::new(OFF).unwrap() +} +const ALL: [u64; TILE_WORDS] = [u64::MAX; TILE_WORDS]; + +#[test] +fn w8_a_pinned_focus_survives_eviction_and_may_exceed_the_budget() { + let r = rows(); + let s = soa(&r); + let (ma, mb, mc) = (map(1), map(2), map(3)); + let mgr = FocusManager::new(); + let a = mgr.load(&s, &ma, Scope(1), facet()).unwrap(); + let want = direct_sum_where(&s, &ma, facet(), &ALL, 0).unwrap(); + drop(mgr.load(&s, &mb, Scope(1), facet()).unwrap()); + drop(mgr.load(&s, &mc, Scope(1), facet()).unwrap()); + assert_eq!(mgr.resident_bytes(), 3 * FOCUS_BYTES); + + let rep = mgr.evict_to_budget(0); + assert_eq!(rep.evicted, 2, "only the two unpinned foci go"); + assert_eq!(rep.resident_bytes, FOCUS_BYTES); + assert_eq!(rep.pinned_bytes, FOCUS_BYTES); + assert!(rep.over_budget, "a live reader may hold the manager over budget"); + assert_eq!(mgr.len(), 1); + // The pinned view is untouched and still correct. + assert_eq!(a.sum_where(&ALL), want); + + drop(a); + let rep = mgr.evict_to_budget(0); + assert_eq!((rep.evicted, rep.resident_bytes, rep.over_budget), (1, 0, false)); + assert!(mgr.is_empty()); +} + +#[test] +fn w8_an_evicted_view_rebuilds_deterministically() { + let r = rows(); + let s = soa(&r); + let m = map(4); + let mgr = FocusManager::new(); + let first = mgr.load(&s, &m, Scope(1), facet()).unwrap(); + let col: Vec = first.column().to_vec(); + let sum = first.sum_where(&ALL); + drop(first); + assert_eq!(mgr.evict_to_budget(0).evicted, 1); + let again = mgr.load(&s, &m, Scope(1), facet()).unwrap(); + assert_eq!(mgr.builds(), 2, "evicted, so it was gathered again"); + assert_eq!(again.column().to_vec(), col); + assert_eq!(again.sum_where(&ALL), sum); +} + +#[test] +fn w8_eviction_takes_the_least_recently_loaded_focus_first() { + let r = rows(); + let s = soa(&r); + let (ma, mb, mc) = (map(1), map(2), map(3)); + let mgr = FocusManager::new(); + for m in [&ma, &mb, &mc] { + drop(mgr.load(&s, m, Scope(1), facet()).unwrap()); + } + // A is loaded again (a hit): B is now the least recently used. + drop(mgr.load(&s, &ma, Scope(1), facet()).unwrap()); + assert_eq!(mgr.builds(), 3); + let rep = mgr.evict_to_budget(2 * FOCUS_BYTES); + assert_eq!((rep.evicted, rep.resident_bytes, rep.over_budget), (1, 2 * FOCUS_BYTES, false)); + // An exact fit evicts nothing. + assert_eq!(mgr.evict_to_budget(2 * FOCUS_BYTES).evicted, 0); + // A and C are hits, B was the one evicted. + drop(mgr.load(&s, &ma, Scope(1), facet()).unwrap()); + drop(mgr.load(&s, &mc, Scope(1), facet()).unwrap()); + assert_eq!(mgr.builds(), 3); + drop(mgr.load(&s, &mb, Scope(1), facet()).unwrap()); + assert_eq!(mgr.builds(), 4); +} + +/// Cyclic switching over `k` foci with an eviction pass after every load. +fn cyclic_builds(k: u64, budget_foci: usize, rounds: usize) -> usize { + let r = rows(); + let s = soa(&r); + let maps: Vec<_> = (1..=k).map(map).collect(); + let mgr = FocusManager::new(); + for _ in 0..rounds { + for m in &maps { + drop(mgr.load(&s, m, Scope(1), facet()).unwrap()); + mgr.evict_to_budget(budget_foci * FOCUS_BYTES); + } + } + mgr.builds() +} + +#[test] +fn churn_cyclic_switching_rebuilds_every_load_once_the_budget_is_one_short() { + // ABCABC... over 3 foci for 10 rounds: 30 loads. + assert_eq!(cyclic_builds(3, 3, 10), 3, "fits: one gather per focus"); + // One focus short: LRU always evicts exactly the focus needed next. + assert_eq!(cyclic_builds(3, 2, 10), 30, "every load is a rebuild"); + assert_eq!(cyclic_builds(3, 1, 10), 30); +} diff --git a/crates/drf-focus-probe/tests/integration/main.rs b/crates/drf-focus-probe/tests/integration/main.rs index 2f3a53dbc..932dcbfa5 100644 --- a/crates/drf-focus-probe/tests/integration/main.rs +++ b/crates/drf-focus-probe/tests/integration/main.rs @@ -1,3 +1,4 @@ +pub mod budget; pub mod focus; #[cfg(feature = "handoff")] pub mod handoff; From e5053e8fb6eb391c12588fa98edc59232eaa1d2f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 00:57:41 +0000 Subject: [PATCH 2/3] lab(drf-w8): evict unpinned foci to a resident budget, least recently loaded first FocusManager::evict_to_budget removes unpinned built foci in LRU order (tick refreshed on every load, hit or miss) until the gathered columns fit the budget. A focus a reader still holds is never evicted, so the report can say over_budget (W8); a slot still being built counts as pinned. An evicted focus rebuilds identically on its next load. resident_bytes and FOCUS_BYTES (the gathered column; the shared map is caller-owned) added. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg --- crates/drf-focus-probe/src/lib.rs | 91 ++++++++++++++++++- .../tests/integration/budget.rs | 15 ++- 2 files changed, 98 insertions(+), 8 deletions(-) diff --git a/crates/drf-focus-probe/src/lib.rs b/crates/drf-focus-probe/src/lib.rs index 39a05d6c1..6bc8cbb31 100644 --- a/crates/drf-focus-probe/src/lib.rs +++ b/crates/drf-focus-probe/src/lib.rs @@ -5,7 +5,7 @@ //! The lease begins at `FocusManager::load`, not at the first fold. use std::collections::HashMap; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; #[cfg(feature = "handoff")] @@ -177,11 +177,35 @@ impl Focus { type Slot = Arc, Refusal>>>; +/// Resident bytes of one built focus: its gathered i32 column. The sealed map +/// is caller-owned and shared through an `Arc`, so it is not counted here. +pub const FOCUS_BYTES: usize = SLOTS * 4; + +/// What one [`FocusManager::evict_to_budget`] pass did. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BudgetReport { + /// Entries removed by this pass. + pub evicted: usize, + /// Resident bytes after the pass. + pub resident_bytes: usize, + /// Of those, bytes held by foci that outside readers still reference. + pub pinned_bytes: usize, + /// Still above the budget: only pinned foci remain to evict (W8). + pub over_budget: bool, +} + +struct Entry { + slot: Slot, + /// Manager tick of the most recent `load` of this key. + last: u64, +} + /// No global statics (L3): the lease table is owned by the manager. #[derive(Default)] pub struct FocusManager { - entries: Mutex>, + entries: Mutex>, builds: AtomicUsize, + tick: AtomicU64, } impl FocusManager { @@ -202,9 +226,15 @@ impl FocusManager { map: map.id, scope, }; + let now = self.tick.fetch_add(1, Ordering::SeqCst); let slot: Slot = { let mut g = self.entries.lock().unwrap_or_else(|e| e.into_inner()); - g.entry(key).or_default().clone() + let e = g.entry(key).or_insert_with(|| Entry { + slot: Slot::default(), + last: now, + }); + e.last = now; + e.slot.clone() }; let res = slot.get_or_init(|| { self.builds.fetch_add(1, Ordering::SeqCst); @@ -214,7 +244,7 @@ impl FocusManager { // A refusal is not a lease: do not leave it resident. let mut g = self.entries.lock().unwrap_or_else(|e| e.into_inner()); if let Some(cur) = g.get(&key) - && Arc::ptr_eq(cur, &slot) + && Arc::ptr_eq(&cur.slot, &slot) { g.remove(&key); } @@ -234,13 +264,64 @@ impl FocusManager { pub fn evict_unused(&self) -> usize { let mut g = self.entries.lock().unwrap_or_else(|e| e.into_inner()); let before = g.len(); - g.retain(|_, slot| match slot.get() { + g.retain(|_, e| match e.slot.get() { Some(Ok(f)) => Arc::strong_count(f) > 1, _ => true, }); before - g.len() } + /// Bytes of every built focus the manager holds, pinned or not. + pub fn resident_bytes(&self) -> usize { + let g = self.entries.lock().unwrap_or_else(|e| e.into_inner()); + g.values() + .filter(|e| matches!(e.slot.get(), Some(Ok(_)))) + .count() + * FOCUS_BYTES + } + + /// Evict unpinned foci, least recently loaded first, until the resident + /// bytes fit `budget_bytes`. A focus an outside reader still holds is + /// never evicted, so the manager may stay over budget (W8); a slot still + /// being built counts as pinned. An evicted focus is rebuilt on its next + /// `load`. + pub fn evict_to_budget(&self, budget_bytes: usize) -> BudgetReport { + let mut g = self.entries.lock().unwrap_or_else(|e| e.into_inner()); + let mut resident = 0; + let mut pinned = 0; + let mut victims: Vec<(u64, FocusKey)> = Vec::new(); + for (k, e) in g.iter() { + match e.slot.get() { + Some(Ok(f)) => { + resident += FOCUS_BYTES; + if Arc::strong_count(f) > 1 { + pinned += FOCUS_BYTES; + } else { + victims.push((e.last, *k)); + } + } + Some(Err(_)) => {} + None => pinned += FOCUS_BYTES, + } + } + victims.sort_unstable_by_key(|v| v.0); + let mut evicted = 0; + for (_, k) in victims { + if resident <= budget_bytes { + break; + } + g.remove(&k); + resident -= FOCUS_BYTES; + evicted += 1; + } + BudgetReport { + evicted, + resident_bytes: resident, + pinned_bytes: pinned, + over_budget: resident > budget_bytes, + } + } + pub fn len(&self) -> usize { self.entries.lock().unwrap_or_else(|e| e.into_inner()).len() } diff --git a/crates/drf-focus-probe/tests/integration/budget.rs b/crates/drf-focus-probe/tests/integration/budget.rs index 2ba82d6af..23bedf3f2 100644 --- a/crates/drf-focus-probe/tests/integration/budget.rs +++ b/crates/drf-focus-probe/tests/integration/budget.rs @@ -54,14 +54,20 @@ fn w8_a_pinned_focus_survives_eviction_and_may_exceed_the_budget() { assert_eq!(rep.evicted, 2, "only the two unpinned foci go"); assert_eq!(rep.resident_bytes, FOCUS_BYTES); assert_eq!(rep.pinned_bytes, FOCUS_BYTES); - assert!(rep.over_budget, "a live reader may hold the manager over budget"); + assert!( + rep.over_budget, + "a live reader may hold the manager over budget" + ); assert_eq!(mgr.len(), 1); // The pinned view is untouched and still correct. assert_eq!(a.sum_where(&ALL), want); drop(a); let rep = mgr.evict_to_budget(0); - assert_eq!((rep.evicted, rep.resident_bytes, rep.over_budget), (1, 0, false)); + assert_eq!( + (rep.evicted, rep.resident_bytes, rep.over_budget), + (1, 0, false) + ); assert!(mgr.is_empty()); } @@ -95,7 +101,10 @@ fn w8_eviction_takes_the_least_recently_loaded_focus_first() { drop(mgr.load(&s, &ma, Scope(1), facet()).unwrap()); assert_eq!(mgr.builds(), 3); let rep = mgr.evict_to_budget(2 * FOCUS_BYTES); - assert_eq!((rep.evicted, rep.resident_bytes, rep.over_budget), (1, 2 * FOCUS_BYTES, false)); + assert_eq!( + (rep.evicted, rep.resident_bytes, rep.over_budget), + (1, 2 * FOCUS_BYTES, false) + ); // An exact fit evicts nothing. assert_eq!(mgr.evict_to_budget(2 * FOCUS_BYTES).evicted, 0); // A and C are hits, B was the one evicted. From 984c2db58dfa49e0d698722b3a38efcebbfa3b91 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 00:58:27 +0000 Subject: [PATCH 3/3] board: D-DRF-W8 budget entry Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg --- .../board/entries/2026-10-25-drf-w8-budget.md | 27 +++++++++++++++++++ .claude/board/entries/README.md | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .claude/board/entries/2026-10-25-drf-w8-budget.md diff --git a/.claude/board/entries/2026-10-25-drf-w8-budget.md b/.claude/board/entries/2026-10-25-drf-w8-budget.md new file mode 100644 index 000000000..de640cb5b --- /dev/null +++ b/.claude/board/entries/2026-10-25-drf-w8-budget.md @@ -0,0 +1,27 @@ +# D-DRF-W8 — eviction under a resident budget while readers hold foci (lab) + +**Status:** TEST-PINNED (lab, `cargo test`; no CI job runs this crate). +**Code:** `crates/drf-focus-probe/src/lib.rs` `FocusManager::{evict_to_budget, resident_bytes}`, +`BudgetReport`, `FOCUS_BYTES`. **Tests:** `tests/integration/budget.rs`. +Plan: `.claude/plans/d-dr64-focus-cache-falsifiers-v1.md` §4 W8, §1 (focus churn). + +- **Budget unit:** `FOCUS_BYTES` = the gathered i32 column, 16 KiB. The 8 KiB sealed map is + caller-owned and shared through an `Arc`, so it is not counted. +- **W8 PASS:** with A held by a reader, B and C idle, `evict_to_budget(0)` evicts B and C only + and reports `over_budget` (resident = pinned = 16 KiB). A still folds to the direct-read + oracle. Once A is dropped, the next pass evicts it and the manager is under budget. +- **Rebuild PASS:** an evicted focus re-gathers on its next `load` (builds 1 → 2) with an + identical column and fold. +- **Order PASS:** least recently *loaded* first (the tick is refreshed on a hit). An exact fit + evicts nothing. +- **Churn, measured:** cyclic switching over 3 foci for 10 rounds (30 loads), one eviction pass + per load. Budget 3 foci → 3 builds; budget 2 or 1 → 30 builds. One focus short of the working + set, LRU always evicts exactly the focus needed next, so every load is a full gather. +- **Disables, each red:** evict pinned foci; no recency refresh on a hit; newest first; strict + `<` fit. + +OPEN: the eviction policy is unmeasured beyond these cases. LRU is the worst policy under a +cyclic working set one larger than the budget (a newest-first disable changed the 30-build +result). Whether real thought workloads switch cyclically is not known. A budget-aware caller +could also fall back to direct reads, which the P2 break-even says cost about the same as a +cold build. Neither is decided here. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 46350fbda..f11a46bfe 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,10 +25,11 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -469 entries, 2026-08-06 .. 2026-10-25. +470 entries, 2026-08-06 .. 2026-10-25. | date | entry id | finding | file | |---|---|---|---| +| 2026-10-25 | `D-DRF-W8` | | [2026-10-25-drf-w8-budget.md](2026-10-25-drf-w8-budget.md) | | 2026-10-25 | `D-DRF-W4` | | [2026-10-25-drf-w4-handoff.md](2026-10-25-drf-w4-handoff.md) | | 2026-10-25 | `D-DRF-W3` | | [2026-10-25-drf-w3-ordered-apply.md](2026-10-25-drf-w3-ordered-apply.md) | | 2026-10-25 | `D-DRF-W1` | | [2026-10-25-drf-w1-writeback.md](2026-10-25-drf-w1-writeback.md) |