Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
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.