Skip to content

Lite CPU chart hover: a bucket read from the stored server wall clock shows no UTC offset in the repeated autumn hour - #4857

Merged
erikdarlingdata merged 3 commits into
devfrom
fix/cpu-hover-stored-wall-clock
Sep 30, 2026
Merged

erikdarlingdata merged 3 commits into
devfrom
fix/cpu-hover-stored-wall-clock

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Follows #4766 and #4848.

Why

Lite's CPU chart plots every point at a UTC instant. Rows collected before the UTC column existed have only the server's stored wall clock, so their buckets have no instant of their own. The chart plots such a bucket at the first instant its wall time names. In the repeated autumn hour, the hover then adds that pass's UTC offset.

Example: an Eastern server, a bucket stored as 01:30 on 2026-11-01. The wall time happens twice, at 05:30Z (-04:00) and at 06:30Z (-05:00). A stored 01:30 bucket that came from the second pass, or that merged readings from both, is plotted at 05:30Z and its hover read "01:30:00 -04:00". That names a pass the data never recorded.

Only a wall time that happens twice on the server's own clock has this problem. A stored wall time that happens once names one instant, and the read converts it exactly. A machine can show the chart in a zone that differs from the server's. That exact instant can then land in the machine zone's repeated hour. Dropping the offset there makes two different instants read the same time.

Example: a Tokyo server has no repeated hour, so its stored 14:30 on 2026-11-01 is 05:30Z exactly. A machine in Eastern time shows that instant as the first pass of its 01:30, so the hover must keep "-04:00". So the mark is only where the stored wall time names two instants.

Behavior change

  • On the CPU tab, a bucket cut on the stored server wall clock now reads "01:30:00" in the server's repeated hour. It used to read "01:30:00 -04:00". Only the offset is dropped: the time and the plotted position are the same.
  • A stored wall time that happens once on the server's clock is not marked. Its hover keeps its offset, in whatever zone the machine shows the chart. The Tokyo example above reads "01:30:00 -04:00" in Eastern time.
  • A server on a fixed offset, or on UTC, has no repeated hour, so no point is marked, in either frame.
  • A bucket that has its own stored instant (rows collected after the UTC column existed) is unchanged. Its two passes still read "01:30:00 -04:00" and "01:30:00 -05:00".
  • Times outside the repeated hour are unchanged.
  • No other chart changes. No other Lite chart and no Darling viewer chart sets the new predicate, so their hovers read as before.

What changes

  • Lite/Services/LocalDataService.Cpu.cs: CpuUtilizationRow gets SampleTimeNamesTwoInstants. It is true when the bucket was cut on the server's stored wall clock and that wall time happens twice there. That is the repeated autumn hour, and SampleTimeUtc is then only the first of the two instants it names. The stamping loop takes the server's zone once (clock.AsTimeZone()). It sets the flag to row.BucketIsServerLocal && serverZone.IsAmbiguousTime(stamp) in the UTC frame and to serverZone.IsAmbiguousTime(stamp) in the server-local frame. stamp is the server's wall time with Kind Unspecified, which the zone reads as its own time.
  • PerformanceMonitor.Ui/ChartHoverHelper.cs: a settable PlainTimeAt predicate that says whether a plotted X is one whose stored wall time names two instants. Clear() sets it back to null, so a predicate never outlives its data. FormatHoverTime gains bool plain = false. With a zone and plain, the line is the zone's wall time alone, worded in the invariant culture like the DisplayZone.Format line it replaces. Otherwise the line is unchanged. The hover passes PlainTimeAt?.Invoke(x) == true for the plotted X.
  • Lite/Controls/ServerTab.Charts.cs: CpuHoverPlainTimes(rows) returns null when no row is marked. Otherwise it returns the Contains of a set built from the marked rows. Each entry is FromOADate(ToOADate(SampleTimeUtc)), the same round trip the hover's X takes. The match is exact, even for a stamp finer than a millisecond. UpdateCpuChart assigns it after the hover's Clear() and Add calls.
  • Darling/Darling.Tests/ConsumedTimestampFrameDisciplineTests.cs: FromOADate is declared as a call on a census column that does not render it (one entry, with its reason).
  • Sweep. In Lite/Controls and Lite/Windows, SampleTimeUtc is read in two places: the CPU tab chart (this change) and the Overview's correlated timeline charts. Those label the cursor's instant through CorrelatedCrosshairManager, which is a true instant, so they are left alone. In the Darling viewer, ToUtc( appears only in job history run times, system events and ViewerTimeHelper.ConvertToDisplay. Its CPU read uses the stored UTC instant, or de-skews in SQL, so it has no stored wall-clock CPU bucket. Its hover does not change, and it compiles against the shared hover.

Test plan

  • New class CpuHoverStoredWallClockTests (14 tests): FormatHoverTime on Eastern time reads "01:30:00 -04:00" at 05:30Z and "01:30:00 -05:00" at 06:30Z, and "01:30:00" for both with plain: true. Outside the repeated hour, plain and not plain match. In the UTC frame, a bucket of rows with no stored instant is marked only in the repeated hour. 01:15 is marked. 00:30 and 03:00 are not. Rows that have their instant are not marked, from either pass. The edges of the hour: 01:00 and 01:45 are marked, 00:45 and 02:00 are not. A bucket that merged both passes is marked, and its hover reads "01:30:00" while a post-change second-pass reading keeps "01:30:00 -05:00". The server-local frame marks the 01:30 point and not the 03:00 one. A server on a fixed offset (-300) or on UTC marks no point, in either frame. That includes 01:30 on 2026-11-01, where a US zone repeats an hour. A Tokyo server's stored 14:30 on 2026-11-01 converts to 05:30Z, is not marked, gives no predicate, and reads "01:30:00 -04:00" in Eastern time (APreRungBucketOnAServerWithNoRepeatedHour_IsNotMarked_AndTheHoverInAnotherZonesRepeatedHourKeepsItsOffset). CpuHoverPlainTimes is true for a marked row's plotted X, false for an unmarked row's, and null when no row is marked. Clear() drops the predicate. UpdateCpuChart sets it after the clear.
  • Red, plant (a): FormatHoverTime ignores plain. Measured when the class had 11 tests, Total: 11, Failed: 3: ThePlainTime_IsWordedInTheInvariantCulture_LikeTheLineItReplaces, TheHoverTime_InTheRepeatedHour_DropsTheOffsetForAPlainX_AndKeepsItOtherwise, ThePreRungBucketThatMergedBothPasses_IsMarked_AndTheHoverDropsItsOffset [FAIL].
  • Red, plant (b): both flag assignments dropped. Total: 14, Failed: 4: TheUtcFrame_MarksAPreRungBucketFromTheFirstToTheLastQuarterHourOfTheRepeatedHour_AndNoOther, ThePreRungBucketThatMergedBothPasses_IsMarked_AndTheHoverDropsItsOffset, TheUtcFrame_MarksAPreRungBucketInTheRepeatedHour_AndNotAPreRungBucketOutsideItNorAPostRungOne, TheServerLocalFrame_MarksOnlyThePointsWhoseWallTimeHappensTwice [FAIL].
  • Red, plants (c), (d) and (e) in one run. (c) Clear() does not reset the predicate. (d) The plain line drops the invariant culture. (e) The set is keyed on the raw SampleTimeUtc. Measured when the class had 11 tests, Total: 11, Failed: 3, one failure each: ClearingTheHover_DropsThePredicate, ThePlainTime_IsWordedInTheInvariantCulture_LikeTheLineItReplaces and TheHoverPredicate_MatchesAMarkedRowsPlottedX_AndNotAnUnmarkedOnes.
  • Red, plant (f): the UTC-frame flag set to row.BucketIsServerLocal alone, so every pre-change bucket is marked. Total: 14, Failed: 4: TheUtcFrame_MarksAPreRungBucketFromTheFirstToTheLastQuarterHourOfTheRepeatedHour_AndNoOther, AServerWithNoRepeatedHour_MarksNoPoint_InEitherFrame, TheUtcFrame_MarksAPreRungBucketInTheRepeatedHour_AndNotAPreRungBucketOutsideItNorAPostRungOne, APreRungBucketOnAServerWithNoRepeatedHour_IsNotMarked_AndTheHoverInAnotherZonesRepeatedHourKeepsItsOffset [FAIL].
  • Red, plant (g): the server-local frame's flag set to true. Total: 14, Failed: 2: AServerWithNoRepeatedHour_MarksNoPoint_InEitherFrame and TheServerLocalFrame_MarksOnlyThePointsWhoseWallTimeHappensTwice [FAIL]. Every plant is restored, and the diff holds none.
  • Lite.Tests and Darling.Tests build with 0 warnings. origin/dev had nothing new to merge.
  • The Lite.Tests classes that name FormatHoverTime, ChartHoverHelper, GetCpuUtilizationAsync or BucketIsServerLocal ran together: Total: 135, Errors: 0, Failed: 0.
    • The 14 classes: CpuHoverStoredWallClockTests, AxesExtensionsTests, ChartClickIsolateTests, ChartTimeConversionClockTests, DisplayZoneTextFrameTests, ServerTabChartUtcFrameTests, TimeRangeSlicerLabelsTests, AlertBodyClockFrameDisciplineTests, OverviewLaneBucketingTests, OverviewComparisonWindowOffsetTests, OverviewLanesUtcFrameTests, TimeHonestyRungTests, WatermarkFrameReadTests and TimeHonestyRungReadTests.
  • Darling.Tests ViewerChartUtcFrameTests, ConsumedTimestampFrameDisciplineTests, TsqlConventionGuardTests, DocCommentHygieneTests: Total: 115, Errors: 0, Failed: 0. ConsumedTimestampFrameDisciplineTests first flagged FromOADate on a census column, and passes with the one declared entry. Lite.Tests has no TsqlConventionGuardTests or DocCommentHygieneTests.
  • Full Lite.Tests, once, no DARLING_TEST_PG: Lite.Tests Total: 6365, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0, Time: 290.434s.
  • Full Darling.Tests, once, no DARLING_TEST_PG, measured before the mark was narrowed: Darling.Tests Total: 18794, Errors: 0, Failed: 0, Skipped: 1188, Not Run: 1, Time: 114.448s. The skips are the live tests that need a Postgres connection string, which that run did not set. The one Not Run is not a test this change adds or touches. The narrowing touches no Darling code, and the four Darling classes above pass on the new head. CI runs the full Darling suite.

CHANGELOG

SECTION: None

…wall clock without a UTC offset in the repeated autumn hour (#4766)
… time names two instants (#4766)

The hover marked every point cut on the server's stored wall clock as one whose offset it could not name. A stored wall time that happens once converts to one instant exactly, and on a machine whose display zone differs from the server's such a point can land in the display zone's repeated hour, where dropping the offset made two points read the same time. The read now marks a point only when its wall time happens twice on the server's own clock (SampleTimeNamesTwoInstants, formerly SampleTimeIsStoredWallClock); a server on a fixed offset or on UTC marks nothing.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 30, 2026 07:57
@erikdarlingdata
erikdarlingdata merged commit bfd4b3d into dev Sep 30, 2026
17 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/cpu-hover-stored-wall-clock branch September 30, 2026 07:59
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