Source: context/test-evidence/layout-cost.md
On this page
- Layout and keypress cost (improvise-felo.13)
- Before and after
- What the ticket's reading got right and what it did not predict
- a_cursor_key_rebuilds_neither_the_layout_nor_the_widths
- an_edit_rebuilds_and_remeasures_so_the_widths_follow_the_value
- switching_view_rebuilds_though_the_effect_is_view_only
- an_axis_lookup_does_work_independent_of_the_row_index
- the_position_index_answers_what_the_walk_answered
- a_full_width_partial_probes_one_cell_instead_of_narrowing
- a_window_over_a_held_summary_equals_the_one_walk_grid_body
- a_frozen_records_snapshot_replaces_the_live_row_index
- a_drill_snapshot_replaced_alone_rebuilds_the_layout
- Observation: the TUI frame
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.