Repository navigation
The Darling viewer's trend charts and calendar tell the same truth the MCP tools learned today: routed by retention tier and saying so, no fabricated first point, and a purged day is grey, not green (#3653 viewer ports) - #3666
Merged
Conversation
erikdarlingdata
enabled auto-merge (squash)
September 18, 2026 23:47
…e MCP tools learned today: routed by retention tier and saying so, no fabricated first point, and a purged day is grey, not green (#3653 viewer ports) Performance Trends: the query-duration, procedure-duration and execution-count charts route through DurationTrendRouting (new, Storage) — the #3590 ladder (age → rollup availability → materialized coverage) onto the hourly rollups — and each chart titles itself with the tier it served from, the first served point and a truncation note when the tier did not hold the window's head. The hourly SQL is one builder both apps run; DarlingTrendReader's constants are pinned equal to it. The ELSE 0 comes out of the viewer's four raw trend texts and every reader skips the unrated first point. Performance Calendar: DailySummaryHorizon (new, Storage) computes the retention horizon the MCP health reader computes — the retention constants and the fleet retention rule move to Storage and the service aliases them — and the viewer's row is judged by DailySummaryRetention.StateFor, folds purged / past-horizon into HasData = false (grey), and its tooltip says why through a state-aware DailyHealthBandCalculator.Describe overload (Lite's row uses it too). PVS top-5 trend: an unmeasured (NULL) size is no longer plotted as 0 MB on either SKU; Lite's shared reader carries it as null for get_pvs_trend.
…asserted the fabricated 0 point now assert its absence; the PVS Lite pin is scoped to the trend read; the shared-resolver count is the two real sites; one merged summary block on Lite's PvsTrendPoint
… horizon the range read already computed, one fleet-override read instead of two
…laration, which the previous slice swallowed
erikdarlingdata
force-pushed
the
fix/3653-viewer-ports
branch
from
September 18, 2026 23:56
fad8503 to
7859d25
Compare
erikdarlingdata
disabled auto-merge
September 19, 2026 00:26
Closed
39 tasks done
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
…spliced once Fifty-nine PRs merged to dev today across the coordinator's lanes and the wave-2 worker's; each lane returned its entry to a buffer instead of touching this file, so that fifty-plus PRs did not each rebase the same twenty lines. This is the one splice. Every entry is one line (the archiver's compact() and the pins read them that way); riders fold into their parent's entry (#3599 under #3590, #3619 under #3611, #3623 under #3616, #3640 under #3633; #3617 test-only and #3661 re-cut as #3666 carry none); #3657's entry is in because it MERGED to dev - the twin to main is what is still pending. [Unreleased] gains a `### Added` above `### Fixed` (Keep-a-Changelog order) for the six new capabilities: per-user theme colours (#3606 / #3577 arm B), routed alert families (#3668 / #3598), the PostgreSQL logging audit tool (#3643 / #3607), the service-side wait sampler (#3645 / #3604), and the log-event classifier with its temp-file / autovacuum parser families (#3646 / #3601, #3664 / #3602 #3603). The other forty-eight are honesty fixes to existing surfaces and append to `### Fixed` after the wave-1 bullets, in PR-number order. Thirty-six reference definitions added for the issues the new entries cite and the index did not yet define; the [Unreleased] group is one ascending run again, which moves [#3557] into its slot (the one deleted line). Nothing under ## [3.8.0] or older is touched; the archive script was not run. One editorial touch: the #3585 entry ended in a dangling "Darling" and now reads "Darling only." (the tool exists only in the Darling MCP host). tools/changelog/changelog_archive.py verify: all PASS (1420 bold entries, floor 1,329; 1,358 distinct refs resolve; CRLF throughout; 356,539 bytes under the 750 KiB ceiling). ChangelogIndexAndArchiveTests: 5/5 pass via a net10.0 harness.
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
…spliced once (#3672) Fifty-nine PRs merged to dev today across the coordinator's lanes and the wave-2 worker's; each lane returned its entry to a buffer instead of touching this file, so that fifty-plus PRs did not each rebase the same twenty lines. This is the one splice. Every entry is one line (the archiver's compact() and the pins read them that way); riders fold into their parent's entry (#3599 under #3590, #3619 under #3611, #3623 under #3616, #3640 under #3633; #3617 test-only and #3661 re-cut as #3666 carry none); #3657's entry is in because it MERGED to dev - the twin to main is what is still pending. [Unreleased] gains a `### Added` above `### Fixed` (Keep-a-Changelog order) for the six new capabilities: per-user theme colours (#3606 / #3577 arm B), routed alert families (#3668 / #3598), the PostgreSQL logging audit tool (#3643 / #3607), the service-side wait sampler (#3645 / #3604), and the log-event classifier with its temp-file / autovacuum parser families (#3646 / #3601, #3664 / #3602 #3603). The other forty-eight are honesty fixes to existing surfaces and append to `### Fixed` after the wave-1 bullets, in PR-number order. Thirty-six reference definitions added for the issues the new entries cite and the index did not yet define; the [Unreleased] group is one ascending run again, which moves [#3557] into its slot (the one deleted line). Nothing under ## [3.8.0] or older is touched; the archive script was not run. One editorial touch: the #3585 entry ended in a dangling "Darling" and now reads "Darling only." (the tool exists only in the Darling MCP host). tools/changelog/changelog_archive.py verify: all PASS (1420 bold entries, floor 1,329; 1,358 distinct refs resolve; CRLF throughout; 356,539 bytes under the 750 KiB ceiling). ChangelogIndexAndArchiveTests: 5/5 pass via a net10.0 harness.
This was referenced Sep 19, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
…lingPvsReader read persistent_version_store_size_mb as a bare double with IsDBNull ? 0, so a collection on which the DMV reported no size for a database became a real zero in the top-5 series — a cliff in a series that had none. The point is now null with a pvs_measured flag beside it, the same two keys Lite's twin has carried since #3666, and a measured 0 MB still travels as 0 (#3653) (#3682)
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
…o the Storage definitions #3666 moved them to — a pin that fails only after two copies have drifted; they are now aliases, so there is one definition and nothing to drift. And the Query Store duration chart, the one Performance Trends chart #3666 left untitled, now says what served it and where the corrected rollup's materialized floor cut the window's head (#3653) (#3684) Half 1 — DarlingTrendReader → DurationTrendRouting, zero behaviour. HourlyBucketSecondsSql is a const alias; QueryDurationTrendHourlySql / ProcedureDurationTrendHourlySql are static readonly bound to the Storage builder with withDatabaseFilter:false (a const cannot be initialized from a call; no consumer needed const-ness); RawTierMargin / TruncationSlack alias the Storage fields; ShouldUseRawTier / ResolveTier / DescribeCoverage delegate; the two "raw"/"hourly" ternaries go through SourceWord. Proven byte-identical by a reflection harness over the built dll before and after (both SQL texts, both TimeSpans, 1,680 ResolveTier cells, 168 ShouldUseRawTier, 840 DescribeCoverage tuples, both Source words — the only diff is the declaration kind). The #3666 equality pins that had become a function compared with itself (the resolver census, the coverage loop) are retired; McpTrendReaderMembers_AreAliasesOfTheStorageDefinitions reads the declarations and fails the moment any is restated. Half 2 — the Query Store chart discloses its floor. GetQueryStoreDurationTrendAsync already resolved RollupFloorUtc for the #2736 seam and threw it away; it now returns QueryStoreTrendSeries (points + route + effective start + unserved-before). QueryStoreTrendRouting gains SourceWord (rollup+raw / raw — the payload's words) and UnservedBefore (the floor when it sits above the requested start; null on raw-only by construction, since raw is complete wherever the rollup has not armed its purge), and DarlingMcpTrendTools reads both instead of its literals, so the tool and the chart cannot disagree. The title goes through the same composer as the siblings' — same frame, this route's own truth in the slots: the grain seam in the parenthetical, and a "data begins" clause only when the head was measurably unserved, with the honest reason (not materialized; --backfill-rollups reaches it) rather than the siblings' "no longer holds".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Three fixes landed today on the Darling MCP surface — tier-routed duration trends (#3590), no fabricated first point on a differenced series (#3642), and the calendar's retention states (#3641) — and the WPF viewer that reads the same store did not get any of them. This PR is their viewer ports, the three items #3653 lists under "Viewer": partial for #3653; the issue stays open.
Performance Trends read raw only, so a 7-day chart on a TimescaleDB store plotted 4 days under an axis that said 7
ViewerDataService.GetQueryDurationTrendAsync/GetProcedureDurationTrendAsync(and the execution-count read beside them) went toquery_stats/procedure_statsand nowhere else. The raw tier of a rolled table is dropped atRawRetentionSpan(four days) regardless of the collector's advertised retention, so the chart silently plotted what had not aged out, with nothing on it marking the difference — exactly the shape #3590 removed from the MCP trio.The mechanism. The routing and the hourly-tier SQL now live in Storage, in a new
DurationTrendRouting— the same homeQueryStoreTrendRouting(#2736) took for the same reason: the viewer does not reference the service assembly, so a decision that must be identical in both apps cannot live inDarlingTrendReaderalone. It holds get_query_trend's two-tier ladder (ShouldUseRawTier/ResolveTier: age → rollup availability (#1664) → materialized coverage (#1759)),RawTierMargin,TruncationSlack,DescribeCoverage, theSourceWordvocabulary, andBuildHourlyTrendSql(view, withDatabaseFilter). WithwithDatabaseFilter: falsethe builder's output isDarlingTrendReader.QueryDurationTrendHourlySql/ProcedureDurationTrendHourlySqlbyte for byte; withtrueit is the viewer's copy, plus exactly one line — the #1319$4::text[]database filter (both hourly rollups group bydatabase_name, so the filter survives the routing).The viewer's three reads route through it and return a
QueryTrendSeries(points + tier +EffectiveStartUtc+Truncated) — the MCPDurationTrendResultshape. The execution-count chart routes with its duration sibling and, on the hourly tier, reads the SAME statement'sexecutions_per_secondcolumn (an ordinal parameter on the one shared read loop) rather than a fourth SQL text. The Query Store trend is not on this ladder: its routing is a materialization watermark, not a tier choice (#2736), and it stays as it was.The chart says what it served. Each routed chart carries a title in the heatmap's title idiom:
Source: raw (one point per collection)orSource: hourly rollup (one point per hour), and — only when the tier did not hold the window's head —— data begins <first served point>; the store no longer holds the rest of this window at this tier. The tier is spelled with the payload's own word (raw/hourly), so a user reading the chart and an agent readingget_query_duration_trendare told the same thing in the same vocabulary.ClearChartresets the plot, so a stale truncation note cannot survive a reload that came back empty. This is #1661's disclosure posture (the FinOps expensive-queries panel's text-horizon note) applied to the chart that most needed it.Why not
DarlingTrendReadermadeinternal/public? The viewer cannot reference the Service project (it never has: #1661, #2530). Why notRetentionTierRouter? That is the built-in tabs' three-tier ladder with a one-DAY margin; the trends' widest window (seven days) never reaches the daily tier, and widening the raw margin to a day would push every 3–4-day chart onto the rollup for nothing (#2353's one-hour trade, stated there). Why not editDarlingTrendReader.csto alias the Storage members? That file belongs to other lanes tonight; its constants are pinned EQUAL to the Storage definitions (ViewerTrendRoutingPortTests) so they cannot drift, and turning them into one-line aliases is a follow-up for whichever lane next opens the file.The viewer's trend SQL fabricated a 0 for the first differenced point
QueryDurationTrendSql,QueryStoreDurationTrendSqlandExecutionCountTrendSqlstill carriedCASE … ELSE 0 END: the first collection in the window has no previous collection to difference against, its rate is unknowable, and it was plotted as a measured quiet instant that dragged every series' opening toward zero. TheELSE 0is gone from all three (the procedure copy went in #3630; the MCP copies in #3642). The rate is NULL and every reader in the file goes through one loop that SKIPS the unrated row — not plotted as 0, not interpolated across (the heatmap's NaN-break posture: an unknowable point is not a point). The execution-count reader, which had its own loop coercing NULL to 0, now shares that loop.The Performance Calendar banded purged days Healthy
The aggregate's day spine is a UNION over nine sources aging out at different horizons: the seven signal tables at 30 days, the collection log at 60, the alert log at 90. For every day between, the spine still names the day while every count the band reads has been purged and COALESCEd to zero — and measured zeros band Healthy. #3641 taught the MCP reader to judge each day (
DailySummaryRetention.StateFor: collected / purged / past_horizon / no_run_record) against the store's retention horizon. The viewer read the sameDailySummarySql.RangeSql—signal_sources_presentand all — and never judged, because the horizon's computation lived inDarlingHealthReaderwithStoreConfigProvider.ResolveFleetRetentionDaysandDarlingRetention's constants, none of which the viewer can see.The mechanism. A new Storage
DailySummaryHorizonholds the signal-collector list, the fleet-override SQL, the fleet read (ReadFleetRetentionOverrideDaysAsync) andShortestSignalRetentionDays(aFunc<string,int>resolver form the service feeds its purge resolver into, and a dictionary form the viewer feeds the read into). The retention facts it needs —DataRetentionBaseDays,CollectionLogRetentionDays,AlertHistoryRetentionDays,BaselineServingRawCollectors— move to Storage asDarlingRetentionHorizons, andDarlingRetentionkeeps its names as aliases of them. The fleet-retention RULE (valid override ≥ 1 else the collector default) isDarlingRetentionHorizons.ResolveFleetRetentionDays;StoreConfigProvider.ResolveFleetRetentionDaysnow locates the fleet row and delegates to it, behaviour-identical (one fleet row per collector exists, so first-match is the match).BaselineMath.BaselineWindowDaysis passed in by both callers because the Analysis assembly is one Storage does not reference.DarlingHealthReaderkeeps its members as aliases/wrappers, so the tool and its tests read as before.The viewer's
GetDailySummaryRangeAsyncreads the horizon once per range (like the banding tiers, so a schedule save mid-read cannot judge half a month against one horizon and half against another), stamps each row'sDataState/RetentionHorizon/SignalSourcesPresent, andToSignals()folds purged / past-horizon intoHasData = false— the MCP row's fold and Lite's row's fold, verbatim. A purged cell is grey. The absent-day row (GetDailySummaryAsync) is judged too, the way the MCP single-day tool judges its absent day.The tooltip says why.
DailyHealthBandCalculator.Describe(Common) gains a state-aware overload: a grey cell used to say only "No data collected.", which for a purged day is false — the day WAS collected; retention took it. Purged: "No verdict: this day is before the store's retention horizon (2026-08-20) and its signal rows have been purged. The zeros are absences, not measurements." Past-horizon names how many of the 7 signal sources still hold rows. No-run-record inside retention keeps the signal lines and appends the disclosure. Collected is the plain overload verbatim. Lite'sDailySummaryRow.SignalsTooltipcalls the same overload (its row already carried the three members), so both SKUs' calendars say the same thing about a purged day — the twin port the boundary grants.And the PVS top-5 trend's NULL → 0
PvsTrendPoint.PvsSizeMbcoerced a NULLpersistent_version_store_size_mb(an unmeasured pass) to 0 MB on both SKUs — a cliff drawn into a series that has none. The Darling viewer's reader, which has no payload consumer, skips the row. Lite's reader is shared withget_pvs_trend, so itsPvsTrendPoint.PvsSizeMbbecomesdouble?, the payload publishes null with the samepvs_measuredflag its latest-snapshot rows carry (contract rule 5: a collection happened; its size is unknown), and the FinOps chart leaves the point out.DarlingPvsReader(the Darling MCP payload) is NOT touched here — see the issue note.Blast radius
StoreConfigProvider.ResolveFleetRetentionDaysis the purge's resolver; the delegation is behaviour-identical for every input shape (fleet row / per-server row / invalid row / frequency-only row / no row), pinned inViewerCalendarRetentionPortTests.FleetRetentionRule_IsOneDefinition_ThePurgeDelegatesToIt, andStoreConfigProviderTests/DarlingRetentionTests/BaselineSupplyTestshold unchanged.Get{Query,Procedure,ExecutionCount}TrendAsyncreturnQueryTrendSeriesinstead ofList<QueryTrendPoint>; the two live tests that called them are updated (the first one had asserted the fabricated0.0first point by name — it now asserts the point is absent).ViewerCommandDeadlines.CurrentInteractiveReadSeconds, and bounded by shape (abucketrange on the rollup; aserver_id IS NULLfilter on a config table with one row per collector).What this does NOT do
DarlingTrendReader.cs/DarlingMcp*.cs/DarlingPvsReader.cs(other lanes). The MCP hourly constants are pinned equal to the Storage builder rather than aliased to it.past_horizondiscloses the ambiguity instead, as on the MCP side.Test plan
New (compile-verified here with
-p:EnableWindowsTargeting=true, first executed in CI):Darling/Darling.Tests/ViewerTrendRoutingPortTests.cs— MCP hourly SQL == Storage builder (no filter); viewer hourly == MCP + the one$4line, inside the WHERE;RawTierMargin/TruncationSlack/HourlyBucketSecondsSqlshared and equal toTimescaleSupport.HourlyBucket;ResolveTierandShouldUseRawTieragree withDarlingTrendReader's on every acceptedhours_back× availability × five coverage shapes (1,680 cells);DescribeCoverage/SourceWordmatch the MCP route's; the viewer routes through the shared resolver with the grain's own flag; the exec-count chart reads ordinal 2 of the shared text;DescribeTrendCoveragewording; every routed chart shows its coverage; PVS both SKUs. PlusViewerTrendRoutingLivePostgresTests(scratch TimescaleDB database, theRollupBackfillLiveTestspattern): a 7-day window routes hourly with exact bucket-width rates (169 buckets, 8000/3600 ms/s), the database filter survives the routing, the exec-count chart rides the same statement, a recent window stays raw and drops its unrated first collection, and a window past the planted history is disclosed as truncated.New:
Darling/Darling.Tests/ViewerCalendarRetentionPortTests.cs— purged / past-horizon band NoData and inside-retention stands; tooltip per state; absent day judged;ShortestSignalRetentionDaysviewer-form == MCP-form for every override shape the MCP test walks plus an invalid 0; the fleet rule is one definition the purge delegates to; the service's constants are aliases;DailySummaryRetentionis one type consumed by both readers (reflection over all four assemblies + source pins of the fold and the ordinal-13 read); Lite/viewer rows are twins on the retention members. PlusViewerCalendarRetentionLivePostgresTests(shared fixture,LiveStoreCleanupfinally): a run-record-only day 10 days before the computed horizon is Purged/NoData withcollection_runs = 1real; a run record + surviving deadlock is PastHorizon (1 of 7); the viewer's horizon equalsDarlingHealthReader's for the same store and the two readers' states agree day for day.New in both
DailyHealthBandTeststwins:Describe_WithRetentionState_SaysWhatTheZerosAre.Changed:
ViewerQueriesRestTests(the ELSE-0 pin flips toDoesNotContain, the reader anchor follows the loop rename, a Query Store first-interval pin is added, the live trend test asserts one point not two),DeltaFamilyIntervalCompletionLivePostgresTests(.Points).Executed locally (the brief's rule): a throwaway console harness outside the worktree referencing the built
PerformanceMonitor.*.dlls ran every pure pin above against the real methods — all green — and, against a throwawaytimescale/timescaledb:2.28.1-pg18container on Dockerdesktop-linux: migrate → enable → hypertables → seed 8 days × 2 databases → ensure CAGGs → backfill, then the VIEWER's hourly text (with$4) returned 169 buckets at exactly 8000/3600 and 40/3600, theDbAfilter gave 2000/3600, the deep window described itself truncated at the first materialized bucket, the viewer's rawQueryDurationTrendSql/ExecutionCountTrendSql(extracted from source) returned NULL for the first collection and exact rates after, andDailySummaryHorizonread a planted fleetdeadlocks = 12(ignoring a NULL-retention fleet row and a per-server row) to a horizonDarlingHealthReader.GetDailySummaryRangeAsyncpublished identically. Container removed, harness deleted.Builds (zero warnings):
PerformanceMonitor.Darling.Storage,.Service,.Viewer,PerformanceMonitorLite,Darling.Tests,Lite.Tests,deprecated/Dashboard.Tests— all-c Debug -v q -nologo -p:EnableWindowsTargeting=true.Which component(s) does this affect?
How was this tested?
See the test plan: compile-verified test suites (first execution in CI), plus the pure pins and the moved/reused SQL executed locally against the built assemblies and a live PG18 + TimescaleDB 2.28.1 container.
Checklist
dotnet build -c Debug)Supersedes #3661 (wedged merge state; closed unmerged). Same four commits, branch freshened against dev.