Skip to content

Bucket the CPU scheduler, session stats, and plan cache trend reads (#4349) - #4361

Merged
erikdarlingdata merged 6 commits into
devfrom
fix/4349-cpu-sessions-plancache-trend-buckets
Sep 26, 2026
Merged

erikdarlingdata merged 6 commits into
devfrom
fix/4349-cpu-sessions-plancache-trend-buckets

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

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.cs CpuSchedulerTrendSql / GetCpuSchedulerTrendAsync: bucketed with date_bin, following the #4234 ruling. 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.cs SessionStatsSql / 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.cs PlanCacheTrendSql / GetPlanCacheTrendAsync: the existing per-collection SUM across objtype groups is unchanged (inner per_collection CTE); 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.AutoMinutes and 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), matching GetTempDbFileIoTrendAsync/GetCpuUtilizationAsync's existing rule. No new shared helper: reused TrendBucketSql.OriginSql, TrendBuckets.AutoMinutes, and TrendBudget.Chart exactly as #4353 and #4340 did.

Lite (PerformanceMonitorLite.Services):

  • LocalDataService.CpuScheduler.cs, LocalDataService.SessionStats.cs, LocalDataService.PlanCache.cs: the same three reads ported to DuckDB's time_bucket/to_minutes dialect (not date_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 own WaitTrendsSql/PerfmonTrendsSql split. Session Stats' non-averageable columns use DuckDB's QUALIFY ROW_NUMBER() ... = 1 in place of Postgres's DISTINCT ON.

No chart-consumer changes were needed: all three reads already took (startUtc, endUtc) / (fromDate, toDate), and neither ViewerServerTab.CpuScheduler/SessionStats/PlanCache.cs nor Lite/Controls/ServerTab.CpuScheduler/SessionStats/PlanCache.cs post-filtered the trend series on the client side (unlike ViewerServerTab.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)
  • Before/after row-count measurement on a seeded rig (see below — not run in this lane's window)
  • New bucketing tests for the three Darling reads and three Lite reads, mirroring ViewerCpuTempDbTests/LiteMemoryFileIoTrendBucketingTests — not added in this pass, listed here so CI/a follow-up can add them:
  • Full Windows test suite (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-pg18 rig on port 55473, migrated via a standalone net10.0 console harness referencing Darling/PerformanceMonitor.Darling.Storage directly, seeded with generate_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:

For the coordinator

pm-pr lane report (tests)

Head sha: 8c19fdef942aeb9383469abe893387b7f2f5fc82 (merged origin/dev onto the opening lane's 234ec39e9b468512593f7fe587a6e0186dec7f90, keeping both sides; no rebase).

Tests added, one new file per product, mirroring #4353's ViewerCpuTempDbTests and #4340's LiteMemoryFileIoTrendBucketingTests:

  • Darling/Darling.Tests/ViewerCpuSessionsPlanCacheTrendBucketingTests.cs: a *SqlTests class with one source check per read (CPU scheduler, Session Stats, Plan Cache) — bucket width parameter, shared origin, first_collection_time/collection_count projection, gauge AVG(COALESCE(..., 0)), and Session Stats' DISTINCT ON (bucket_start) ... ORDER BY bucket_start, collection_time DESC for its four attribution columns. A [Collection("live-postgres")] class against DARLING_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 *SqlTests class checking time_bucket(to_minutes(...)), the shared origin, and DuckDB's QUALIFY ROW_NUMBER() OVER (PARTITION BY bucket_start ORDER BY collection_time DESC) = 1 for 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, container pmpr-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.

Build: Darling/Darling.Tests/Darling.Tests.csproj and Lite.Tests/Lite.Tests.csproj both 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 test refuses VSTest-mode execution on Darling.Tests/Lite.Tests, and the live-postgres/DuckDB classes need DARLING_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_series at 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)

pm-pr lane report (CRLF pin fix)

  • New head: 366171b (was 55c4fd0). Fixed CpuSchedulerTrendSql_SelectsPressureCounts_OverTheWindow_ThenBucketed_OrderedByBucket and PlanCacheTrendSql_SumsSingleVsMultiUseMb_PerCollection_ThenBucketed_OrderedByBucket.
  • Both asserted a \n-joined multi-line SQL substring against a CRLF source string, which never matched on CI (Windows CRLF checkout).
  • Fixed with the house pattern already used elsewhere in this suite (DarlingDmvResourceDatabaseSentinelTests.cs, etc.): normalize with sql.ReplaceLineEndings("\n") before the Assert.Contains. No other multi-line Assert.Contains on 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.

…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
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.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 26, 2026 01:54
@erikdarlingdata
erikdarlingdata merged commit ebbe83c into dev Sep 26, 2026
15 of 16 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4349-cpu-sessions-plancache-trend-buckets branch September 26, 2026 01:54
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