Skip to content

Charts and time windows keep exact instants across daylight saving changes, in Lite and the Darling viewer (#4766) - #4848

Merged
erikdarlingdata merged 70 commits into
devfrom
fix/4766-display-frame
Sep 30, 2026
Merged

erikdarlingdata merged 70 commits into
devfrom
fix/4766-display-frame

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Fixes #4766.

Why

Lite and the Darling viewer plotted each chart point at the server's wall-clock time. In the repeated autumn hour, two real hours shared one X value and the line ran back on itself. A drill or a typed range there sometimes read the wrong hour or came back empty. In the spring gap, the axis showed an hour that never happened.

Now every chart point sits at its UTC instant. The display mode (Server, Local or UTC) applies only when text is drawn: ticks, hover, the crosshair, labels, grids and the CSV export. Time windows stay UTC from the picker, slicer or drill to the read.

What changes

Rules

  • A range in the repeated hour reads the hour it names. A drill there opens 60 real minutes.
  • A typed FROM takes the earliest instant whose wall clock reads that time, and a typed TO takes the latest. A time in the spring gap takes the change instant.
  • Text in the repeated hour adds the UTC offset, for example "2026-11-01 01:30:00 -05:00". Grid columns that show a server's time do not add it yet (see "Not in this change").

Shared parts (new)

  • ServerClock.AsTimeZone() returns the server's zone, a fixed-offset zone, or UTC.
  • PerformanceMonitor.Ui/DisplayZone writes a UTC instant in a display zone. ToUtcBound turns a typed wall time into a UTC bound, and WallTicks lists wall-clock ticks.
  • DisplayZoneTickGenerator puts chart ticks at whole wall times of the display zone. A skipped time gets no tick, and a repeated time gets one tick per occurrence. AxesExtensions.DateTimeTicksBottomUtc sets it on an axis.
  • CustomRangeState holds a custom range as a UTC pair and applies a typed edit.
  • TimeWindows has ChartAxis, Drill, OverviewReference and GhostX. The last two line up on the server's wall clock N days back.
  • ChartHoverHelper, CorrelatedCrosshairManager and the four shared renderers take a display zone. The renderers are CpuSchedulerChartRenderer, GroupedTrendChartRenderer, SessionStatsChartRenderer and SystemHealthChartRenderer.

Lite

  • Server tab charts, the Overview lanes, the CPU chart, the version store chart and the three history windows plot the instant. The CPU read can bucket on each sample's instant, so two readings in the repeated hour stay two points.
  • The Overview comparison lines and ghosts line up on the server's wall clock.
  • Time slicers label whole hours of the display zone, in the invariant culture. The range caption adds the UTC offset in the repeated hour.
  • Text times, the alert history list and recommendation cards show each server's time on that server's own clock. A server with no collected clock uses its open tab's clock, else this machine's current offset. Alert rows carry their server's clock.
  • The "Showing since" banner, the drill indicator, the "LIVE at" line and "Last refresh" follow the tab's own clock. "Last refresh" used this machine's clock before.
  • The time zone label shows the offset at that instant. A -0:30 offset keeps its sign.
  • A history window shows its grid times in the window's own zone, like its chart. Its server-clock columns (creation, cached and last execution times) use the clock of the tab that opened it, not the selected tab's. The instant columns are now 180 px wide, so a time with its UTC offset fits. The server-clock columns stay at 130 px.
  • The chart CSV export writes times in the display zone, and its header names the zone: DateTime (<zone id>).
  • The wait drill-down window, the chart menu and the Overview lanes take the display zone as a required argument. The Alerts, FinOps and Recommendations tabs take the open tab's clock as a required argument.
  • Removed: the app-wide chart time hook (UiTimeContext.ConvertForDisplay, set in App.xaml.cs), GetCurrentWindowServerLocal, the old GetTimezoneLabel overloads, ServerZoneLabel and ServerTimeHelper.ToServerTime.
  • A failed Overview ghost read no longer throws from its log line.

Darling viewer

  • Charts plot the instant. Ticks, hover, the crosshair and the CSV export show it in the display zone. The three history windows' axes changed too.
  • Typed time ranges keep the instants they name. Multi-server lists show each server's own time.
  • The clock for a server with no collected clock is chosen in one place, ViewerTimeHelper.
  • Alert clocks are cached for 5 minutes per connection.
  • The chart CSV header names the zone, as Lite's does. A zone id that holds a comma is quoted.
  • The drill indicator and the heatmap hover show the UTC offset in the repeated hour, as Lite's do.
  • Apply to All reloads only the tab on screen. A hidden tab keeps the new range and reloads when it is selected, so it cannot put its server's clock on the visible tab.
  • DisplayToNaiveUtc and ConvertFromDisplay are deleted.

Not in this change

  • Server tab grids print a server's time with no UTC offset in the repeated hour, while history windows print one.
  • A chart CSV export prints two rows in the repeated hour with the same wall time. The header names the zone, and no row carries an offset.
  • The Query_Heatmap and Memory_Pressure_Events chart exports write only the header row, as before. They draw a heatmap and bars, not scatter series.

Test plan

  • RED for the new rules, each planted, run and put back. Each line is the failing test:
    • TypedBoundTests.RepeatedHour_FromTakesTheFirst_ToTakesTheSecond: expected 06:45Z, got 05:45Z.
    • TypedBoundTests.SkippedHour_BothBoundsTakeTheChangeInstant: expected 07:00Z, got 07:30Z.
    • TypedBoundTests.AnEditInsideTheSecondOccurrence_TakesTheFirstForFrom: expected 05:15Z, got 06:15Z.
    • TimeWindowsTests.ADrillInTheRepeatedHour_OpensSixtyRealMinutes: expected 05:15Z to 06:15Z, got 05:15Z to 07:15Z.
    • SecondOccurrenceReadTests.AWindowOverTheSecondOccurrence_ReturnsOnlyItsRows: expected 06:00, got 11:00.
    • ServerTabDrillWindowTests.AClickInTheRepeatedAutumnHour_OpensSixtyRealMinutes: expected 05:00, got 01:00.
    • WindowedReadServerClockTests.ACustomUtcWindow_ReachesTheReadUnchanged: expected 06:30, got 11:30.
    • ServerTabChartWindowTests.ASpringChangeInsideThePresetWindow_LeavesNoRowLeftOfTheAxis.
    • ViewerChartUtcFrameTests.NoDisplayTimeProjectionFeedsAChartX and NoViewerChartUsesADisplayFrameAxis.
    • OverviewLanesUtcFrameTests.TheOverviewLanes_ConvertNothingThroughTheServersClock.
    • OverviewComparisonWindowOffsetTests.GetOverviewComparisonRange_Yesterday_AcrossSpringForward_IsTheSameWallHoursOnTheServersClock.
    • ServerTimeHelperClockTests.FormatServerTime_InTheRepeatedHour...: expected "2026-11-01 06:30:00", got "05:30:00".
    • ChartCsvExportDisplayZoneTests.ThePointAt0630Z_OnTheAutumnChangeDay_IsWritten0130InEastern_And0630InUtc: expected 01:30:00, got 06:30:00.
    • DisplayZoneTextFrameTests.GetTimeRangeDescription_NamesBothEndsInTheZoneItIsGiven_ThroughTheRepeatedHour.
    • HistoryGridZoneTests.AZone_WordsEveryInstantColumn_InThatZone(rowType: "QueryStoreHistoryRow").
    • HistoryGridZoneTests.TheServerClockColumns_AreConvertedOnTheRowsOwnClock_NotTheSelectedTabs(rowType: "QueryStatsHistoryRow").
    • ViewerDrillDownTests.TheDrillIndicator_InTheRepeatedHour_NamesTheOffsetOfEachEnd_ForBothDrills: expected -04:00 and -05:00, got "01:15 → 01:15".
    • ViewerApplyToAllClockTests.ApplyExternalTimeRange_ReloadsOnlyWhenTheTabIsVisible_AndItsReloadIsTheLastStatement. It is a source pin, because a viewer tab cannot be built in a test. It cannot catch a hidden tab whose IsVisible reads true, or another writer of the clock.
  • New test classes: DisplayZoneTests, DisplayZoneTickTests, TypedBoundTests, CustomRangeStateTests, TimeWindowsTests, ServerClockAsTimeZoneTests, DisplayZoneRendererTests, SecondOccurrenceReadTests, ServerTabHeldRangeTests, ServerTabChartUtcFrameTests, ViewerChartUtcFrameTests, ViewerAlertClockCacheTests, OverviewLanesUtcFrameTests, ChartCsvExportDisplayZoneTests, DisplayZoneTextFrameTests, HistoryGridZoneTests and ViewerApplyToAllClockTests.
  • ServerTabDrillWindowTests, WindowedReadServerClockTests and ViewerDrillDownTests gain repeated-hour cases. Pins that asserted the old frame now assert the instant. OverviewComparisonWindowOffsetTests and CorrelatedLanesXAxisWindowTests were rewritten.
  • Lite.Tests and Darling.Tests build with 0 warnings at the head. Full Lite.Tests at the head: 6161 tests, 0 failures. The last full Darling.Tests run, after the viewer fixes, had 18541 tests and 0 failures. 1177 of them were skipped because they need PostgreSQL, which CI runs. CI runs both suites at the head.
  • The live PostgreSQL tests run in CI.

CHANGELOG

SECTION: Fixed
ENTRY:

… bounds and UTC windows across daylight saving changes (fails until the next commit) (#4766)
…C windows for exact times across daylight saving changes (#4766)
…read, so a range in the repeated hour reads the hour it names (#4766)
…daylight saving changes, and multi-server lists show each server's own time (#4766)
…sen in one place, ViewerTimeHelper, for the Recommendations loader and both lists (#4766)
…r reads the instants it names, and the held range survives a display switch (#4766)
…air and the CSV export show it in the display zone (#4766)
…umn property, so the timestamp-frame census reads it as the conversion it is (#4766)
…w each server's time in the display mode, on that server's own clock (#4766)
…the instant frame, and a time test no longer depends on the machine's zone (#4766)

- The alert row time test names its display mode, its server clock and its zone, so it holds on any machine and any date. It failed on a US Eastern machine once the clocks went back, because Server mode ran on a fixed offset read at startup. Its class joins the collection that serializes tests of the process-wide time statics.
- The chart CSV export writes each point through a pure line builder. A test pins that a point at 06:30Z on 2026-11-01 reads 01:30 in US Eastern (the second 01:30) and 06:30 in UTC.
- The renderer host pin now covers any chart renderer a viewer tab builds, and requires X to pass through untouched (static utc => utc) with the display zone as the last argument.
- The alert history read takes the fleet's server clocks from a short-lived cache on the data service instead of sorting the retained server_properties rows on every refresh tick (every 30 seconds by default). A failed read is never held. Pinned without a database.
- Docs that described a chart X as ForDisplay(utc) or the display frame now say the instant is plotted and drawn in CurrentDisplayZone, and that ForDisplay is for text only.
…nd the zoneless hover out (#4766)

Every chart now plots the UTC instant and words its axis and hover in a display zone, so nothing reads the shared time hook: it is no longer wired at startup.
…ive-UTC instant and the text is drawn in the display zone (#4766)
…lock, and the viewer's chart export names its zone (#4766)
…istory rows' clock arms, the Worded formatters and TimeWindows.GhostX (#4766)
…ame (#4766)

Comment and summary text only; no code line changes. Every chart plots the UTC
instant and the display zone applies only to text, so the remaining wording that
described the server wall-clock frame now says what the code does: the
comparison-range note, the alert-badge window, the drill and round-trip test
summaries, the shared-renderer summaries, and the grid-refresh note.
… and the drill indicator shows the offset in the repeated hour (#4766)

A tab behind the one on screen no longer reloads when Apply to All hands it a range, so it cannot leave its own server's clock on the visible tab's hover and ticks; it reloads when it is selected. The two drills that open Active Queries and the heatmap hover word their times through DisplayZone.Format, like Lite, so the repeated hour of an autumn change carries each end's UTC offset. The DisplayZone remarks on a shift past the ends of the calendar now say what the code does.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 30, 2026 01:37
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