fix(settings-ui): release page-owned resources on navigation #50649

Open
niels9001 opened 8:13am on September 18, 2026 wants to merge 1 commit into microsoft/powertoys niels9001-stable-dashboard-shortcuts from
niels9001-settings-launch-benchmarks
Diff Delta:
0
About 62 Diff Delta/hour
Classified as:  General

niels9001's Description of Work

Summary of the Pull Request

Release Settings page-owned view models, subscriptions, callbacks and native registrations when navigating away, while preserving reload behavior and standalone Quick Access hide/show behavior.

Draft follow-up to #50581. This PR targets niels9001-stable-dashboard-shortcuts rather than main, so its net diff contains only the lifetime follow-ups. The measured candidate is the unchanged commit 3eda888f4be05b18b1ebaf03277df2b558bb0db9; baseline is #50581 at 2bdc6357aa7b135533bcb2121b1ed3cabf3c7705. Existing PRs, including #50530, are unchanged.

PR Checklist


  • [x] Communication: Discussed and reviewed with the coordinating contributor.

  • [x] Tests: Added/updated focused unit coverage; Debug and Release results are listed below. The separate isolated collection limitation remains explicitly open.

  • [x] Dev docs: Added page-lifetime guidance and a Power Display lifetime checklist.

Issue closure: N/A; this is an investigation follow-up. Localization/new binaries/installer or CI wiring/public documentation changes: N/A; no new product binaries, dependencies or user-facing strings.

Detailed Description of the Pull Request / Additional comments



  • Dashboard and shared Quick Access ownership: src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs, src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs, src/settings-ui/Settings.UI/ViewModels/PageViewModelLifetime\1.cs, andsrc/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessViewModel.csdispose owned subscriptions/work and recreate models on page reload.src/settings-ui/QuickAccess.UI/ViewModels/LauncherViewModel.csandsrc/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml.csrelease the shared child on window close, not hide.src/settings-ui/Settings.UI.Library/EnabledModules.csandGeneralSettings.cs` remove only the callback still owned by that instance.


  • General page: src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs and src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs clean up repository/shortcut/IPC registrations and cancel stale delayed work. Early Loading registration and Bindings.StopTracking() prevent long-lived shared publishers from retaining old generated binding trackers and models. PageViewModelBase.cs releases the exact conflict-manager subscription and ignores disposed/queued updates.


  • Power Display: src/settings-ui/Settings.UI/Helpers/NativeEventWaiter.cs, src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml.cs and src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs own cancellable event registrations and async work, guard six dialog callers against stale generations, and reject disposed Launch/Dismiss commands. The command guards are safety changes, not a claim to eliminate the remaining command-rooted model.


  • Managed/native hook lifetime: src/common/ManagedCommon/HotkeySettingsControlHook.cs releases callback targets and the native proxy after successful disposal. src/common/interop/KeyboardHook.cpp and KeyboardHook.h retain a strong dispatch snapshot so callbacks may close registrations without invalidating active dispatch. The native IDL/export contract is unchanged.


  • Guidance: doc/devdocs/core/settings/page-lifetimes.md and doc/devdocs/modules/powerdisplay-settings-lifetime-checklist.md document ownership and validation expectations.

No R2R/AOT, hidden-window reveal state machine, Quick Access launch-policy change, trust/signature/IPC-authentication change, elevation/GPO policy change, or installer change is included. Benchmark/diagnostic adapters were removed; only reviewed product/test/docs changes are committed.

Audited partial benchmark β€” not the planned full sample

Normal Runner-backed ordinary x64 Debug processes, naturally warm filesystem caches, full Dashboard, ten General/Home plus ten Power Display/Home round trips. Both private layouts contained Runner/Settings/QA and normal dependencies but zero native utility modules; this is not shipping Release or full-module memory validation. Native process counters were sampled with fixed windows and final-10-second medians; no forced GC/capture/build occurred during primary runs.

Only two complete balanced blocks are included: n=4 fresh sessions per variant, two independent blocks. Block 2 is wholly invalid because display geometry changed in its final session; all four attempts were retained. Block 3 was not attempted. The planned n=8 per variant was not achieved. Old B2/CIM/GC/pilot/A/A/different-context data are excluded.

| Metric (per-variant medians) | Baseline A2 | Candidate C3 |
|---|---:|---:|
| Settings private resident WS, Home 0 | 93.45 MiB | 93.39 MiB |
| Settings private resident WS, after 20 | 365.77 MiB | 196.06 MiB |
| Settings private resident growth | 272.32 MiB | 102.41 MiB |
| Settings private commit, after 20 | 400.18 MiB | 218.98 MiB |
| Settings cumulative CPU, after 20 | 53.617 s | 44.617 s |
| Family private resident WS, after 20 | 381.42 MiB | 211.85 MiB |
| Family private commit, after 20 | 453.68 MiB | 272.96 MiB |

The predeclared paired-block growth contrasts (C3 minus A2) were -165.60 MiB and -169.18 MiB, median -167.39 MiB. These differ from subtracting separate per-variant medians. With only two blocks there is no robust confidence-interval or population-tail claim. This supports reduced repeated-navigation retention, not faster first-frame presentation.

Post-close differences are retained rather than hidden: family private WS medians were 15.15/15.49 MiB and summed total WS 66.54/72.42 MiB (A2/C3). Summed working sets duplicate shared pages and are not unique physical RAM; private commit is not resident RAM.

Remaining limitations

A separate normal C3 post-GC diagnostic observed Dashboard/shared QA at one each and no retained General models/General trackers after 20 visits. One Power Display model, one ShortcutControl, an empty managed hook and three WinRT callback delegates remained; the native hook projection count was zero. The PD model had a COM/native-command-to-Action root. Native owner, permanence and disposed primitive state were not established from the graph.

The isolated real-WinUI whole-tracker criterion remains FAIL: empty native-callback-rooted General trackers survived, although old General model/page/binding weak survivors were zero and publisher listeners/caches were cleared. This draft does not claim zero leaks, full native/control cleanup, or that all remaining memory growth is native caches.

After the display interruption, narrow guarded recovery preserved the new placement, restored the original runtime/profile/task state, and removed private rollback data only after verification. No raw settings, task XML, heap dumps, screenshots or session artifacts are uploaded with this PR.

Validation Steps Performed


  • Existing repository build wrappers: ordinary x64 Debug and Release builds completed for the combined reviewed changes.


  • Each configuration: 134 focused Settings tests, 19 managed-hook tests and 2 native no-hook tests passed (155 total). Coverage includes page reload/disposal, shared Quick Access subscriptions, General compiled binding tracker teardown, Power Display cancellation/dialog generations/disposed commands, managed/native cycle release and reentrant native dispatch ownership. Unit validation did not install global keyboard hooks or inject keys.

  • Test paths include src/settings-ui/Settings.UI.UnitTests/Helpers/NativeEventWaiterTests.cs, the lifetime/compiled-binding/Power Display cases in src/settings-ui/Settings.UI.UnitTests/ViewModelTests/, src/common/interop/interop-tests/HotkeySettingsControlHookTests.cs, and src/common/UnitTests-CommonUtils/KeyboardHook.Tests.cpp.

  • Separate actual WinUI diagnostics covered same-instance page reload, 18 Power Display dialog boundary cases, and standalone QA hide/show/close. The known whole-tracker failure is documented above; these are not an unqualified UI-suite pass.

  • Separate normal A2/C3 Dashboard captures were byte-identical at the accepted context. Primary runs were uninstrumented apart from an external native counter observer.

  • All eight accepted primary sessions passed 20-trip/40-leg workload checks, ten steady batches per phase, exact family 2/3/2, zero missing counters, 1,557 strict geometry observations, timing guards and graceful shutdown. Native query brackets were at most 30.073 ms and sample gaps at most 1,016.239 ms. Observer/controller CPU was recorded separately and excluded from the PowerToys family.

  • All frozen participant hashes and unchanged common/runtime/resource inputs were checked. No new validation run or source rewrite was performed solely for publication.

CI and wider Release/multi-module validation remain pending draft review.

0 total changed files
Congrats, you've cleared the final file
LGTM-land is officially in view