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

5.7 KiB

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.