Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .claude/board/entries/2026-10-25-drf-w8-budget.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion .claude/board/entries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
91 changes: 86 additions & 5 deletions crates/drf-focus-probe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -177,11 +177,35 @@ impl Focus {

type Slot = Arc<OnceLock<Result<Arc<Focus>, 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<HashMap<FocusKey, Slot>>,
entries: Mutex<HashMap<FocusKey, Entry>>,
builds: AtomicUsize,
tick: AtomicU64,
}

impl FocusManager {
Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -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()
}
Expand Down
140 changes: 140 additions & 0 deletions crates/drf-focus-probe/tests/integration/budget.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//! 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<Row> {
(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<SealedMap> {
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<i32> = 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);
}
1 change: 1 addition & 0 deletions crates/drf-focus-probe/tests/integration/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod budget;
pub mod focus;
#[cfg(feature = "handoff")]
pub mod handoff;
Loading