Skip to content

A hidden Lite tab's alert badge counts over a window skewed by the selected tab's UTC offset #2976

Description

@erikdarlingdata

A hidden Lite tab's alert badge counts over a window skewed by the selected tab's UTC offset

This is the concrete wrong answer that #2977's ambient read produces today. #2977 is the shape; this is the one reachable consequence I could establish, and it is narrower and more conditional than the first version of this issue claimed.

Why the IsVisible gate does not dispose of it

The obvious reading is that Lite never refreshes a hidden tab, so the offset static is always the right one. ServerTab.Refresh.cs:94 does gate on it:

if (IsVisible)
    await RefreshVisibleTabAsync(hoursBack, fromDate, toDate, subTabOnly: true);
else
    _refreshPendingWhileHidden = true;

Two lines below, outside that gate, is an unconditional windowed read:

/* Always keep alert badge current even when Blocking tab is not visible */
if (MainTabControl.SelectedIndex != 8)
    await RefreshAlertCountsAsync(hoursBack, fromDate, toDate);

hoursBack, fromDate, toDate were computed at :85, before the gate. RefreshAlertCountsAsync:160 passes them straight to _dataService.GetAlertCountsAsync(_serverId, hoursBack, fromDate, toDate) — the tab's own _serverId — and LocalDataService.Blocking.cs:275 windows it through GetTimeRange(hoursBack, fromDate, toDate), the custom-range branch that reads the process-wide static.

So the gate skips the charts and grids, which is what its comment says it does. It does not skip the badge, which is what its comment also says. Every ServerTab's own 60s timer reaches this path whether or not the tab is visible.

The condition that makes it real, and the two that make it cancel

GetCurrentWindow() (:60) converts the pickers with ServerTimeHelper.DisplayTimeToServerTime(local, ServerTimeHelper.CurrentDisplayMode), and GetTimeRange then subtracts the offset again. Whether those two cancel depends entirely on the display mode:

CurrentDisplayMode DisplayTimeToServerTime net offset applications correct?
UTC +_utcOffsetMinutes +1 then −1 → cancels yes, by accident
LocalTime ToUniversalTime() then +_utcOffsetMinutes +1 then −1 → cancels yes
ServerTime _ => displayTime, applies nothing −1, uncancelled no

ServerTime is the default (ServerTimeHelper.cs:55). So in the default display mode there is exactly one uncancelled read of the ambient static, and it is the selected tab's offset rather than the tab's own.

Both cancelling modes cancel because the two conversions read the same static within one call — which is worth recording, because it means the bug is not "the offset is applied twice" and not "the round trip is lossy". It is specifically that the one surviving application belongs to the wrong server.

What an operator sees

All of these together:

  1. Two or more monitored servers whose UTC offsets differ.
  2. A custom range selected on the background tab (IsCustomRange; otherwise fromDate/toDate are null and this branch is never entered).
  3. CurrentDisplayMode == ServerTime — the default.

Then the background tab's blocking count, deadlock count and latest-event time are computed over a window shifted by the difference between the two servers' offsets. The badge under- or over-reports; nothing errors and no row looks malformed. It is a wrong number on a background tab rather than a wrong grid, so the blast radius is small — but the badge exists precisely to be trusted without looking at the tab.

Fix

#2977's fix subsumes this: make the offset a required parameter, as GetTimeRangeServerLocal (LocalDataService.cs:147) already does, and the ambient read stops being available. Fixing this one alone by having RefreshAlertCountsAsync pass its own tab's offset would close this path and leave the shape intact for the next caller. Prefer #2977. This issue is worth keeping separate only as the reachability evidence — if #2977 lands, close this with it.

Not verified

Derived from source, not observed in a running Lite instance with two servers in different zones; that needs a Windows host. Whether the ServerTime default is what real deployments actually run on was not checked — if it is commonly switched to UTC or LocalTime, the offsets cancel and this is unreachable in practice. The Darling Viewer's ViewerTimeHelper is the same static shape but does not have this path: it feeds display conversion rather than a window, and it lives in the Viewer assembly, which PerformanceMonitor.Darling.Service/ does not reference at all (zero hits). Whether the Viewer's own UI has an equivalent per-tab problem was not examined.

Correction to this issue's first version

It originally claimed the defect applies broadly to Lite's UI reads on the strength of 76 GetTimeRange call sites passing fromDate/toDate. That count is accurate but was doing no work: nearly all of those calls sit behind the IsVisible gate, where the selected tab's offset is the correct one. The alert-badge path above is the one I could show escapes it, and the display-mode dependence — which decides whether the offset cancels at all — was missing entirely.

Activity

  1. changed the title [-]A Lite background refresh windows in the selected tab's UTC offset, not its own server's[/-] [+]A hidden Lite tab's alert badge counts over a window skewed by the selected tab's UTC offset[/+] on Sep 5, 2026
  2. erikdarlingdata commented on Sep 5, 2026

    @erikdarlingdata
    OwnerAuthor

    Closed by #2987 (892d5263), which fixed this as part of #2977 rather than separately — the required-offset change removes the ambient read by construction, so this path cannot recur.

    The specific fix: RefreshAlertCountsAsync now derives its own window via GetCurrentWindow(UtcOffsetMinutes) and passes UtcOffsetMinutes to GetAlertCountsAsync, both on the tab's own server. That also eliminates the "window computed before the gate, used after it" shape this issue described, rather than only correcting the value.

    Pinned by TheBadgeCountsInItsOwnServersOffset_InEveryDisplayMode_WhateverTheDesktopStaticHolds.

    One correction to this issue's text: RefreshAlertCountsAsync has two call sites, not one. The second is inside RefreshBlockingAsync, reached when the Blocking tab is visible — unchanged in effect, but it now derives its own window too.

    The operator-visible symptom remains derived from source, not observed — reproducing it needs a Windows host with two servers in different zones. The display-mode dependence recorded here held up and turned out to be load-bearing for the fix: a mutation moving only the display-side conversion onto the static fails UTC and LocalTime while leaving ServerTime green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions