Skip to content

Bucket Lite's memory clerk and File I/O trend reads (#4234) - #4340

Merged
erikdarlingdata merged 16 commits into
devfrom
fix/4234-lite-clerks-fileio-buckets
Sep 25, 2026
Merged

erikdarlingdata merged 16 commits into
devfrom
fix/4234-lite-clerks-fileio-buckets

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Part of #4234.

This branch was built on top of #4331, which has now merged to dev (fa05e9fd). The merge of origin/dev (fd1fdb20) took dev's copy of LocalDataService.WaitStats.cs and LocalDataService.Perfmon.cs, which this PR does not change. The diff now shows only this PR's files: Lite/Services/LocalDataService.Memory.cs, Lite/Services/LocalDataService.FileIo.cs, Lite/Controls/ServerTab.Refresh.cs, Lite/Mcp/McpIoTools.cs, and the new Lite.Tests/LiteMemoryFileIoTrendBucketingTests.cs.

Why

Same ruling as #4331/#4304: Lite's memory-clerk and File I/O charts ran one query per collection over the whole visible window, so a 7-day view could return thousands of points per file/clerk-type series. This bucket those reads to the chart's point budget, the same GREATEST(time_bucket(...), $2) shape #4331 gave the wait-stats and perfmon reads, and caches the memory-clerk picker's name list the same way #4331 cached the wait-type picker's.

What changes

  • GetDistinctMemoryClerkTypesForPickerAsync (new): wraps the existing uncached GetDistinctMemoryClerkTypesAsync with LiteNameListCache, keyed on (server, window length), TTL 15 minutes — exactly GetDistinctWaitTypesForPickerAsync's shape. Only the two picker call sites in ServerTab.Refresh.cs (sub-tab refresh and full refresh) move to it; the shared read stays uncached for any future MCP caller.
  • GetMemoryClerkTrendsByTypesAsync: now buckets via a new MemoryClerkTrendsSql(clerkTypeCount). A clerk's memory is a gauge, so a bucket is the plain AVG(memory_mb) of its collections — no rated/unrated split, no HAVING, since Measurement-layer campaign: the delta honesty contract (11 findings, one keystone) #3540 only marks delta rows.
  • GetFileIoLatencyTrendAsync: buckets via a new FileIoLatencyTrendSql. The top-10-files ranking (top_files) stays an unbucketed whole-window scan, unchanged. A rated CTE nulls (not filters) an unrated row's read/write/stall contributions, so an unrated collection still counts toward collection_count; HAVING COUNT(rated_reads) > 0 drops a bucket with no rated row. A bucket's latency is SUM(stall) / SUM(operations), never a mean of the collections' own ratios. This read also feeds the Overview correlated timeline's I/O lane (CorrelatedTimelineLanesControl.xaml.cs, both the live fetch and the comparison-period fetch) — that control just groups the already-bucketed points by CollectionTime and averages across files client-side, so it needed no changes of its own.
  • GetFileIoThroughputTrendAsync: buckets via a new FileIoThroughputTrendSql. The LAG-derived interval (falling back for pre-v60 rows with no stored sample_interval_seconds) is still computed once over the raw per-collection rows, before bucketing. A bucket's rate is SUM(bytes) / SUM(rated seconds), time-weighted, never a mean of per-collection rates.
  • GetTempDbFileIoTrendAsync: buckets the same way as the latency read (own TempDbFileIoTrendSql) since it feeds UpdateTempDbFileIoChart over the same windows. No top-10 ranking pass (tempdb's file count is already small) and no queued-latency columns (this chart never drew them). Preserves a pre-existing quirk exactly: the reader maps the SQL's file_name column into FileIoTrendPoint.DatabaseName, because that's the field UpdateTempDbFileIoChart groups its series on — documented in the new doc comment rather than changed, to keep this PR to bucketing only.
  • Every read gets the same "when every bucket in the call holds exactly one physical collection, stamp at first_collection_time; otherwise bucket_start" singleton rule Bucket Lite's Wait Stats and Perfmon trend and picker reads (#4234) #4331 established.
  • In-lane fix: a one-line comment in McpIoTools.cs called the desktop chart's GetFileIoLatencyTrendAsync "untouched: a chart wants every collection" — true before this PR, false after. Updated it to say the chart read is bucketed too now, on its own budget, separate from the MCP tool's own GetFileIoTrendAsync.

I left the FileIoTrendPoint.DatabaseName-holds-file_name naming quirk in GetTempDbFileIoTrendAsync alone rather than renaming it — fixing that touches a DTO shared with the top-10 latency read (where DatabaseName is used correctly) and a chart grouping call, and is unrelated to bucketing. Flagging it here rather than filing a separate issue, since it's cosmetic (the data is correct, just confusingly named) and not worth a queue entry.

Test plan

  • dotnet build Lite.Tests/Lite.Tests.csproj: 0 Warning(s), 0 Error(s), both before and after the origin/dev merge.
  • New file Lite.Tests/LiteMemoryFileIoTrendBucketingTests.cs (18 tests): source checks that each of the four new SQL statements carries its bucket-width parameter and projects first_collection_time/collection_count; live-DuckDB tests for the 7-day row-budget cap (one per read), point equality when every bucket holds one collection seeded off the minute grid (:37 seconds), an unrated collection alone in a bucket leaving that point absent (latency and throughput), a merged bucket proving SUM/SUM rather than a mean of per-collection ratios/rates (latency and throughput), and the clerk picker's TTL cache (hit inside 15 minutes, miss past it, shared read never caches).
  • Proven 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 shape: temporarily reverted LocalDataService.Memory.cs, LocalDataService.FileIo.cs and ServerTab.Refresh.cs to their HEAD~2 (pre-bucketing) content and rebuilt with the new test file in place. It fails to compile — GetDistinctMemoryClerkTypesForPickerAsync, MemoryClerkTrendsSql, FileIoLatencyTrendSql, FileIoThroughputTrendSql and TempDbFileIoTrendSql don't exist on the old shape at all, which is a stronger guarantee than a runtime assertion failure would be. Restored the bucketed files from my own commits (git checkout HEAD -- <paths>) afterward and confirmed the build is clean again.
  • Targeted run: *LiteMemoryFileIoTrendBucket*, *Overview*, *CorrelatedTimeline*, *Memory*, *FileIo*, *TrendBucket*, *DocCommentHygiene* — 133 total, 0 failed.
  • Merged origin/dev (brought in the Overview CPU/wait/memory lane bucketing from Bucket Lite's Overview CPU, wait and memory lane reads (#4234) #4337 and other unrelated work); rebuilt clean, no conflict markers left in any touched file.
  • Full suite once, after the merge: Lite.Tests.exe — 5455 total, 0 errors, 0 failed, 0 skipped (386.9s).
  • Not run: Installer.Tests (excluded per lane orders), and no live SQL Server/Postgres rig (Lite only, embedded DuckDB, per the brief).

CHANGELOG entry

SECTION: Changed
ENTRY:

🤖 Generated with Claude Code

https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ

erikdarlingdata and others added 15 commits September 25, 2026 13:01
GetWaitStatsTrendsByTypesAsync now buckets to TrendBudget.Chart's per-series point
budget instead of returning every collection (196,800 rows at 7 days for 20 types,
per #4234's measured number), following the #4304 ruling: a rated CTE sums wait and
signal wait over rated seconds per bucket, avg_ms_per_wait is the summed wait over
summed waiting tasks, and a point is stamped at its bucket's raw first_collection_time
only when every bucket in the call holds exactly one physical collection.

GetDistinctWaitTypesAsync is memoized through the new LiteNameListCache (per server and
window length, 15-minute TTL, a hit also requires the cached window's end to stay within
the TTL of the requested end) so the picker's full-window DISTINCT no longer reruns on
every 1-minute auto-refresh. The cache is a standalone file so the Perfmon picker (and a
later memory-clerk picker) can reuse it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
GetPerfmonTrendsByCountersAsync now wraps the unchanged per-collection SUM as a
subquery and re-aggregates into TrendBudget.Chart-sized buckets instead of returning
every collection (118,080 rows at 7 days for 12 counters, per #4234's measured
number), following the #4304 ruling: Value is the bucket's rounded gauge average,
DeltaValue/SampleIntervalSeconds are summed only over rated collections so their
ratio downstream stays the ruling's summed-deltas-over-summed-intervals rate, and a
point is stamped at its bucket's raw first_collection_time only when every bucket in
the call holds exactly one physical collection.

GetDistinctPerfmonCountersAsync is memoized through the LiteNameListCache T3b added
alongside the Wait Stats picker, so this picker's full-window DISTINCT also stops
rerunning on every 1-minute auto-refresh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…oInt64/ToDouble

DuckDB promotes SUM() over an INTEGER column to HUGEINT, which DuckDB.NET hands back
as a boxed BigInteger; Convert.ToInt64/Convert.ToInt32 and the reader's typed GetInt64/
GetDouble accessors cannot cast that. PerfmonCounterTypeReadTests caught this against
the new GetPerfmonTrendsByCountersAsync bucketing (its SUM(sample_interval_seconds)
FILTER column). Both new readers now route their SUM-derived columns through the
existing ToInt64/ToDouble helpers, matching GetWaitBucketsAsync/GetPerfmonBucketsAsync's
own established pattern for the same FILTER-summed shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
WaitTrendsSql(waitTypeCount) and PerfmonTrendsSql(counterCount) mirror Darling's
ViewerDataService.WaitTrendsSql / PerfmonTrendsSql (#4234, PR #4304): the SQL text
generation moves out of the two Get*TrendsByTypesAsync/ByCountersAsync methods into
its own testable method, so a source check for the bucket width and the
first_collection_time/collection_count columns needs no live DuckDB. No behavior
change, same generated text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…eads

Four groups, all against embedded DuckDB (no live SQL Server/Postgres needed):
LiteTrendBucketWidthSqlTests (source checks: WaitTrendsSql/PerfmonTrendsSql carry a
time_bucket width plus first_collection_time/collection_count), LiteNameListCacheTests
(the cache's hit/miss rules, ported from Darling's ViewerNameListCacheTests),
LiteTrendBucketingLiveTests' two budget-cap tests (a 7-day, one-per-minute window
returns at most TrendBudget.Chart.AutoPoints * series rows) and two point-equality
tests (three :37-second collections come back with their raw timestamps and values
unchanged, not floored to the bucket grid), plus the two pickers' cache proof (a
second call inside 15 minutes misses a wait type/counter the store gained since, a
call past the TTL picks it up).

Proven by hand against the pre-#4234 text (read directly while building this PR):
the old GetWaitStatsTrendsByTypesAsync/GetPerfmonTrendsByCountersAsync had no
time_bucket, first_collection_time or collection_count anywhere, so the source
checks fail there outright, and the old per-collection read returns one row per
collection -- 10,080 for the 7-day one-per-minute seed, over the budget the cap
tests assert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
… aggregation

PerfmonIntervalAggregationTests read the whole Perfmon.cs source and pinned exact
occurrence counts from before bucketing existed. The bucketed batched trend adds a
second, correct layer of aggregation on top of the unchanged per-collection one: the
outer GROUP BY counter_name, bucket_start sums each collection's own already-MAX'd
interval and already-summed delta across the collections a bucket holds (the ruling's
summed-deltas-over-summed-intervals rate) -- the full suite caught both stale pins.

NoPerfmonTrendRead_SumsTheInterval (renamed *AcrossInstanceRows) now strips the new
SUM(sample_interval_seconds) FILTER (WHERE sample_interval_seconds > 0) form before
asserting no bare SUM(sample_interval_seconds) remains, so it still catches the
original bug shape (summing the interval across a collection's instance rows) while
allowing the new bucket-level one, which always carries that exact FILTER.
TheAdditiveColumnsStaySummed's SUM(delta_cntr_value) count moves from 2 to 3 for the
same reason; SUM(cntr_value) stays 2 because that outer layer AVERAGES (the ruling's
gauge rule), adding no third SUM.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
#4234 ruling: GetDistinctWaitTypesAsync and GetDistinctPerfmonCountersAsync
went back to their dev shape (uncached, no nowUtc seam) since MCP's wait
and perfmon tools call them directly and must never answer with a picker
snapshot up to 15 minutes stale. The TTL cache moved to two new entry
points, GetDistinctWaitTypesForPickerAsync and
GetDistinctPerfmonCountersForPickerAsync, which only ServerTab's picker
refresh calls now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
PerfmonIntervalAggregationTests pins the inner MAX(sample_interval_seconds)
in source text, but no live test seeded more than one instance row per
collection, so a regression turning that MAX into a SUM would have passed
every DuckDB test in the file. Adds two: a single multi-instance collection
(12 rows) landing alone in its bucket, and three such collections merged
into one bucket, both asserting the bucket's DeltaValue/SampleIntervalSeconds
land on the ruling's summed-deltas-over-summed-intervals rate. Proven by
hand: reverting the inner MAX to SUM fails exactly these two tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
)

GetMemoryClerkTrendsByTypesAsync now buckets to TrendBudget.Chart's
per-series point budget, the same GREATEST(time_bucket(...), $2) shape
PR #4331 gave GetWaitStatsTrendsByTypesAsync / GetPerfmonTrendsByCountersAsync.
A clerk's memory is a gauge, so a bucket is the plain AVG(memory_mb) of
its collections, with no rated/unrated split.

GetDistinctMemoryClerkTypesForPickerAsync wraps the existing uncached
read with LiteNameListCache, exactly like GetDistinctWaitTypesForPickerAsync,
so the clerk picker's full-window DISTINCT runs at most once per 15
minutes instead of on every 1-minute auto-refresh. Only the picker's two
callers in ServerTab.Refresh.cs move to it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
GetFileIoLatencyTrendAsync, GetFileIoThroughputTrendAsync and
GetTempDbFileIoTrendAsync now bucket to TrendBudget.Chart's per-series
point budget, the GREATEST(time_bucket(...), $2) shape PR #4331 gave
the wait/perfmon trend reads. Latency stays a ratio (summed stall over
summed operations, over rated rows only); throughput stays a rate
(summed bytes over summed rated seconds). An unrated collection alone
in a bucket still leaves that point absent (HAVING drops the bucket),
same as before. The top-10-files ranking in the latency and throughput
queries stays an unbucketed whole-window scan, unchanged.

These reads feed the File I/O tab charts and the Overview correlated
timeline's I/O lane (both its live and reference-period fetches), which
read GetFileIoLatencyTrendAsync directly and need no changes of their
own.

Also fixes a stale comment in McpIoTools.cs that called the desktop
chart's read "untouched" — it is bucketed now too, on its own budget,
separately from the MCP tool's own GetFileIoTrendAsync.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Source-check tests pin each new SQL statement's bucket-width parameter
and first_collection_time/collection_count projection. Live DuckDB
tests cover the 7-day row-budget cap, point equality when every bucket
holds one collection, an unrated collection alone leaving its bucket
absent, a merged bucket using summed-over-summed (not a mean of
per-collection ratios/rates), and the clerk picker's TTL cache
(hit inside 15 minutes, miss past it, shared read never caches).

Proven against the pre-#4234 shape: none of these tests can even
compile against it, since GetDistinctMemoryClerkTypesForPickerAsync,
MemoryClerkTrendsSql, FileIoLatencyTrendSql, FileIoThroughputTrendSql
and TempDbFileIoTrendSql did not exist before this branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Unstacks this PR from #4331, which squash-merged to dev as fa05e9f with its CI fix. LocalDataService.WaitStats.cs (a conflict) and LocalDataService.Perfmon.cs (which auto-merged with PerfmonTrendsSql twice) take dev's copy, since this PR's own commits never touch either file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 25, 2026 19:18
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 25, 2026 19:18
…r methods

CI's DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks caught four
method summaries stranded above the SQL-text members this PR pulled out of
each method. Each stranded block is the METHOD's own summary; the SQL member
already had its own summary directly below it, leaving the method itself
undocumented.

Moved each block verbatim to directly above its method, after the SQL
member's closing line and the blank line:
- LocalDataService.FileIo.cs: GetFileIoLatencyTrendAsync, GetFileIoThroughputTrendAsync,
  GetTempDbFileIoTrendAsync
- LocalDataService.Memory.cs: GetMemoryClerkTrendsByTypesAsync

No wording changed, no lines added or removed beyond the move itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
@erikdarlingdata

Copy link
Copy Markdown
Owner Author

Fixed the four DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks failures. Each stranded first block was the method's own summary, displaced above the SQL member this PR pulled out of it. Moved each verbatim to directly above its method, after the SQL member's closing line and the blank line, wording unchanged:

  • Lite/Services/LocalDataService.FileIo.cs 73-86 -> above GetFileIoLatencyTrendAsync
  • Lite/Services/LocalDataService.FileIo.cs 219-231 -> above GetFileIoThroughputTrendAsync
  • Lite/Services/LocalDataService.FileIo.cs 344-352 -> above GetTempDbFileIoTrendAsync
  • Lite/Services/LocalDataService.Memory.cs 255-267 -> above GetMemoryClerkTrendsByTypesAsync

All four moved line ranges matched what the CI failure and the brief pointed at exactly. git diff --stat: 2 files changed, 49 insertions(+), 49 deletions(-) — only the four blocks moved, nothing else touched.

One commit, 14a25915, pushed to fix/4234-lite-clerks-fileio-buckets (new head; was fd1fdb20).

Builds: Lite/PerformanceMonitorLite.csproj and Darling/Darling.Tests/Darling.Tests.csproj, both 0 Warning(s), 0 Error(s). Also built Lite.Tests/Lite.Tests.csproj (0 Warning(s), 0 Error(s)) to run its tests.

Tests:

  • Darling.Tests.exe -class "*DocCommentHygiene*": Total 77, Failed 0, Errors 0.
  • Lite.Tests.exe: the brief's class name LiteMemoryFileIoTrendBucketingTests isn't the actual class in that file — it holds LiteMemoryFileIoTrendBucketWidthSqlTests and LiteMemoryFileIoTrendBucketingLiveTests. Ran both with -class "*LiteMemoryFileIoTrendBucket*": Total 18, Failed 0, Errors 0.

No live test run (no rig requested). No full suite run. No Installer.Tests run.

@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 25, 2026 20:15
@erikdarlingdata
erikdarlingdata merged commit 0738cb5 into dev Sep 25, 2026
15 of 16 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4234-lite-clerks-fileio-buckets branch September 25, 2026 20:21
erikdarlingdata added a commit that referenced this pull request Sep 25, 2026
* Bucket the Darling viewer's TempDB file I/O trend (#4234)

TempDbFileIoTrendSql returned one row per (collection_time, file_name)
for a tempdb file, unbounded on the window end, unlike the File I/O
tab's own reads (#4329) and Lite's already-bucketed twin (#4340). A
7-day window at 1-minute cadence for 4 files measured 40,324 rows on
a throwaway rig seed; the same seed through the new date_bin bucket
(auto-picked to 10 minutes for that window) measures 4,036.

GetTempDbFileIoTrendAsync now takes (serverId, startUtc, endUtc) like
the other bucketed trend reads, computes its bucket width from
TrendBuckets.AutoMinutes, and stamps points at their own raw
collection time instead of the bucket grid when every returned bucket
is a singleton. The #3540 "no delta knowable" marker is carried into
a rated CTE (nulled, not filtered) so collection_count still counts
an unrated row and a bucket is never mistaken for a true singleton.

LoadTempDbAsync passes endUtc through and drops the now-redundant
client-side post-filter on the file-I/O series.
erikdarlingdata added a commit that referenced this pull request Sep 26, 2026
…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
erikdarlingdata added a commit that referenced this pull request Sep 26, 2026
…double-count

Adds source-check and live-store tests for the lock-wait, waiting-task
and blocked-session trend reads, mirroring ViewerCpuTempDbTests (#4234)
and LiteMemoryFileIoTrendBucketingTests (#4340): a 7-day row-budget
cap, point equality for the rate/count formulas, and singleton-window
raw-timestamp stamping, for both products.

Fixes a defect the tests surfaced: BlockedSessionTrendSql/its Lite
twin summed a per-snapshot gauge (COUNT(*) blocked sessions) across
every merged collection in a bucket, double- (or N-tuple-) counting
purely because a wide bucket merged more snapshots, with no more
blocking having happened. Both reads now average the per-collection
count over the merged bucket (matching the CPU tab's gauge-averaging
idiom), via a per_collection CTE that keeps the un-bucketed read's own
count unchanged.

Refs #4349.
erikdarlingdata added a commit that referenced this pull request Sep 26, 2026
…essions (#4349) (#4362)

* Bucket the blocking-trend reads: lock waits, waiting tasks, blocked sessions (#4234) (#4349)

GetLockWaitTrendAsync, GetWaitingTaskTrendAsync, and GetBlockedSessionTrendAsync
returned one row per collection for both products, unbucketed at the 7-day window
unlike #4353's (Darling) and #4340's (Lite) already-bucketed twins for the other
trend charts. Each now buckets to TrendBudget.Chart's per-series point budget via
TrendBuckets.AutoMinutes (date_bin on Darling, time_bucket on Lite, both anchored to
the shared bucket origin), stamps points at their own raw collection time instead of
the bucket grid when every returned bucket holds exactly one physical collection, and
carries the #3540 unrated-collection rule for the lock-wait rate (nulled into a rated
CTE, not filtered, so collection_count still counts an unrated row).

Waiting-task duration and blocked-session count have no interval-based delta to rate
(a waiting-task row is a point-in-time snapshot, not a counter), so every row in those
two reads is unconditionally "rated" and collection_count is a plain COUNT(*).

Darling: ViewerDataService.BlockingTrends.cs. Lite: LocalDataService.Blocking.cs,
LocalDataService.WaitingTasks.cs.

* Add #4349 bucketing tests for the blocking trio; fix blocked-session double-count

Adds source-check and live-store tests for the lock-wait, waiting-task
and blocked-session trend reads, mirroring ViewerCpuTempDbTests (#4234)
and LiteMemoryFileIoTrendBucketingTests (#4340): a 7-day row-budget
cap, point equality for the rate/count formulas, and singleton-window
raw-timestamp stamping, for both products.

Fixes a defect the tests surfaced: BlockedSessionTrendSql/its Lite
twin summed a per-snapshot gauge (COUNT(*) blocked sessions) across
every merged collection in a bucket, double- (or N-tuple-) counting
purely because a wide bucket merged more snapshots, with no more
blocking having happened. Both reads now average the per-collection
count over the merged bucket (matching the CPU tab's gauge-averaging
idiom), via a per_collection CTE that keeps the un-bucketed read's own
count unchanged.

Refs #4349.

* Fix two #4349 test pins: outer-select scope and the rated-CTE rewrite (#4349)

BlockedSessionTrendSql_CarriesABucketWidth test asserted no COUNT(*) AS
blocked_count anywhere in the SQL, but the inner per_collection CTE
legitimately keeps that per-snapshot COUNT(*) - it's what the outer AVG
averages. Scoped the assertion to the outer SELECT (after the CTE closes)
instead of the whole SQL text.

LockWaitTrendSql_FiltersLckPrefix_PerSecondRateViaLag pinned the old
per-row "delta_wait_time_ms / interval_seconds" text. The bucketed SQL now
computes the same per-second rate through a time-weighted rated CTE
(SUM(rated_wait_ms) / SUM(rated_seconds)), matching WaitStatsTrendsSql's
existing idiom. Updated the pin to the new text and kept every behavioural
assertion.

* Fix CRLF-sensitive outer-SELECT locator in blocking trend bucket test

* Fix outer-SELECT locator: raw-string dedent strips leading indentation
erikdarlingdata added a commit that referenced this pull request Sep 26, 2026
…4349) (#4361)

* Bucket the CPU scheduler, session stats, and plan cache trend reads (#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.

* Add bucketing tests for CPU scheduler, Session Stats, and Plan Cache 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

* Update three stale trend-order pins for the #4234/#4349 bucketed reads

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.

* Normalize line endings before the GROUP BY/ORDER BY substring assert in the #4349 trend-bucket pins

* Assert GROUP BY/ORDER BY order in the trend-bucket pins independent of raw-string indentation
erikdarlingdata added a commit that referenced this pull request Sep 26, 2026
…4364)

* Bucket the memory-grant and TempDB usage trend reads (#4234)

MemoryGrantTrendSql, MemoryGrantChartDataSql (Darling) and their Lite
twins GetMemoryGrantTrendAsync/GetMemoryGrantChartDataAsync returned
one row per collection (or per collection+pool) for the 7-day window,
unbucketed like the reads #4353 and #4340 already fixed. TempDbTrendSql
(Darling) and Lite's GetTempDbTrendAsync were the same, plus start-only
on the window end.

All four now bucket to TrendBudget.Chart's point budget with the
GREATEST(date_bin/time_bucket(...), window start) shape #4234's other
PRs use: every gauge (sizing MB, grantee/waiter counts, tempdb space)
averages per bucket; the two true deltas in the grant chart
(timeout_error_count_delta/forced_grant_count_delta) sum over a rated
CTE that nulls (not drops) an unrated row per #3540. TempDB's
top_session_id/top_session_tempdb_mb are not averaged (a session ID
average is meaningless) -- both take the bucket's last raw collection's
pair. A point is stamped at its bucket's raw first_collection_time only
when every bucket the call returned holds exactly one physical
collection.

GetTempDbTrendAsync (Darling) now takes (serverId, startUtc, endUtc)
like the other bucketed reads; LoadTempDbAsync passes endUtc through
and drops the now-redundant client-side post-filter, matching #4353's
fileIo change to the same method.

* Add bucketing tests for memory-grant/tempdb trend reads (#4349); fix TempDbTrendAsync reader-reuse bug

* Route memory-grant chart's derived interval_seconds through NULLIF (#4349); update stale per-collection pins to the bucketed shape

Fixes item 4 (Lite/Darling MeasurementContractCensusTests): MemoryGrantChartDataSql's per_collection CTE aliased MAX(sample_interval_seconds) AS interval_seconds bare, so the rated CTE's IS DISTINCT FROM 0 test read the value outside NULLIF -- the census's rule-1 shape. Wrapped in NULLIF(...,0) like every other derived interval_seconds alias on the tree; the rated CASE now tests IS NOT NULL (equivalent once the value is NULLIF'd). Same fix in Lite's MemoryGrants.cs mirror.

Also fixes items 1-2 (ViewerMemorySqlTests.MemoryGrantChartDataSql_GroupsPerPool.../MemoryGrantTrendSql_SumsGrantedMb...): the pins asserted the pre-#4349 per-collection-only text (bare GROUP BY / ORDER BY collection_time). Updated to assert the surviving per_collection shape PLUS the new outer bucket (date_bin, AVG, GROUP BY pool_id/1, MIN/COUNT singleton columns) -- every prior behavioral assertion (MB cast to double, counts to bigint, grouping) kept.

* Resolve #4364's MCP/viewer parity pin: extract the shared per-collection read (#3548, #4349)

Both DarlingTrendReader.MemoryGrantTrendSql (MCP) and ViewerDataService.MemoryGrantTrendSql
(viewer) now build on TrendBucketSql.MemoryGrantPerCollectionSql, the byte-for-byte shared
per-collection CTE body #3548's one-shared-read doctrine requires. Each side keeps its own
outer bucketing wrapper: MCP's unbucketed read (fed into its own MemoryGrantTrendBucketedSql)
is unchanged in behavior, and the viewer's bucketed overlay is unchanged in behavior. The
pin now asserts both SKUs' SQL contains the shared constant, and that MCP's read is exactly
the shared constant plus its trailing ORDER BY.

No Lite MCP/viewer parity pin for this read exists (Lite's get_memory_trend calls
GetGrantBucketsAsync, which already shares Lite's own DuckDB SQL with the chart — no
duplicate string to pin).

* Fix Memory Grants chart: SUM true deltas per bucket, don't average them (#4349)

The outer bucketed select in MemoryGrantChartDataSql (Darling) averaged
timeout_error_count_delta and forced_grant_count_delta across a bucket's
merged collections instead of summing them. Those two columns are true
accumulating event counts, not gauges - averaging them silently drops
real events (2 collections with deltas 3 and 4 averaged to 3.5, CAST to
bigint rounded to 4, and small counts like a single-digit total rounded
to 0).

Fixed both the per_collection CTE (CAST(SUM(...) AS bigint), matching
dev's pre-bucket shape) and the outer rated-CTE aggregate (CAST(SUM(...)
AS bigint) instead of the implicit SUM-without-cast that the pin
expected, and no longer AVG). Lite's twin (LocalDataService.MemoryGrants.cs)
was already correct - it already summed the rated deltas at the outer
level; no change needed there. The memory-grant overlay (MemoryGrantTrendSql)
and TempDbTrendSql carry no delta/counter columns (only gauges), so no
fix was needed on those sibling reads.

Verified with a throwaway net10.0 harness against a live TimescaleDB
container reproducing the exact per_collection/rated CTE shape: the old
outer-AVG form returned 4 for two merged collections with deltas 3 and 4
(matching the CI failure's Expected 7 / Actual 0 pattern - a larger
merge of ~10 collections carrying one 7-count event averaged to <1,
rounding to 0); the fixed outer-SUM form returns 7.

Added a merged-bucket live pin (MemoryGrantChart_MergedBucket_SumsDeltas_AveragesGauges_AgainstDevPostgres)
asserting gauges AVERAGE and deltas SUM when two collections merge into
one bucket, plus a SQL-shape assertion that the outer select uses
CAST(SUM(rated_*_delta) AS bigint), never AVG.

* Fix Memory Grants chart's unknown-interval delta drop; redesign merged-bucket pin (#4364)

The NULLIF(sample_interval_seconds, 0) guard collapses a genuine
restart's known-zero interval and a pre-V128 row's true NULL (never
recorded) into the same NULL, so the rated CTE's IS NOT NULL test
dropped BOTH cases' deltas instead of only the restart's. Track the
raw MAX(sample_interval_seconds) alongside the NULLIF alias and rate
off IS DISTINCT FROM 0 against the raw value, so an unknown interval
keeps its delta while a known-zero interval still nulls it -- Lite's
twin gets the same fix.

Also fixes the merged-bucket live pin: two collections 7 days apart
do not share a date_bin bucket at a 7-day window's auto-chosen width
(tens of minutes), so the pin never exercised the merge it claimed to
test. Both collections now land in the same floored minute instead.

* Anchor the merged-bucket test seeds to a minute boundary (#4349)
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