Skip to content

Fix Lite Overview ghost-line comparison offset under a preset range (#4296) - #4309

Merged
erikdarlingdata merged 3 commits into
devfrom
fix/4296-lite-ghost-line-offset
Sep 25, 2026
Merged

erikdarlingdata merged 3 commits into
devfrom
fix/4296-lite-ghost-line-offset

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Closes #4296.

Why

Lite's Overview tab has a "Compare to" ghost-line overlay on the correlated timeline lanes (CPU, waits,
blocking, memory, file I/O). ServerTab.RefreshOverviewAsync builds the reference (ghost-line) time
window and hands it to CorrelatedTimelineLanesControl.RefreshAsync, which reads it straight into
LocalDataService.GetCpuUtilizationAsync, GetTotalWaitTrendAsync, and friends. Those reads treat a
supplied fromDate/toDate as the monitored server's own local time, not UTC.

Under a PRESET range (no custom dates picked), the window was built from DateTime.UtcNow instead of the
server's local now. On a server not on UTC, the reads got a window shifted by the server's offset. The
ghost line came from the wrong hours entirely, not just a display skew. The same raw DateTime.UtcNow
fallback was duplicated in two more places: RefreshAsync's timeShift (the ghost line's X-axis
alignment) and ComparisonLabel (the "yesterday" / "last week" text). So even after fixing the reference
window alone, the overlay stayed misplaced on the chart and mislabeled. A CUSTOM range was already correct:
the date pickers convert to server time before RefreshOverviewAsync ever sees fromDate/toDate.

The three call sites are RefreshOverviewAsync, RefreshAsync's timeShift and ComparisonLabel. With the
two changed files reverted to this branch's parent commit, the new source pin test failed.

The Darling viewer has no ghost-line overlay to fix in parallel. Its
CorrelatedTimelineLanesControl.xaml.cs header says so directly: "Deliberately dropped for v1 (noted in
the PR): Lite's comparison-range ghost-line overlay (the whole comparisonRange block, AddGhostLine,
ComparisonLabel)."

What changes

  • Lite/Controls/CorrelatedTimelineLanesControl.xaml.cs: two new internal static helpers,
    GetCurrentWindowServerLocal and GetOverviewComparisonRange. Under a preset range, they build the
    current window from the server's own local now: utcNow.AddMinutes(utcOffsetMinutes). That is the result
    ServerTimeHelper.ToServerTime gives, without its static state. Under a custom
    range they pass the already-server-local fromDate/toDate through unchanged. GetOverviewComparisonRange
    returns CurrentFrom with From and To. So RefreshAsync's timeShift and ComparisonLabel use the
    same current-window start that the reference range came from. Before, each one read DateTime.UtcNow again. That makes the shift exact arithmetic
    (currentStart - (currentStart - N days) = N days) instead of two independent clock reads a network
    round-trip apart.
  • Lite/Controls/ServerTab.Refresh.cs: RefreshOverviewAsync now calls
    CorrelatedTimelineLanesControl.GetOverviewComparisonRange instead of the shared
    ServerTab.Comparison.cs GetComparisonRange(). Under a preset range, that shared helper falls
    back to UTC. That is right for the Queries-tab comparisons it also serves, because they read the UTC
    collection_time directly. It is wrong for the correlated lanes. So the Overview tab gets its own
    derivation, and the Queries tab keeps the shared one.
  • Lite.Tests/OverviewComparisonWindowOffsetTests.cs (new) pins both helpers for a server at UTC+5 and one
    at UTC-7, under a preset and a custom range. A source pin also checks the three call sites.
  • Filed Dashboard: Overview ghost-line comparison likely has the same UTC-offset bug as Lite (#4296) #4305 to check whether the deprecated Dashboard app's byte-for-byte
    copy of this control (deprecated/Dashboard/Controls/CorrelatedTimelineLanesControl.xaml.cs, flagged by
    its own "SYNC WARNING" header) has the same bug. Not fixed here: Dashboard is a separate, SQL-Server-only
    product with its own data service. Confirming the same UTC-vs-server-local read semantics apply there
    needs its own look, not a copy-paste of this fix.

This branch also merges in #4302 (#4284's Queries-tab fix), which landed on dev while this PR was in
progress and touched the same RefreshOverviewAsync method. Its own comment there named #4296 as the
mismatch it did not fix. The merge conflict was resolved by keeping this PR's derivation, which supersedes
that comment.

Test plan

  • dotnet build Lite.Tests/Lite.Tests.csproj -c Debug: 0 Warning(s), 0 Error(s), both before and
    after merging origin/dev.
  • Lite.Tests.exe -class "PerformanceMonitorLite.Tests.OverviewComparisonWindowOffsetTests": 9/9
    passed.
  • The source pin fails on the old code. The parent commit's copies of both files still have the old
    DateTime.UtcNow lines and lack the new ones.
  • Full suite after merging origin/dev, Lite.Tests.exe with no filter: Total 5398, Errors 0,
    Failed 0, Skipped 0, Not Run 0.
  • GitHub Actions CI on 66b3a337: build, Darling PostgreSQL tests and Lite tests all pass.

CHANGELOG entry

SECTION: Fixed
ENTRY:

🤖 Generated with Claude Code

https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ

erikdarlingdata and others added 3 commits September 25, 2026 11:41
RefreshOverviewAsync built the correlated-lanes comparison window from
DateTime.UtcNow under a preset range, but the lanes' own reads
(GetCpuUtilizationAsync, GetTotalWaitTrendAsync, etc.) treat a supplied
fromDate/toDate as server-local. On a server not on UTC that shifted the
ghost-line reference window, and CorrelatedTimelineLanesControl.RefreshAsync's
timeShift/ComparisonLabel had the same fallback. GetOverviewComparisonRange
and GetCurrentWindowServerLocal build the current window from the server's
own local now instead, and thread CurrentFrom through so the shift is exact
arithmetic rather than a second DateTime.UtcNow sample.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…4296)

Covers GetCurrentWindowServerLocal/GetOverviewComparisonRange under a preset
and a custom range, for a server east (UTC+5) and west (UTC-7) of UTC, plus a
source-scan pin for the RefreshOverviewAsync/RefreshAsync/ComparisonLabel call
sites. Verified against this branch's parent commit (git show HEAD~1) that
every assertion in the source-scan test fails on the pre-fix source.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…ne-offset

# Conflicts:
#	Lite/Controls/ServerTab.Refresh.cs
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 25, 2026 16:22
@erikdarlingdata
erikdarlingdata merged commit 1483c8b into dev Sep 25, 2026
16 of 17 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4296-lite-ghost-line-offset branch September 25, 2026 16:22
erikdarlingdata added a commit that referenced this pull request Sep 25, 2026
…eset range (#4305) (#4317)

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.


Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
erikdarlingdata added a commit that referenced this pull request Sep 25, 2026
…4321)

* Dashboard Overview ghost line uses the server's local time under a preset 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

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

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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