Pair every desktop clock render with its column's frame, in both SKUs (#3207, #3221) - #3227
Conversation
…#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.
…ters 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.
Review summaryReviewed the timestamp-frame fixes across Lite and Darling (ServerTimeHelper/ViewerTimeHelper renderer swaps, plus the new census/wrapper test infrastructure in Correctness of the frame fixes: I traced each renderer swap against its source column (XE Parity check: Went through every changed site and its counterpart in the other SKU (Deadlock.LastTranStarted, Blocking's three report attributes, QuerySnapshots.TranStartTime, PlanCorrection's four stamps, QueryStats/ProcedureStats creation/cached/last-execution, RunningJobs.StartTime, and the query_store_stats family across QueryStore/QueryStoreRegressions/ViewerHistoryRows). Lite and Darling agree on frame classification everywhere I checked, and where one SKU was already correct pre-PR (e.g. Lite's One parity gap worth flagging (left as an inline comment on No other correctness, security, or T-SQL-convention issues found — this PR touches no T-SQL. The new test infrastructure (wrapper census, render-verdict tests, arithmetic tests pinned against one instant in two frames) is unusually thorough and appears self-consistent; I did not attempt to hand-verify every pinned count in 🤖 Generated with Claude Code |
|
Reviewed the diff against Correctness: Spot-checked every renderer swap ( Lite/Darling parity: The two new arithmetic test files ( Test harness: The No correctness bugs, parity drift, or security/perf issues found. (Not flagging missing-index DMV usage per review instructions — none present anyway.) |
…atics
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.
…-frames # Conflicts: # Darling/Darling.Tests/ConsumedTimestampFrameDisciplineTests.cs
|
Reviewed this against the diff ( Traced every one of the 20 changed production/test files and cross-checked the frame claims against the actual collector source rather than taking the write-up on faith:
No correctness, security, or Lite/Darling parity issues found. This is a well-scoped, thoroughly self-verified fix (the arithmetic tests and the census/wrapper set-equality tests give real teeth against regressions in either direction). |
…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.
|
Reviewed the diff against the actual collector SQL for every touched column, since the PR body's frame claims are the whole basis for the fix:
Checked every fixed site's counterpart in the other SKU (the parity requirement) and didn't find a case where one SKU was fixed and its counterpart wasn't — the four/six pairs above were either fixed in both places or were already correct in the untouched one. No T-SQL in this PR, so the T-SQL style conventions don't apply. No security, secrets, or perf concerns — this is pure desktop rendering logic, no query or I/O changes. Copyright headers present on the two new test files. Nothing to flag. This is unusually well self-audited (the collector-source cross-check above didn't turn up anything the PR's own frame census got wrong). |
…e 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.
|
Reviewed the diff in full (all 22 changed C# files + the census/arithmetic test additions). Summary: This is a display-layer-only fix (no T-SQL, no schema/storage changes), so the T-SQL style conventions and two-store schema-parity rules in CONTRIBUTING.md don't apply here. I traced every changed render site by hand against the frame each column is documented to hold (
I did not find correctness, parity, or security issues in this diff. No inline comments to post. Note CI ( |
…prose The entry was written from #3230's description before that description carried an entry-ready block, and the description's measurement section was taken at c9f04f3 -- before #3227's rebase moved Deadlock.cs LastTranStartedLocal onto FormatServerClock, which took it out of both the inventory and the string-literal subset. KnownTruncatedRanges has 30 entries; the retired entry said 31, and carried 13, 544, 510, 2,238 and 42,927 besides. The 544 and 510 predate the content-trim and cannot be recomputed from shipped code at all, and the file and declaration totals move with every commit. The lane's own block names only what shipped, so it replaces the entry wholesale rather than the numbers being patched. Its one quantity is cross-checked against the shipped array by the verification battery, so the entry cannot restate a count the code does not have.
…d pull requests, and strike an unsound claim from #3199's (#3232) * Record the CHANGELOG entries for seventeen changes across ten merged pull requests Applies the [Unreleased] entries for the pull requests merged to dev after #3213's batch pass, and strikes an unsound arithmetic claim from the #3199 entry that pass shipped. CHANGELOG.md only: 17 entries prepended inside [Unreleased] -- 5 under Added, 12 under Fixed -- plus the 15 link-reference definitions they need, and one edited line. Entry text comes from each pull request's own body where it carried one. * Cite #3224's own number on its entry, not the pull request it follows The Azure credential-recording entry led with [#3218], the pull request it is a follow-up to, which resolves to a different change. No issue exists behind #3224, so under the file's rule -- issue numbers where an issue exists, pull-request numbers where one does not -- its own number belongs in the citation position, with the follow-up relationship kept as prose. #3224 was the only one of the ten merged pull requests with no issue behind it whose number went uncited. The [#3218] definition is swapped for [#3224] rather than added, since nothing else cited it. * Absorb #3230, which merged mid-batch #3230 was open when this batch was cut and merged at 19:31:55Z while it was being verified, making it the newest merge in the range. origin/dev is merged in rather than rebased, so e459539 stays intact in the history. Its body carries no entry text and neither queue directory holds any, so the entry is written here from the description. No issue exists behind it, so it cites its own number, and its definition takes its ascending place between [#3226] and [#3231]. * Take #3230's entry from its own CHANGELOG block, not its measurement prose The entry was written from #3230's description before that description carried an entry-ready block, and the description's measurement section was taken at c9f04f3 -- before #3227's rebase moved Deadlock.cs LastTranStartedLocal onto FormatServerClock, which took it out of both the inventory and the string-literal subset. KnownTruncatedRanges has 30 entries; the retired entry said 31, and carried 13, 544, 510, 2,238 and 42,927 besides. The 544 and 510 predate the content-trim and cannot be recomputed from shipped code at all, and the file and declaration totals move with every commit. The lane's own block names only what shipped, so it replaces the entry wholesale rather than the numbers being patched. Its one quantity is cross-checked against the shipped array by the verification battery, so the entry cannot restate a count the code does not have.
The comment stated 544, 34, 13 and 510 as facts. Two of them describe a state before the content trim existed, so the shipped detector cannot reproduce them at all; the other two moved when #3227 rewrote one of the members, within an hour of the comment being written. The live figure is KnownTruncatedRanges, which a test holds to the tree, so the comment points there and keeps only the ratio that carries the argument.
* Stop the truncation doc asserting counts no test holds The comment stated 544, 34, 13 and 510 as facts. Two of them describe a state before the content trim existed, so the shipped detector cannot reproduce them at all; the other two moved when #3227 rewrote one of the members, within an hour of the comment being written. The live figure is KnownTruncatedRanges, which a test holds to the tree, so the comment points there and keeps only the ratio that carries the argument. * Drop the counts from KnownTruncatedRanges' own doc too It said 31 member bodies, the current 31, and Thirteen stranding a literal, sitting on top of an array of 30. Recomputed against the shipped code rather than decremented: 30 in the swept trees, 12 stranding a literal. Both figures are removed rather than corrected. The array is asserted at set equality, so a numeral in the prose beside it is the one claim in the file nothing can fail on, which is the defect this file exists to report. * Drop the last count, in the paragraph the argument rides on TheMemberScan_ReadsEveryDeclarationWhole's own summary said Thirteen members strand a literal today. Measured against the shipped code it is twelve, and today asserts currency, so a reader has no cue to read it as historical. The earlier sweep missed it because it was bounded by passages I authored, and this numeral sits in a paragraph I only partly edited. A sweep scoped by authorship cannot see a stale figure in a paragraph someone else wrote; re-swept by pattern over both whole files instead. * Stop quantifying what the inventory strands Most was false: twelve of thirty is a minority. Removing a numeral is not removing a claim, and a quantifier is worse in one way - nothing greps for a word, so Thirteen at least invited the suspicion that found it. The argument needs no quantity. A literal contained by nothing is labelled Unknown, which is only loud if some census looks for a site of that kind, and that holds for one entry or for all of them. Both occurrences now say the same thing and assert nothing that can rot, including the out-of-scope note that said a few. * Drop two more counts in this file that no test holds Both are the same defect as the truncation doc: a quantity in prose beside a population nothing asserts, wrong now and with no way to fail when it drifts. The where-clause count read nine; the scanned trees carry seventeen members with a constraint clause, plus a type-level one on a record. The block-comment count read six files; the corpus carries sixty with a `--` anywhere in a block comment, forty-eight on a non-asterisked continuation line -- the line a prefix filter actually misreads -- and thirty-eight once files whose only hits are CLI flags come out. That spread is the sharper point: the claim never said which corpus or which predicate, so it could not have been pinned even in principle, only removed. Both sentences keep their universal and lose only the tally, which is this file's own stated doctrine: a universal quantifier names the one counter-example that would break it, while a count only tells you to count again. Two other numerals in the file were checked and left. The tenth-bullet reference is held by the disposition map's equality assertion against CONTRIBUTING.md, so it cannot drift without reddening first. The 101 figure is explicitly dated to when it landed, scoped to a retired SKU outside the scanned trees, and labelled as evidence for a decision rather than a live figure. --------- Co-authored-by: erikdarlingdata <erikdarlingdata@users.noreply.github.com>
Fixes #3207. Fixes #3221.
Twenty-nine desktop render sites across both SKUs, wrong in opposite directions, and each SKU correct exactly where the other is wrong — which is why no parity check ever caught it. A server-local column through
ForDisplay/FormatServerTimeadds the collected offset a second time and displays four hours early on the fleet's measured −240; a naive-UTC column throughFormatServerClockrenders raw and displays four hours late. A blanket "add the offset" or "stop adding it" fixes half and breaks half, so every site here is paired against its column's frame taken from #3220's catalog-derived census, not from the column's name and not from the neighbouring grid.#3221 is an external reporter reproducing eight of these from the outside, on
Lite/Services/LocalDataService.QueryStats.cs. That is the whole file's offending set and it lands exactly where the census put it.The frames, measured on collected production data
Both directions, from the store rather than from the collectors' C#.
server_properties.utc_offset_minutesis −240 across the fleet (one self-monitored server at 0), and a stored server-clock value is thereforeutc + offset, so recovering UTC subtracts the offset:blocked_process_reports:blocked_last_tran_started−event_time, same rowquery_stats.last_execution_time−collection_timeprocedure_stats.last_execution_time−collection_timecpu_utilization_stats:max(sample_time)−collection_timequery_store_stats.last_execution_time−collection_timequery_store_stats.first_execution_time(max) −collection_timeThe first four are the columns #3207 and #3221 say are server-local; the last two are the ones both issues argue are UTC from
QueryStoreCollector's((DateTimeOffset)…).UtcDateTimeline. Nobody had measured either claim. The blocked-process row is the sharpest of the six: both values sit in the same row, one is the XE@timestampand one is an XML attribute, and a blocked process's last transaction started seconds — not four hours — before the report fired, so the −240 is frame skew with no age mixed in.The arithmetic direction is therefore checked against data, not against the sign in the source. Two of the store columns land in the tests as
−240with a same-instant fixture pair, so a sign error is eight hours out rather than plausible.Every site, its column's frame, and its renderer
Line numbers are post-fix. Census row says whether #3220's
DesktopRenderFrameMismatchinventory carried the site; the seventeen rows it did carry are deleted in this change, and set equality fails in the removing direction, so each deletion is proved by the fix that earned it.Darling viewer — naive UTC rendered RAW, four hours late
ViewerDataService.QueryStore.cs:99FirstExecutionTimeLocalfirst_execution_time→query_store_statsFormatServerClockViewerTimeHelper.ForDisplayViewerDataService.QueryStore.cs:104LastExecutionTimeLocallast_execution_time→query_store_statsFormatServerClockForDisplayViewerDataService.QueryStoreRegressions.cs:68LastExecutionTimeLocallast_execution_time→query_store_statsFormatServerClockForDisplayViewerHistoryRows.cs:237FirstExecutionTimeLocalfirst_execution_time→query_store_statsFormatServerClockForDisplayViewerHistoryRows.cs:241LastExecutionTimeLocallast_execution_time→query_store_statsFormatServerClockForDisplayViewerHistoryRows.cs:106-107and:166-167are thequery_statsandprocedure_statshistory rows in the same file, on same-named columns, and they are correct as they stand — they keepFormatServerClock. That is the whole reason the census keys its inventory on the resolved TABLE and not on the column: a fix at onelast_execution_timesite in this file and a regression at another would cancel in a bare count.Darling viewer — server-local sent through
ForDisplay, four hours earlyViewerDataService.RunningJobs.cs:141StartTimeLocalstart_time→running_jobsForDisplayFormatServerClockViewerDataService.Deadlock.cs:76LastTranStartedLocallasttranstartedForDisplayFormatServerClockViewerDataService.PlanCorrection.cs:215ValidSinceLocalvalid_since→plan_correctionLocal(…)→ForDisplayFormatServerClockViewerDataService.PlanCorrection.cs:216LastRefreshLocallast_refresh→plan_correctionLocal(…)→ForDisplayFormatServerClockViewerDataService.PlanCorrection.cs:217ExecuteActionInitiatedTimeLocalexecute_action_initiated_time→plan_correctionLocal(…)→ForDisplayFormatServerClockViewerDataService.PlanCorrection.cs:218RevertActionInitiatedTimeLocalrevert_action_initiated_time→plan_correctionLocal(…)→ForDisplayFormatServerClockLite — server-local sent through
FormatServerTime, four hours earlyLocalDataService.Blocking.cs:1002LastTranStartedLocallasttranstartedFormatServerTimeFormatServerClockLocalDataService.Blocking.cs:1058-1060Blocked{LastTranStarted,LastBatchStarted,LastBatchCompleted}Localblocked_process_reportsFormatServerTimeFormatServerClockLocalDataService.Blocking.cs:1120TranStartTimeLocalquery_snapshots.tran_start_timeFormatServerTimeFormatServerClockLocalDataService.PlanCorrection.cs:210-213plan_correction×4FormatServerTimeFormatServerClockLocalDataService.QueryStats.cs:1480, 1671CreationTimeLocalquery_stats.creation_timeFormatServerTimeFormatServerClockLocalDataService.QueryStats.cs:1602, 1725CachedTime{Formatted,Local}procedure_stats.cached_timeFormatServerTimeFormatServerClockLocalDataService.QueryStats.cs:1479, 1603, 1672, 1726LastExecutionTimeLocalprocedure_stats+query_statsFormatServerTimeFormatServerClockLocalDataService.RunningJobs.cs:164StartTimeLocalrunning_jobs.start_timeDateTime.ToLocalTime()FormatServerClockDeadlockTimeLocal(deadlocks.deadlock_time),EventTimeLocal(the XE@timestampon both blocking arms) and everyCollectionTimeLocalkeepFormatServerTime: they are naive UTC and were already right. Three of the six blocked-process XML stamps — theBlocking*half — are read into properties and never bound, so there is no render site to fix; the census lists the three that are.Lite had no server-clock renderer, so its columns had nowhere correct to go
ServerTimeHelper.FormatServerTimenames its parameterutcTimeand adds the collected offset before converting — it is Lite'sForDisplay, not itsFormatServerClock, and there was no raw renderer at all.ServerTimeHelper.FormatServerClockis that renderer. It is not a new conversion:ConvertForDisplay's documented input is already the server's clock, so the new method isFormatServerTimewith the offset add removed. Exactly one add on the naive-UTC path, none on this one.It honours the Server / Local / UTC preference, through the existing
ConvertForDisplay, and that matters rather than being a nicety. #3221's reproduction is in Local Time mode on a UTC+02 host against a UTC−05 server, and reports the row rendering at 15:00 where 20:00 is correct. A renderer that emitted the server's clock verbatim would fix Server mode and leave the reporter's own repro rendering 13:00 — a different wrong answer. Lite also prints the mode's timezone label in its status bar (ServerTab.Refresh.cs:122), so a raw render underUTCwould sit under a label saying UTC.Darling's same-named
ViewerDataService.FormatServerClocknow honours the mode too, so the frame fix costs no preference. It did not at first: it emitted the server's own clock in all three modes, so movingrunning_jobs.start_timeonto it fixed the frame and dropped the mode, at a site that had respected the user's choice. The review bot caught that, correctly.It composes out of
ViewerTimeHelper.ConvertToDisplay's existing arms rather than adding any. The naive-UTC twin of a server-clock value isserverLocal - offset, soConvertServerClockToDisplayis one subtraction in front of the conversion already there, and every arm — the machine-local one especially — is reused rather than re-derived. That was checked 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.14:00— it is the server's clock14:0018:0014:00, labelled UTC20:0014:00The pair is now symmetric across the SKUs and the doc comments say so: Darling's
ConvertToDisplaytakes naive UTC so its server-clock conversion subtracts the offset first; Lite'sConvertForDisplaytakes the server's clock so its naive-UTC renderer adds it. One offset step between the frames, either way round.Both modes are pinned, which the old renderer could not support: with no mode input it was untestable on this axis, and that is how a raw render shipped unnoticed.
ViewerTimeHelperTests.FormatServerClock_HonoursTheDisplayMode_AtTheFleetOffsetasserts the same server-local value rendering14:00under Server and17:45-shaped under UTC at the fleet's measured −240, andViewerClockRendererArithmeticTestsasserts the composition and the non-interchangeability of the two conversions — the latter runs locally, against the shippedViewerTimeHelper.cs.Seven doc comments that said "renders raw" now say what the code does.
Lite is untouched.
ServerTimeHelperalready honours the mode. Its two job-history getters (RunTimeLocal,NextScheduledRunLocal) showrun_datetimeandnext_scheduled_runas-is and are documented as deliberately showing "the time SSMS shows"; whether that should change is a product question and stays one, in the queue.The comments that made this spread
Four, all corrected with the code, because a comment stating the wrong frame is the mechanism by which one wrong site became several:
ViewerDataService.QueryStoreRegressions.cs:29stated the frame outright — "the SQL server's local wall clock in Darling's store, shown RAW … exactly like the sibling Query Store tab" — and appealed to a sibling tab as precedent. The renderer follows the column, never the neighbouring grid; that sentence is what turned one site into three.ViewerHistoryRows.cs:19-22lumped Query Store in with the DMVs: "the DMV / Query Store wall-clock stamps … are the SQL server's own local time". True ofquery_stats/procedure_stats— whose rows sit in the same file and are correct — and false ofquery_store_stats. The header now says the three row types do not agree and that the store table decides it.ViewerDataService.ItemHistory.cs:33listedfirst_execution_timein the same breath as the DMV stamps. Same error, different file.ViewerDataService.QuerySnapshots.cs:84-85justified the correct Darling site by parity: "Mirrors Lite'sQuerySnapshotRow.TranStartTimeLocal(raw server-clockFormatServerTime)".FormatServerTimeis not raw. The Darling site was right, the Lite site it claimed to mirror was double-skewed, and the sentence asserting they agreed is what kept the divergence invisible.Two class summaries in
ViewerDataService.FinOps.cs(:337,:545) claimedLastExecutionTime localized in read/LastUserAccess localized in read. Both reads take the value verbatim and say so, with the reason and the contrast against thecollection_time-derived timestamps in the same port — so the summary asserted a conversion the code deliberately does not perform, on a server-local column, which is the same mechanism as the four above. Both now defer to their read. The coordinator found one; a grep for the phrase finds two, and the second is wrong for the same reason.Three raw-render getters in
Lite/Services/LocalDataService.JobHistory.csshowrun_datetimeandnext_scheduled_runas-is, and only one of the three said so.RunTimeLocal:235carried the reasoning ("the time SSMS shows");LastSuccessfulRunLocal:240—MAX(run_datetime)over the successful step-0 rows, the same column and the same frame — had nothing at either property or class level, andNextScheduledRunLocal:322had it in the class summary only. Both now state it at the property, where a reader checks. No behaviour changes: whether Lite should honour the display preference on that grid at all is a product question (Lite is single-server and mirroring SSMS is a coherent choice; Darling is fleet-wide and normalises, which is why it de-skews the same two columns in SQL and says so three times), and it is not settled here.ViewerDataService.RunningJobs.cs's class doc claimedstart_timewas "the store's naive-UTC start_time"; the collector proves otherwise on the next line of its own query. It now states both frames in the row and what happens if they are crossed.The census, and the four sites it could not see
#3220's
DesktopRenderFrameMismatchlabel carried 17 rows / 22 sites, pinned at set equality in both directions. All 17 are deleted here, and the ratchet is what makes the deletions evidence rather than assertion: reverting any one fix while leaving its row behind fails, and reverting any one row while leaving the fix behind fails. The label is now empty, which is a result and not an omission — the scan still fails on a new offender, because a non-empty found set against no rows is a failure.DesktopRenderMismatchSitesgoes 22 → 0, andAssertMatchesInventory'sAssert.NotEmpty(actual)had to go with it: it stood in for "the scan actually looked", and an assertion that blocks the fix it exists to prove is worse than none. Reach is asserted per scan instead. The MCP arm keeps an explicitAssert.NotEmpty(found)because it still has 33 offenders (#3206's, via #3212), with a note saying it goes when that lane empties its label. The render arm's floor on sites judged is now the exact measured baseline of 43 rather than a loose 30, because with no offenders left it is the only thing standing between a crippled matcher and a green census. Calibrated red-first: at 44 with nothing planted the suite fails and reportsonly 43 render sites were judged.The four
plan_correctionsites in the Darling viewer are not in the census, and could not have been.ValidSinceLocaland its three siblings reachedForDisplaythroughPlanCorrectionRow.Local(…)— a one-hop wrapper — and the render scan keys on the renderer's name. A wrapper is a different name reaching the same renderer, so the sites were invisible to a census that calls itself closed. They are real: the register classifies all four columns server-local fromPlanCorrectionCollector's own SQL,sys.dm_db_tuning_recommendationsdocuments them in the instance's clock, and #3212 is de-skewing the same four on the MCP side.Two changes close that hole rather than fixing the four instances:
judged38 → 43, with Lite's running-job start time the fifth addition).RenderWrappersdeclares the six one-hop wrappers in the two UI trees with the renderer each reaches, pinned at set equality against a derived set, so a new or renamed wrapper fails the build and has to declare its frame.NoRenderWrapper_IsHandedAColumnEveryTableFramesTheOtherWaythen judges each wrapper's call sites over census columns — per (file, method), because two different wrappers in the Darling viewer are both namedLocalover different frames and a name-keyed scan would judge each one's sites against the other's renderer.That check's bound is deliberate and stated: unanimous disagreement only. Where a column name spans both frames —
event_timeis five tables and two of them — a wrapper site cannot be judged from the name, and the direct-call scan's resolution machinery does not transfer, because a wrapper is not the site of the read and the file it sits in is not the file whose SQL decides the table. Sixteenevent_timesites through the twoSystemEventswrappers are therefore out of reach rather than cleared, and the count of sites examined is floored (16) with the number of wrappers reaching a census column pinned exactly (2) — because with no offenders left, "examined nothing" and "found nothing" are the same result.Writing that check's both-directions pin found a bug in it.
WrapperSignaturematches a doc comment quoting a declaration, and this guard's own file is full of prose about these very methods, so a note would have been derived as a wrapper and failed set equality against the real ones. The derivation strips comments first, and the pin asserts the raw pattern does match the quoted form so the strip is visibly load-bearing rather than tidy. No such comment exists today; a planted one turns it red.The renderer map's completeness, pinned two ways
FormatStoredUtcis ported from #3228 with its doc comment, and it is the right change for a reason different from the one it was asked for. It is the Darling viewer's named UTC renderer, besideFormatServerClock, so the choice between the pair is reviewable at every site rather than spelled out in a ternary — and picking the wrong one of the pair is silent, since both return a plausible timestamp and they differ by the server's whole offset. The fivequery_store_statssites take it.The stated reason for the port was that the inline ternaries left those sites using no registered renderer, so the scan could not see them. That is not the case, and it is measurable.
RenderCallallows an optional receiver and a trailing member access, soViewerTimeHelper.ForDisplay(FirstExecutionTime.Value)matches\bForDisplay\s*\(\s*(?:[A-Za-z_]\w*\.)?FirstExecutionTime\b. Renaming ONE of the five ternaries' renderer to an unregistered name dropsjudgedfrom 43 to 42 and reds the floor — so all five were judged before the port, and they are judged after it. RegisteringFormatStoredUtcis what keeps that true, and the sites were never invisible.The map's completeness had no criterion, which is the class the guard exists to catch. Two checks now cover it from different directions:
TheOneHopRenderWrappers_AreExactlyTheDeclaredSetderives every ALIAS — a static formatter over aDateTimereaching a registered renderer under a different name — and pins the set at equality againstRenderWrappers. Six today. A new one fails the build and has to declare the frame it renders.NoUnregisteredRenderCapableMember_IsAppliedToACensusColumnasks nothing about signatures. Every identifier applied to a census timestamp column across the render surface is derived — 72 applications, eight distinct names — and each must be a registered renderer, a declared alias, or declared as not rendering with what it actually is (if (…HasValue),new DateTime(…Year, …Hour, 0, 0),Nullable.Compare). Three further directions are asserted: a registered renderer that reaches no census column is a map typo, a stale exemption that no longer appears is an assertion about nothing, and every exemption carries its reasoning.The two do not subsume each other, and the mutation matrix shows it: an unregistered instance method handed a census column is invisible to the shape criterion and caught by the second check alone.
The walker rewrite that was backed out, and the defect it found
The completeness derivation was first written on
CSharpMemberMap, the repository's own C# member walker, because a regex over a fixed byte window after a signature is exactly what that walker exists to replace. It found 3 of the 6 aliases and reported nothing wrong.CSharpMemberMap's brace walk closes an expression-bodied member on the{of anis { }property pattern, soLocal,LocalandTimestamp— whose bodies all beginutc is { } t ? …— were read as 68, 74 and 80 characters and did not contain their ownForDisplaycall.ShapeOfreturnedWholeMemberfor all three.ViewerHistoryRows.CollectionLocal, identical in every other respect but with nois { }, read whole.The only reason the shortfall was visible is that the regex derivation had already answered the same question, so there was something to disagree with; written walker-first, it would have pinned a hand list of three and called the map complete. The walker version is backed out, the regex derivation that finds 6 of 6 is kept, and the
CSharpMemberMapdefect is staged for whoever owns that file rather than fixed from here — it is shared infrastructure with its own pins.The seven sites with no census row, and how each is covered
The census is derived from
CollectorCatalog.All's timestamp columns, so a site it cannot reach needs saying rather than leaving as an unexplained count. Twenty-two of the twenty-nine have rows. The other seven are three distinct kinds:ViewerDataService.Deadlock.cs:76andLocalDataService.Blocking.cs:1002.lasttranstartedis walked out of the storeddeadlock_graph_xmlbyDeadlockGraphProcessParserat READ time, so noCollectorColumndeclares it and no derivation from the catalog can see it. Covered by a doc comment in each SKU stating both frames in the row and why the census cannot reach the pair; a guard for read-time XML provenance is a different shape and Guard a server-local column reaching a UTC-assuming consumer, with the census (#3208) #3220 already says so.ViewerDataService.PlanCorrection.cs:215-218. The columns are in the register; the render SITE was invisible. Now covered twice: the sites name their renderer so the existing scan judges them, andRenderWrapperspins the wrapper set so the next one cannot hide.LocalDataService.RunningJobs.cs:164wasStartTime.ToLocalTime().running_jobs.start_timeis in the register, butDateTime.ToLocalTime()is not one of the three renderers the scan keys on, so the site was outside the judged population. It carried two defects:ToLocalTime()on aKind=Unspecifiedvalue treats it as UTC when it is msdb Agent's own clock, and it rendered in the Lite host machine's zone regardless of the display preference every other timestamp in the app honours. NowFormatServerClock, which fixes both, and the site enters the judged population — which is why the floor moved to 43 rather than 42.Verified
Two
net10.0xunit.v3 harnesses over the shipped sources, since both suites targetnet10.0-windowsand cannot run on macOS.framecheck—ConsumedTimestampFrameDisciplineTestsplusStoreSqlClockDisciplineTests,CollectorTimestampFrameTests,DocCommentHygieneTests,CommentFilterAdoptionTestsandTsqlConventionGuardTests, compiled withAssemblyName=Darling.Tests. Baseline before this changeTotal: 113, Failed: 0, Skipped: 0, Not Run: 0— byte-identical to Guard a server-local column reaching a UTC-assuming consumer, with the census (#3208) #3220's recorded baseline. After:Total: 116, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0(three new guards), andTotal: 117, Failed: 0, Skipped: 0, Not Run: 0after mergingorigin/devin, which brought Weld concatenated literals before scanning store SQL, and make the PostgreSQL parse-check population what the reader files declare #3223's additions toStoreSqlClockDisciplineTests— the sibling guard this one cross-checks its ambiguous-name register against — and left both green.clockcheck— the two new arithmetic test files compiled against the SHIPPEDServerTimeHelper.cs,ViewerTimeHelper.csandTimeDisplayMode.cs, linked rather than referenced (neither production file touches WPF, so the real arithmetic runs; a retyped copy would prove the transcription). BaselineTotal: 21, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0.The two suites cover different halves and neither covers the other's: the census is a SOURCE scan and cannot see inside a renderer's body, while the arithmetic tests know nothing about which site calls which renderer. Both mutation classes below confirm that split rather than assuming it.
The row-getter wiring is not separately tested, deliberately: "this getter calls the wrong renderer" is exactly what the census scan judges, at set equality, over all 43 sites. A per-getter test would be a second, weaker copy of that.
Everything below the frame boundary is CI's:
Lite.Tests(which is whereServerClockRendererArithmeticTestsships) andDarling.Testsrun on Windows inbuild.Red-proof by mutation
Baseline printed above. Forty mutations, each committed first, applied by byte replacement with a uniqueness-checked anchor, verified present by content and not only by
git diff --numstat(several are one-line-for-one-line and report an identical1 1), rebuilt, run, restored, and verified restored by content with the tree confirmed clean after every row.The fixes, one direction at a time
ForDisplay(4h early)creation_timeback throughFormatServerTime(#3221's own column)last_tran_startedback throughFormatServerTimeplan_correction.valid_sinceback throughFormatServerTimeDateTime.ToLocalTime()plan_correction.valid_sinceback behind theLocal()wrapperBoth directions of the defect are represented, in both SKUs, and the last row is the shape that was invisible before this change.
The renderers' arithmetic — which the census structurally cannot see
FormatServerClockadds the offset again (the original defect, inside the new renderer)FormatServerClocksubtracts the offset (sign error, eight hours out)FormatServerClockrenders raw, ignoring the display mode (Darling's semantics)FormatServerTimestops adding the offset (the naive-UTC path, broken the other way)ViewerTimeHelper's Server-mode conversion subtracts the offset (sign error)Six rows where
framecheckstays green andclockcheckreds. That is the split stated as a measurement: a source scan over renderer names cannot see a renderer's body, and this is what would have shipped a correctly-paired call to a wrong renderer. The third row is the one that matters for #3221 — a raw renderer passes Server mode and fails Local and UTC, which is exactly the half-fix its reproduction would have caught.The last row mutates the instrument: with the two fixtures no longer one instant, six assertions move, so the same-instant premise is falsifiable rather than decorative.
The instrument
Litefrom the render root listFormatServerClockfrom the renderer tableRenderCall==for!=)WrapperSignatureaccepts onlyprivatemembersonly 43 render sites were judgedThe comment-strip pair is one argument. Removing the strip changes no result today, which is precisely why it needed a mutation that fails without it: planting a doc comment that quotes a declaration turns it red. A bound that only holds for the comments currently in the tree is one note away from not holding, and this guard's own file is full of prose about these very methods.
The three floor rows are one argument. A negative control that survives being crippled was never a control, so the floor was calibrated red first: at 44 with nothing planted the suite fails, which fixes the baseline at 43. Planting a correctly framed site then makes 44 pass — proving the new site entered the judged population — while the offender set stays empty. Green there is evidence the scan discriminates, not evidence it did not look.
The failure this PR's first CI run reported, and what it was right about
buildandDarling PostgreSQL testsboth failed on headd6da0314e, on the same single test:LiveCleanupConversionRatchetTests.NoLiveTestCleansUpOnItsOwnBodysConnection, namingViewerQueriesTests.cs:417. The mode-blindness pin had been written intoViewerQueriesTests.cswith atry/finallyrestoring the two process-wideViewerTimeHelperstatics. That file also holds a[Collection("live-postgres")]class, so the whole file is in that ratchet's population, and afinallyin it must route throughLiveStoreCleanup.The ratchet was right about the file for a reason beyond the one it reports:
ViewerQueriesTestsis in no collection, and flippingCurrentDisplayModefrom there races the three classes that ARE in[Collection("viewer-time-statics")]. The pin now lives inViewerTimeHelperTests, which owns those statics and is in that collection, and it gained 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.Reproduced and red-proofed locally by adding the ratchet class to
framecheck(baselineTotal: 132, Failed: 0, Skipped: 0, Not Run: 0): putting a non-storefinallyback into that file turnsNoLiveTestCleansUpOnItsOwnBodysConnectionred, and removing it turns it green. The ratchet had never been in this lane's harness, which is why the shape reached CI.Two pins this lane could not exercise locally
Both are the Darling mode-blindness pin, and both are green locally for the same reason:
ViewerTimeHelperTestsreachesViewerDataService, hence the WPF Viewer assembly, so neithernet10.0harness can compile it.FormatServerClockhonour the display mode — closing the gap the note declines — went GREEN on both harnesses.Neither is a hole in the guard; both are the limit of what a
net10.0harness can compile. CI'sbuildis the arbiter forFormatServerClock_IgnoresTheDisplayMode_AtTheFleetOffset, and that is stated rather than reported as covered.Corrections to #3207, #3221 and #3220
#3207 numbers 9 sites; the census resolves them to 22 plus 7 it cannot reach, and the two counts are not comparable. Several of #3207's entries name multiple lines (its sites 3, 5 and 7 are two, two and three render sites), and its site 9 is a third mechanism rather than a mis-paired renderer.
#3207's site 1 cites
ViewerDataService.RunningJobs.cs:117. TheForDisplay(StartTime)call was at:135; #3220 already noted this.#3207's fix note says sites 3, 4 and 5 need
ForDisplayand that Lite's 6, 7, 8 need a raw renderer "which Lite does not have". Correct on the first count. On the second, "raw" is the wrong shape for Lite: a renderer that emits the server's clock verbatim leaves #3221's own reproduction rendering 13:00 where 20:00 is correct, because that repro is in Local Time mode. Lite'sConvertForDisplayalready takes the server's clock and honours the mode, so the renderer Lite needed wasFormatServerTimeminus the offset add, not a raw one. #3207's parenthetical "(render raw, no offset add)" reads as those being the same thing; they differ in two of three display modes.#3221 lists eight affected columns and gets all eight right, including the four
last_execution_timeones, and its row-type attribution (QueryStatsRow,ProcedureStatsRow,QueryStatsHistoryRow,ProcedureStatsHistoryRow) matches the file exactly. It is also right thatcollection_timein the same rows is genuinely UTC and correctly formatted, which is what makes each history row visibly mixed-frame. Nothing in it needed correcting.#3221's "Confirmed in v3.6.0 source and current
dev" holds for the frame defect. Its note that #2987's shared-offset custom-range fix does not change the per-column source-frame mismatch is also right, and the two are independent: #2987 moved which server's offset a window used, this moves whether a column gets the offset at all.#3220's body says the census found "Eight render sites #3207 does not list". Twelve of its rows are outside #3207's list, not eight: the four
plan_correctionstamps and thecreation_time/cached_timepairs are annotated as such, and the fourLocalDataService.QueryStats.cslast_execution_timesites are named in the same section without being counted into the eight. #3220 states the fact and undercounts it in the same paragraph.#3220 says two of #3207's sites sit outside the census. Seven sites sit outside it, of three kinds — the two read-time XML ones it names, the four wrapper-hidden
plan_correctionones it could not see, and Lite's running-job start time, whose column IS in the register but whose renderer wasDateTime.ToLocalTime(). The last two kinds are both matcher reach, not catalog reach, which is why they are closed here rather than declared.CHANGELOG entry text
Here rather than in
CHANGELOG.md: every concurrent lane appends to the same[Unreleased]block, so a per-PR edit turns one clean append into as many conflicting ones as there are lanes landing after it.Fixed
sys.dm_exec_*creation / cache / last-execution stamps,plan_correction's recommendation and action stamps, the blocked-process report's transaction and batch attributes,query_snapshots.tran_start_time, msdb Agent's job start time and the deadlock graph'slasttranstarted— was sent through the renderer that adds the collected UTC offset, displaying it four hours early on a fleet at UTC-4; and Query Store's first- and last-execution stamps, which the collector normalises to UTC, were rendered raw and displayed four hours late. Each of twenty-nine render sites now takes the renderer matching its column's frame, and Lite gainsServerTimeHelper.FormatServerClockfor the frame it had no renderer for, so a server-clock value converts exactly once and still honours the Server / Local / UTC display preference (Desktop clock frames are wrong in opposite directions in both SKUs, and each SKU is right where the other is wrong #3207, [BUG] Lite query and procedure grids render server-local DMV timestamps through the UTC formatter #3221).Added
plan_correctionstamps in the Darling viewer were in the wrong frame and invisible to it, because the render scan keys on the renderer's name and a wrapper is a different name reaching the same renderer. The wrapper set is derived and pinned at set equality, so a new one must declare the frame it renders, and no wrapper may be handed a column every table declaring it frames the other way (Desktop clock frames are wrong in opposite directions in both SKUs, and each SKU is right where the other is wrong #3207).