Skip to content

Render every desktop timestamp through the renderer for its own clock frame (#3207) - #3228

Closed
erikdarlingdata wants to merge 2 commits into
devfrom
fix/3207-desktop-render-frames
Closed

Render every desktop timestamp through the renderer for its own clock frame (#3207)#3228
erikdarlingdata wants to merge 2 commits into
devfrom
fix/3207-desktop-render-frames

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Fixes #3207.

Twenty-four desktop render sites across both SKUs rendered a timestamp through the renderer built for the other clock frame. Both directions were live and both are silent — the wrong renderer returns a plausible timestamp, off by the server's whole offset:

  • a server-local value through ForDisplay / FormatServerTime subtracts the offset a second time and displays 4 hours early on the fleet's measured −240;
  • a naive-UTC value through FormatServerClock displays 4 hours late.

Root cause: each SKU was missing one half of the pair

Lite had no raw renderer at all. ServerTimeHelper.FormatServerTime is Lite's ForDisplay — it adds the offset unconditionally and names its parameter utcTime — so there was no way to say "this value is already on the server's clock". Sixteen sites therefore pushed server-local DMV and XML values through it. ServerTimeHelper.FormatServerClock supplies the missing half, and unlike Darling's namesake it routes through ConvertForDisplay, so these columns now honour the user's Server/Local/UTC choice like every other Lite timestamp rather than ignoring it.

Darling had no way to say "this column is UTC" in a display property. Five query_store_stats sites reached for the raw renderer instead. ViewerDataService.FormatStoredUtc is now the named counterpart to FormatServerClock — Query Store returns datetimeoffset and QueryStoreCollector normalises through DateTimeOffset.UtcDateTime, so those five are UTC. The sixth Darling site goes the other way: running_jobs.start_time is the msdb Agent's clock (RunningJobsCollector ships ja.start_execution_date verbatim and measures duration against GETDATE()), so it moves off ForDisplay onto FormatServerClock.

Inlining ViewerTimeHelper.ForDisplay(x).ToString(...) would behave identically; a named method beside its opposite is what makes the choice reviewable, which is the whole failure mode here.

Two sites #3220's census structurally cannot see

Found by hand, and worth naming because they show the guard's reach rather than a gap in its care:

  • the deadlock graph's lasttranstarted (ViewerDataService.Deadlock.cs) — parsed from XML into DeadlockProcessInfo, with no store column of that name for the frame register to resolve. DeadlockTimeLocal two lines above is the XE @timestamp and correctly stays on ForDisplay: two frames, two renderers, one class.
  • Lite's RunningJobRow.StartTimeLocal, which used DateTime.ToLocalTime() — not a renderer the guard's RenderCall tracks. That one carried two defects: it treated an msdb-local value as UTC, and it rendered in the Lite host machine's zone regardless of the display-mode setting every other timestamp honours.

Three doc comments stated the wrong frame

Corrected with the code, including the one that hid the Lite half. ViewerDataService.QuerySnapshots called Lite's FormatServerTime a "raw server-clock" renderer and asserted the two SKUs agreed. They did not — Lite was double-skewing while the comment said otherwise. They agree now, and the comment says why it used to be wrong rather than being quietly deleted.

Guard changes, and why each direction differs from #3206's

FormatStoredUtc is registered in the Renderers map rather than left out. An unlisted renderer is invisible to that scan, so omitting it would have dropped five sites out of the census as the price of fixing them.

The seventeen DesktopRenderFrameMismatch rows come out, and DesktopRenderMismatchSites goes to 0. That is the opposite of what #3206 did with its MCP rows, and the difference is in the discriminator: the render scan flags only a disagreement between column frame and renderer, so a corrected site genuinely leaves the found-set and a row for it would fail set equality from the other side. The MCP scan keys on a payload field NAME, which survives the fix — which is why those rows had to be relabelled DeSkewedAtRead instead. Zero is the strongest form of this pin, and a regression is still caught: it would be found by the scan and not be in the inventory.

Verification

Darling.Tests and Lite.Tests are net10.0-windows and cannot execute on macOS, so CI is the arbiter for the suite. Run here: all four affected projects build with 0 errors, every changed file keeps CRLF with no bare LF, and the two existing FormatServerClock behaviour pins (ViewerQueriesTests.FormatServerClock_ShowsRawServerWallClock_EmptyForNull and ViewerQueriesRestTests.QuerySnapshotRow_TranStartTimeLocal_RendersRawServerClock_...) pin columns this change does not touch — query_stats/procedure_stats and query_snapshots.tran_start_time — so they still hold.

I have not run the render scan itself; its frame register is derived from collector provenance at runtime and my ability to replay that faithfully is the thing that has been wrong twice on the sibling PR, so I am not claiming it here.

Merge order

This shares ConsumedTimestampFrameDisciplineTests.cs with #3212, which edits the MCP rows and two different constants. Whichever lands second needs a small mechanical conflict resolution in that one file — different regions, no overlapping rows. #3212 first is the easier order, since its inventory edit is the larger of the two.

… frame

Twenty-four sites across both SKUs rendered a timestamp through the renderer
built for the OTHER frame. Both directions were live and both are silent: a
server-local value through ForDisplay/FormatServerTime subtracts the offset a
second time and displays four hours EARLY on the fleet's measured -240, while a
naive-UTC value through FormatServerClock displays four hours LATE.

Lite had no raw renderer at all, which is the root of sixteen of them: its
FormatServerTime is its ForDisplay -- it adds the offset and names its
parameter utcTime -- so there was no way to say "this value is already on the
server's clock". ServerTimeHelper.FormatServerClock supplies that, and unlike
Darling's namesake it routes through ConvertForDisplay, so these columns honour
the user's Server/Local/UTC choice like every other Lite timestamp.

Darling gains the opposite helper for the same reason: five query_store_stats
sites had no way to say "this column is UTC" and reached for the raw renderer,
so ViewerDataService.FormatStoredUtc is now the named counterpart to
FormatServerClock. Query Store returns datetimeoffset and QueryStoreCollector
normalises through DateTimeOffset.UtcDateTime, so those five are UTC and the
sixth Darling site -- running_jobs.start_time, the msdb Agent clock -- goes the
other way, off ForDisplay and onto FormatServerClock.

Two sites are outside #3220's census and were found by hand, because its scan
structurally cannot see them: the deadlock graph's lasttranstarted has no store
column of that name, and Lite's running-jobs StartTimeLocal used
DateTime.ToLocalTime(), which is not a tracked renderer -- that one ignored the
display-mode setting entirely as well as treating an msdb-local value as UTC.

Three doc comments stated the wrong frame and are corrected, including the one
in ViewerDataService.QuerySnapshots that called Lite's FormatServerTime a "raw
server-clock" renderer and asserted the two SKUs agreed. They did not, and that
claim is how the Lite side stayed hidden.

FormatStoredUtc is registered in the guard's Renderers map rather than left out:
an unlisted renderer is invisible to that scan, so omitting it would have
dropped five sites out of the census as the price of fixing them. The seventeen
DesktopRenderFrameMismatch rows come out and DesktopRenderMismatchSites goes to
0 -- that scan flags only DISAGREEMENT, so a corrected site leaves the found-set
and a row for it would fail set equality from the other side. Zero is the
strongest form of the pin, and a regression is still caught: it would be found
and not be in the inventory.
[
("ForDisplay", ClockFrame.Utc),
("FormatServerTime", ClockFrame.Utc),
("FormatStoredUtc", ClockFrame.Utc),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding FormatStoredUtc makes Renderers.Length 4, but the pin at line ~1418 (RenderCallMechanics or nearby test, not touched by this diff) still reads:

/* And the two renderer families are distinguished, not merged: three names, two expectations. */
Assert.Equal(3, Renderers.Length);

That assertion — and the "three names" comment above it — predate this PR and weren't updated for the new entry. As written it will fail at runtime (Renderers.Length is now 4, not 3), which goes red on the Windows "build" / "Darling PostgreSQL tests" CI jobs (this project compiles on Linux but only runs there per CONTRIBUTING.md, so the Linux build job won't catch it). Needs Assert.Equal(4, Renderers.Length) and the comment updated to "four names, two expectations".

@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown

Reviewed the diff (Lite ServerTimeHelper.FormatServerClock + Darling ViewerDataService.FormatStoredUtc, and the render-site swaps to use them).

The production logic checks out: verified independently that running_jobs.start_time is genuinely stored as server-local with no UTC normalization anywhere in the write path (Lite's DuckDB appender and Darling's Npgsql writer both pass it through verbatim, only Naive()-stripping the DateTimeKind tag on the Postgres side), and that query_store_stats.first_execution_time/last_execution_time are genuinely normalized to UTC via DateTimeOffset.UtcDateTime in QueryStoreCollector.ReadRowsAsync. Both match the PR description's premise, and the Lite/Darling renderer choices line up correctly site-by-site.

Left one inline comment: the new FormatStoredUtc entry in ConsumedTimestampFrameDisciplineTests.Renderers grows that array to 4, but the untouched pin Assert.Equal(3, Renderers.Length) further down in the same file wasn't updated, so it'll fail at runtime on the Windows CI jobs that actually execute Darling.Tests.

No other correctness, parity, or security issues found in this diff.

…renderer

Two CI failures, both mine, both about the guard rather than the fix.

AssertMatchesInventory asserts the found set is NOT empty. That is the right
floor for the MCP fact, whose scan can only report sites it found -- an empty
result there cannot be told apart from a scan that stopped matching. It is the
one assertion a fully-fixed render tree cannot satisfy, and I had zeroed
DesktopRenderMismatchSites without noticing the helper forbids it.

The render fact does not need that floor, because it already carries a stronger
one: judged >= 30 counts the sites the scan LOOKED AT rather than the sites that
failed, and its own comment says why -- "a matcher that stopped recognising the
renderers would otherwise report an empty offender set over 459 files". So this
fact now spells its set equality inline and tolerates the empty set that is now
correct. The ratchet is unchanged where it matters: a reintroduced mismatch lands
in found, is absent from the inventory, and reds.

Renderers went from three names to four, so the count pin follows, plus an
expectation pin for FormatStoredUtc and one asserting FormatServerClock is still
the only member on the server-local side -- the property that makes "three take
UTC, one takes the server's clock" checkable rather than a comment.
@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown

Reviewed the diff in detail — this is a careful, well-scoped fix and I didn't find correctness or parity bugs.

Verified independently:

  • RunningJobsCollector (shared between Lite/Darling in PerformanceMonitor.Collectors) ships ja.start_execution_date verbatim and measures duration against GETDATE(), confirming start_time is msdb Agent local time on both SKUs — so moving both RunningJobRow.StartTimeLocal implementations onto FormatServerClock/raw rendering is correct and keeps the SKUs in agreement.
  • The deadlock graph's lasttranstarted attribute is parsed by the shared DeadlockGraphProcessParser (Common) into a server-local DateTime in both BlockedProcessReportCollector and the deadlock XML walk — confirms the doc comments' claim and the renderer switch on both DeadlockProcessDetail.LastTranStartedLocal implementations.
  • Lite.ServerTimeHelper.ConvertForDisplay treats its input as already-server-local time uniformly across all three display modes (ServerTime/LocalTime/UTC), which matches how the existing FormatServerTime calls it (after adding the offset) — so the new FormatServerClock(serverLocalTime) overload feeding it directly is consistent, not a new code path with different semantics.
  • Checked for orphaned/missed sites: QueryStore.cs's remaining FormatServerTime calls on Lite are Query-Store times (naive UTC, correctly left alone), and BlockingLastTranStarted (the "Blocking" counterpart to the fixed "Blocked" columns) is only ever surfaced through the MCP JSON payload (ToString("o"), no display-mode conversion needed), not a desktop grid cell — so it's not a missed site.
  • The test rewrite in ConsumedTimestampFrameDisciplineTests.cs (switching from AssertMatchesInventory — which requires a non-empty found set — to an inline Assert.Equal for the now-legitimately-empty DesktopRenderFrameMismatch set, while keeping the judged >= 30 floor as the anti-vacuity check) is sound and well-justified in the added comment.

One thing worth a follow-up issue rather than a blocker here: the PR's own doc comment in Lite/Services/ServerTimeHelper.cs calls out that Lite's new FormatServerClock is mode-aware (routes through ConvertForDisplay, honoring the Server/Local/UTC setting) while Darling's ViewerDataService.FormatServerClock renders "literally raw" regardless of display mode. That's a pre-existing Darling behavior this PR doesn't touch, but it does mean that for columns like query_stats.last_execution_time, switching Darling's display mode to UTC or Local won't convert the value the way the equivalent Lite grid cell now correctly does. Given this PR's whole premise is desktop-render frame/SKU parity, it might be worth a tracked follow-up to make Darling's FormatServerClock mode-aware too (or explicitly document why it deliberately isn't).

🤖 Generated with Claude Code

erikdarlingdata added a commit that referenced this pull request Sep 9, 2026
…mpleteness two ways

FormatStoredUtc is the counterpart to FormatServerClock, ported from #3228 with its doc comment: named for
the frame it takes, because picking the wrong one of the pair is silent - both return a plausible timestamp
and they differ by the server's whole offset. The five query_store_stats sites take it instead of an inline
ternary, and it is REGISTERED, which is what keeps those sites inside the judged population; the scan keys
on the renderer's name.

The map's completeness had no criterion, which is the class the guard itself exists to catch. Two checks now
cover it from different directions. The alias derivation pins every static formatter over a DateTime that
reaches a registered renderer under another name - the shape that hid four plan_correction stamps - at set
equality. That criterion is a SHAPE and a shape can be evaded, so a second check asks nothing about
signatures: no identifier may be applied to a census timestamp column unless it is a registered renderer, a
declared alias, or declared as not rendering, with each exemption carrying what it actually is. An
unregistered INSTANCE method handed a census column is invisible to the first and caught by the second.
@erikdarlingdata

Copy link
Copy Markdown
Owner Author

Closing in favour of #3227, which covers strictly more.

Both PRs were opened for #3207 within six minutes of each other — #3227 at 17:43, this one at 17:49 — and I opened this one without checking whether a PR already existed. That check is the cheap one and I skipped it.

#3227 is the more complete fix and now carries everything this branch had:

  • it fixes four sites this branch does not touch — ViewerDataService.PlanCorrection.cs lines 209-212, where Local() is a ViewerTimeHelper.ForDisplay wrapper (its parameter is literally named naiveUtc) applied to four server-local sys.dm_db_tuning_recommendations columns. Guard a server-local column reaching a UTC-assuming consumer, with the census (#3208) #3220's census does not contain them, because Local is not in its Renderers map — the same invisibility that hid Lite's ToLocalTime site;
  • it also corrects ItemHistory.cs, which carries the same wrong "render raw" claim I fixed in ViewerHistoryRows.cs and which this branch missed entirely, plus FinOps.cs and the JobHistory frame docs;
  • it adds 335 lines of renderer-arithmetic tests pinning the ±240 direction, which this branch has no equivalent for;
  • and it has since taken ViewerDataService.FormatStoredUtc, its registration in the Renderers map, and the Assert.Equal(4, Renderers.Length) pin — the only things this branch had that it lacked.

So there is nothing left to port and no reason to keep two PRs open against one issue. Branch left in place rather than deleted, in case any of the reasoning here is worth lifting.

Two things from this branch's CI worth having on #3227's radar: AssertMatchesInventory asserts the found set is not empty, so zeroing DesktopRenderMismatchSites fails there — the render fact's own judged >= 30 floor is the better guard once the class is fixed, and that fact needs its set equality spelled inline rather than routed through the helper. And dev has moved twice under both branches (#3212 as 849ea2e, #3225 before it), so the MCP rows in that census are now DeSkewedAtRead with McpPayloadUnmarkedSites at 1.

erikdarlingdata added a commit that referenced this pull request Sep 9, 2026
…#3207, #3221) (#3227)

* Pair every desktop clock render with its column's frame, in both SKUs (#3207, #3221)

Twenty-six render sites, both directions. A server-local column through
ForDisplay / FormatServerTime adds the collected offset a second time and
displays four hours early on the fleet's measured -240; a naive-UTC column
through FormatServerClock renders raw and displays four hours late. Each site
now takes the renderer that matches its column's frame, read off #3220's
catalog-derived census rather than by hand.

Lite had no server-clock renderer at all, so its server-local columns had
nowhere correct to go: ServerTimeHelper.FormatServerClock is that renderer,
built on the existing ConvertForDisplay, whose input contract is already the
server's clock. That keeps exactly one add of the offset on the naive-UTC path
and none on this one, and honours the Server / Local / UTC preference the rest
of the app honours.

Three doc comments asserted the wrong frame outright and are why one wrong site
became several; a fourth claimed cross-SKU parity with a renderer that does not
behave the way it said. All four state the frame the code now uses.

The census's seventeen DesktopRenderFrameMismatch rows are deleted with the
sites they described. Its render scan could not see a renderer reached through a
one-hop wrapper, which is how four server-local plan_correction stamps in the
Darling viewer stayed out of the census; the wrapper set is now derived and
pinned at set equality, and no wrapper may be handed a column every census table
frames the other way.

Two class summaries in ViewerDataService.FinOps.cs claimed "localized in read" for columns their own
reads take verbatim, with the read's comment stating the opposite and giving the reason. Same defect one
file over, so the summaries now defer to the reads.

* State the frame on Lite's two undocumented job-history wall-clock getters

RunTimeLocal carries a stated justification for showing run_datetime as-is; its two siblings in the same
file showed the same frame the same way with nothing said at the property. LastSuccessfulRun is
MAX(run_datetime) over the successful step-0 rows and had nothing said at either level;
NextScheduledRunLocal had the frame in its class summary only. Both now state it where a reader checks,
and neither behaviour changes.

* Move the mode-blindness pin to the class that owns the viewer-time statics

The pin flips ViewerTimeHelper.CurrentDisplayMode, and only [Collection("viewer-time-statics")]
serializes that against the three other classes that flip it - ViewerQueriesTests is in no such
collection, so the pin raced them. It also introduced a non-store finally into a file that carries a
[Collection("live-postgres")] class, which LiveCleanupConversionRatchetTests reports as a teardown not
routed through LiveStoreCleanup; that is the failure both Windows jobs reported, and the ratchet is right
about the file even though the block was restoring statics rather than store state.

The pin now sits in ViewerTimeHelperTests, in the right collection, and gains a control: the naive-UTC
renderer in the same viewer must MOVE with the mode, so the mode-blind assertion is about this renderer
rather than about a preference nothing honours.

* Name the Darling viewer's UTC renderer, and pin the renderer map's completeness two ways

FormatStoredUtc is the counterpart to FormatServerClock, ported from #3228 with its doc comment: named for
the frame it takes, because picking the wrong one of the pair is silent - both return a plausible timestamp
and they differ by the server's whole offset. The five query_store_stats sites take it instead of an inline
ternary, and it is REGISTERED, which is what keeps those sites inside the judged population; the scan keys
on the renderer's name.

The map's completeness had no criterion, which is the class the guard itself exists to catch. Two checks now
cover it from different directions. The alias derivation pins every static formatter over a DateTime that
reaches a registered renderer under another name - the shape that hid four plan_correction stamps - at set
equality. That criterion is a SHAPE and a shape can be evaded, so a second check asks nothing about
signatures: no identifier may be applied to a census timestamp column unless it is a registered renderer, a
declared alias, or declared as not rendering, with each exemption carrying what it actually is. An
unregistered INSTANCE method handed a census column is invisible to the first and caught by the second.

* Make the Darling server-clock renderer honour the display mode, so the frame fix costs no preference

FormatServerClock emitted the server's own clock in all three modes, so moving running_jobs.start_time onto
it fixed the frame and dropped the preference: a user who selected UTC read server time with nothing saying
so, at a site that had respected the choice.

It composes out of ConvertToDisplay's existing arms rather than adding any. The naive-UTC twin of a
server-clock value is serverLocal - offset, so ConvertServerClockToDisplay is one subtraction in front of
the conversion already there and every arm - the machine-local one in particular - is reused. Verified as
arithmetic before it was written: a server-clock value converted for display equals the same instant's
naive-UTC value converted for display, in every mode, which is the composition rather than a restatement of
any arm.

The pair is now symmetric across the SKUs. Darling's ConvertToDisplay takes naive UTC, so its server-clock
conversion subtracts the offset first; Lite's ConvertForDisplay takes the server's clock, so its naive-UTC
renderer adds it. One offset step between the frames, either way round, and both SKUs honour the preference.

Both modes are pinned: the same server-local value must render differently under UTC and Server mode, by
exactly the offset, at the fleet's measured -240. That assertion is what the old renderer could not support -
with no mode input it was untestable on this axis, which is how a raw render shipped. Seven doc comments
that said "renders raw" now say what the code does.

Lite is untouched. ServerTimeHelper already honours the mode; whether its two job-history getters should is
a separate product question and stays one.
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