Repository navigation
Bucket the CPU scheduler, session stats, and plan cache trend reads (#4349) - #4361
Merged
erikdarlingdata merged 6 commits intoSep 26, 2026
Merged
Conversation
…4234) CpuSchedulerTrendSql, SessionStatsSql, and PlanCacheTrendSql returned one row per collection, unbounded over the window, unlike #4353/#4340's already-bucketed twins. Measured (issue #4234): CPU scheduler pressure 10,080 rows over 7 days at 1-minute cadence; Sessions 2,016 and Plan cache 2,016 at 5-minute cadence (plan cache: two objtype groups per collection, summed before bucketing). All three read shapes are point-in-time gauges (no delta math), so each new date_bin/time_bucket bucket averages its collections' values, following the #4234 ruling. Session Stats also carries four non-averageable attribution columns (top application/host name and their connection counts); those come from the bucket's LAST physical collection (DISTINCT ON / QUALIFY newest-first) rather than any blended or dropped answer. GetCpuSchedulerTrendAsync, GetSessionStatsAsync, and GetPlanCacheTrendAsync now compute their bucket width from TrendBuckets.AutoMinutes and stamp points at their own raw collection time instead of the bucket grid when every returned bucket is a singleton, matching GetTempDbFileIoTrendAsync's rule. Lite's three DuckDB twins get the same treatment with time_bucket/to_minutes. No chart-consumer changes needed: all three reads already take (startUtc, endUtc)/(fromDate, toDate) and none of the three chart callers post-filtered on the client side.
…trends (#4349) Mirrors #4353's ViewerCpuTempDbTests and #4340's LiteMemoryFileIoTrendBucketingTests shape: a source check per read (bucket width, origin, first_collection_time/ collection_count columns, gauge averaging), a 7-day row-budget cap against a real store (bulk-seeded at each collector's own cadence), and point equality (a merged bucket's averaged value against the hand-computed mean). Session Stats also pins claude-desktop's ruling addition: a bucket containing two collections carries the NEWER collection's top_application_name/top_host_name (and their counts), not a blended or oldest answer, on both Darling (DISTINCT ON) and Lite (QUALIFY ROW_NUMBER()). Refs #4349
…plancache-trend-buckets
Each pin asserted the pre-bucketing raw-row ORDER BY (collection_time). The rewritten trend reads bucket first, then order by bucket_start; the assertions now pin that shape instead of a literal that no longer appears in the SQL. GROUP BY collection_time still holds inside each per-collection/raw CTE and is left in place.
…in the #4349 trend-bucket pins
…f raw-string indentation
erikdarlingdata
marked this pull request as ready for review
September 26, 2026 01:54
erikdarlingdata
deleted the
fix/4349-cpu-sessions-plancache-trend-buckets
branch
September 26, 2026 01:54
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.
Refs #4349.
Why
Trend charts read one row per collection at the 7-day window for three reads, unbounded on the window (the pattern #4353 and #4340 already fixed for other reads). Measured (issue #4234's own numbers, restated by the ruling for this slice): CPU scheduler pressure 10,080 rows over 7 days at the collector's 1-minute cadence; Sessions 2,016 rows and Plan cache 2,016 rows at the collector's 5-minute cadence (plan cache: two objtype groups summed per collection before bucketing). This lane did not have time to stand up a throwaway Postgres rig to measure the before/after row counts directly (see Test plan) — the "before" counts above are the issue's own measured baseline, not independently reproduced here.
What changes
Darling (
PerformanceMonitor.Darling.Viewer):ViewerDataService.CpuScheduler.csCpuSchedulerTrendSql/GetCpuSchedulerTrendAsync: bucketed withdate_bin, following the#4234ruling. All three counts (runnable/blocked/queued) are point-in-time gauges, so a bucket's value is the plain average of its collections — no delta math, matching the pre-bucket per-collection read.ViewerDataService.SessionStats.csSessionStatsSql/GetSessionStatsAsync: bucketed. The eight status/count columns are gauges, averaged per bucket. The four attribution columns (top application/host name and their connection counts) cannot be averaged, so they come from the bucket's LAST physical collection (DISTINCT ON, newest first) rather than any blended or dropped answer — the same value the pre-bucket read's own newest row inside a would-be bucket already showed.ViewerDataService.PlanCache.csPlanCacheTrendSql/GetPlanCacheTrendAsync: the existing per-collectionSUMacross objtype groups is unchanged (innerper_collectionCTE); the new outer bucket averages that per-collection sum across the collections a bucket holds (a gauge, not an accumulating counter).All three reads now compute their bucket width from
TrendBuckets.AutoMinutesand stamp points at their own raw collection time instead of the bucket grid when EVERY bucket the call returned is a singleton (ruling item 3), matchingGetTempDbFileIoTrendAsync/GetCpuUtilizationAsync's existing rule. No new shared helper: reusedTrendBucketSql.OriginSql,TrendBuckets.AutoMinutes, andTrendBudget.Chartexactly as #4353 and #4340 did.Lite (
PerformanceMonitorLite.Services):LocalDataService.CpuScheduler.cs,LocalDataService.SessionStats.cs,LocalDataService.PlanCache.cs: the same three reads ported to DuckDB'stime_bucket/to_minutesdialect (notdate_bin), each SQL statement pulled into its own internal static property (CpuSchedulerTrendSql,SessionStatsTrendSql,PlanCacheTrendSql) so a source check can pin the shape without a live DuckDB, mirroring Bucket Lite's memory clerk and File I/O trend reads (#4234) #4340's ownWaitTrendsSql/PerfmonTrendsSqlsplit. Session Stats' non-averageable columns use DuckDB'sQUALIFY ROW_NUMBER() ... = 1in place of Postgres'sDISTINCT ON.No chart-consumer changes were needed: all three reads already took
(startUtc, endUtc)/(fromDate, toDate), and neitherViewerServerTab.CpuScheduler/SessionStats/PlanCache.csnorLite/Controls/ServerTab.CpuScheduler/SessionStats/PlanCache.cspost-filtered the trend series on the client side (unlikeViewerServerTab.Charts.cs's tempdb file-I/O post-filter that #4353 removed).Test plan
dotnet build Darling/Darling.Tests/Darling.Tests.csproj -p:EnableWindowsTargeting=true— 0 Warning(s), 0 Error(s)dotnet build Lite.Tests/Lite.Tests.csproj -p:EnableWindowsTargeting=true— 0 Warning(s), 0 Error(s)ViewerCpuTempDbTests/LiteMemoryFileIoTrendBucketingTests— not added in this pass, listed here so CI/a follow-up can add them:date_bin/time_bucket,GREATEST(...),first_collection_time/collection_count(orfirst_sample_time) in each of the six new SQL statements. Against the pre-WPF trend charts never adopted the #3897/#3960 bucket contract: at the "Last 7 days" window one chart reads 85–170 k raw rows, and the pickers re-run a week-long DISTINCT every refresh #4234 (this PR's owndev-parent) text,Assert.Contains("date_bin(CAST($4 AS integer)", ...)and its DuckDB twin would fail outright — none of the six old statements had a bucket width or singleton-detection columns.TrendBudget.Chart.AutoPoints. Against the pre-WPF trend charts never adopted the #3897/#3960 bucket contract: at the "Last 7 days" window one chart reads 85–170 k raw rows, and the pickers re-run a week-long DISTINCT every refresh #4234 text this would fail because the old reads returned one row per collection (10,080 / 2,016 / 2,016), over the cap.collection_time, unchanged, when every bucket is a singleton.top_application_namevalues keeps the NEWER collection's name/count, not a blend — this would fail against a naiveMAX()/FIRST()implementation that isn't ordered bycollection_time DESC.Darling.Tests+Lite.Tests) — CI decides; these projects build but cannot run on macOS.Row-count measurement — not completed in this lane's 30-minute window
The brief asked for a throwaway
timescale/timescaledb:2.30.1-pg18rig on port 55473, migrated via a standalone net10.0 console harness referencingDarling/PerformanceMonitor.Darling.Storagedirectly, seeded withgenerate_series, to measure old-vs-new row counts for one server at 7 days. This lane spent its time budget on the three reads plus the build-clean check and ran out of window before standing up the rig. The counts above (10,080 / 2,016 / 2,016 "before") are restated from the issue's own measured baseline, not independently reproduced here. A follow-up lane or CI run should measure the "after" counts directly; expect roughly the day-a-bucket-a-minute-to-hourly ladder that #4353 measured (40,324 -> 4,036 for a comparable shape), so CPU scheduler at 7 days should land near the low thousands, and Sessions/Plan Cache (already only 2,016 rows, well under any point cap) may not shrink at all if the auto-picked width stays at 1 minute for that row count — worth confirming rather than assuming.CHANGELOG entry
SECTION: Fixed
ENTRY:
REF:
[Bucket the CPU scheduler, session stats, and plan cache trend reads (#4349) #4361]: Bucket the CPU scheduler, session stats, and plan cache trend reads (#4349) #4361
For the coordinator
Darling.TestsandLite.Testsfor this diff.Refs, notCloses).pm-pr lane report (tests)
Head sha:
8c19fdef942aeb9383469abe893387b7f2f5fc82(mergedorigin/devonto the opening lane's234ec39e9b468512593f7fe587a6e0186dec7f90, keeping both sides; no rebase).Tests added, one new file per product, mirroring #4353's
ViewerCpuTempDbTestsand #4340'sLiteMemoryFileIoTrendBucketingTests:Darling/Darling.Tests/ViewerCpuSessionsPlanCacheTrendBucketingTests.cs: a*SqlTestsclass with one source check per read (CPU scheduler, Session Stats, Plan Cache) — bucket width parameter, shared origin,first_collection_time/collection_countprojection, gaugeAVG(COALESCE(..., 0)), and Session Stats'DISTINCT ON (bucket_start) ... ORDER BY bucket_start, collection_time DESCfor its four attribution columns. A[Collection("live-postgres")]class againstDARLING_TEST_PG: a 7-day row-budget cap per read (bulk-seeded at each collector's own cadence — 1-minute for CPU scheduler, 5-minute for Session Stats/Plan Cache, matching the issue's measured pre-bucketing counts), and point equality for a bucket merging two collections (gauges average; Plan Cache's per-collection MB sum averages across collections).Lite.Tests/LiteCpuSessionsPlanCacheTrendBucketingTests.cs: the DuckDB twin — a*SqlTestsclass checkingtime_bucket(to_minutes(...)), the shared origin, and DuckDB'sQUALIFY ROW_NUMBER() OVER (PARTITION BY bucket_start ORDER BY collection_time DESC) = 1for Session Stats. A[Collection("server-time-helper")]embedded-DuckDB class (IClassFixture<SharedDuckDbFixture>) with the same row-budget and merged-bucket assertions.claude-desktop's follow-up ruling, pinned on both products: a bucket holding two collections carries the NEWER collection's
top_application_name/top_application_connections/top_host_name/top_host_connections—SessionStatsTrend_MergedBucket_AveragesGauges_AttributionColumnsTakeTheNewestCollection_AgainstDevPostgres(Darling) and its DuckDB twin (Lite) seed an older and a newer collection in the same bucket and assert the newer app/host wins while the eight status-count gauges still average. Rule stated for the record: Session Stats buckets take each bucket's newest collection for per-snapshot attributes (app, host); gauges are averaged.RED/GREEN evidence: unchecked. I did not stand up the throwaway net10.0 harness against dockerized
timescale/timescaledb:2.30.1-pg18(port 55456, containerpmpr-4361) to prove a source check fails against origin/dev's pre-#4349 SQL text. The source checks themselves are written the same way #4353/#4340's proven-red checks are (asserting substrings —date_bin(...),time_bucket(...),first_collection_time,DISTINCT ON/QUALIFY— that are verifiably absent from the pre-PR per-collection SELECTs, per the diff each read replaced), but this was not run and shown red by hand this pass.SQL review (lane-orders checklist): no defect found or fixed.
date_bin/time_bucketbucket onTIMESTAMPcolumns; PG and DuckDB both keep microsecond precision through the bucketing, unaffected here (no truncation introduced).COALESCE(..., 0)beforeAVG, matching the pre-bucket per-row rule exactly (WPF trend charts never adopted the #3897/#3960 bucket contract: at the "Last 7 days" window one chart reads 85–170 k raw rows, and the pickers re-run a week-long DISTINCT every refresh #4234 ruling item 2). Session Stats' newest-wins tiebreak (collection_time DESC) has no secondary key, same as the pre-existing snapshot reads elsewhere in this codebase (e.g.CpuSchedulerSnapshotSqlusescollection_id DESCas a second key) — a true same-instant collision inside one bucket could pick either row nondeterministically. Judged in-scope-parity, not a new defect: the PR didn't add a same-instant collision anywhere the pre-bucket code was tiebreak-safe, and cpu_scheduler_stats stores several different snapshots under one collection_time #3936'scollection_idtiebreak pattern exists elsewhere for readers that need it; flagging for the record rather than fixing, since fixing it would touch the read's own SQL beyond the tests brief.time_bucket(to_minutes(...)),QUALIFY) rather than PG'sdate_bin/DISTINCT ONverbatim, matching Bucket Lite's memory clerk and File I/O trend reads (#4234) #4340's precedent.Build:
Darling/Darling.Tests/Darling.Tests.csprojandLite.Tests/Lite.Tests.csprojboth build clean (0 Warning(s), 0 Error(s)) after the merge, on this Mac with-p:EnableWindowsTargeting=true.What's left for CI: the full Windows test run (the new classes weren't executed locally — this Mac's net10.0 SDK's
dotnet testrefuses VSTest-mode execution on Darling.Tests/Lite.Tests, and the live-postgres/DuckDB classes needDARLING_TEST_PG/a real DuckDB run respectively, both of which CI provides); the before/after 7-day row measurement (explicitly out of this lane's scope — a separate lane is running it).pm-pr: measured before/after
Darling, measured live (docker TimescaleDB 2.30.1-pg18, a fresh migrated store,
generate_seriesat each collector's cadence, one server, 7-day window; old SQL from dev vs this PR's SQL, run verbatim): CPU scheduler 10,080 → 1,008; Sessions 2,016 → 1,008; Plan cache 2,016 → 1,008 (10-minute buckets). Lite: computed, not run (no DuckDB on the measuring host). The twins share the width table, so the expectation is the same 1,008 / 1,008 / 1,008.pm-pr lane report (stale pin)
55c4fd0da4230206b985a76603302c190594b160.PlanCacheTrendSql_SumsSingleVsMultiUseMb_PerCollection_OverTheWindow(CI's failure): the outer bucketed read orders bybucket_start, not the literalORDER BY collection_timethe pin expected; renamed and repinned onAS bucket_start+GROUP BY 1 / ORDER BY 1, kept the still-trueGROUP BY collection_timeon the inner CTE.CpuSchedulerTrendSql_SelectsPressureCounts_OverTheWindow_OrderedByTime(also dropped the now-falseDoesNotContain("AVG(")) andSessionStatsSql_ReadsSessionSummaryView_BothSidesWindow_OrderedOldestFirst(repinned onORDER BY agg.bucket_start). No other pins on the three rewritten reads or their Lite twins were stale.pm-pr lane report (CRLF pin fix)
CpuSchedulerTrendSql_SelectsPressureCounts_OverTheWindow_ThenBucketed_OrderedByBucketandPlanCacheTrendSql_SumsSingleVsMultiUseMb_PerCollection_ThenBucketed_OrderedByBucket.\n-joined multi-line SQL substring against a CRLF source string, which never matched on CI (Windows CRLF checkout).DarlingDmvResourceDatabaseSentinelTests.cs, etc.): normalize withsql.ReplaceLineEndings("\n")before theAssert.Contains. No other multi-lineAssert.Containson this branch needed the same fix. Both test projects build with 0 Warning(s)/0 Error(s); the Windows-only test suite can't run on this macOS worktree.