Skip to content

Keep slow work off the UI thread and contain failures - #6

Merged
mzagozda merged 1 commit into
masterfrom
resilience-and-performance-fixes
Oct 11, 2026
Merged

mzagozda merged 1 commit into
masterfrom
resilience-and-performance-fixes

Conversation

@mzagozda

Copy link
Copy Markdown
Contributor

Why

The low-level keyboard hook runs on the UI thread, so anything that blocks that thread delays key presses and can make Windows silently drop the hook. Several things did block it, and a number of failures could crash the app or leave it unable to start or exit.

What changed

Responsiveness

  • Window icons load on a background thread. WPF runs a value converter on the UI thread even for an IsAsync binding, so icon retrieval froze the switcher (194 ms for 93 windows on the test machine, 156 ms of it one busy window). Cached icons are attached before the list is shown.
  • The Alt+Tab hook callback only claims the key press; the window is shown from the dispatcher afterwards. The hook is installed only while the Alt+Tab option is on.
  • Titles are read once per load and formatted before the list is bound, instead of being filled in afterwards in batches separated by sleeps.
  • SignificantCharactersMatcher no longer compiles a regex per keystroke (16.7 ms -> 1.8 ms for 60 titles).
  • Highlight markup is parsed as plain XML instead of with XamlReader (280 us -> 6 us per title, and no 110 ms first-use cost).

Resilience

  • A corrupt user.config is set aside and the app restarts with defaults, instead of crashing on every launch.
  • Unhandled exceptions from the dispatcher, Windows Forms callbacks and the hook callback are logged; the overlay is hidden and the app keeps running.
  • Disposal never throws. A hook already removed by Windows used to make Exit fail and leave a process without tray icon holding the single-instance mutex.
  • Window titles with characters XML cannot represent no longer blank the rest of the list.
  • A process that exited or cannot be queried no longer aborts the whole filter run; windows destroyed during enumeration are skipped.
  • ToUnicodeEx gets a real 256-byte key state, and the EnumPropsEx callback uses pointer-sized parameters.

Behaviour changes to review

  • The one-second window handle cache is removed. It saved under 1 ms and reused a stale window order when the switcher was reopened quickly.
  • A pending filter is applied before Enter, the arrow keys or Ctrl+W act on the list, so a command issued right after typing uses the typed text.
  • The "Press Alt + S to search" hint is no longer cleared when the list finishes loading.
  • WindowHandleToIconConverter and WindowHandleToCachedIconConverter are replaced by WindowIconImageLoader.

Testing

  • Debug and Release build with 0 warnings; 45 unit tests pass (8 new).
  • Load, filter and icon paths were driven headlessly against 93 real windows (window never shown).
  • Corrupt-settings recovery was run end to end on an isolated copy of the built exe.
  • Not exercised: on-screen rendering of the rows, real hotkey and Alt+Tab key presses, and the Options dialog. Please try these before merging.

🤖 Generated with Claude Code

The low-level keyboard hook runs on the UI thread, so anything that
blocks that thread delays key presses and can make Windows drop the
hook. Several things did block it, and a number of failures could take
the whole application down or leave it unable to start or exit.

Responsiveness
- Load window icons on a background thread. WPF runs a value converter
  on the UI thread even for an IsAsync binding, so retrieving icons
  (which waits for a reply from each window) froze the switcher.
  Cached icons are attached before the list is shown.
- Only claim the key press inside the Alt+Tab hook callback and show
  the window from the dispatcher afterwards.
- Install the Alt+Tab hook only while that option is enabled.
- Read window and process titles once per load and format them before
  the list is bound, instead of filling them in afterwards in batches
  separated by sleeps.
- Stop compiling a new regex for every keystroke in
  SignificantCharactersMatcher; interpreting it is about nine times
  cheaper for the few dozen titles it is matched against.
- Parse the highlight markup as plain XML instead of with XamlReader.
- Read process start times through one limited-access handle.

Resilience
- Recover from a corrupt user.config by setting it aside and
  restarting, instead of crashing on every launch.
- Log unhandled exceptions from the dispatcher, Windows Forms callbacks
  and the hook callback, hide the overlay and keep running.
- Never let disposal throw: a hook that Windows already removed made
  Exit fail and left a process without tray icon holding the mutex.
- Drop characters that XML cannot represent from window titles; one
  such title used to leave the rest of the list blank.
- Treat a process that exited or cannot be queried as having no name
  instead of aborting the whole filter run.
- Skip windows that are destroyed while being enumerated.
- Pass a real 256-byte key state to ToUnicodeEx and use pointer-sized
  parameters for the EnumPropsEx callback.

Behaviour
- Remove the one-second window handle cache. It saved under a
  millisecond and reused a stale window order when the switcher was
  reopened quickly.
- Apply a pending filter before Enter, the arrow keys or Ctrl+W act on
  the list, so a command issued right after typing uses the typed text.
- Keep the "Press Alt + S to search" hint, which was cleared as soon as
  the list had loaded.

WindowHandleToIconConverter and WindowHandleToCachedIconConverter are
replaced by WindowIconImageLoader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 11, 2026 00:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Native hooks, P/Invoke changes, and UI-thread behavior require the manual rendering and keyboard testing identified in the PR description.

0 open findings

What changed in this PR

Moves blocking window discovery, icon loading, and filtering work off the UI thread while improving recovery from configuration, hook, and callback failures.

Changes:

  • Performs window metadata, icon loading, and filtering asynchronously with cancellation and caching.
  • Adds exception containment, settings recovery, and safer shutdown/disposal.
  • Optimizes highlighting, matching, keyboard translation, and Win32 interop.
File Description
TaskSwitcher/​WindowIconImageLoader.cs Adds background-safe icon loading.
TaskSwitcher/​WindowHandleToIconConverter.cs Removes synchronous icon converter.
TaskSwitcher/​WindowHandleToCachedIconConverter.cs Removes cached-icon converter.
TaskSwitcher/​Program.cs Adds settings recovery and global exception logging.
TaskSwitcher/​MainWindow.xaml.cs Refactors loading, filtering, hooks, and disposal.
TaskSwitcher/​MainWindow.xaml Binds rows to preloaded icons.
TaskSwitcher/​FormattedTextAttribute.cs Replaces XAML parsing with XML parsing.
TaskSwitcher/​BoolToWhateverConverter.cs Safely handles non-boolean values.
TaskSwitcher/​AppWindowViewModel.cs Caches titles and exposes icon state.
TaskSwitcher/​App.xaml.cs Handles dispatcher failures.
TaskSwitcher/​AltTabHook.cs Contains hook callback exceptions.
ManagedWinapi/​Hook.cs Makes hook callbacks and disposal safer.
Core/​XamlHighlighter.cs Adds fast, safe markup escaping.
Core/​WindowFinder.cs Skips windows that fail inspection.
Core/​WinApi.cs Corrects Win32 interop declarations.
Core/​Matchers/​SignificantCharactersMatcher.cs Avoids per-query regex compilation.
Core/​KeyboardHelper.cs Corrects keyboard-state translation.
Core/​AppWindow.cs Contains process-query failures and optimizes identity lookup.
Core.UnitTests/​XamlHighlighterTests.cs Covers escaping and invalid characters.
Core.UnitTests/​SignificantCharactersMatcherTests.cs Expands matching and cache coverage.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@mzagozda
mzagozda merged commit 4817999 into master Oct 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants