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:
- Two or more monitored servers whose UTC offsets differ.
- A custom range selected on the background tab (
IsCustomRange; otherwise fromDate/toDate are null and this branch is never entered).
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.
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
IsVisiblegate does not dispose of itThe obvious reading is that Lite never refreshes a hidden tab, so the offset static is always the right one.
ServerTab.Refresh.cs:94does gate on it:Two lines below, outside that gate, is an unconditional windowed read:
hoursBack, fromDate, toDatewere computed at:85, before the gate.RefreshAlertCountsAsync:160passes them straight to_dataService.GetAlertCountsAsync(_serverId, hoursBack, fromDate, toDate)— the tab's own_serverId— andLocalDataService.Blocking.cs:275windows it throughGetTimeRange(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 withServerTimeHelper.DisplayTimeToServerTime(local, ServerTimeHelper.CurrentDisplayMode), andGetTimeRangethen subtracts the offset again. Whether those two cancel depends entirely on the display mode:CurrentDisplayModeDisplayTimeToServerTimeUTC+_utcOffsetMinutesLocalTimeToUniversalTime()then+_utcOffsetMinutesServerTime_ => displayTime, applies nothingServerTimeis 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:
IsCustomRange; otherwisefromDate/toDateare null and this branch is never entered).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 havingRefreshAlertCountsAsyncpass 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
ServerTimedefault 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'sViewerTimeHelperis 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, whichPerformanceMonitor.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
GetTimeRangecall sites passingfromDate/toDate. That count is accurate but was doing no work: nearly all of those calls sit behind theIsVisiblegate, 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.