Skip to content

Bucket Lite's Overview CPU, wait and memory lane reads (#4234) - #4337

Merged
erikdarlingdata merged 3 commits into
devfrom
fix/4234-lite-overview-lanes-buckets
Sep 25, 2026
Merged

erikdarlingdata merged 3 commits into
devfrom
fix/4234-lite-overview-lanes-buckets

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Part of #4234.

Why

Lite's Overview tab (Correlated Timeline Lanes) redraws its CPU, wait and memory lines from three reads that shipped one row per collection: GetCpuUtilizationAsync, GetTotalWaitTrendAsync and GetMemoryTrendAsync. A multi-day window on a busy server handed WPF thousands of points per line. GetCpuUtilizationAsync and GetMemoryTrendAsync also feed their own dedicated CPU and Memory tab charts — the only other callers, both charts, same problem.

What changes

All three reads now bucket in DuckDB with time_bucket, widths chosen by the existing TrendBuckets.AutoMinutes ladder against TrendBudget.Chart's 1,500-point budget (the same machinery the MCP trend family already uses, at that family's own narrower McpPointBudget, per the top-of-file note in LocalDataService.TrendBuckets.cs, which I extended by one paragraph to say so). seriesCount is always 1 in that call, matching the #4234 ruling: CPU's two gauges and memory's four gauges each ride the SAME row/bucket, not separate series, and the wait lane is already a single aggregated line.

  • CPU (GetCpuUtilizationAsync, Lite/Services/LocalDataService.Cpu.cs): a gauge, AVG(COALESCE(x, 0)) per bucket, rounded and cast back to INTEGER so CpuUtilizationRow.SqlServerCpu/OtherProcessCpu keep their existing int type (their derived TotalCpu/IdleCpu properties need no change).
  • Wait (GetTotalWaitTrendAsync, Lite/Services/LocalDataService.WaitStats.cs): a rate. Kept the existing per_collection CTE (interval per collection, MAX over that collection's wait types, LAG fallback for pre-v60 rows, Measurement-layer campaign: the delta honesty contract (11 findings, one keystone) #3540) and added a rated CTE plus bucket aggregation on top: a bucket's rate is SUM(rated_ms) / SUM(rated_seconds) (time-weighted, never an average of per-collection rates), and HAVING COUNT(rated_seconds) > 0 drops a bucket with no rated collection, so it stays absent rather than 0 — the same thing the per-collection read always did to a single unrated collection.
  • Memory (GetMemoryTrendAsync, Lite/Services/LocalDataService.Memory.cs): four gauges, AVG(COALESCE(x, 0)) per bucket, kept as double (unchanged type).

Every statement's SQL text is pulled into an internal static member (CpuUtilizationTrendSql, MemoryTrendSql, TotalWaitTrendSql(exclude)) so a source test can read it without a live DuckDB, matching the pattern in the sibling PR #4331 (WaitTrendsSql).

Singleton stamping, same rule as #4331: when EVERY bucket a call returns holds exactly one physical collection, each point is stamped at that collection's own clock (first_sample_time / first_collection_time) instead of the bucket grid line, so a window narrow enough to never merge two collections renders byte-identical to the pre-#4234 per-collection read. Any merged bucket anywhere in the call switches the whole call to bucket_start.

Clamping: each statement's bucket_start is GREATEST(time_bucket(...), <window start>), so a bucket never renders earlier than the window the caller asked for. For CPU (bucketing on server-local sample_time, not UTC collection_time) that clamp needed a new parameter carrying the server-local window start, since the existing UTC parameters aren't in the same frame.

I left GetWaitStatsTrendAsync (single wait-type) and GetWaitStatsTrendsByTypesAsync (batched-by-type) untouched — both are PR #4331's own scope, not this one's — and left the blocking/deadlock lanes exactly as they are, per the #4234 ruling (issuecomment-5836582794).

Test plan

New file Lite.Tests/OverviewLaneBucketingTests.cs, four tests against a real embedded DuckDB:

  • BucketedTrendSql_CarriesABucketWidth — each of the three SQL members contains time_bucket.
  • SevenDayWindow_StaysWithinChartBudget_ForAllThreeLanes — 2,016 raw collections per lane (5-minute cadence over 7 days, bulk-seeded with one generate_series-based INSERT...SELECT per table, not a C# loop), all three reads return <= TrendBudget.Chart.AutoPoints (1,500) rows.
  • SingletonBuckets_MatchThePerCollectionReadExactly_ForAllThreeLanes — three collections five minutes apart, seeded at :37 seconds past the minute (off the bucket grid on purpose). A 3-hour window auto-sizes to a 1-minute bucket, so every bucket is a singleton; all three lanes return the exact seeded timestamps and values.
  • TotalWaitTrend_Bucketed_DropsAnIsolatedUnratedCollection — an isolated 0-interval (restart) collection between two rated ones stays absent from the wait lane, not a 0.00 point.

Proved the regression power directly: built against the parent commit (pre-bucketing) with the source-check test disabled — SevenDayWindow_StaysWithinChartBudget_ForAllThreeLanes failed (CPU returned 2005 points, over the 1,500 cap), and separately the source-check test fails to even compile against that commit (CpuUtilizationTrendSql/etc. don't exist yet). The singleton and unrated-drop tests pass on both commits by design — they pin behavior that must hold identically before and after bucketing, not a behavior change.

  • Every class in the touched test files plus *Overview*, *CorrelatedTimeline*, *Cpu*, *Memory*, *WaitStats*, *DocCommentHygiene* (184 tests) — 0 failed.
  • dotnet build Lite.Tests/Lite.Tests.csproj — 0 Warning(s), 0 Error(s).
  • Merged origin/dev (clean, no conflicts — the incoming changes were Darling-side File I/O/memory viewer work, untouched by this PR).
  • Full Lite.Tests.exe once after the merge: 5419 total, 0 errors, 0 failed, 272.8s.

Existing tests re-checked for regressions

Three pre-existing test files call these reads directly and all still pass (32 tests): DeltaFamilyUnknowableRowReadTests (including TotalWaitTrend_TakesMaxStoredIntervalPerCollection_DropsAnAllUnknowableCollection, whose four collections are exactly 1 minute apart — each its own singleton bucket at the resolved 1-minute width, so it renders identically), TimeHonestyRungTests (GetCpuUtilizationAsync under the v63 UTC-instant/offset-fallback logic — likewise all singleton buckets), OverviewComparisonWindowOffsetTests.

What the coordinator should double-check

  • The new server-local clamp parameter ($6) in CpuUtilizationTrendSql — CPU is the only one of the three bucketing on a server-local column rather than UTC collection_time, so it needed its own clamp value instead of reusing the UTC window parameter the other two clamp against.
  • I added one paragraph to the existing top-of-file comment in LocalDataService.TrendBuckets.cs (the The rest of the trend family still returns every collection: a week of get_tempdb_trend is 2 MB, get_memory_trend 1.5 MB, get_wait_trend 1.1 MB #3960 MCP-bucketing context block) noting that three of the per-collection reads it names are no longer left "exactly as they are" — they're bucketed in place now, at the chart's wider budget, for their own (all-chart) callers.

CHANGELOG entry

SECTION: Changed
ENTRY:

🤖 Generated with Claude Code

https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ

erikdarlingdata and others added 3 commits September 25, 2026 13:50
GetCpuUtilizationAsync, GetTotalWaitTrendAsync and GetMemoryTrendAsync
fed the Overview correlated lanes (and, for CPU/memory, their own
dedicated tab charts) one row per collection, so a multi-day window
shipped thousands of points to a WPF chart. Each now buckets in DuckDB
to TrendBuckets.AutoMinutes against TrendBudget.Chart, the same ladder
and origin the MCP trend family already uses, with the SQL text pulled
into an internal static member so a source test can check it without a
live DuckDB. A bucket whose collections are all a single physical row
stamps at that row's own clock instead of the bucket grid line, so a
window narrow enough to never merge two collections renders exactly as
the old per-collection read did.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Four claims against the three reads bucketed in the prior commit: each
statement's text carries a bucket width; a 7-day window (2,016 raw
collections per lane) stays within TrendBudget.Chart's 1,500-point
cap, which the raw row count alone would have missed; a window narrow
enough that every bucket holds exactly one physical collection renders
byte-identical to the pre-#4234 per-collection read; and the wait
lane still drops an isolated unrated collection (#3540) rather than
rendering it as 0. Proved the budget claim fails pre-bucketing (2,005
points, over the 1,500 cap) and the source-check claim fails to
compile pre-bucketing, by building against the parent commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
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