fix: stop the open tab polling while the window is hidden to the tray - #1714
Merged
Conversation
Closes #1596. Poll-loop gating was wired only to tab navigation. MainWindowViewModel.SetActive was called exclusively from OnSelectedNavChanged, and closing the window hides it rather than closing it (MinimizeToTray is the default), so nothing ever cleared IsActive. A tab left open therefore kept sampling once a second for as long as the PC stayed on: Process Manager re-enumerating every process and extracting icons, Bandwidth Monitor re-measuring throughput and appending to disk every five seconds. None of it visible, and on a laptop it is battery spent on work nobody can see — for a tool whose whole pitch is making the machine run better. The gate itself already existed and every affected view-model already honours IsActive; it was simply never told the window went away. So this extends the existing condition to "selected AND on screen" rather than inventing a second mechanism: - MainWindowViewModel gains IsWindowVisible (defaults to true — defaulting to false would kill every poll loop until the user happened to minimize and restore, a worse bug than the one being fixed). - OnSelectedNavChanged now activates the entered tab with IsWindowVisible instead of an unconditional true, because the tray menu and the Dashboard quick actions can navigate while hidden. - MainWindow hooks IsVisibleChanged and overrides OnStateChanged. Both are needed: Hide() flips IsVisible, while a minimize to the taskbar does not (WPF keeps IsVisible true for a minimized window). Both use the same combined condition so they can never contradict each other. IsVisibleChanged rather than OnClosing because the tray's "Volume mixer" item calls ShowWindow BEFORE it navigates, so the flag has to be true by then or that tab would open paused. Deliberately untouched: ResourceHistoryService's sampler and DarkModeViewModel's schedule are supposed to run while hidden, and neither is in SetActive's switch — verified rather than assumed. The gate is extracted as ApplyPollGate (internal static) so it can be tested without constructing the shell: MainWindowViewModel's own constructor runs About's startup update check, which is a network call, so its tests live in the integration project — and CI only compile-checks that project, meaning a regression pinned there would never actually run. The five new tests are in the unit suite instead, including one that proves the gate never materialises a never-opened lazy tab (reading Content would construct it, undoing the lazy-startup fix). Verified red before / green after against pristine main via reflection probe: "the app has no way to pause polling when the window is hidden" (exit 1) → "hiding pauses the loop" (exit 0). All four projects rebuild with 0 warnings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1596.
The problem
Poll-loop gating was wired only to tab navigation.
MainWindowViewModel.SetActivewas called exclusively fromOnSelectedNavChanged, and closing the window hides it rather than closing it (MinimizeToTrayis the default) — so nothing ever clearedIsActive.A tab left open therefore kept sampling once a second for as long as the PC stayed on:
Nothing was on screen to show it. On a laptop that is battery spent on work nobody can see — for a tool whose entire pitch is making the machine run better.
The fix
The gate already existed and every affected view-model already honours
IsActive— it was simply never told the window went away. So this extends the existing condition to selected AND on screen rather than inventing a second mechanism:MainWindowViewModel.IsWindowVisible, defaulting to true. Defaulting to false would kill every poll loop until the user happened to minimize and restore — a worse bug than the one being fixed, and there's a test pinning the default.OnSelectedNavChangedactivates the entered tab withIsWindowVisibleinstead of an unconditionaltrue, because the tray menu and Dashboard quick actions can navigate while hidden.MainWindowhooksIsVisibleChangedand overridesOnStateChanged. Both are needed:Hide()flipsIsVisible, but a minimize to the taskbar does not — WPF keepsIsVisibletrue for a minimized window. Both use the same combined condition so they can't contradict each other.IsVisibleChangedrather thanOnClosingfor a specific reason: the tray's "Volume mixer" item callsShowWindowbefore it navigates, so the flag has to already be true by then or that tab would open paused.Deliberately untouched
ResourceHistoryService's sampler andDarkModeViewModel's schedule are supposed to run while hidden. Neither is inSetActive's switch — verified against source, not assumed. A broader "pause everything when hidden" change would have broken both.Testability
The gate is extracted as
ApplyPollGate(internal static, matching the existingUpdateSelectionStateprecedent) so it can be tested without constructing the shell.That matters:
MainWindowViewModel's constructor runs About's startup update check, which is a network call, so its existing tests live inSysManager.IntegrationTests— and CI only compile-checks that project, never runs it. A regression pinned there would never execute. I initially wrote these tests there, noticed that, and moved them to the unit suite where CI actually runs them.Five tests added, including one proving the gate never materialises a never-opened lazy tab — reading
Contentwould construct its view-model, undoing the lazy-startup fix.Verification
Red before / green after, against pristine main via a reflection probe that compiles against both revisions:
All four projects rebuild
--no-incrementalwith 0 errors, 0 warnings.Not verifiable on this workstation: the actual tray interaction (hide → check idle CPU → restore) needs the app running, which is the secondary workstation's job. The logic gate itself is covered by the tests above.