Files
improvise/context/test-evidence/layout-cost.md
T

104 lines
5.7 KiB
Markdown

# 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.