Skip to content

Lite shows server times right across a daylight saving change, in the desktop and in MCP tools (#4766, #4793) - #4841

Merged
erikdarlingdata merged 34 commits into
devfrom
fix/4793-lite-mcp-server-clock
Sep 29, 2026
Merged

erikdarlingdata merged 34 commits into
devfrom
fix/4793-lite-mcp-server-clock

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Part of #4766 (Lite's half, plus one axis fix in the Darling viewer; the rest of the viewer's half was #4783). Fixes #4793 (Lite's MCP tools; Darling's were #4829).

Why

In Server time mode, the default, Lite converted every time with one UTC offset. For a server in a zone with daylight saving, chart times and the time-range picker on the far side of a clock change showed an hour off the server's wall clock. Lite now follows the same rule as the Darling viewer (ServerClock): the zone id when the server reported one (SQL Server 2022 and later), else the fixed offset, else UTC. ServerClock.ToUtc never throws: a skipped local time moves forward by the gap and a repeated one takes its first occurrence.

The display and its partners have to change together. The UTC and Local displays convert a server-local value back through the clock, and a picked range goes into a read that turns it back into UTC. A partner left on one offset stops cancelling, and shows an hour off where it was exact before. So this change moves every partner: the chart X values, the pickers, the windowed reads, the Overview lanes and the three History windows. The windowed reads also had the flaw on their own. GetTimeRange turned a custom range's server-local bounds into UTC with one offset, and GetTimeRangeServerLocal built "now" in server time with the same offset. A custom range across a clock change was an hour off at its far bound, and an hours-back window across a change was an hour long or short.

Four Lite MCP tools return a server-local stamp converted to UTC, and each converted every stamp with the one offset the server last reported, so a stamp from before a daylight saving change came back an hour off. A US Eastern server whose newest offset is the daylight one (-240) returned 05:30 UTC for a stamp taken at 01:30 EST on 8 March 2026, where 06:30 UTC is right. A stamp after the change was already right, which is why it went unnoticed. The four tools and the stamps they convert:

  • get_blocked_process_reports: the six blocked_/blocking_ transaction, batch-start and batch-completed stamps.
  • get_running_jobs: start_time.
  • get_index_usage: last_user_access.
  • get_pvs_stats: the four cleaner times.

Two more, get_top_queries_by_cpu and get_top_procedures_by_cpu, passed no clock, so their last_execution_time floor used a zero offset on every server.

What changes

#4766: Server time mode and System Events

  • Lite/Services/ServerTimeHelper.cs holds a ServerClock (ActiveServerClock). ToServerTime, ConvertForDisplay, DisplayTimeToServerTime, FormatServerTime and the timezone label go through it. New clock overloads (ConvertForDisplay(.., ServerClock), DisplayTimeToServerTime(.., ServerClock), ToServerTime(.., ServerClock)) plus ServerTimeToUtc; the int offset overloads stay and delegate to a fixed-offset clock. UtcOffsetMinutes still exists: the getter is the offset in force now, the setter installs a fixed-offset clock.
  • LocalDataService.GetServerClockAsync reads time_zone_id with utc_offset_minutes from the same newest server_properties row (skipping NULL offsets, like GetServerUtcOffsetMinutesAsync). It returns null when nothing is collected yet.
  • ServerTab holds a clock instead of a constructor-time offset. RefreshAllDataAsync calls RefreshServerClockAsync first on every refresh; a failed read or a null result keeps the clock in use, and a visible tab also installs it as the active one. Selecting a tab installs that tab's clock (MainWindow).
  • The tab's 72 x.AddMinutes(UtcOffsetMinutes) chart X value, axis range and slicer conversions became ToServerLocal(x) / ToUtcFromServerLocal(x) (clock-based).
  • GetCurrentWindow takes a clock; the picker value converts through it.
  • System Events (Default Trace): GetDefaultTraceEventsAsync resolves the window to exact UTC bounds through the clock, uses the server-local bounds only as a SQL pre-filter widened by one hour on each side, converts each row's event_time through the clock, and holds the converted rows to the exact window (inclusive at both ends, as before). It takes the clock only; the int offset parameter is gone. get_default_trace_events (MCP) passes the server's own clock (McpServerLocalWindow.ClockForAsync).
  • Where the time zone applies: the clock is built by ServerClock.Resolve(time_zone_id, utc_offset_minutes), the shared ServerClock class Darling uses. time_zone_id is collected only where SQL Server reports one: the collector reads CURRENT_TIMEZONE_ID() in its own batch inside TRY/CATCH, gated on SQL Server 2022 and later (or Azure SQL Database and Managed Instance), and writes NULL elsewhere. A NULL or unresolvable id falls back to the fixed offset, so an older server keeps the fixed offset.

#4766: the windowed reads

  • GetTimeRange, GetTimeRangeServerLocal and GetQueriesTabWindowUtc take a ServerClock in place of int utcOffsetMinutes. The first two are now internal so the tests can call them.
    • A custom range converts each bound with serverClock.ToUtc, so a winter start and a summer end use -300 and -240 (US Eastern).
    • GetTimeRangeServerLocal renders both UTC ends on the clock: end is ToServerLocal(anchor), start is ToServerLocal(anchor - hoursBack). Its custom range stays as given.
  • SelectedServerTabUtcOffsetMinutes is now SelectedServerTabServerClock => ServerTimeHelper.ActiveServerClock. Its doc comment keeps the pairing rule with ServerTab.GetCurrentWindow: the picker's conversion and the window's must name the same server's clock, or they stop cancelling.
  • The 114 references to the three methods: 82 used the selected-tab property (one sed, diff read), 18 passed utcOffsetMinutes: 0 and now pass ServerClock.Utc, 7 were the Queries tab window calls in ServerTab.Comparison.cs and ServerTab.Refresh.cs and now pass ServerTimeHelper.ActiveServerClock, and the rest are the definitions and the reads below that pass their own parameter.
  • Six reads that took an offset now take ServerClock serverClock in the same position (nine signatures changed with the three window methods):
    • GetAlertCountsAsync (required), GetCpuBucketsAsync (required).
    • GetCpuUtilizationAsync, GetTopQueriesByCpuAsync, GetTopProceduresByCpuAsync: optional. Null means the selected-tab clock for CPU utilization (as before) and UTC for the two top-N reads (the old zero default).
    • GetDefaultTraceEventsAsync, above.
  • Callers: the alert badge, the two top-N reads on the Queries tab and both Slicers calls pass the tab's own ServerClock (they passed the tab's own offset); the MCP get_cpu_utilization tool resolves McpServerLocalWindow.ClockForAsync.
  • GetCpuUtilizationAsync and GetCpuBucketsAsync take their UTC bounds from GetTimeRange on one anchor instead of converting the server-local bounds back with one offset. A round trip through server-local time would put an anchor inside a repeated hour an hour early. The CPU read's bucket width now comes from the window's real length, not its wall-clock span, which a change moves by an hour.
  • A row collected before schema v63 has no sample_time_utc. GetCpuUtilizationAsync and GetCpuBucketsAsync compare such a row's server-local sample_time with the window's server-local bounds from GetTimeRangeServerLocal, and every other row's sample_time_utc with the UTC bounds. The predicate no longer takes an offset, so no row is shifted by one offset for the whole window.
  • ServerTimeHelper.ToServerTime(DateTime, int) and DisplayTimeToServerTime(DateTime, TimeDisplayMode, int) had only test callers. They are deleted with their two tests, and AlertBadgeServerOffsetTests builds its clock with ServerClock.FixedOffset instead.
  • Pins moved to the clock shape: ComparisonWindowUtcTests (the source pin now expects ActiveServerClock), and the callers in AlertBadgeServerOffsetTests, QueryWindowTruncationTests, OverviewLaneBucketingTests, TimeHonestyRungTests and DefaultTraceServerClockTests.

#4766: the Overview lanes and the History windows

  • The Overview lanes window (CorrelatedTimelineLanesControl): GetCurrentWindowServerLocal, GetBaselineReferenceTimeUtc, GetOverviewComparisonRange and RefreshAsync take a ServerClock in place of int utcOffsetMinutes.
    • A preset window is ToServerLocal(utcNow) back to ToServerLocal(utcNow - hoursBack), so it is hoursBack real hours long across a change (the same rule as GetTimeRangeServerLocal). With only toDate given, the start is ToServerLocal(ToUtc(toDate) - hoursBack).
    • The baseline reference for a custom range is serverClock.ToUtc(fromDate), so a fromDate on the far side of a change converts with its own offset.
    • ServerTab.RefreshOverviewAsync passes ServerTimeHelper.ActiveServerClock (it passed ServerTimeHelper.UtcOffsetMinutes).
  • Chart times: the Overview lanes plotted nine series (five lanes, four comparison ghost lines) at utc.AddMinutes(ServerTimeHelper.UtcOffsetMinutes), and the three History windows (ProcedureHistoryWindow, QueryStatsHistoryWindow, QueryStoreHistoryWindow) did the same for their CollectionTime X values and their "samples from ... to ..." summary line. That is today's offset, but the axis and crosshair convert back through the clock, so a US Eastern sample at 15:00 UTC on 1 March viewed in September would have been labelled 16:00 UTC. All of them now use ServerTimeHelper.ToServerTime(...).
  • SyncXAxes takes its window from a new GetXAxisWindow, which reuses GetCurrentWindowServerLocal for a preset range, so the axis is hoursBack real hours and does not cut off the first hour when a change falls inside it.
  • The source pin ServerTab_ConvertsThroughTheClock_AndRereadsItOnEveryRefresh scanned only Lite/Controls/ServerTab*.cs, and its regex missed Services.ServerTimeHelper.UtcOffsetMinutes and a utcOffset local. It now scans every file in Lite/Controls and Lite/Windows with \.AddMinutes\(-?\s*((Services\.)?ServerTimeHelper\.)?[uU]tcOffset(Minutes)?\). A hit has to be listed in OneOffsetExceptions with its reason, and a listed file that no longer hits fails too. The list is empty. ChartTimeConversionClockTests adds pins for the lanes and the three windows, and a pure round trip: a plotted X reads back as the sample's own instant in UTC and Local display, for samples on both sides of both changes.

#4766: the ServerTab charts and drills

  • The 30 ServerTab charts that plot a preset window take their axis window from the new ServerTab.GetChartWindow. They are in ServerTab.Charts.cs, BlockingStats, CpuScheduler, LatchSpinlock, Pickers, PlanCache, SessionStats and SystemHealthCharts. It returns the Overview lanes' GetCurrentWindowServerLocal, so the charts and the lanes share one window of hoursBack real hours. Each row plots at the server's clock on its own date. An axis start on the wall clock (the server-local end minus hoursBack) cuts off the first real hour when the window spans the spring change. The CPU chart plots its stored server-local stamps, and its read covers the same real hours, so the same window fits it.
  • The five chart drill-downs open a window of real minutes around the clicked instant. The new ServerTab.GetDrillWindow takes that instant in UTC: the clicked server-local time through the clock, or the heatmap bucket's own bucketTimeUtc. It renders 30 minutes either side (the heatmap: 5 before, 10 after) on the server's clock. Adding the minutes to the wall clock put one end of a drill just after the spring change inside the skipped hour. That end converts to the same instant as the other end, so the window came back empty. The heatmap's log line shows the offset in force at the bucket (OffsetMinutesAtBucket) instead of today's.

#4793: Lite's MCP tools

  • Each of the four stamp tools reads serverClock = await McpServerLocalWindow.ClockForAsync(dataService, resolved.ServerId) and converts each stamp with serverClock.ToUtc(...). No read passes the offset into a query; the conversion is in the output only, as before.
  • A NULL stamp stays NULL. ToUtc never throws on a saved local time that was skipped (02:30 on the spring-forward day reads as 03:30 EDT) or repeated (01:30 on the fall-back day reads as its first occurrence, the daylight one).
  • A server that reported no time zone keeps its fixed offset all year, and a server that reported no offset still reads as UTC. Both are unchanged and are pinned.
  • The output format is unchanged: round-trip "o" strings with no Z, as before.
  • get_top_queries_by_cpu and get_top_procedures_by_cpu resolve McpServerLocalWindow.ClockForAsync and pass it, so the last_execution_time floor is the window start on the server's clock.
  • McpServerLocalWindow.OffsetForAsync is removed. Its last caller was the CPU tool, and its rationale moved onto ClockForAsync.

Darling viewer: the Overview lanes time axis

The viewer's lanes window (startUtc/endUtc) and baseline referenceTime are naive UTC and add no offset, and every plotted sample goes through ViewerTimeHelper.ForDisplay, so those needed no change. But SyncXAxes built a preset axis start by subtracting hoursBack from the display end as wall clock. When a clock change in the display time zone fell inside the window, the axis started an hour off, and after a spring change it cut the first hour of samples off the chart. It now takes the window from a new GetXAxisWindow: the display time of utcNow - hoursBack to the display time of utcNow. CorrelatedLanesXAxisWindowTests covers it.

The server-time collection guard

ServerTimeHelperCollectionTests lists the members that read or write the settings shared by the whole test process, so a test class that uses them must carry the server-time collection. It did not know ActiveServerClock or ServerTimeToUtc, so a class using only those could have run beside a writer.

  • The member pattern names both, and the summary now names the shared settings as they are: the server clock (which UtcOffsetMinutes now reads and writes) and CurrentDisplayMode.
  • A new test fails when a public static member of ServerTimeHelper is missing from the pattern, so the next member added cannot slip past the guard the same way.
  • The new QueriesTabWindowRoundTripTests uses those members and carries the collection.

Darling.Tests source pins that read Lite's sources

Five Darling.Tests source pins still looked for the old shapes (OffsetForAsync, AddMinutes(-utcOffsetMinutes), GetDateTime(0).AddMinutes(-offset)) and failed on this branch. Updated, not weakened:

  • McpPayloadClockFrameDisciplineTests: the Lite tool sites must resolve the clock, must not subtract one offset, and each affected stamp must go through the clock. The UtcOrNull body is pinned too, so a call site alone proves nothing. The matcher's own cases accept the three shipped forms and reject the bare stamp and the single subtracted offset.
  • ConsumedTimestampFrameDisciplineTests (three tests): the payload-site scan counts a UtcOrNull call as the stamp, ToUtc and UtcOrNull are declared as conversions rather than renderers, and the twelve Lite conversion strings name the new forms.
  • ServerLocalReadFrameDisciplineTests: Lite's Default Trace read is converted per row with the clock.

Doc comments

These now describe the server's clock where they described one offset:

  • The pairing rule in LocalDataService.cs and the window notes in ServerTab.TimeRange.cs, ServerTab.Refresh.cs and ServerTab.Charts.cs.
  • LocalDataService.SystemEvents.cs: the read resolves the window to exact UTC through the clock, uses the local bounds widened by an hour only as a SQL pre-filter, converts each row's event_time at the row's own date, and keeps only the rows inside the exact window.
  • PerformanceMonitor.Ui/AxesExtensions.cs: Lite's converter follows the date, so converted values can decrease across the spring gap (ToUtc(02:30) is 07:30Z and ToUtc(03:00) is 07:00Z). The pass-reset comparison counts an equal or lower value as "did not increase".
  • The CPU reads' notes on rows from before schema v63: such a row compares its server-local stamp with server-local bounds. The v63 migration note in DuckDbInitializer.cs says the same.
  • ServerTimeHelper: FormatServerClock (Darling's twin also converts through the server's clock). McpDefaultTraceTools.cs.
  • ServerTab.BlockingStats.cs and ServerTab.Charts.cs name the server's clock where they named UtcOffsetMinutes. The StatsChartRange summary says server-local where it said display-local. A drill comment that said 15 minutes above 30-minute code now says 30.

Reads checked for other Lite offset uses

  • Job History (GetJobHistoryAsync): windows run_datetime, the server's wall clock, against the desktop's local now (DateTime.Now). Its comment gives that as the design for a desktop in the server's time zone. No offset is applied in SQL or C#. Unchanged.
  • Agent status (agent_status.next_scheduled_run): rendered as saved, no offset. Unchanged.
  • The long-running job alert and the analysis-pass reads (plan creation times, query performance facts, config-change attribution) were converted per row by Analysis and job alerts: a SQL Server's local times convert with the offset in force at each row, in both apps (#4821) #4838. Not touched here.
  • FindingStore.GetPriorOccurrencesSql subtracts the newest offset and documents that as its approximation. Unchanged.

What is left

  • The repeated autumn hour, a known limit. When the clocks go back, one hour of server time happens twice (01:00 to 02:00 in US zones). ServerClock.ToUtc resolves a time in that hour to its first occurrence. On dev, UTC and Local display were exact in that hour, because the one offset went out and came back and cancelled. A drill there always opened 60 minutes. Here, two things are wrong in that hour, and Server-time mode uses one UTC offset for the whole window, so times across a DST change are an hour off #4766 stays open for both:

    • Charts: under UTC and Local display, a row from the second occurrence gets the first occurrence's label, an hour early. Its point draws over the first hour. Around the spring change, an axis tick in the skipped hour can repeat a label under UTC and Local display.
    • Bounds: a picker, slicer or drill bound in the repeated hour resolves to the first occurrence. A bound meant for the second occurrence lands an hour early. Picker bounds do this under UTC and Local display. Slicer and drill bounds do it in every display mode. A drill from a point in the second half of that hour comes back empty. Its end resolves to the same instant as its start.

    The fix is a frame per display mode, as the Darling viewer has, and it includes the bounds sent to the reads. It is due before the first autumn change, on October 25.

  • GetTopQueriesByCpuAsync and GetTopProceduresByCpuAsync also pass one offset ($5 in last_execution_time >= $2 + $5 * INTERVAL '1' MINUTE). It shifts a single bound, so it is exact with the offset at the window start, which is what it gets. Listed only because it looks like the one-offset fallback the CPU reads had.

  • The Recommendations tab's "Ask AI" prompt shows a finding's window in server time with one offset (LiteRecommendationsViewModel ~160-162, fed ServerTimeHelper.UtcOffsetMinutes by RecommendationsTab ~177 and ~269), as dev does. It is not changed here, and Server-time mode uses one UTC offset for the whole window, so times across a DST change are an hour off #4766 stays open for it.

Test plan

  • Lite.Tests and Darling.Tests build with 0 warnings, 0 errors, after merging origin/dev.
  • RED for Server-time mode uses one UTC offset for the whole window, so times across a DST change are an hour off #4766's display change. The clock-taking bodies in ServerTimeHelper.cs were made to apply one shift (clock.OffsetMinutesAt(DateTime.UtcNow)), run, and put back (git diff clean afterwards). ServerTimeHelperClockTests: 3 of 10 fail. These are ServerMode_ShowsTheServersWallClock_OnEachSideOfSpringForward (expected 2026-03-08T01:30, got 02:30), ServerMode_ShowsTheServersWallClock_OnEachSideOfFallBack (expected 2026-11-01T01:30, got 02:30) and PickerInverse_SkippedLocalTime_MovesForwardByTheGap_AndDoesNotThrow (expected 2026-03-08T07:30, got 06:30). PickerInverse_RepeatedLocalTime_TakesTheFirstOccurrence_AndDoesNotThrow passes in September, because September's offset is the first occurrence's; it would fail in winter. With the Default Trace per-row conversion made one offset the same way, DefaultTraceServerClockTests: 4 of 5 fail. These are DefaultTrace_RowsOnBothSidesOfSpringForward_ComeBackWithTheRightUtcTimes (expected 06:30, got 05:30), DefaultTrace_RowsAcrossFallBack_ComeBackWithTheRightUtcTimes (expected 07:30, got 06:30), DefaultTrace_StoredSkippedAndRepeatedLocalTimes_DoNotThrow (expected 07:30, got 06:30) and DefaultTrace_TheExactUtcWindowHolds_InsideThePreFilterMargin (expected ["at-the-end","inside"], got ["at-the-end"]). DefaultTrace_FixedOffsetServer_ReadsTheSameRowsAsBefore and UtcDisplay_RoundTripsTheServerClock_OnBothSidesOfTheChange pass by design. When the tests were first written, the old code did not compile against them (36 errors), which is a compile failure, not a behavioral one.
  • RED for the windowed reads (commit 07cd289, compile-only overloads on the one-offset shape): WindowedReadServerClockTests Total: 11, Failed: 6. The six that fail are the winter-to-summer range, the summer-to-winter range, the winter-only range, the two hours-back windows just after the spring and fall changes, and the end-to-end alert count (3 rows kept, expected 2). The five that pass are the summer-only range, ServerClock.Utc and FixedOffset(-240), the preset range, and the custom range that stays as given. After the fix: 11 of 11.
  • RED for the picker-to-window round trip. A one-offset custom branch was planted in GetTimeRange (clock.OffsetMinutesAt(DateTime.UtcNow) for both bounds), run, and put back. QueriesTabWindowRoundTripTests.UtcPickers_ComeBackAsTheSameUtcRange_ThroughTheWindow: 6 of 7 rows fail, for example (from: "2026-03-01 09:00", to: "2026-03-02 09:00"), expected 2026-03-01 09:00 to 2026-03-02 09:00, actual 2026-03-01 08:00 to 2026-03-02 08:00. The summer-only row passes in September and would fail in winter; the rows across a change fail in any season. The class also checks that every display mode reads the instant the picker shows (EveryDisplayMode_ReadsTheInstantThePickerShows).
  • RED for the Overview lanes and History window chart times and the widened scan (commit 949548d, tests only): ChartTimeConversionClockTests plus ServerTimeHelperClockTests Total: 20, Failed: 5. These are the lanes pin, the three History window pins, and the scan of Lite/Controls and Lite/Windows (it names CorrelatedTimelineLanesControl.xaml.cs). The two pure round-trip tests pass on both shapes, as they test the conversions the chart composes. After the fix (commit 1c08b0d) with OverviewComparisonWindowOffsetTests: Total: 42, Failed: 0.
  • OverviewComparisonWindowOffsetTests (every old case moved to the clock, plus US Eastern cases: a preset window across the spring and the fall change, a toDate-only window, a custom fromDate on each side of the spring change, a fixed-offset clock, and GetXAxisWindow): Total: 22, Failed: 0.
  • RED for MCP tools: server-local times are an hour off for rows across a DST change #4793, the four stamp tools (measured when the tests were written): the test commit alone on the one-offset code gives Lite.Tests Total: 6, Errors: 0, Failed: 5, Skipped: 0, Not Run: 0. The five failures are the blocking, running job (both), index usage and PVS tests, each with a pre-change stamp an hour off (Expected: 2026-03-08T06:30:00, Actual: 2026-03-08T05:30:00). The sixth test (fixed-offset and no-offset servers) passes on the old code by design. LiteMcpServerClockTests calls each tool end to end and reads the JSON it returns, on a US Eastern server (Eastern Standard Time) whose newest collected offset is the post-change one (-240), with one row before the 2026-03-08 change and one after; a row with every nullable stamp NULL asserts each stays null.
  • RED for MCP tools: server-local times are an hour off for rows across a DST change #4793, the two top-N tools (commit 67b4098, tests only): LiteMcpServerClockTests Total: 10, Failed: 4. On the US Eastern server the query and the procedure last run 1 hour into a 4-hour window are missing, and on the UTC+5:30 server the row from before the window is returned. After the fix (commit ddf3b71): 10 of 10.
  • RED for the Darling viewer axis. The old wall-clock start (display end minus hoursBack) was planted in GetXAxisWindow, run, and put back. CorrelatedLanesXAxisWindowTests Total: 4, Failed: 1: PresetRange_ServerTime_AcrossSpringForward_StartsHoursBackRealHoursBeforeNow, expected 2026-03-07T22:00, actual 2026-03-07T23:00. The other three (PresetRange_UtcDisplay_IsHoursBackOfUtc, CustomRange_ConvertsEachBoundWithItsOwnOffset, OneBoundOnly_FallsBackToThePresetWindow) pass on both shapes.
  • Collection guard, red first: with ActiveServerClock and ServerTimeToUtc taken back out of the pattern, the new test fails naming ServerTimeToUtc, ActiveServerClock. With the pattern restored, a scratch test class that used only ServerTimeHelper.ActiveServerClock and ServerTimeHelper.ServerTimeToUtc and had no collection made the scan fail naming that file (the scratch class was deleted). ServerTimeHelperCollectionTests: 3 of 3 pass.
  • Darling.Tests source pins, red first: on the branch as it stood, 5 failed (McpPayloadClockFrameDisciplineTests.EveryLiteToolSite_ResolvesThisServersOffset_AndDeSkewsEveryAffectedField, ServerLocalReadFrameDisciplineTests.EveryKnownReader_CarriesItsDeSkew_AndNoBareEventTime, and three in ConsumedTimestampFrameDisciplineTests). After the update the three classes pass: 31 of 31. The other seven Darling classes that read Lite or shared sources pass too (CollectorTimestampFrameTests, McpPayloadContractCensusTests, PerfmonCounterTypeRungTests, ProductiveZeroBandingTests, TsqlConventionGuardTests, ViewerPerfmonShapingParityTests, ViewerTimeHelperTests).
  • Targeted, before the ServerTab chart, drill and CPU fallback changes: Lite 63 passed (the new round-trip class, ServerTimeHelperClockTests, DefaultTraceServerClockTests, WindowedReadServerClockTests, every class that names GetQueriesTabWindowUtc or AxesExtensions); Darling 112 passed (the three discipline classes and DocCommentHygieneTests). On 9746d79, before the last merge: CorrelatedLanesXAxisWindowTests with the three discipline classes, DocCommentHygieneTests, LaneAxisAlignerTests and ViewerTimeHelperTests, Total: 142, Failed: 0.
  • The chart axis window (commit be1991f). ServerTabChartWindowTests (6 tests) cover four cases. A US Eastern 24-hour preset window across the spring change leaves no row left of the axis and starts at the first row. Across the autumn change it leaves no empty hour at the left. A UTC server, and a daylight saving server with no change in the window, get exactly 24 hours. A custom range passes through as given. A source pin fails if any ServerTab*.cs file takes hoursBack off anything but DateTime.UtcNow. RED, each planted and put back: with the old wall-clock start in GetChartWindow, ASpringChangeInsideThePresetWindow_LeavesNoRowLeftOfTheAxis fails ("4 of 97 rows plot outside the axis"). With one site put back, NoServerTabFile_TakesHoursBackOffAnEndThatIsNotUtcNow fails, naming ServerTab.CpuScheduler.cs.
  • The drill windows (commit 4cff5b2). ServerTabDrillWindowTests (12 tests): a click at 03:10 just after the spring change keeps 60 real minutes (01:40 to 03:40). An ordinary click gets 30 minutes either side. At the spring change the heatmap keeps 5 minutes before and 10 after (07:02Z and 07:05Z). A source pin requires every drill site to build its window through GetDrillWindow, and allows AddMinutes( nowhere else in the file. RED: with wall-clock arithmetic in GetDrillWindow, AClickJustAfterTheSpringForward_KeepsSixtyRealMinutes fails (expected 01:40, actual 02:40). With OnDeadlockDrillDown put back, EveryDrillSite_BuildsItsWindowThroughGetDrillWindow and TheDrillFile_AddsMinutesOnlyInsideGetDrillWindow fail.
  • The CPU fallback (commit 8f7b015), on the shared DuckDB fixture, for a US Eastern server with rows that have no sample_time_utc. A custom range across the spring change keeps them from its first instant to its last. GetCpuBucketsAsync keeps them on both sides of the change. A row with a UTC stamp outside the range stays out, which covers the IS NULL guard. RED, with the old predicate restored: TheCpuWindow_CustomRangeAcrossASpringForward_KeepsPreRungRowsFromTheFirstInstantToTheLast fails (expected 189, actual 185), and TheCpuBuckets_WindowAcrossASpringForward_KeepPreRungRowsOnBothSidesOfTheChange fails (expected 25, actual 21).
  • The first-occurrence result in the repeated autumn hour (06:30Z, the second 01:30) is pinned as it lands, each case with a comment that names the limit: APlottedX_InTheRepeatedAutumnHour_ReadsBackAsTheFirstOccurrence_KnownLimit in ChartTimeConversionClockTests, ARangeStartingInTheRepeatedAutumnHour_StartsAtTheFirstOccurrence_KnownLimit in QueriesTabWindowRoundTripTests, AClickInTheRepeatedAutumnHour_ResolvesAroundTheFirstOccurrence_KnownLimit in ServerTabDrillWindowTests (it also pins the empty window), and a 06:30Z case in ServerTimeHelperClockTests.
  • Full Lite.Tests suite, once, on 8ec6d3b, which has every code change here (the one commit after it edits a comment): Total: 5910, Failed: 0. An earlier run under full-suite load had one failure in DuckDbSentinelConnectionTests.RunMemoryTrimCycle_OverThreshold_IncrementsCompletedCycleCount, a test this change does not touch. It passed alone (14 of 14) and in the final run.
  • Full Darling.Tests suite, once, without DARLING_TEST_PG, before the ServerTab chart, drill and CPU fallback changes: Total: 18161, Failed: 0, Skipped: 1169. The skips are the live PostgreSQL classes. Those changes touch no Darling code (the shared AxesExtensions.cs changed a comment only). Darling.Tests builds with 0 warnings on them, and DocCommentHygieneTests passes, 77 of 77.
  • Not run: the live PostgreSQL tests, and the desktop app by hand across a real clock change. The daylight saving behavior is covered by the DuckDB, clock and MCP tests above.

CHANGELOG

SECTION: Fixed
ENTRY:

…ht saving change (#4766)

ServerTimeHelper used one offset for every time, so a time on the far side of a clock change showed an hour off the server's wall clock. It now converts through a ServerClock (the zone id where the engine reported one, else the fixed offset): Server mode, the picker's inverse and the UTC and Local displays all go through it, a skipped local time moves forward by the gap and a repeated one takes its first occurrence. The int-offset overloads stay and delegate to a fixed-offset clock.

Each server tab reads its clock again on every refresh (time_zone_id with utc_offset_minutes from the same newest server_properties row); a failed read or nothing collected yet keeps the clock in use. The tab's chart X values and axis ranges convert each time through the clock instead of adding one offset to all of them, and selecting a tab installs that tab's clock.
…ow through the server clock (#4766)

The Default Trace read subtracted the newest utc offset from every server-local event_time, so a row on the far side of a daylight saving change came back an hour off in UTC. It now resolves the window to exact UTC bounds through the server's clock, uses the server-local bounds only as a SQL pre-filter widened by one hour on each side, converts each row through the clock, and holds the converted rows to the exact window. The MCP tool passes the server's own clock. Job History and Agent status render the stored server wall clock with no offset applied, so they are unchanged.
…job, index usage and version store times (fails until the fix in the next commit) (#4793)
…n store times with the server's clock instead of one offset (#4793)
#4766)

ServerTimeHelper gained ActiveServerClock and ServerTimeToUtc, and
UtcOffsetMinutes now reads and writes that same clock, but the guard's member
pattern did not name the two new members. A test class that used only those
could have run beside a writer without the server-time collection.

The pattern now names both, and the summary names the shared settings as they
are now: the server clock and CurrentDisplayMode. A new test fails when a
public static member of ServerTimeHelper is missing from the pattern, so the
next member added cannot slip past the guard the same way. No existing test
file is newly flagged.
…server clock (#4766, #4793)

Lite's blocking, running job, index usage and version store MCP tools now
convert each stamp with the server's ServerClock (McpServerLocalWindow.
ClockForAsync and serverClock.ToUtc, through a file-local UtcOrNull for the
nullable stamps), and the Default Trace read converts each row with the clock.
Five source pins in Darling.Tests still looked for the old shape (OffsetForAsync
and AddMinutes(-utcOffsetMinutes)) and failed:

- McpPayloadClockFrameDisciplineTests: the Lite tool sites must resolve the
  clock, must not subtract one offset, and each affected stamp must go through
  the clock; the UtcOrNull body is pinned too, so a call site alone proves
  nothing. The matcher's own cases now accept the three shipped forms and
  reject the bare stamp and the single subtracted offset.
- ConsumedTimestampFrameDisciplineTests: the payload-site scan counts a
  UtcOrNull call as the stamp, ToUtc and UtcOrNull are declared as conversions
  rather than renderers, and the twelve Lite conversion strings name the new
  forms.
- ServerLocalReadFrameDisciplineTests: Lite's Default Trace read is converted
  per row with the clock, not with one subtracted offset.
…er-clock

# Conflicts:
#	Darling/Darling.Tests/McpPayloadClockFrameDisciplineTests.cs
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 29, 2026 17:22
…change (fails until the fix in the next commit) (#4766)

GetTimeRange and GetTimeRangeServerLocal shift every bound by one offset, so a custom range that spans a clock change is an hour off at the far bound and an hours-back server-local window across a change is an hour long or short. These tests pin the per-bound behaviour with US Eastern dates on both sides of both changes, plus one read (the alert badge counts) end to end through the DuckDB fixture. Compile-only overloads that still use the one-offset shape stand in for the clock overloads so the tests build; six of the eleven fail on them.
…in force at each bound, so a range across a daylight saving change is right at both ends (#4766)

GetTimeRange, GetTimeRangeServerLocal and GetQueriesTabWindowUtc take the server's clock instead of one offset. A custom range converts each bound on its own, and an hours-back server-local window is the server-local rendering of its two UTC ends. The six reads that took an offset take a clock in the same position (alert counts, CPU utilization, top queries, top procedures, CPU buckets; System Events keeps its optional clock and loses the int). The 82 reads that used the selected tab's offset use its clock, and the calls that passed zero pass ServerClock.Utc. The offset that still feeds SQL over many rows is taken at the window end (CPU pre-v63 fallback, CPU buckets); the last_execution_time floor in the top-queries and top-procedures reads converts one bound, so it uses the offset at the window start.
… server's clock across a daylight saving change (#4766)
…for the last-run floor on the server's clock (fails until the fix in the next commit) (#4793)
…st-run floor uses the server's clock, not UTC (#4793)
…odes, and the docs describe the server's clock (#4766)

A round-trip test drives the Queries tab pickers' conversion and the window read together, in every display mode and in ranges inside each season and across each clock change, so a window that applies one offset to both bounds fails whatever the season the test runs in. The System Events, window-pairing, slicer-range and axis-label comments now describe the conversion through the server's clock by the date of each bound.
…ting through the server's clock, and the one-offset scan over every control and window (fail until the fix in the next commit) (#4766)
…the server's clock, so a sample from before a daylight saving change is not an hour off the axis (#4766)
…ce tool and ServerTimeHelper name the server's clock, not one offset (#4766)
…lock's doc describe the server's clock, not one offset (#4766)
…ross a daylight saving change, so the first hour is no longer cropped (#4766)
@erikdarlingdata
erikdarlingdata marked this pull request as draft September 29, 2026 19:46
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 29, 2026 19:53
@erikdarlingdata
erikdarlingdata merged commit de95dc3 into dev Sep 29, 2026
19 of 20 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4793-lite-mcp-server-clock branch September 29, 2026 20:01
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