Skip to content

fix: stop the open tab polling while the window is hidden to the tray - #1714

Merged
laurentiu021 merged 1 commit into
mainfrom
fix/pause-polling-hidden-to-tray
Aug 6, 2026
Merged

fix: stop the open tab polling while the window is hidden to the tray#1714
laurentiu021 merged 1 commit into
mainfrom
fix/pause-polling-hidden-to-tray

Conversation

@laurentiu021

Copy link
Copy Markdown
Owner

Closes #1596.

The problem

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:

Tab What kept running, invisibly
Process Manager re-enumerating every process + icon extraction, 1 Hz
Bandwidth Monitor re-measuring throughput 1 Hz, appending to disk every 5 s
Dashboard vitals poll
Volume mixer peak-meter timer

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.
  • OnSelectedNavChanged activates the entered tab with IsWindowVisible instead of an unconditional true, because the tray menu and Dashboard quick actions can navigate while hidden.
  • MainWindow hooks IsVisibleChanged and overrides OnStateChanged. Both are needed: Hide() flips IsVisible, but a minimize to the taskbar does not — WPF keeps IsVisible true for a minimized window. Both use the same combined condition so they can't contradict each other.

IsVisibleChanged rather than OnClosing for a specific reason: the tray's "Volume mixer" item calls ShowWindow before it navigates, so the flag has to already 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. Neither is in SetActive'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 existing UpdateSelectionState precedent) 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 in SysManager.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 Content would 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:

IsWindowVisible property : ABSENT
ApplyPollGate method     : ABSENT
  FAIL  the app has no way to pause polling when the window is hidden
RESULT: red   (exit 1)
IsWindowVisible property : present
ApplyPollGate method     : present
  PASS  hiding pauses the loop
RESULT: green (exit 0)

All four projects rebuild --no-incremental with 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.

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.
@laurentiu021
laurentiu021 merged commit 9b8a377 into main Aug 6, 2026
4 checks passed
@laurentiu021
laurentiu021 deleted the fix/pause-polling-hidden-to-tray branch August 6, 2026 14:00
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.

[Bug]: Cross-app - Monitor tabs keep polling at 1 Hz after the window is hidden to the tray - IsActive only tracks tab sw...

1 participant