# 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. ### a_frozen_records_snapshot_replaces_the_live_row_index `view::layout::lookup_cost_tests`. Three records are frozen, two are then cleared, and `with_frozen_records` over the snapshot must draw 3 rows (the control: with no snapshot it draws 1). A drill held open while another client removes records is the production analogue. Rival (deleting `layout.reindex()` after the snapshot swap): fails `left: 1, right: 3`; the whole workspace passed it before this test. ### a_drill_snapshot_replaced_alone_rebuilds_the_layout `ui::app::tests`. `StartDrill` is `ViewOnly`, so a batch of it alone has no `Model` effect to mark the layout stale; the drill pointer in `LayoutBasis` is the only thing that rebuilds it. The test starts one snapshot, then a second: the first step alone passes even with a constant pointer (drill absent to present changes the basis), which is why the second step is the test. Rival (pointer replaced by a constant): fails `no rebuild for a second drill snapshot`. ### Observation: the TUI frame `tui_frame_grid_of_whole` is still 47-53 ms at 19,683 rows after this change: the draw path projects the whole grid, linear and out of scope for this ticket.