Skip to content

[CmdPal] Fast first paint: render deterministic results before slow fallbacks#49194

Open
michaeljolley wants to merge 5 commits into
dev/mjolley/provider-weightingfrom
dev/mjolley/dev-mjolley-fast-first-paint
Open

[CmdPal] Fast first paint: render deterministic results before slow fallbacks#49194
michaeljolley wants to merge 5 commits into
dev/mjolley/provider-weightingfrom
dev/mjolley/dev-mjolley-fast-first-paint

Conversation

@michaeljolley

@michaeljolley michaeljolley commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Warning

This PR is one in a series of PRs focused on rearchitecting the search/scoring logic of the MainListPage. An explanation of the entire search/scoring logic can be found in the first PR of the change (#49189).

This PR should not be merged until PR #49195 is merged into it.

Summary

Phase 4 of the CmdPal search overhaul (stacked on dev/mjolley/provider-weighting). Makes search FEEL fast by ensuring first paint renders deterministic in-proc results (top-level commands + installed apps) immediately, and only folds in slow, out-of-proc fallback contributions asynchronously as they resolve, without re-blocking the UI or letting late fallbacks jarringly reorder deterministic results.

Finding

The typing path (MainListPage.UpdateSearchTextCore) was ALREADY async with respect to slow fallback work:

  • FallbackUpdateManager.BeginUpdate is fire-and-forget on a dedicated thread pool; the typing path never awaits the COM UpdateQuery calls.
  • The tier ladder (MainListRanker) classifies every fallback at FallbackFloor (tier 1), strictly below any deterministic Fuzzy+ match (tier 2+), so late fallbacks can never leapfrog deterministic results.
  • Supersession is already handled: each keystroke cancels the prior CancellationToken and FallbackUpdateManager guards its change callback on that token.

The one remaining coupling (the concrete win)

Global/special fallbacks were scored EAGERLY at keystroke time with FilterListWithScores, freezing the integer score against the fallback's stale (not-yet-resolved) title. When the dynamic title resolved asynchronously, the live Title updated but the frozen score did not.

Fix: defer global-fallback scoring to the render path (GetSearchViewItems) so slow fallback results fold into the already-rendered list with fresh scores from the SAME ranker. First paint is unchanged (deterministic commands + apps never depend on this). Only within-floor ordering of fallbacks updates; no cross-tier movement.

Threading / cancellation approach

  • A keystroke snapshots the global-fallback source list and its precomputed query together (_globalFallbackSources + _globalFallbackQuery); a superseding keystroke atomically replaces both.
  • Scoring is deferred to the render path via the new internal static ScoreDeferredFallbacks helper, always reflecting the latest snapshot.
  • Async title resolution folds in through the existing _onFallbackChanged -> RequestRefresh(false) path, which is guarded by the per-keystroke cancellation token, so stale slow results are dropped.
  • All list mutations remain marshaled to the UI thread as before; no new threading was introduced.

Tests

New FastFirstPaintTests (6 tests) covering: deterministic-only first paint with no fallback contribution, unresolved fallback absent at first paint, slow fallback folds in when its title resolves, re-score reflects the current title (not a frozen value), superseding query replaces the snapshot (stale strong match not applied), and late fallback merges below deterministic matches (no leapfrog). Uses a fake mutable-title source; no dependency on real extensions.

Build & test

  • Build: ools\build\build.ps1 -Path src\modules\cmdpal -Platform x64 -Configuration Debug -> exit 0.
  • All CmdPal *.UnitTests pass (x64 host). ViewModels: 163/163. Only non-green is the known pre-existing WindowWalker 'Zero tests ran'.

…allbacks

Defer scoring of global/special fallbacks from keystroke time to the render
path (GetSearchViewItems) so slow, asynchronously-resolved fallback titles fold
into the already-rendered list with fresh scores from the same ranker, instead
of a score frozen against a stale title. Deterministic in-proc results
(top-level commands + apps) are unchanged and never wait on the async fallback
work, so first paint stays fast. Fallbacks remain pinned at the FallbackFloor
tier, so the deferred re-score only reorders them among themselves and can never
leapfrog deterministic command/app matches. A superseding keystroke atomically
replaces the snapshot (sources + query) and the existing per-keystroke
cancellation token drops stale async fallback callbacks.

Extract ScoreDeferredFallbacks as an internal static helper and add guardrail
unit tests covering deterministic-only first paint, late fallback fold-in with a
fresh score, no leapfrog, and supersession by snapshot replacement.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added the Product-Command Palette Refers to the Command Palette utility label Jul 8, 2026
michaeljolley and others added 4 commits July 8, 2026 13:45
…into dev/mjolley/dev-mjolley-fast-first-paint
…into dev/mjolley/dev-mjolley-fast-first-paint
- ClearResults now also resets _globalFallbackQuery so the snapshot pair
  (_globalFallbackSources + _globalFallbackQuery) is cleared symmetrically.
  Behavior is unchanged since ScoreDeferredFallbacks short-circuits on a null
  source list; this just avoids a confusing leftover value.
- Rename the superseding-query test to ReScoreUsesLatestSnapshot_StaleStrongMatchNotApplied
  and document that it exercises the render-path re-score at the helper level,
  while the true atomic field swap is guaranteed by the lock in
  UpdateSearchTextCore / GetItems.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…into dev/mjolley/dev-mjolley-fast-first-paint
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Product-Command Palette Refers to the Command Palette utility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant