We were unable to construct the commit group for this pull request: Author does not currently possess a GitClear subscription seat.

[Shortcut Guide] Manifest index creation refactor and bug fixing #50609

Open
daverayment opened 6:42pm on September 16, 2026 wants to merge 1549 Ξ” into microsoft/powertoys main from
refactor/shortcut-guide-indexing-perf
Reviewing
99 fewer changed lines
(24% less) vs GitHub
This saves about 42.0 hours per year vs conventional diff tools
daverayment authored
of work between September 9 and September 16
Diff Delta:
1549
About 54 Diff Delta/hour
Classified as:  Bug Fix, Feature Add, General

daverayment's Description of Work

Summary of the Pull Request

This PR refactors the index generation and startup pipeline of Shortcut Guide, improving launch speed, removing redundancy and fixing concurrency issues. Several related bug fixes - including for a crash bug and for startup window flicker - are also included. It builds upon the prior PR #50497, which centred on fixing the lack of error handling and file checking in the index generation project. The commits from this PR are the latest 3. (I couldn't create a stacked PR because I don't have the necessary permissions, sorry.)

This PR is focused on the manifest file index creation and initial application startup and includes a further 12 unit tests on top of the 5 from the earlier PR.

PR Checklist


  • [x] Closes: #50570

  • [x] Closes: #50571

  • [x] Closes: #50586

  • [x] Closes: #50588

  • [x] Closes: #50590

  • [ ] Communication: I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected

  • [x] Tests: Added/updated and all pass

  • [ ] Localization: All end-user-facing strings can be localized

  • [ ] Dev docs: Added/updated

  • [ ] New binaries: Added on the required places



  • [ ] Documentation updated: If checked, please file a pull request on our docs repo and link it here: #xxx

Detailed Description of the Pull Request / Additional comments

Fixes


  • Fixed crash bug caused by the _getAppIdsTask Task in MainPaneControl being Disposed while it was still active. If the application was closed and then reopened immediately, this would cause the crash because two threads were attempting to update the nav items list concurrently. Tasks typically don't need to be disposed of manually. To fix the underlying issue, an invocation generation counter was added to the MainPaneControl, so an old Task can never update a newer instance of the control. A log entry is added when this is detected and mitigated.

  • Hardened MainPaneControl.InitializeNavItemsAsync against a latent null-reference: the background app-id task is now captured to a local variable before use, so it can no longer be cleared concurrently (by Hide()/OnUnloaded()) between the null-check and the subsequent await.

  • Fixed the UI flashing immediately upon first activation before the full overlay window is shown. This was a combination of an event ordering issue, the Visibility property of MainPane being set to Visible too soon (in the Loaded handler), and the module runner passing SW_SHOW instead of SW_HIDE.

  • The PowerToys hotkeys manifest file is now generated before the index file is created. Previously, this order was reversed, meaning Shortcut Guide had to be restarted twice to pick up any PowerToys shortcut changes by the user.


  • PowerToysShortcutsPopulator.Populate() now diffs its generated content against what's on disk and only writes if it actually changed.

  • Fixed the foreground window only being logged at application start instead of when the shortcut window is actually requested. The HWND/module/exe information is now logged each time Shortcut Guide is triggered.

  • The index file writing is now atomic. Previously, it was possible for the UI project to read a part-written index file, leading to an exception. Now, the index file is created as a temporary file before being Move()d to its destination.

Performance


  • Manifest files are not copied if the bundled file has the same modified date or older.

  • The index file is not recreated if all manifest files in the destination folder are older than it.

  • In the UI project, the YamlDotNet Deserializer is no longer created for each file's parsing - a shared class-level Deserializer is used instead, and set to Lazy<> so we don't pay for the setup cost if it isn't used.

  • In PowerToysShortcutsPopulator.HotkeySettingsToYaml, we now use a StringBuilder instead of concatenating string instances when creating shortcut entries for YAML, saving allocations and time, especially as there are already hundreds of shortcuts to process.

  • The index generator is called in-process instead of shelling out to an exe via Process.Start(). Shelling out can cost more than 150 ms, and the UI cannot be displayed until this step is complete. The index generator exe is still available for external callers, and is now just a thin wrapper around calling the shared CreateIndexYmlFile().

  • Manifest index generation now processes the headers only, and uses a small custom deserializer which is Span-based and allocation-free. Previously, YamlDotNet deserialized each manifest file completely, including the shortcuts, which were not needed. Manifest file opening and processing is done in parallel, which helps to mitigate the cost of anti-virus scanning of any newly-copied manifest files.

  • Index file serialization is now a few lines of StringBuilder code and removes the need for YamlDotNet's serializer, saving startup memory and time.

  • In the UI project, logging the foreground window is done via a background Task, as it calls relatively expensive process-interrogation APIs.


  • GetCachedIndexYamlFile() is called once in GetAllCurrentApplicationIds() and IndexFile is cached, instead of every time a lookup happens. The file does not change after generating, so loading and deserializing the data each time is not required.


  • Tasks used instead of Threads (see below).

Manifest Index Generator project


  • Now free of dependencies. The project is now AOT compilation compliant and publishing an AOT compiled version has been tested.


  • ManifestIndexGenerator is shared between the console utility (PowerToys.ShortcutGuide.IndexYmlGenerator.exe) and the desktop application.

  • The manifest file path is owned by this project now, under IndexYmlGenerator.ManifestIndexGenerator.DefaultManifestsPath. This means the UI project dependency can be removed, as it was only in place for this single path.

  • The new manifest header parser and index file writer allowed for the removal of the YamlDotNet dependency and a significant increase in speed.

  • A Managed.Common.Logger-compliant mini-logger was added, so CLI-callers can get logs output by file and/or via the console.

  • The AOT-compiled exe runs in 4 ms instead of 900 ms or more, including file reading, parsing and generating the index for 43 files.

Asynchrony

Changed new Thread() calls with TAP throughout:
1. Manifest and Index Generation (Program.cs)
- Old: Used a raw new Thread() for file copying/generation, which was later synchronously blocked on the UI thread using .Join() during the window's initialization.
- New: Converted to a fire-and-forget Task.Run(). The UI now safely awaits the new ManifestInitializationTask during startup, guaranteeing the WinUI message pump is never frozen while files are being copied or parsed.
2. PowerToys Runner Watcher (Program.cs)
- Old: Created a dedicated OS thread that blocked on a synchronous runnerProcess.WaitForExit() call, paired with a redundant global WaitHandle registration in App.xaml.cs.
- New: Replaced with Task.Run(async () => ...) and await runnerProcess.WaitForExitAsync(). This frees the underlying thread while waiting for the runner to close. The legacy ThreadPool.RegisterWaitForSingleObject infrastructure was removed.
3. Activation Event Listener (App.xaml.cs)
- Old: Used a raw Thread with a while(true) Win32 handle listener, which was stopped using .Join(timeout) during shutdown.
- New: Migrated to Task.Factory.StartNew(..., TaskCreationOptions.LongRunning). This preserves the dedicated background thread required for the blocking WaitAny loop, but standardises the lifecycle. It now shuts down cleanly using Task.Wait(), automatically catching any crashes inside a safe AggregateException rather than allowing unhandled thread exceptions to silently tear down the process.

Miscellaneous


  • Added AnonymizePath() for logging paths, which replaces a user's username in a path with verbatim <username> for privacy.

  • Index generation is now deterministic and independent of the order in which manifest files are retrieved. The same index.yml file will be produced for the same set of manifest files.

Performance

Release builds on Core i5-1135G7 laptop @ 2.4 GHz:

Old
1075 ms (cold start, no files exist in destination folder)
965 ms (warm start, no files exist in destination folder)
902 ms (all manifest files exist, only index needs recreating)
892 ms (30 manifest files and index missing)
954 ms (index and all manifest files already present)

New
117 ms (cold start no files exist in destination folder, copying files took 65 ms)
83 ms (warm start no files exist in destination folder, copying files took 60 ms)
37 ms (30 manifest files and index missing, 30 files copied in 35 ms)
27 ms (all manifest files exist, only index needs recreating)
6 ms (index and all manifest files already present)


Validation Steps Performed



  • Unit Tests:


    • Added / updated ShortcutGuide.UnitTests.IndexGeneratorTests covering:

    • Standard manifest extraction

    • Quoted strings and unquoted values

    • Empty files and missing required properties (PackageName, WindowFilter)

    • Manifests with comments and trailing hashes

    • Reserved words, executable names with and without .exe

    • Case sensitivity and deterministic sorting

    • Index regeneration rules

    • All unit tests pass:





  • Manual Verification:


    • Verified overlay displays correct application shortcuts and dynamic PowerToys hotkeys.

    • Stress-tested rapid hotkey toggling to confirm the fix for the _getAppIdsTask Dispose bug.

    • Verified no visual flash / empty overlay box on first activation.

    • Verified user-added manifests in %LocalAppData%\Microsoft\WinGet\KeyboardShortcuts trigger re-indexing on the next launch.

    • Verified console output and file logging when invoking PowerToys.ShortcutGuide.IndexYmlGenerator.exe standalone.

    • Confirmed click-away behaviour and hotkey activation/deactivation work as before, and the overlay window appears in the foreground reliably.



Building updated commit group diff...
17 total changed files
(3 files ignored)
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...