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
24 changes: 24 additions & 0 deletions .claude/board/entries/2026-10-25-drf-churn-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# D-DRF-CHURN — LRU vs MRU victim order under focus churn (lab)

**Status:** MEASURED (lab, `cargo test`; no CI job runs this crate).
**Code:** `crates/drf-focus-probe/src/lib.rs` `Evict`, `FocusManager::evict_to_budget_by`
(`evict_to_budget` stays LRU). **Tests:** `tests/integration/budget.rs` `churn_*`.
Follows `2026-10-25-drf-w8-budget.md` (its OPEN point on the policy).

One eviction pass after every load, 3 foci. Builds counted; each expected count was worked out
by hand before the run, and all matched.

| workload | budget | LRU | MRU |
|---|---|---|---|
| cyclic ABC x10 (30 loads) | 3 | 3 | 3 |
| cyclic ABC x10 | 2 | 30 | 12 |
| cyclic ABC x10 | 1 | 30 | 21 |
| phased AAAA BBBB CCCC x2 (24 loads) | 2 | 6 | 10 |

- Neither order wins both workloads. MRU keeps two of three cyclic foci hot; under phased use it
evicts the focus just loaded, so every load in the third phase rebuilds.
- **Disable, red:** making `Evict::Mru` sort like LRU turns both `churn_*` comparison tests red.

OPEN: which access pattern thought workloads have is still unmeasured, so no default change.
A scan-resistant policy (one that adapts between the two) or a direct-read fallback are the
next candidates; neither is tried 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,7 +25,7 @@ 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.

471 entries, 2026-08-06 .. 2026-10-25.
472 entries, 2026-08-06 .. 2026-10-25.

| date | entry id | finding | file |
|---|---|---|---|
Expand All @@ -34,6 +34,7 @@ exactly one of them, never both.
| 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) |
| 2026-10-25 | `D-DRF-G-FACET` | | [2026-10-25-drf-focuskey-facet.md](2026-10-25-drf-focuskey-facet.md) |
| 2026-10-25 | `D-DRF-CHURN` | | [2026-10-25-drf-churn-policy.md](2026-10-25-drf-churn-policy.md) |
| 2026-10-24 | `tanh-precision-symmetric-i8` | | [2026-10-24-tanh-precision-symmetric-i8.md](2026-10-24-tanh-precision-symmetric-i8.md) |
| 2026-10-24 | `D-DRF-P2` | | [2026-10-24-drf-p2-breakeven.md](2026-10-24-drf-p2-breakeven.md) |
| 2026-10-24 | `D-DRF-P1` | | [2026-10-24-drf-p1-focus-lease-probe.md](2026-10-24-drf-p1-focus-lease-probe.md) |
Expand Down
20 changes: 19 additions & 1 deletion crates/drf-focus-probe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,15 @@ pub struct BudgetReport {
pub over_budget: bool,
}

/// Victim order for [`FocusManager::evict_to_budget_by`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Evict {
/// Least recently loaded first.
Lru,
/// Most recently loaded first.
Mru,
}

struct Entry {
slot: Slot,
/// Manager tick of the most recent `load` of this key.
Expand Down Expand Up @@ -291,6 +300,12 @@ impl FocusManager {
/// being built counts as pinned. An evicted focus is rebuilt on its next
/// `load`.
pub fn evict_to_budget(&self, budget_bytes: usize) -> BudgetReport {
self.evict_to_budget_by(budget_bytes, Evict::Lru)
}

/// [`evict_to_budget`](Self::evict_to_budget) with the victim order
/// chosen by `order`. Pinning and the stop rule are the same.
pub fn evict_to_budget_by(&self, budget_bytes: usize, order: Evict) -> BudgetReport {
let mut g = self.entries.lock().unwrap_or_else(|e| e.into_inner());
let mut resident = 0;
let mut pinned = 0;
Expand All @@ -309,7 +324,10 @@ impl FocusManager {
None => pinned += FOCUS_BYTES,
}
}
victims.sort_unstable_by_key(|v| v.0);
match order {
Evict::Lru => victims.sort_unstable_by_key(|v| v.0),
Evict::Mru => victims.sort_unstable_by_key(|v| std::cmp::Reverse(v.0)),
}
let mut evicted = 0;
for (_, k) in victims {
if resident <= budget_bytes {
Expand Down
46 changes: 46 additions & 0 deletions crates/drf-focus-probe/tests/integration/budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,49 @@ fn churn_cyclic_switching_rebuilds_every_load_once_the_budget_is_one_short() {
assert_eq!(cyclic_builds(3, 2, 10), 30, "every load is a rebuild");
assert_eq!(cyclic_builds(3, 1, 10), 30);
}

/// Builds for a load sequence (indices into `k` maps) with an eviction pass
/// after every load.
fn builds_for(k: u64, seq: &[usize], budget_foci: usize, order: Evict) -> usize {
let r = rows();
let s = soa(&r);
let maps: Vec<_> = (1..=k).map(map).collect();
let mgr = FocusManager::new();
for &i in seq {
drop(mgr.load(&s, &maps[i], Scope(1), facet()).unwrap());
mgr.evict_to_budget_by(budget_foci * FOCUS_BYTES, order);
}
mgr.builds()
}

fn cycle(rounds: usize) -> Vec<usize> {
(0..rounds).flat_map(|_| [0, 1, 2]).collect()
}

/// AAAA BBBB CCCC, `rounds` times.
fn phases(rounds: usize) -> Vec<usize> {
(0..rounds)
.flat_map(|_| [0, 1, 2])
.flat_map(|i| [i; 4])
.collect()
}

#[test]
fn churn_mru_keeps_part_of_a_cycle_that_lru_loses_whole() {
// 30 cyclic loads over 3 foci. Expected counts worked out by hand before
// the run: MRU evicts the focus just loaded, so the other two stay hot.
assert_eq!(builds_for(3, &cycle(10), 2, Evict::Lru), 30);
assert_eq!(builds_for(3, &cycle(10), 2, Evict::Mru), 12);
assert_eq!(builds_for(3, &cycle(10), 1, Evict::Lru), 30);
assert_eq!(builds_for(3, &cycle(10), 1, Evict::Mru), 21);
// Both policies agree when the set fits.
assert_eq!(builds_for(3, &cycle(10), 3, Evict::Mru), 3);
}

#[test]
fn churn_lru_wins_when_foci_are_used_in_phases() {
// AAAA BBBB CCCC twice, budget 2. MRU evicts C right after each load, so
// every C load in the phase rebuilds; LRU rebuilds once per phase.
assert_eq!(builds_for(3, &phases(2), 2, Evict::Lru), 6);
assert_eq!(builds_for(3, &phases(2), 2, Evict::Mru), 10);
}
Loading