perf(tui): lower the settings filter once per pass, not once per row (#6213 T6) - #6267
Conversation
…6213 T6) `row_matches_filter` lowercased and split `self.filter` on every row, and the settings view runs it twice per interaction — once from `visible_items` and once from `matching_row_indices`. The filter is the same string for every row in a pass, so that work was repeated `2 * rows` times per keystroke. Split the term list into `filter_terms()` and pass it in. Empty terms still mean "everything matches" and the matching expression itself is unchanged, so what the filter accepts is identical. Part of #6213 (item T6). The per-row lowercasing of the row's own label, value, scope and hint — the larger half of this item — is not in this change; those still allocate once per row per pass, and caching them needs the `row_display_value` state dependency settled first, because that one is not a pure function of the row. Verification: cargo check -p codewhale-tui --all-targets --all-features --locked (clean) test result: ok. 329 passed; 0 failed; 0 ignored; 0 measured; 12479 filtered out (tui::views::) Signed-off-by: CodeWhale Bot <bot@codewhale.net>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Claude finished @Hmbown's task in 1m 54s —— View job Review checklist
Review:
|
There was a problem hiding this comment.
Codewhale review
Complete review coverage: 1/1 passes, 1 file patches, sha256:a2b103022e9acc5b7e52c6ab8107c5932eb17cc818cbe9e541ec78b15c3c4ac2.
Pass 1: The diff hoists the lowercased/split filter terms out of the per-row path into a new filter_terms() helper and threads &[String] through row_matches_filter, updating both call sites (matching_row_indices, visible_items). The matching expression itself is unchanged, and the empty-terms shortcut is behaviorally equivalent to the previous filter.trim().is_empty() early return.
Assessment
Pass 1: No defect could be established in the changed code. Behavioral equivalence holds: trim().to_lowercase() followed by split_whitespace() was already the exact sequence applied per row, and trim()/split_whitespace() use the same char::is_whitespace predicate, so a whitespace-only or empty filter still yields zero terms and terms.is_empty() still short-circuits to true exactly as filter.is_empty() did. The comparison chain (section.contains(term) || ...) is untouched; term is &String from terms.iter(), and str::contains accepts &String via the std Pattern impl, so the types are consistent with the previous &str terms. The borrows in both updated loops are all shared (&self, &terms), so there is no aliasing problem. Note the deliberate non-goal: row_display_value/label lowering is still per-row, and the PR itself says so; that is pre-existing work, not a regression here. Unverified context: the review only saw this one file's diff, and repository source excerpts were unavailable, so I could not enumerate every caller of the private row_matches_filter to confirm the two updated call sites are the only ones (a missed third caller would be a compile error). The PR's cargo check/test claims are self-reported and no build or tests were run for this review. Also pre-existing and unchanged by this diff: filtering = !self.filter.is_empty() treats a whitespace-only filter as active, disabling the category rail while showing all rows; that quirk predates this change and is worth a separate look, but it is not introduced here.
Advisory review by Codewhale (codewhale review --pr 6267 --post, head 75e032b61d93a391660c1006022cacf1e010aebc). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
|
Re-ran The original failure was not the SHA-6380 env-var class the handoff note guessed. Under cargo-nextest each test runs in its own process, and this test spawns the real binary with |
…Ubuntu (#6270) `codewhale-cli::telemetry_kill_switch_dispatch::missing_preference_defaults_on_without_inventing_acceptance` has failed twice on Ubuntu CI at `telemetry_kill_switch_dispatch.rs:180` ("default-on writes dry run") on changes that touch neither telemetry nor the CLI: PR #6104 (`c810bc0458`, 2026-09-12) and PR #6267 (`75e032b61d`, 2026-09-16). Both times macOS and Windows passed. It is not an env race. CI runs cargo-nextest, which gives every test its own process, and the test never mutates process-global state — each case spawns the real binary with `env_clear()` and a fresh `TempDir` HOME/CODEWHALE_HOME (`telemetry_kill_switch_dispatch.rs:231-276`). The panic is a *missing file*: `$CODEWHALE_HOME/telemetry/dryrun.jsonl` was never written. The mechanism is a wall-clock deadline. `features list` resolves to `Surface::Cli`, whose exit path waits `CLI_PERSIST_TIMEOUT` — 250 ms (`crates/telemetry/src/lib.rs:75`) — for a detached writer thread that must re-run `decision::re_decide` against disk and then fsync an append before the process exits. The code deliberately fails open, so a missed deadline silently produces no receipt. All three existing test-group overrides filter `binary(integration)`, and this is a *different* binary, so none of them ever matched it: the five cases ran at full parallelism beside 15,774 tests. The CI log shows the runner was saturated at that moment — neighbouring subprocess-spawning tests took 3.5-3.6 s for work that normally finishes well under a second, while this one failed in 0.747 s. This adds the missing override, putting the binary in the existing `telemetry-contract` group (max-threads = 1) for the same reason that group exists: these tests spawn the real binary and cannot absorb scheduler latency. Verified on this machine (macOS aarch64): cargo nextest show-config test-groups -p codewhale-cli --all-features \ --locked -E 'binary(telemetry_kill_switch_dispatch)' group: telemetry-contract (max threads = 1) * override for default profile with filter 'binary(telemetry_kill_switch_dispatch)': codewhale-cli::telemetry_kill_switch_dispatch: (all 5 tests) group: spawns-binaries (max threads = 3) (no matches) sh scripts/with-hermetic-test-home.sh cargo nextest run -p codewhale-cli \ --all-features --locked -E 'binary(telemetry_kill_switch_dispatch)' Summary [0.621s] 5 tests run: 5 passed, 0 skipped Not fixed here: the 250 ms budget itself. Bounding the group removes the load that makes the deadline reachable, but a slow enough host can still miss it. The product-side options — raise CLI_PERSIST_TIMEOUT, hoist the `re_decide` disk read out of the deadline window, or have the CLI path join the writer for the local non-network case — are a behavioural decision, not a test fix, and are left for the issue. Signed-off-by: CodeWhale Bot <bot@codewhale.net> Co-authored-by: CodeWhale Bot <bot@codewhale.net> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Part of #6213 (item T6).
No-Issue: partial work on #6213 — the filter-term hoist only; T4, T5 and the per-row half of T6 remain, so that issue must stay open.
What changed
row_matches_filterlowercased and splitself.filteron every row, and the settings view calls it twice per interaction — once fromvisible_items, once frommatching_row_indices. For a filter likeauto approve, that is the sametrim().to_lowercase()and the samesplit_whitespace()repeated2 × rowstimes per keystroke.The term list moves into
filter_terms(), computed once per pass and passed in as&[String]. Empty terms still mean "everything matches", and the matching expression is untouched, so what the filter accepts is identical.What deliberately did not change
The larger half of T6 — lowercasing each row's own label, value, scope and hint (about 10 allocations per row per pass) — is not here. Caching those needs the
row_display_valuedependency settled first: unlike the rest, it is not a pure function of the row, so a cached blob keyed on the row alone could go stale on screen. I would rather leave that visible than ship a cache with an unsound invalidation rule.Verification
Local only, macOS/aarch64. No hosted CI claim; no benchmark, so the saving is argued from the removed work rather than measured.