Skip to content

Dashboard query comparison grids use the server's local time (#4313) - #4321

Merged
erikdarlingdata merged 4 commits into
devfrom
fix/4313-dashboard-query-grid-comparison
Sep 25, 2026
Merged

erikdarlingdata merged 4 commits into
devfrom
fix/4313-dashboard-query-grid-comparison

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Closes #4313.

Builds on #4317 (#4305), not yet merged: this branch is fix/4313-dashboard-query-grid-comparison,
created off origin/fix/4305-dashboard-ghost-line-offset at 4723bf9. Its diff against that base
branch is the 4 files below; the diff against dev will also carry #4317's commits until #4317 merges.

Why

Lane U3 (#4317) fixed the Dashboard Overview ghost line building its "current window" from
DateTime.UtcNow while its own reads treat a supplied range as server-local. #4317's commit message
explicitly named this issue's two spots as out of its scope:

GetComparisonRange()'s own preset-range fallback (DateTime.UtcNow) is untouched here: it still
feeds the Query Performance grids below, unchanged (#4305's scope is the Server Trends ghost line
only; the grids' likely-matching bug is tracked separately as #4313).

Confirmed both named spots against the reads they feed:

  • ServerTab.xaml.cs's GetComparisonRange() (_globalToDate ?? DateTime.UtcNow under a preset
    range) feeds PerformanceTab.SetComparisonRange(...), which reaches
    DatabaseService.QueryPerformance.Comparison.cs's GetQueryStatsComparisonAsync,
    GetProcedureStatsComparisonAsync and GetQueryStoreComparisonAsync -- all three filter on
    collection_time.
  • QueryPerformanceContent.Comparison.cs's RefreshComparisonAsync() (_queryStatsToDate ?? DateTime.UtcNow) calls those same three methods directly.
  • collect.query_stats, collect.proc_stats and collect.query_store_stats all default
    collection_time to SYSDATETIME() -- server-local, not UTC.

On a server not running in UTC, both spots picked a current window offset from the server's actual
current time by the server's UTC offset, so the "current" side of the comparison silently included or
excluded the wrong rows.

What changes

The two named spots, both now reuse #4305's tested, testable statics
(CorrelatedTimelineLanesControl.GetOverviewComparisonRange / GetCurrentWindowServerLocal) instead
of independently resampling a raw UTC now:

  • ServerTab.xaml.cs's GetComparisonRange() now calls GetOverviewComparisonRange(...) and maps
    the result to (From, To).
  • QueryPerformanceContent.Comparison.cs's RefreshComparisonAsync() now calls
    GetCurrentWindowServerLocal(...) for its (currentStart, currentEnd).

A third spot, found while checking every other DateTime.UtcNow used as "server now" in the
Dashboard's comparison and Overview code (per brief step 2), fixed in the same PR:

CorrelatedTimelineLanesControl.xaml.cs's RefreshAsync computed referenceTime (baseline-band
lookup) the same buggy way: fromDate ?? DateTime.UtcNow.AddHours(-hoursBack). That feeds
SqlServerBaselineProvider.GetBaselineAsync, which buckets by analysisTime.Hour /
.DayOfWeek against collection_time (also server-local) to find the matching baseline bucket. Same
bug shape, same fix: now GetCurrentWindowServerLocal(...).Start.

Swept and ruled out (grepped every other DateTime.UtcNow in deprecated/Dashboard/*.cs and
Controls/*.cs, read each in context):

  • ServerTab.xaml.cs/ServerTab.Refresh.cs's _refreshStartedUtc two-minute auto-refresh throttle,
    and heatmap-hover debounces in QueryPerformanceContent.Heatmap.cs / FinOpsContent.ObjectHeatmap.cs
    -- pure wall-clock timers, not server reads.
  • MainWindow.AlertEngine.cs's alert-cooldown now -- deliberately UTC, with a comment explaining
    ServerTimeHelper.ServerNow would make cooldowns jump when the offset changes between tab switches.
    Left alone; it is correct as written.
  • RecommendationsContent.xaml.cs's OpenInActiveQueries_Click fallback -- documented as the "RAW UTC
    window" contract; the host converts. Left alone.
  • ResourceMetricsContent.xaml.cs:372 already passes DateTime.UtcNow as an explicit parameter into
    GetOverviewComparisonRange, which converts it internally -- the Dashboard: Overview ghost-line comparison likely has the same UTC-offset bug as Lite (#4296) #4305-fixed call shape, correct as
    is.
  • RecommendationsViewModel.cs:228 already does the offset math inline
    (DateTime.UtcNow.AddMinutes(_utcOffsetMinutes)) -- already server-local, just not routed through
    ServerTimeHelper; a style-only observation, not a behavior bug, so left alone.

Checked that no other caller re-resolves a stale comparison range (brief step 2's other risk, the
kind #4305 also fixed for the ghost line): grepped RefreshComparisonAsync|SetComparisonRange across
deprecated/Dashboard. The only caller of either is CompareToCombo_SelectionChanged, so the grids
have no periodic-refresh path that would reuse a resolved range across ticks; the in-place fix is
complete on its own.

Checked custom ranges are already server-local, so left untouched, per ServerTab.TimeRange.cs
(_globalFromDate = ServerTimeHelper.DisplayTimeToServerTime(...)) and ServerTab.DrillDown.cs's
OnOpenActiveQueriesForFinding (explicit // UTC -> server-local (collection_time is stored in server-local time) conversion before assigning _globalFromDate/_globalToDate).

Updated two comments that #4313's fix makes false: ServerTab.xaml.cs's
CompareToCombo_SelectionChanged comment (previously said the grids' bug was untouched/tracked
separately as #4313), and GetOverviewComparisonRange's doc summary in
CorrelatedTimelineLanesControl.xaml.cs (previously said neither #4305 nor this method touched the
grids).

Lite parity, deferred to a follow-up issue, not fixed here:
Lite/Controls/CorrelatedTimelineLanesControl.xaml.cs carries a SYNC WARNING that it mirrors the
Dashboard copy of this file. After merging origin/dev (which turned out to already carry a separate,
already-merged fix for Lite's ghost-line timeShift/currentStart), one matching call site is still
open there: line 189's baseline referenceTime, same shape as the third spot this PR fixes. Filed as
#4320 rather than fixed here,
because it needs Lite.Tests (not in this lane's brief), and because a nearby comment in that same
file states Lite's Queries-tab comparison reads are correctly UTC-based by design (unlike the
Dashboard's), so Lite's fix needs its own confirmation of which reads are server-local before
generalizing #4313's fix shape to it. (I initially filed #4320 citing three open call sites, read
before merging origin/dev; that merge turned out to already fix two of them, so I corrected the issue
to the one still open.)

Test plan

  • Added deprecated/Dashboard.Tests/QueryComparisonWindowOffsetTests.cs (9 tests): the grids'
    current window on a server east of UTC (UTC+5) and west of UTC (UTC-7), under both a preset range
    (asserts the window end is the server-local now, not the raw UTC now supplied) and a custom range
    (asserts supplied bounds pass through verbatim); the same for GetComparisonRange's three dropdown
    options (Yesterday / Last week / Same day last week); and a source-pin test asserting each of the
    three fixed call sites now calls the shared server-local helper and not a bare ?? DateTime.UtcNow
    fallback.
  • Proved the source-pin test fails on the old code: hand-reverted GetComparisonRange() to its
    pre-fix _globalToDate ?? DateTime.UtcNow body, rebuilt, ran only that test -- it failed with
    GetComparisonRange still falls back to a raw UTC now under a preset range (#4313) -- then restored
    the fix and reran (passes).
  • dotnet build deprecated/Dashboard/Dashboard.csproj -c Debug: Build succeeded, 0 Warning(s), 0
    Error(s).
  • dotnet build deprecated/Dashboard.Tests/Dashboard.Tests.csproj -c Debug: Build succeeded, 0
    Warning(s), 0 Error(s).
  • Merged origin/dev (clean, no conflicts; it touched AG topology, Darling query-heatmap tests and
    Lite files, none of the four files this PR touches).
  • Ran the full Dashboard.Tests.exe suite once, after the origin/dev merge: Total: 838, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0.
  • Not run: Installer.Tests (excluded by lane orders) and any live/manual Dashboard run against a
    non-UTC SQL Server (no monitored server touched; this lane's brief is source + unit tests only).

Merge note

The PR tender merged origin/dev into this branch (11c33dd). #4317's squash merge conflicted with this branch's copy of #4317 in CorrelatedTimelineLanesControl.xaml.cs and ServerTab.xaml.cs. Both conflicts resolved to this branch's version, which already held all of #4317 (byte-identical for these files) plus #4313's edits.

CHANGELOG entry

SECTION: Fixed
ENTRY:

🤖 Generated with Claude Code

https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3

erikdarlingdata and others added 3 commits September 25, 2026 12:13
…eset range (#4305)

CorrelatedTimelineLanesControl.xaml.cs is a copy of Lite's control (SYNC WARNING),
which had the identical bug PR #4309 (#4296) fixed: under a preset range, the
comparison ghost line built its reference window from DateTime.UtcNow while the
correlated lanes' own reads treat a supplied fromDate/toDate as server-local
(collection_time defaults to SYSDATETIME() throughout install/*.sql). Mirrors
that fix here: GetCurrentWindowServerLocal/GetOverviewComparisonRange, CurrentFrom
threaded through RefreshAsync's timeShift and ComparisonLabel, and the caller
(ResourceMetricsContent/ServerTab) switched to deriving the range fresh, per
refresh, from the Compare dropdown's index rather than a value resolved once and
reused across later auto-refreshes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Query Stats / Proc Stats / Query Store comparison grids built their preset-range
current window from DateTime.UtcNow (ServerTab.xaml.cs's GetComparisonRange,
QueryPerformanceContent.Comparison.cs's RefreshComparisonAsync) while the reads
they feed (GetQueryStatsComparisonAsync, GetProcedureStatsComparisonAsync,
GetQueryStoreComparisonAsync) filter collection_time, which is server-local
(collect.query_stats/proc_stats/query_store_stats default it to SYSDATETIME()).
Same bug family as #4305/#4317's Overview ghost line, on the two spots #4305
explicitly left for this issue. Also fixes a third spot #4305 didn't touch:
CorrelatedTimelineLanesControl.xaml.cs's RefreshAsync baseline referenceTime fed
SqlServerBaselineProvider.GetBaselineAsync's hour-of-day/day-of-week bucket match,
also against collection_time, from the same raw-UTC fallback.

All three now reuse #4305's tested GetCurrentWindowServerLocal/
GetOverviewComparisonRange statics instead of independently resampling UTC now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Conflicts in CorrelatedTimelineLanesControl.xaml.cs and ServerTab.xaml.cs came from #4317's squash merge:
this branch already carried #4317's commit (4723bf9, byte-identical to the squash for these files) plus
#4313's edits on the same lines. Resolved to this branch's version of both files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 25, 2026 16:45
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 25, 2026 16:45
@erikdarlingdata
erikdarlingdata merged commit 195514b into dev Sep 25, 2026
16 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4313-dashboard-query-grid-comparison branch September 25, 2026 17:07
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.

1 participant