Skip to content

Bucket Lite's Wait Stats and Perfmon trend and picker reads (#4234) - #4331

Merged
erikdarlingdata merged 12 commits into
devfrom
fix/4234-lite-waits-perfmon-buckets
Sep 25, 2026
Merged

erikdarlingdata merged 12 commits into
devfrom
fix/4234-lite-waits-perfmon-buckets

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Part of #4234.

Why

Lite's Wait Stats and Perfmon trend charts read one row per collection over the whole window. At 7
days that is 196,800 rows for 20 wait types and 118,080 rows for 12 counters (#4234's measured
numbers). The two picker reads (GetDistinctWaitTypesAsync, GetDistinctPerfmonCountersAsync) also
ran a full-window DISTINCT on every 1-minute auto-refresh. This is the Lite twin of PR #4304 (the
Darling/WPF viewer side, now merged to dev), applying the same #4234 ruling to Lite's DuckDB reads.

What changes

  • Lite/Services/LocalDataService.WaitStats.cs: GetWaitStatsTrendsByTypesAsync now buckets to
    TrendBudget.Chart's point budget PER SERIES via a new rated CTE (summed wait/signal over summed
    rated seconds, avg_ms_per_wait as summed wait over summed waiting tasks) grouped on
    time_bucket(...). When every bucket the whole call returns holds exactly one physical collection,
    points are stamped at that collection's own raw first_collection_time; a single merged bucket
    anywhere keeps the bucket_start grid line throughout. The SQL text moved into a new internal static
    WaitTrendsSql(waitTypeCount) so its shape is checkable without a live DuckDB, mirroring
    ViewerDataService.WaitTrendsSql.
  • Lite/Services/LocalDataService.Perfmon.cs: GetPerfmonTrendsByCountersAsync now wraps the
    unchanged per-collection SUM as a subquery and re-aggregates into buckets: Value is the bucket's
    rounded gauge average, DeltaValue/SampleIntervalSeconds are summed only over rated collections so
    their ratio downstream stays the ruling's rate, CntrType keeps the per-collection MIN=MAX rule now
    double-aggregated. Same singleton-bucket raw-timestamp rule and same PerfmonTrendsSql(counterCount)
    extraction.
  • Lite/Services/LiteNameListCache.cs (new): a small TTL cache — per (server, window length), 15
    minutes, a hit also requires the cached window's end to sit within the TTL of the requested end —
    ported from Darling's ViewerNameListCache. GetDistinctWaitTypesAsync and
    GetDistinctPerfmonCountersAsync each get their own cache instance, but see the T3b2 section below:
    the cache check and the nowUtc clock seam ended up on new picker-only wrapper methods instead of on
    these two shared ones. A later lane can reuse this cache for the memory-clerk picker.
  • Lite.Tests/PerfmonIntervalAggregationTests.cs: two 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 source-census pins asserted exact
    textual counts of SUM(sample_interval_seconds)/SUM(delta_cntr_value) across the whole file. The
    new bucket-level aggregation legitimately adds one more of each (summing each collection's own
    already-aggregated value across the collections in a bucket — the ruling's rate, not the original
    12-17x-inflation bug the pins guard against). Narrowed/updated rather than weakened: see the commit
    and in-file comments for exactly what each pin now allows and why.

Both Get*ByTypesAsync/Get*ByCountersAsync read collection_time as UTC already (GetTimeRange
without fromDate/toDate uses DateTime.UtcNow directly, "since collection_time is stored in UTC"
per its own comment) — bucketing keeps that, no timezone handling changed.

Diagnosis check

Confirmed by reading the pre-#4234 method bodies directly (both were plain per-collection
SELECT ... GROUP BY counter_name/wait_type, collection_time with no bucketing of any kind) rather
than re-measuring row counts against a live store — the brief pointed at #4234's body and
issuecomment-5835625728 for those numbers rather than asking this lane to re-measure them.

A real regression caught and fixed in-lane

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 can't cast that. PerfmonCounterTypeReadTests (an existing test, not one I added) caught
this immediately against the new SUM(sample_interval_seconds) FILTER (...) column. Fixed by routing
every new aggregate-derived column through the existing ToInt64/ToDouble helpers, matching
GetWaitBucketsAsync/GetPerfmonBucketsAsync's own established pattern for the same FILTER-summed
shape. Caught and fixed before the full-suite run, in the same PR.

T3b2: three review follow-ups, all decided by ruling

  1. MCP must not read the picker cache. GetDistinctWaitTypesAsync
    (Lite/Services/LocalDataService.WaitStats.cs) and GetDistinctPerfmonCountersAsync
    (LocalDataService.Perfmon.cs) went back to their dev shape — uncached, no nowUtc parameter —
    because McpWaitTools.cs/McpPerfmonTools.cs call them directly and must never answer with a
    picker snapshot up to 15 minutes stale. The TTL cache moved onto two new entry points,
    GetDistinctWaitTypesForPickerAsync and GetDistinctPerfmonCountersForPickerAsync, which only
    ServerTab.Refresh.cs's wait-type/perfmon-counter picker refresh calls now. Updated the two
    existing cache-TTL tests to call the picker methods (only they take nowUtc), and added one test
    per list proving the shared method never caches: two back-to-back calls with a new name seeded
    between them, and the second call sees it.
  2. A real multi-instance Perfmon test. 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 inner MAX into a SUM would have passed every DuckDB
    test in the file. Added two tests to LiteWaitPerfmonTrendBucketingTests.cs, both Transactions/sec-
    shaped with 12 instance rows per collection (delta 100, interval 300 each): one collection alone in
    its bucket returns DeltaValue 1,200 / SampleIntervalSeconds 300 (not 3,600 — the 12x-inflation
    shape); three collections merged into one bucket return 3,600 / 900, still 4/second regardless of
    how many collections merged. Proven by hand: reverting the inner MAX to SUM in
    PerfmonTrendsSql's inner CTE fails exactly these two new tests and no others (10/10 passed before
    the revert, 8/10 after, reverted back afterward).
  3. The unrated one-collection bucket — no code change. Traced ServerTab.Pickers.cs's
    DeltaSample/DeltaSeriesShaping.Shape (PerformanceMonitor.Common/DeltaSeriesShaping.cs). The
    decisive line is 220: if (s.Delta is not long delta || s.IntervalSeconds == 0) → NaN (no rate
    point), checked before any basis-specific branch. dev's raw pass-through for a lone unrated
    collection (a real, non-null delta_cntr_value paired with sample_interval_seconds literally 0)
    hits this via the IntervalSeconds == 0 arm; PerfmonTrendsSql's FILTER-driven NULL/NULL for
    that same shape hits it via the Delta is not long arm instead. Both land on the same NaN, for
    every basis except Level (a gauge), which reads only Value and never touches Delta/
    IntervalSeconds at all. So the two shapes are not distinguishable downstream — no code change, no
    new test (the brief's own fallback: pin only the branch where they diverge).

Test plan (T3b2 addendum)

  • Lite.Tests/Lite.Tests.csproj builds 0 Warning(s)/0 Error(s) after each of the three items above.
  • LiteWaitPerfmonTrendBucketingTests.cs's three classes, run by exact class name: 18 total (3
    LiteTrendBucketWidthSqlTests + 5 LiteNameListCacheTests + 10 LiteTrendBucketingLiveTests,
    including the 2 renamed picker-cache tests, the 2 new shared-method-never-caches tests, and the 2
    new multi-instance tests), 0 failed.
  • PerfmonIntervalAggregationTests (4), PerformanceMonitorLite.Tests.PerfmonCounterTypeReadTests
    (2), McpStatusEnvelopeTests (9) — 0 failed. (*McpWait*/*McpPerfmon* as literal class-name
    wildcards matched 0 classes; no test file in Lite.Tests has "McpWait" or "McpPerfmon" in a class
    name today. McpWaitTools.cs/McpPerfmonTools.cs themselves are untouched by item 1 — same
    method names, same call shape — so this is a naming mismatch in the brief, not a gap; the full
    suite below covers whatever does exercise those tool classes under their actual names.)
  • Merged origin/dev (brought in PR Bucket the Darling viewer's File I/O and memory clerk trend charts (#4234) #4329, the Darling viewer's File I/O/memory bucketing, and
    Lite Overview baseline bands use the server's actual hour under a custom range (#4320) #4325's baseline-band fix; unrelated to this lane), rebuilt clean, ran the FULL Lite.Tests.exe
    suite once: 5,433 total, 0 errors, 0 failed, 0 skipped.

Test plan

  • Lite/PerformanceMonitorLite.csproj builds with 0 Warning(s), 0 Error(s).
  • Lite.Tests/Lite.Tests.csproj builds with 0 Warning(s), 0 Error(s).
  • New Lite.Tests/LiteWaitPerfmonTrendBucketingTests.cs (14 tests, all against embedded DuckDB or
    in-memory, no live SQL Server/Postgres): source checks that WaitTrendsSql/PerfmonTrendsSql
    carry a time_bucket width plus first_collection_time/collection_count; LiteNameListCache
    hit/miss unit tests (ported from Darling's ViewerNameListCacheTests); a 7-day, one-collection-
    per-minute seed returns at most TrendBudget.Chart.AutoPoints * seriesCount rows for both reads;
    three collections seeded off the minute grid (:37 seconds) come back with raw timestamps and
    values unchanged, not floored to the bucket grid, for both reads; both pickers' cache proof (a
    second call inside 15 minutes misses a wait type/counter the store gained since that call, a call
    past the TTL picks it up).
  • Regression-checked the existing tests that exercise the two changed reads, run by exact class
    name (not file name, per the skill's "filter by every class" rule):
    DeltaFamilyUnknowableRowReadTests (12), PerfmonCounterTypeTests (17, two classes in that one
    file — PerfmonCounterTypeTests and PerfmonCounterTypeReadTests), PerfmonIntervalAggregationTests
    (4, after the pin fix). All pass.
  • Full Lite.Tests.exe suite, run once after merging origin/dev: 5424 total, 0 errors, 0
    failed
    (first run found 2 failures in PerfmonIntervalAggregationTests, both fixed and
    individually re-verified as described above; not re-run at full scale a second time given this
    lane's context budget, but the fix is narrow, mechanical, and independently verified).
  • No live SQL Server/PostgreSQL test needed — Lite's store is the embedded DuckDB itself.
  • Lite.Tests has no DocCommentHygiene* class (that pin is Darling-only); nothing to run there.

CI fix

The CI run at 29411369 failed two Darling tests. The product code is unchanged; c7f6e21c (after a merge of origin/dev) fixes both. Report: PR comment 5837999969.

  • DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks: the new PerfmonTrendsSql and WaitTrendsSql members had been inserted between an existing method's <summary> and the method. Each "Batched sibling of ..." block moved back to its own method (GetPerfmonTrendsByCountersAsync, GetWaitStatsTrendsByTypesAsync); the moved lines are unchanged.
  • PerfmonCounterTypeRungTests: Lite's LocalDataService.Perfmon.cs now holds the agreed counter-type expression three times, not two: once in the single-counter read and twice in the bucketed batched read (per collection inside, per bucket outside), as both Darling twins do. The pin says 3 and why.

CHANGELOG entry

SECTION: Changed
ENTRY:

For the coordinator to double-check

🤖 Generated with Claude Code

https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ

erikdarlingdata and others added 10 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
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 25, 2026 18:01
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 25, 2026 18:01
erikdarlingdata added a commit that referenced this pull request Sep 25, 2026
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
erikdarlingdata and others added 2 commits September 25, 2026 14:52
GetPerfmonTrendsByCountersAsync and GetWaitStatsTrendsByTypesAsync each got a
new internal SQL-text member inserted between their own <summary> and their
signature. Move each displaced block back to the method it describes; the
block already sitting on PerfmonTrendsSql/WaitTrendsSql does not move.

PerfmonCounterTypeRungTests pinned Lite's agreed-type expression count at 2;
Lite's Perfmon.cs now holds it 3 times (once in the single-counter read, twice
in the bucketed batched read, per collection inside and per bucket outside,
the same double aggregation both Darling twins use). Update the pin and its
summary text to match.

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

Copy link
Copy Markdown
Owner Author

CI fix pushed: c7f6e21c (on top of merging origin/dev at 8c92bd79, base was 29411369).

Failure 1: DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks. This PR inserted a new internal static string ...Sql(...) member between an existing method's <summary> and the method itself, in both files. Moved each displaced block back down to the member it actually describes; the block already sitting directly on the SQL-text member did not move. No attribute line sat between either moved block and its old position.

  • Lite/Services/LocalDataService.Perfmon.cs: the "Batched sibling of GetPerfmonTrendAsync..." block (previously lines 175-191, stacked above PerfmonTrendsSql) now sits directly above GetPerfmonTrendsByCountersAsync's signature (line 231 pre-merge / line 214 post-move in the current file). PerfmonTrendsSql's own "The bucketed batched-trend statement text..." block stayed put, now the sole <summary> on that member.
  • Lite/Services/LocalDataService.WaitStats.cs: same move for the "Batched sibling of GetWaitStatsTrendAsync..." block (previously lines 250-265, stacked above WaitTrendsSql), now directly above GetWaitStatsTrendsByTypesAsync's signature. WaitTrendsSql's own block stayed put.

Failure 2: PerfmonCounterTypeRungTests.EveryDarlingPerfmonRead_SelectsTheType_AndTheRowsKeepNullAsNull (Expected: 2 Actual: 3). Lite's LocalDataService.Perfmon.cs now holds the agreed-type expression (CASE WHEN MIN(cntr_type) = MAX(cntr_type) THEN MAX(cntr_type) END AS cntr_type) three times: once in the single-counter GetPerfmonTrendAsync read, and twice in the bucketed batched read added by this PR (once per collection in the inner subquery, once per bucket in the outer aggregation) — the same double aggregation both Darling twins (ViewerDataService.Perfmon.cs, DarlingTrendReader.cs) already carry on dev. Confirmed the count directly: grep -c of the literal expression against the file returns 3.

Changed, in Darling/Darling.Tests/PerfmonCounterTypeRungTests.cs:

  • line ~228 (now ~230): Assert.Equal(2, ...) → Assert.Equal(3, ...), with the comment the brief specified above it.
  • the method's <summary> (~215): "byte-identical to each other and to Lite's two" → "byte-identical to each other and to every copy in Lite's trend reads" (rewrapped the paragraph's line breaks to match the file's normal width; no wording beyond the specified swap changed).

Revert-proof: set the pin back to Assert.Equal(2, ...), rebuilt, ran the class — failed with Expected: 2 Actual: 3 on EveryDarlingPerfmonRead_SelectsTheType_AndTheRowsKeepNullAsNull, matching CI's original failure exactly. Restored to 3, rebuilt, reran — green.

Verification (all -c Debug, all builds 0 Warning(s)/0 Error(s)):

  • Lite/PerformanceMonitorLite.csproj, Lite.Tests/Lite.Tests.csproj, Darling/Darling.Tests/Darling.Tests.csproj all build clean.
  • Darling DocCommentHygiene*: Total: 77, Failed: 0 (checked twice, before and after the revert-proof).
  • Darling PerfmonCounterTypeRungTests: Total: 7, Failed: 0.
  • Lite LiteWaitPerfmonTrendBucketingTests.cs holds three classes, not one named for the file — ran each: LiteTrendBucketWidthSqlTests (Total: 3), LiteNameListCacheTests (Total: 5), LiteTrendBucketingLiveTests (Total: 10). All passed, 0 failed.
  • Lite PerfmonIntervalAggregationTests: Total: 4, Failed: 0.
  • git status after the merge and edits showed only the three files the brief named.
  • Not run: PerfmonCounterTypeLivePostgresTests, a second class living in the same file as PerfmonCounterTypeRungTests. It needs a live PostgreSQL target; per the brief this lane is unit-tests-only with no PostgreSQL rig, and CI's own PostgreSQL job covers it.

PR left as draft, not readied, not merged, not armed for auto-merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3

@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 25, 2026 19:02
@erikdarlingdata
erikdarlingdata merged commit fa05e9f into dev Sep 25, 2026
17 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4234-lite-waits-perfmon-buckets branch September 25, 2026 19:14
erikdarlingdata added a commit that referenced this pull request Sep 25, 2026
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 added a commit that referenced this pull request Sep 25, 2026
* Bucket Lite's Wait Stats trend and picker reads (#4234)

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

* Bucket Lite's Perfmon trend and picker reads (#4234)

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

* Fix InvalidCastException: read the new bucketed SUM columns through ToInt64/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

* Pull the two bucketed statements into internal static SQL-text methods

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

* Pin the #4234 ruling for Lite's Wait Stats and Perfmon trend/picker reads

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

* Narrow the pre-#4234 interval/delta SUM pins for the new bucket-level 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

* MCP wait/perfmon name-list tools stay uncached, only the picker memoizes

#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

* Add a live multi-instance Perfmon test for the 12-17x interval bug

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

* Bucket Lite's memory clerk trend and cache its picker's name list (#4234)

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

* Bucket Lite's File I/O latency and throughput trend reads (#4234)

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

* Add regression tests for Lite's clerk and File I/O bucketing (#4234)

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

* Fix doc-comment displacement: move 4 method summaries back above their 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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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