docs(evidence): layout cost evidence and measurements; fmt the merged bench (improvise-felo.13)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (felo13)
2026-10-02 19:01:46 -07:00
committed by Edward Langley
parent 3b168a2ec8
commit 14cef311dd
7 changed files with 336 additions and 71 deletions
+104 -23
View File
@@ -79,7 +79,11 @@ fn document(c: &Cfg) -> (Workbook, Vec<CellKey>) {
} }
let view = wb.active_view_mut(); let view = wb.active_view_mut();
for ci in 0..c.cats { for ci in 0..c.cats {
let axis = if c.mode == "records" || c.mode == "none" { Axis::None } else { Axis::Row }; let axis = if c.mode == "records" || c.mode == "none" {
Axis::None
} else {
Axis::Row
};
view.set_axis(&format!("C{ci}"), axis); view.set_axis(&format!("C{ci}"), axis);
} }
if c.mode == "records" { if c.mode == "records" {
@@ -103,7 +107,11 @@ fn rss_mb() -> f64 {
.args(["-o", "rss=", "-p", &std::process::id().to_string()]) .args(["-o", "rss=", "-p", &std::process::id().to_string()])
.output() .output()
.expect("ps"); .expect("ps");
String::from_utf8_lossy(&out.stdout).trim().parse::<f64>().unwrap_or(0.0) / 1024.0 String::from_utf8_lossy(&out.stdout)
.trim()
.parse::<f64>()
.unwrap_or(0.0)
/ 1024.0
} }
/// Sample `f` up to `reps` times, stopping once `budget_ms` is spent (one sample minimum). /// Sample `f` up to `reps` times, stopping once `budget_ms` is spent (one sample minimum).
@@ -132,7 +140,10 @@ fn sample(name: &str, reps: usize, budget_ms: f64, mut f: impl FnMut(usize)) {
fn once<T>(name: &str, f: impl FnOnce() -> T) -> T { fn once<T>(name: &str, f: impl FnOnce() -> T) -> T {
let t = Instant::now(); let t = Instant::now();
let v = f(); let v = f();
println!("{name} min={0:.3} med={0:.3} max={0:.3} reps=1", t.elapsed().as_secs_f64() * 1000.0); println!(
"{name} min={0:.3} med={0:.3} max={0:.3} reps=1",
t.elapsed().as_secs_f64() * 1000.0
);
std::io::stdout().flush().ok(); std::io::stdout().flush().ok();
v v
} }
@@ -144,7 +155,10 @@ fn key(code: KeyCode) -> KeyEvent {
fn main() { fn main() {
let args: HashMap<String, String> = std::env::args() let args: HashMap<String, String> = std::env::args()
.skip(1) .skip(1)
.filter_map(|a| a.split_once('=').map(|(k, v)| (k.to_string(), v.to_string()))) .filter_map(|a| {
a.split_once('=')
.map(|(k, v)| (k.to_string(), v.to_string()))
})
.collect(); .collect();
let c = Cfg { let c = Cfg {
k: arg(&args, "k", "1000").parse().expect("k"), k: arg(&args, "k", "1000").parse().expect("k"),
@@ -153,19 +167,35 @@ fn main() {
prune: arg(&args, "prune", "1") == "1", prune: arg(&args, "prune", "1") == "1",
mode: arg(&args, "mode", "grid"), mode: arg(&args, "mode", "grid"),
groups: arg(&args, "groups", "0") == "1", groups: arg(&args, "groups", "0") == "1",
ops: arg(&args, "ops", "open,key,frame,window,setcell,host,reshape,io"), ops: arg(
&args,
"ops",
"open,key,frame,window,setcell,host,reshape,io",
),
}; };
let rows_cap: f64 = arg(&args, "rows_cap", "30000000").parse().expect("cap"); let rows_cap: f64 = arg(&args, "rows_cap", "30000000").parse().expect("cap");
let product = (c.k as f64).powi(c.cats as i32); let product = (c.k as f64).powi(c.cats as i32);
if c.mode == "grid" && product > rows_cap { if c.mode == "grid" && product > rows_cap {
println!("REFUSED product_rows={product:.3e} (> {rows_cap:.0}: layout would materialize it)"); println!(
"REFUSED product_rows={product:.3e} (> {rows_cap:.0}: layout would materialize it)"
);
return; return;
} }
let want = |o: &str| c.ops.split(',').any(|x| x == o); let want = |o: &str| c.ops.split(',').any(|x| x == o);
let (wb, recs) = document(&c); let (wb, recs) = document(&c);
println!("cells={} records={} base_rss_mb={:.0}", wb.model.data.iter_cells().count(), recs.len(), rss_mb()); println!(
"cells={} records={} base_rss_mb={:.0}",
wb.model.data.iter_cells().count(),
recs.len(),
rss_mb()
);
let mut app = once("open", || App::new(wb, None)); let mut app = once("open", || App::new(wb, None));
println!("rows={} cols={} rss_after_open_mb={:.0}", app.layout.row_count(), app.layout.col_count(), rss_mb()); println!(
"rows={} cols={} rss_after_open_mb={:.0}",
app.layout.row_count(),
app.layout.col_count(),
rss_mb()
);
app.term_width = 200; app.term_width = 200;
app.term_height = 50; app.term_height = 50;
let name = app.view_name().to_string(); let name = app.view_name().to_string();
@@ -176,7 +206,12 @@ fn main() {
}); });
} }
if want("frame") { if want("frame") {
let whole = |app: &App| Viewport { row_offset: 0, col_offset: 0, rows: app.layout.row_count(), cols: app.layout.col_count() }; let whole = |app: &App| Viewport {
row_offset: 0,
col_offset: 0,
rows: app.layout.row_count(),
cols: app.layout.col_count(),
};
sample("tui_frame_grid_of_whole", 3, 8000.0, |_| { sample("tui_frame_grid_of_whole", 3, 8000.0, |_| {
std::hint::black_box(project::grid_of(&app, whole(&app))); std::hint::black_box(project::grid_of(&app, whole(&app)));
}); });
@@ -185,9 +220,14 @@ fn main() {
sample("tui_render_gridwidget_buffer", 5, 4000.0, |_| { sample("tui_render_gridwidget_buffer", 5, 4000.0, |_| {
let mut buf = ratatui::buffer::Buffer::empty(area); let mut buf = ratatui::buffer::Buffer::empty(area);
let w = improvise::ui::grid::GridWidget::new( let w = improvise::ui::grid::GridWidget::new(
&cache, app.cursor(), &app.view_state.active_view, &app.layout, &cache,
&app.view_state.mode, &app.view_state.search_query, app.cursor(),
&app.view_state.buffers, app.drill_state.as_ref(), &app.view_state.active_view,
&app.layout,
&app.view_state.mode,
&app.view_state.search_query,
&app.view_state.buffers,
app.drill_state.as_ref(),
); );
ratatui::widgets::Widget::render(w, area, &mut buf); ratatui::widgets::Widget::render(w, area, &mut buf);
std::hint::black_box(buf); std::hint::black_box(buf);
@@ -196,7 +236,12 @@ fn main() {
if want("window") { if want("window") {
let mid = app.layout.row_count() / 2; let mid = app.layout.row_count() / 2;
for (label, off) in [("top", 0), ("mid", mid)] { for (label, off) in [("top", 0), ("mid", mid)] {
let vp = Viewport { row_offset: off, col_offset: 0, rows: 45, cols: 12 }; let vp = Viewport {
row_offset: off,
col_offset: 0,
rows: 45,
cols: 12,
};
sample(&format!("browser_grid_of_{label}"), 5, 6000.0, |_| { sample(&format!("browser_grid_of_{label}"), 5, 6000.0, |_| {
std::hint::black_box(project::grid_of(&app, vp)); std::hint::black_box(project::grid_of(&app, vp));
}); });
@@ -205,29 +250,49 @@ fn main() {
if want("setcell") && !recs.is_empty() { if want("setcell") && !recs.is_empty() {
let existing = recs[recs.len() / 2].clone().with("_Measure", "Rev"); let existing = recs[recs.len() / 2].clone().with("_Measure", "Rev");
sample("setcell_existing", 5, 6000.0, |r| { sample("setcell_existing", 5, 6000.0, |r| {
app.apply_effects(vec![Box::new(effect::SetCell(existing.clone(), CellValue::Number(10.0 + r as f64)))]); app.apply_effects(vec![Box::new(effect::SetCell(
existing.clone(),
CellValue::Number(10.0 + r as f64),
))]);
}); });
let mut s = 99u64; let mut s = 99u64;
sample("setcell_new_record", 5, 6000.0, |_| { sample("setcell_new_record", 5, 6000.0, |_| {
let mut coords: Vec<(String, String)> = (0..c.cats).map(|ci| (format!("C{ci}"), format!("i{}", lcg(&mut s, c.k)))).collect(); let mut coords: Vec<(String, String)> = (0..c.cats)
.map(|ci| (format!("C{ci}"), format!("i{}", lcg(&mut s, c.k))))
.collect();
coords.push(("_Measure".into(), "Rev".into())); coords.push(("_Measure".into(), "Rev".into()));
app.apply_effects(vec![Box::new(effect::SetCell(CellKey::new(coords), CellValue::Number(1.0)))]); app.apply_effects(vec![Box::new(effect::SetCell(
CellKey::new(coords),
CellValue::Number(1.0),
))]);
}); });
} }
if want("reshape") && c.mode == "grid" { if want("reshape") && c.mode == "grid" {
let v = || ViewTarget::Active; let v = || ViewTarget::Active;
sample("reshape_hide_item", 3, 6000.0, |r| { sample("reshape_hide_item", 3, 6000.0, |r| {
app.apply_effects(vec![Box::new(effect::HideItem { view: v(), category: "C1".into(), item: format!("i{r}") })]); app.apply_effects(vec![Box::new(effect::HideItem {
view: v(),
category: "C1".into(),
item: format!("i{r}"),
})]);
}); });
if c.groups { if c.groups {
sample("reshape_toggle_group", 3, 6000.0, |_| { sample("reshape_toggle_group", 3, 6000.0, |_| {
app.apply_effects(vec![Box::new(effect::ToggleGroup { view: v(), category: "C0".into(), group: "G0".into() })]); app.apply_effects(vec![Box::new(effect::ToggleGroup {
view: v(),
category: "C0".into(),
group: "G0".into(),
})]);
}); });
} }
let last = format!("C{}", c.cats - 1); let last = format!("C{}", c.cats - 1);
sample("reshape_set_axis", 4, 6000.0, |r| { sample("reshape_set_axis", 4, 6000.0, |r| {
let axis = if r % 2 == 0 { Axis::Page } else { Axis::Row }; let axis = if r % 2 == 0 { Axis::Page } else { Axis::Row };
app.apply_effects(vec![Box::new(effect::SetAxis { view: v(), category: last.clone(), axis })]); app.apply_effects(vec![Box::new(effect::SetAxis {
view: v(),
category: last.clone(),
axis,
})]);
}); });
sample("reshape_toggle_prune", 2, 6000.0, |_| { sample("reshape_toggle_prune", 2, 6000.0, |_| {
app.apply_effects(vec![Box::new(effect::TogglePruneEmpty(v()))]); app.apply_effects(vec![Box::new(effect::TogglePruneEmpty(v()))]);
@@ -249,15 +314,31 @@ fn main() {
sample("json_encode_snapshot", 3, 4000.0, |_| { sample("json_encode_snapshot", 3, 4000.0, |_| {
std::hint::black_box(serde_json::to_vec(&snap.msg).expect("json")); std::hint::black_box(serde_json::to_vec(&snap.msg).expect("json"));
}); });
println!("snapshot_kb={:.1}", serde_json::to_vec(&snap.msg).expect("json").len() as f64 / 1024.0); println!(
"snapshot_kb={:.1}",
serde_json::to_vec(&snap.msg).expect("json").len() as f64 / 1024.0
);
let rec = recs[recs.len() / 2].clone(); let rec = recs[recs.len() / 2].clone();
let cell = RecordCell { record: RecordHash::of(&rec), measure: Some("Rev".into()) }; let cell = RecordCell {
record: RecordHash::of(&rec),
measure: Some("Rev".into()),
};
let mut enc: Vec<(f64, usize)> = Vec::new(); let mut enc: Vec<(f64, usize)> = Vec::new();
sample("host_handle_setcell_and_project", 4, 8000.0, |r| { sample("host_handle_setcell_and_project", 4, 8000.0, |r| {
let wire = caught_up(host.app(), ModelCommand::SetCell { view: name.clone(), cell: cell.clone(), value: CellValue::Number(50.0 + r as f64) }); let wire = caught_up(
host.app(),
ModelCommand::SetCell {
view: name.clone(),
cell: cell.clone(),
value: CellValue::Number(50.0 + r as f64),
},
);
let out = host.handle(id, ClientFrame::command(wire)); let out = host.handle(id, ClientFrame::command(wire));
let t = Instant::now(); let t = Instant::now();
let bytes: usize = out.iter().map(|a| serde_json::to_vec(&a.msg).expect("json").len()).sum(); let bytes: usize = out
.iter()
.map(|a| serde_json::to_vec(&a.msg).expect("json").len())
.sum();
enc.push((t.elapsed().as_secs_f64() * 1000.0, bytes)); enc.push((t.elapsed().as_secs_f64() * 1000.0, bytes));
}); });
println!("json_encode_setcell_updates ms={:?}", enc); println!("json_encode_setcell_updates ms={:?}", enc);
+103
View File
@@ -0,0 +1,103 @@
# Layout and keypress cost (improvise-felo.13)
Measured on an **Apple M4 Max, 64 GB**, macOS, release build, with
`benches/scale_bisect.rs` (merged from `bench/scale-bisect`, `13918bb9`), one
configuration per process, 3 categories of K items on Row, N = 100,000 capped at
K^3/2 records, prune off unless stated. Baseline is this branch with only the
tests added; "after" is the branch head. `key` is `App::dispatch_key(Down)`;
"cold" is the first key after open or after an edit, which pays the one
whole-grid measurement; "warm" is every key after it.
## Before and after
| rows | before key | after key warm | after key cold | before browser window | after browser window | SetCell before / after |
|---|---|---|---|---|---|---|
| 729 (K=9) | 5.5 | 0.00 | 1.4 | 5.0 | 0.13 | 0.9 / 0.9 |
| 4,913 (K=17) | 108 | 0.00 | 6.0 | 105 | 0.12 | 5.8 / 5.9 |
| 5,832 (K=18) | | 0.00 | 7.7 | | 0.12 | 6.8 |
| 6,859 (K=19) | | 0.00 | 8.8 | | 0.12 | 8.6 |
| 19,683 (K=27) | 1,271 | 0.00 | 24.5 | 1,259 | 0.14 | 23.0 / 22.7 |
| 64,000 (K=40) | 10,771 | 0.00 | 84 | 10,746 | 0.20 | 79 / 80 |
Prune on: K=17 (2,456 rows) key 264 ms -> 0.00 warm, 3.6 cold; prune toggle
172 -> 8.8 ms; K=27 prune toggle 2,189 -> 41.5 ms. Records mode (3 x 1000, 14,500
records): key 1,004 ms -> 0.00 warm, 2.8 cold; **SetCell 1,511 ms -> 1,512 ms,
unchanged** (not this ticket's cost).
Boundaries, warm key and browser window: inside 8 ms at every size measured (to
216,000 rows, 303 ms cold). Cold key: inside 8 ms to K=18 (5,832 rows), over 8 ms at
K=19. Before: inside 8 ms to 729 rows, over 100 ms at 4,913, over 1 s at 19,683.
The remaining cold cost is linear (1.2-1.4 us per row) and is the whole-grid width
and total pass; the warm path does not repeat it.
## What the ticket's reading got right and what it did not predict
The O(rows^2) in `cell_key` was real and is one of three causes. Two more sat
under it. First, `compute_col_widths` ran on **every** keypress, through
`cmd_context`, and `dispatch_key` and `apply_effects` each rebuilt the layout
whether or not anything had moved. Second, once the index was O(1) the width pass
was still O(K^2) per cell: with `_Index`/`_Dim` on `Axis::None` the aggregation is
not `Exact`, so each cell went through `DataStore::matching_values`, which
narrows by the smallest index set (about N/K candidates) and probes the rest. A
partial as wide as the widest stored cell can match only one cell, so
`exact_positions` probes it directly. That second cause is why K=40's cold key
stayed at 8 s after the first two fixes.
### a_cursor_key_rebuilds_neither_the_layout_nor_the_widths
`ui::app::tests`. Two keys after a warm-up leave `layout_builds` and
`width_passes` where they were. Failed before the fix on the layout count (a
cursor key rebuilt it). Rival 1 (`refresh_layout` always rebuilding): fails on
`layout rebuilt on a cursor key`. The test seats the cursor at row 2, so "the
keys moved the cursor" is asserted and a key that does nothing cannot pass.
### an_edit_rebuilds_and_remeasures_so_the_widths_follow_the_value
The control for the test above, and the guard on invalidation. A `SetCell` of a
long text must rebuild the layout and remeasure, and the cached widths must equal
a fresh `compute_col_widths`. Rival 2 (`rebuild_layout` not dropping the cached
geometry): fails on `an edit must remeasure`. It also asserts the widths changed,
so a fixture where the edit does not widen a column cannot pass.
### switching_view_rebuilds_though_the_effect_is_view_only
`SwitchView` is `ViewOnly` (gotcha 3), so skipping the rebuild by `EffectKind`
alone would leave the old view's layout. The layout records the view it drew and
the drill snapshot it froze, and rebuilds when either differs.
### an_axis_lookup_does_work_independent_of_the_row_index
`view::layout::lookup_cost_tests`. Counts the items `data_items` yields during
`cell_key`, `row_tuple`, `row_label` and `column_key`; row 399 of 400 must cost no
more than row 0 plus two. Failed before the fix: `cell_key: row 399 cost 402 steps
against 3 at row 0`. Rival (`cell_key` going back to `data_items(..).nth(row)`):
`cost 400 steps against 1 at row 0`. Limit: the counter lives in `data_items`, so
a different linear walk written without it is not seen; the next test is what
holds the index to the walk's answers.
### the_position_index_answers_what_the_walk_answered
Compares `row_tuple`, `data_row_to_visual` and `row_folds_a_group` against the
walk over a grouped, collapsed and pruned axis, including two indices past the
end. Rival (`data_index_to_visual` reading `data_idx + 1`): fails at `visual 0`.
### a_full_width_partial_probes_one_cell_instead_of_narrowing
`model::cell::exact_path_tests`. A full-width partial must not reach set
narrowing, and an absent cell and an unseen item must match nothing. Rival
(`exact_positions` returning `None`): the narrowing count is 1, not 0. Rival
(`widest <= partial.len() + 1`): fails `a_wider_cell_turns_the_shortcut_off_until_it_is_removed`,
which stores a cell one coordinate wider and expects it matched.
`a_narrower_partial_still_narrows_and_matches_every_superset` is the control: the
shortcut is not a blanket exact lookup. `an_unsorted_full_width_partial_is_still_answered`
holds the canonical-order condition: a partial not in a stored key's order falls
back.
### a_window_over_a_held_summary_equals_the_one_walk_grid_body
`ui::viewport::tests`. The host keeps one `GridSummary` (widths, total formats,
totals) per layout and reads a window over it with `grid_window`; it must equal
what `grid_body` builds for the same window, over the whole grid, an interior
window, the last row and the empty window. Rival (`grid_window` reading column
`ci + 1`): fails at `window 0..2 x 0..10`. Widths stay the **whole-grid** maximum,
because the summary is the same walk `grid_body` did; nothing the user sees moves.
+28 -11
View File
@@ -467,8 +467,7 @@ pub struct App {
pub term_width: u16, pub term_width: u16,
pub term_height: u16, pub term_height: u16,
/// **Derived cache**: a pure function of the slices, rebuilt by /// **Derived cache**: a pure function of the slices, rebuilt by
/// `rebuild_layout()`. Owned by `App` because the rebuild is the host's job. /// `rebuild_layout()`; a writer that bypasses effects must call it (felo.13).
/// A writer that bypasses effects must call it: a keypress no longer does.
pub layout: GridLayout, pub layout: GridLayout,
/// How many times `rebuild_layout` has run: what a keypress that changes /// How many times `rebuild_layout` has run: what a keypress that changes
/// neither the model nor the view must leave alone (improvise-felo.13). /// neither the model nor the view must leave alone (improvise-felo.13).
@@ -941,9 +940,8 @@ impl App {
self.geometry = std::cell::OnceCell::new(); self.geometry = std::cell::OnceCell::new();
} }
/// Rebuild the layout only if something it is a function of has moved: the /// Rebuild only if the model, the view drawn or the drill snapshot moved; a
/// model (`layout_stale`), the view it draws or its drill snapshot. A cursor /// cursor key moves none of them (layout-cost.md).
/// key moves none of them (improvise-felo.13, `context/code/command.md`).
fn refresh_layout(&mut self) { fn refresh_layout(&mut self) {
if self.layout_stale || self.layout_basis != self.current_layout_basis() { if self.layout_stale || self.layout_basis != self.current_layout_basis() {
self.rebuild_layout(); self.rebuild_layout();
@@ -953,7 +951,10 @@ impl App {
fn current_layout_basis(&self) -> LayoutBasis { fn current_layout_basis(&self) -> LayoutBasis {
LayoutBasis { LayoutBasis {
view: self.view_name().to_string(), view: self.view_name().to_string(),
drill: self.drill_state.as_ref().map(|d| Rc::as_ptr(&d.records) as usize), drill: self
.drill_state
.as_ref()
.map(|d| Rc::as_ptr(&d.records) as usize),
} }
} }
@@ -1993,13 +1994,27 @@ mod tests {
let key = CellKey::new(vec![("Row".into(), "A".into()), ("Col".into(), "X".into())]); let key = CellKey::new(vec![("Row".into(), "A".into()), ("Col".into(), "X".into())]);
let long = CellValue::Text("a value far wider than a column".into()); let long = CellValue::Text("a value far wider than a column".into());
app.apply_effects(vec![Box::new(SetCell(key, long))]); app.apply_effects(vec![Box::new(SetCell(key, long))]);
assert!(app.layout_builds > builds, "an edit must rebuild the layout"); assert!(
app.layout_builds > builds,
"an edit must rebuild the layout"
);
app.dispatch_key(down()); app.dispatch_key(down());
assert!(app.width_passes.get() > widths, "an edit must remeasure"); assert!(app.width_passes.get() > widths, "an edit must remeasure");
let model = &app.model_state.workbook.model; let model = &app.model_state.workbook.model;
let fresh = crate::ui::viewport::compute_col_widths(model, &app.layout, &FormatPlan::of(model, app.view())); let fresh = crate::ui::viewport::compute_col_widths(
assert_eq!(app.grid_geometry().summary.col_widths, fresh, "stale widths"); model,
assert_ne!(fresh, narrow, "the edit widened a column, or this proves nothing"); &app.layout,
&FormatPlan::of(model, app.view()),
);
assert_eq!(
app.grid_geometry().summary.col_widths,
fresh,
"stale widths"
);
assert_ne!(
fresh, narrow,
"the edit widened a column, or this proves nothing"
);
} }
/// improvise-felo.13: a `ViewOnly` effect that changes which view is drawn /// improvise-felo.13: a `ViewOnly` effect that changes which view is drawn
@@ -2010,7 +2025,9 @@ mod tests {
app.model_state.workbook.create_view("Second"); app.model_state.workbook.create_view("Second");
app.rebuild_layout(); app.rebuild_layout();
let builds = app.layout_builds; let builds = app.layout_builds;
app.apply_effects(vec![Box::new(crate::ui::effect::SwitchView("Second".into()))]); app.apply_effects(vec![Box::new(crate::ui::effect::SwitchView(
"Second".into(),
))]);
assert_eq!(app.view_state.active_view.as_str(), "Second"); assert_eq!(app.view_state.active_view.as_str(), "Second");
assert!(app.layout_builds > builds, "the layout still draws Default"); assert!(app.layout_builds > builds, "the layout still draws Default");
} }
+9 -5
View File
@@ -142,13 +142,17 @@ pub fn grid_in(app: &App, view_name: &str, viewport: Viewport) -> GridCache {
let plan = FormatPlan::of(model, view); let plan = FormatPlan::of(model, view);
let rows = viewport.rows_in(layout.row_count()); let rows = viewport.rows_in(layout.row_count());
let cols = viewport.cols_in(layout.col_count()); let cols = viewport.cols_in(layout.col_count());
// ONE walk: the widths, the totals and this window's values all come off the // One walk gives widths, totals and window (improvise-jmjz); the host's layout
// same evaluation — `context/code/command.md` (improvise-jmjz). // keeps the first two until rebuilt, so its window costs its own cells.
// The host's own layout keeps its whole-grid measurements until it is rebuilt, so
// a window costs its own cells. Another view's grid has no such cache.
let hosted = elsewhere.is_none() && derived.is_none(); let hosted = elsewhere.is_none() && derived.is_none();
let body = match hosted { let body = match hosted {
true => grid_window(model, layout, &app.grid_geometry().summary, rows.clone(), cols.clone()), true => grid_window(
model,
layout,
&app.grid_geometry().summary,
rows.clone(),
cols.clone(),
),
false => grid_body(model, layout, &plan, rows.clone(), cols.clone()), false => grid_body(model, layout, &plan, rows.clone(), cols.clone()),
}; };
+10 -3
View File
@@ -327,8 +327,16 @@ mod tests {
let plan = FormatPlan::of(&wb.model, view); let plan = FormatPlan::of(&wb.model, view);
let summary = grid_summary(&wb.model, &layout, &plan); let summary = grid_summary(&wb.model, &layout, &plan);
let (rows, cols) = (layout.row_count(), layout.col_count()); let (rows, cols) = (layout.row_count(), layout.col_count());
assert!(rows >= 2 && cols >= 3, "a window strictly inside is possible"); assert!(
for (r, c) in [(0..rows, 0..cols), (1..2, 1..3), (rows - 1..rows, 0..1), (0..0, 0..0)] { rows >= 2 && cols >= 3,
"a window strictly inside is possible"
);
for (r, c) in [
(0..rows, 0..cols),
(1..2, 1..3),
(rows - 1..rows, 0..1),
(0..0, 0..0),
] {
let walked = grid_body(&wb.model, &layout, &plan, r.clone(), c.clone()); let walked = grid_body(&wb.model, &layout, &plan, r.clone(), c.clone());
let held = grid_window(&wb.model, &layout, &summary, r.clone(), c.clone()); let held = grid_window(&wb.model, &layout, &summary, r.clone(), c.clone());
assert_eq!(held.window, walked.window, "window {r:?} x {c:?}"); assert_eq!(held.window, walked.window, "window {r:?} x {c:?}");
@@ -337,5 +345,4 @@ mod tests {
assert_eq!(held.col_formats, walked.col_formats); assert_eq!(held.col_formats, walked.col_formats);
} }
} }
} }
+45 -17
View File
@@ -212,8 +212,8 @@ pub struct DataStore {
/// Reverse index for the client-facing surrogate (ADR 002), plus each /// Reverse index for the client-facing surrogate (ADR 002), plus each
/// record's write counter. Why it lives here: core.md §8. /// record's write counter. Why it lives here: core.md §8.
records: HashMap<RecordHash, RecordEntry>, records: HashMap<RecordHash, RecordEntry>,
/// How many stored cells have each number of coordinates. Its largest key /// How many stored cells have each number of coordinates; its largest key
/// says when a full-width partial can only match one exact cell (§8). /// says when a partial can match only one exact cell.
arities: std::collections::BTreeMap<usize, usize>, arities: std::collections::BTreeMap<usize, usize>,
} }
@@ -406,17 +406,16 @@ impl DataStore {
.unwrap_or_default() .unwrap_or_default()
} }
/// The answer without narrowing, when `partial` names as many coordinates as /// The answer without narrowing, when `partial` is as wide as the widest cell
/// the widest stored cell has: only a cell equal to it can match (core.md §8). /// stored: only an equal cell can match (layout-cost.md). `None` otherwise.
/// `None` where that does not hold, and the narrowing path answers.
fn exact_positions(&self, partial: &[(String, String)]) -> Option<Vec<usize>> { fn exact_positions(&self, partial: &[(String, String)]) -> Option<Vec<usize>> {
let widest = *self.arities.keys().next_back()?; let widest = *self.arities.keys().next_back()?;
// Strictly sorted by category, the order a stored key is in, so one probe is the lookup. // Strictly sorted by category, the order a stored key is in, so one probe is the lookup.
let canonical = partial.windows(2).all(|w| w[0].0 < w[1].0); let canonical = partial.windows(2).all(|w| w[0].0 < w[1].0);
(!partial.is_empty() && widest <= partial.len() && canonical).then(|| { (!partial.is_empty() && widest <= partial.len() && canonical).then(|| {
let found = self.intern_partial(partial).and_then(|pairs| { let found = self
self.cells.get_index_of(&InternedKey(pairs)) .intern_partial(partial)
}); .and_then(|pairs| self.cells.get_index_of(&InternedKey(pairs)));
found.into_iter().collect() found.into_iter().collect()
}) })
} }
@@ -1402,14 +1401,21 @@ mod exact_path_tests {
use super::{CellKey, CellValue, DataStore, NARROWED}; use super::{CellKey, CellValue, DataStore, NARROWED};
fn key(a: &str, b: &str, c: &str) -> CellKey { fn key(a: &str, b: &str, c: &str) -> CellKey {
CellKey::new(vec![("A".into(), a.into()), ("B".into(), b.into()), ("C".into(), c.into())]) CellKey::new(vec![
("A".into(), a.into()),
("B".into(), b.into()),
("C".into(), c.into()),
])
} }
fn store() -> DataStore { fn store() -> DataStore {
let mut d = DataStore::new(); let mut d = DataStore::new();
for i in 0..6 { for i in 0..6 {
for j in 0..6 { for j in 0..6 {
d.set(key(&format!("a{i}"), &format!("b{j}"), "c"), CellValue::Number((i * 6 + j) as f64)); d.set(
key(&format!("a{i}"), &format!("b{j}"), "c"),
CellValue::Number((i * 6 + j) as f64),
);
} }
} }
d d
@@ -1427,12 +1433,21 @@ mod exact_path_tests {
let d = store(); let d = store();
let full = key("a2", "b3", "c").0; let full = key("a2", "b3", "c").0;
let mut got = Vec::new(); let mut got = Vec::new();
assert_eq!(narrowings(|| got = d.matching_values(&full).into_iter().cloned().collect()), 0); assert_eq!(
narrowings(|| got = d.matching_values(&full).into_iter().cloned().collect()),
0
);
assert_eq!(got, vec![CellValue::Number(15.0)]); assert_eq!(got, vec![CellValue::Number(15.0)]);
let missing = key("a2", "b9", "c").0; let missing = key("a2", "b9", "c").0;
assert!(d.matching_values(&missing).is_empty(), "an absent cell matches nothing"); assert!(
d.matching_values(&missing).is_empty(),
"an absent cell matches nothing"
);
let unknown = key("a2", "b3", "zzz").0; let unknown = key("a2", "b3", "zzz").0;
assert!(d.matching_values(&unknown).is_empty(), "an unseen item matches nothing"); assert!(
d.matching_values(&unknown).is_empty(),
"an unseen item matches nothing"
);
} }
/// The control: a narrower partial still sums its supersets, so the shortcut /// The control: a narrower partial still sums its supersets, so the shortcut
@@ -1440,7 +1455,10 @@ mod exact_path_tests {
#[test] #[test]
fn a_narrower_partial_still_narrows_and_matches_every_superset() { fn a_narrower_partial_still_narrows_and_matches_every_superset() {
let d = store(); let d = store();
let narrow = vec![("A".to_string(), "a2".to_string()), ("C".to_string(), "c".to_string())]; let narrow = vec![
("A".to_string(), "a2".to_string()),
("C".to_string(), "c".to_string()),
];
let mut n = 0; let mut n = 0;
assert_eq!(narrowings(|| n = d.matching_values(&narrow).len()), 1); assert_eq!(narrowings(|| n = d.matching_values(&narrow).len()), 1);
assert_eq!(n, 6); assert_eq!(n, 6);
@@ -1451,13 +1469,23 @@ mod exact_path_tests {
fn a_wider_cell_turns_the_shortcut_off_until_it_is_removed() { fn a_wider_cell_turns_the_shortcut_off_until_it_is_removed() {
let mut d = store(); let mut d = store();
let wide = CellKey::new(vec![ let wide = CellKey::new(vec![
("A".into(), "a2".into()), ("B".into(), "b3".into()), ("C".into(), "c".into()), ("D".into(), "d".into()), ("A".into(), "a2".into()),
("B".into(), "b3".into()),
("C".into(), "c".into()),
("D".into(), "d".into()),
]); ]);
d.set(wide.clone(), CellValue::Number(100.0)); d.set(wide.clone(), CellValue::Number(100.0));
let full = key("a2", "b3", "c").0; let full = key("a2", "b3", "c").0;
assert_eq!(d.matching_values(&full).len(), 2, "the exact cell and the wider cell under it"); assert_eq!(
d.matching_values(&full).len(),
2,
"the exact cell and the wider cell under it"
);
d.remove(&wide); d.remove(&wide);
assert_eq!(narrowings(|| assert_eq!(d.matching_values(&full).len(), 1)), 0); assert_eq!(
narrowings(|| assert_eq!(d.matching_values(&full).len(), 1)),
0
);
} }
/// An unsorted partial is not a stored key's order, so it must not be probed. /// An unsorted partial is not a stored key's order, so it must not be probed.
+37 -12
View File
@@ -252,9 +252,8 @@ thread_local! {
static DATA_ITEM_STEPS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) }; static DATA_ITEM_STEPS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
} }
/// Where each data item sits in `items`: entry `positions(items)[n]` is data item /// Entry `positions(items)[n]` is data item `n`: a lookup by data index is O(1),
/// `n`. Built once per axis so a lookup by data index is O(1), not a walk past /// not a walk past every header before it (layout-cost.md).
/// every header and item before it (improvise-felo.13, `core.md` §20).
fn positions(items: &[AxisEntry]) -> Vec<usize> { fn positions(items: &[AxisEntry]) -> Vec<usize> {
items items
.iter() .iter()
@@ -3083,12 +3082,26 @@ mod lookup_cost_tests {
wb.active_view_mut().toggle_group_collapse("Row", &["Q2"]); wb.active_view_mut().toggle_group_collapse("Row", &["Q2"]);
wb.active_view_mut().prune_empty = true; wb.active_view_mut().prune_empty = true;
for i in 0..5 { for i in 0..5 {
let key = CellKey::new(vec![("Row".into(), format!("r{i}")), ("Col".into(), "c0".into())]); let key = CellKey::new(vec![
("Row".into(), format!("r{i}")),
("Col".into(), "c0".into()),
]);
wb.model.set_cell(key, CellValue::Number(1.0)); wb.model.set_cell(key, CellValue::Number(1.0));
} }
let layout = GridLayout::with_frozen_records(&wb.model, wb.active_view(), None); let layout = GridLayout::with_frozen_records(&wb.model, wb.active_view(), None);
assert!(layout.row_items.iter().any(|e| !matches!(e, AxisEntry::DataItem { .. }))); assert!(
assert!(layout.row_items.iter().any(|e| matches!(e, AxisEntry::DataItem { collapsed: true, .. }))); layout
.row_items
.iter()
.any(|e| !matches!(e, AxisEntry::DataItem { .. }))
);
assert!(layout.row_items.iter().any(|e| matches!(
e,
AxisEntry::DataItem {
collapsed: true,
..
}
)));
let walked: Vec<&Vec<String>> = data_items(&layout.row_items).collect(); let walked: Vec<&Vec<String>> = data_items(&layout.row_items).collect();
assert_eq!(layout.row_count(), walked.len()); assert_eq!(layout.row_count(), walked.len());
let visual: Vec<usize> = layout let visual: Vec<usize> = layout
@@ -3099,19 +3112,31 @@ mod lookup_cost_tests {
.map(|(at, _)| at) .map(|(at, _)| at)
.collect(); .collect();
for i in 0..walked.len() + 2 { for i in 0..walked.len() + 2 {
assert_eq!(layout.row_tuple(i), walked.get(i).map(|t| t.as_slice()), "row {i}"); assert_eq!(
assert_eq!(layout.data_row_to_visual(i), visual.get(i).copied(), "visual {i}"); layout.row_tuple(i),
walked.get(i).map(|t| t.as_slice()),
"row {i}"
);
assert_eq!(
layout.data_row_to_visual(i),
visual.get(i).copied(),
"visual {i}"
);
let folded = matches!( let folded = matches!(
data_items_entry(&layout.row_items, i), data_items_entry(&layout.row_items, i),
Some(AxisEntry::DataItem { collapsed: true, .. }) Some(AxisEntry::DataItem {
collapsed: true,
..
})
); );
assert_eq!(layout.row_folds_a_group(i), folded, "fold {i}"); assert_eq!(layout.row_folds_a_group(i), folded, "fold {i}");
} }
} }
fn data_items_entry(items: &[super::AxisEntry], n: usize) -> Option<&super::AxisEntry> { fn data_items_entry(items: &[super::AxisEntry], n: usize) -> Option<&super::AxisEntry> {
items.iter().filter(|e| matches!(e, super::AxisEntry::DataItem { .. })).nth(n) items
.iter()
.filter(|e| matches!(e, super::AxisEntry::DataItem { .. }))
.nth(n)
} }
} }