Skip to content

WPF Wait Stats and Perfmon trend charts bucket server-side (#4234) - #4304

Merged
erikdarlingdata merged 6 commits into
devfrom
fix/4234-wpf-wait-perfmon-trend-buckets
Sep 25, 2026
Merged

erikdarlingdata merged 6 commits into
devfrom
fix/4234-wpf-wait-perfmon-trend-buckets

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Part of #4234. (File I/O, memory clerks, the Overview lanes, Performance Trends and Lite are later lanes.)

Why

Lane T1 built the Wait Stats and Perfmon trend bucketing and the two name-list caches (build succeeded,
0 warnings) but never stood up a rig, wrote no tests, and ran no suite. The coordinator reviewed the diff
against the #4234 ruling and found two defects before any of that verification had happened. This PR
finishes the lane: fixes both defects, writes the ruling's four required tests plus an MCP-twin equality
pin, measures on a real rig, and runs the full suite.

What was wrong

  1. Ruling item 3 ("when the budget covers every collection in the window, the chart gets the raw points
    unchanged") was not met.
    At a width that gave each collection its own bucket, WaitTrendsSql and
    PerfmonTrendsSql still stamped every point at its date_bin bucket start, never the raw
    collection_time — T1's own PR description called this out as a deliberate, accepted difference
    ("only the timestamp changes"). Proven live: seeding three collections five minutes apart with
    non-zero seconds (:37) and reading them back at the finest width returned 09:00:00, 09:05:00, 09:10:00 instead of the seeded 09:00:37, 09:05:37, 09:10:37.
  2. ViewerNameListCache keyed a hit on (server, window length) alone. A 7-day custom range from last
    month could reuse this week's cached 7-day wait-type or counter list, because both windows share one
    length-keyed slot and nothing checked which window was actually fetched.

What changes

  • WaitTrendsSql / PerfmonTrendsSql now also project first_collection_time (MIN(collection_time),
    mirroring DurationTrendRouting.BuildBucketedRawTrendSql's own column of that name) and
    collection_count (COUNT(*)) per bucket. GetWaitStatsTrendsByTypesAsync and
    GetPerfmonTrendsByCountersAsync buffer every row and, only when EVERY bucket the whole call returned
    holds exactly one physical collection, stamp each point at its bucket's first_collection_time instead
    of bucket_start — reproducing 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 read's timestamps exactly when nothing was merged, and
    falling back to bucket_start (what the MCP twins always serve) the instant any bucket in the call
    merged two or more collections. The MCP twins (WaitTrendBucketedSql, PerfmonTrendBucketedSql,
    DurationTrendRouting.BuildBucketedRawTrendSql) carry no such switch themselves — they stamp bucket
    starts at every width — so this is the least-invention reading of item 3 available: it reuses the
    twins' own first_collection_time metadata column rather than adding a new mechanism, and the decision
    is made once per whole call (not per series, not per row), which keeps it simple to state and to test.
  • ViewerNameListCache.Entry now also carries the fetched window's WindowEndUtc. A hit requires BOTH
    the existing wall-clock freshness check AND the cached entry's window end to sit within the 15-minute
    TTL of the requested end (Duration(), checked both directions — a custom range's end can move earlier
    than the cached one, not just later like a sliding preset's auto-refresh always does).
    GetDistinctWaitTypesAsync / GetDistinctPerfmonCountersAsync thread endUtc through TryGet/Set.
  • Two PRE-EXISTING tests (WaitTrendsSql_KeepsLitesPerTypeLagPerSecondMath,
    PerfmonTrendsSql_SumsValueAndDelta_ButTakesTheIntervalAsMax) pinned the old per-collection SQL's exact
    text and were broken by T1's original bucketing commit, silently, because the suite was never run.
    Fixed to pin the new bucketed shape instead of reverting the SQL.
  • New Darling/Darling.Tests/ViewerTrendBucketingReviewTests.cs: the ruling's four required tests plus
    one equality pin (see Test plan).

Measurement (rig: PostgreSQL 18 + TimescaleDB, port 55973, UTC)

Seeded 7 days at the real 1-minute cadence (CollectorScheduleDefaults) for 20 wait types (201,620 rows,
close to the issue's cited 170,020) and 12 counters (120,972 rows, close to the issue's cited 102,012) on
a dedicated probe database. EXPLAIN (ANALYZE, BUFFERS), old SQL via git show origin/dev:<file>
against the new:

Read Rows seeded Old (pre-#4234) rows returned Old exec time New (bucketed, width=10min) rows returned New exec time
Wait trend, 20 types 201,620 201,620 1,117 ms 20,180 1,530 ms
Perfmon trend, 12 counters 120,972 120,972 1,133 ms 12,108 788 ms
Wait-type DISTINCT (name list) 201,620 20 rows either way — SQL unchanged, item 4 changes only how often it runs 172 ms (same SQL) (same SQL)
Perfmon-counter DISTINCT (name list) 120,972 12 rows either way — SQL unchanged 43 ms (same SQL) (same SQL)

Both trend reads cut the row count returned by about 10x at this window/width, matching the issue's
complaint that every raw collection shipped to a chart that cannot draw more points than it has pixels.
Backend execution time is NOT uniformly faster — the wait trend's new plan does more CPU work per row
(an extra GroupAggregate layer over the same 201,620-row scan) and measured slightly slower in-database
despite returning 10x fewer rows; the perfmon trend measured both faster and smaller. The real win either
way is the payload actually sent to and rendered by the WPF chart, not raw backend CPU time, and that
drops by an order of magnitude on both reads. The name-list EXPLAIN is unchanged before/after, as
expected — ruling item 4 only changes how often it runs (at most once per 15 minutes now, was every
1-minute auto-refresh), never the SQL itself.

Test plan

  • dotnet build Darling/PerformanceMonitor.Darling.Viewer/PerformanceMonitor.Darling.Viewer.csproj -c Debug
    — Build succeeded, 0 Warning(s), 0 Error(s).
  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug — Build succeeded, 0 Warning(s),
    0 Error(s), after merging origin/dev.
  • Source pin: WaitTrendsSql/PerfmonTrendsSql carry a bucket width (date_bin(CAST($n AS integer)...), proven once by hand to fail 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 SQL: git show origin/dev:.../ViewerDataService.Waits.cs (and the Perfmon twin) contain zero occurrences of
    date_bin.
  • Live pin: a 7-day, 2-series read returns at most budget × series rows
    (ViewerTrendBucketingLiveTests.WaitTrend_SevenDayWindow_ReturnsAtMostBudgetTimesSeriesRows and its
    Perfmon twin).
  • Live pin: point equality when the budget covers every collection — raw timestamps (off the minute
    grid) AND values unchanged
    (ViewerTrendBucketingLiveTests.WaitTrend_BudgetCoversEveryCollection_ReturnsRawTimestampsAndValuesUnchanged
    and its Perfmon twin). Proven to fail on the pre-fix behavior by temporarily forcing
    everyBucketSingleton = false and reverting — both failed exactly as expected (09:00:00 instead
    of the seeded 09:00:37), confirmed passing again after revert.
  • Cache pin: a second refresh inside 15 minutes runs no DISTINCT (ViewerNameListCacheTests, unit
    tests directly against ViewerNameListCache — hit within TTL, miss at 15 minutes, PLUS the defect-2
    pin: miss when the cached window's end is far from the requested end despite a fresh fetch, and the
    two-sided early-end case).
  • Extra equality pin: for one wait type and one counter, the WPF read's bucket starts and values match
    DarlingDataReader.GetWaitBucketsAsync / DarlingTrendReader.GetPerfmonBucketsAsync at the same
    explicit width (60 minutes, multi-collection buckets, so this exercises the shared aggregation math
    rather than the singleton-collapsing wrapper the point-equality pins above already cover).
  • Targeted classes (*ViewerWaitStats*, *ViewerPerfmon*, *ViewerTrendBucket*,
    *ViewerNameListCache*): 40/40 passed against the rig.
  • git merge origin/dev — clean, no conflicts.
  • Full Darling.Tests.exe run against a freshly dropped/recreated darlingtest, on a UTC rig, after
    the merge: Total: 14063, Errors: 0, Failed: 4, Skipped: 49, Not Run: 1 (914.8s). All 4 failures
    are outside this lane's files (TrendPayloadBudgetLiveTests, CaptureDownChunkOrderTests,
    ServerListAndSummaryPlanShapeTests, DarlingCliCommandsHostCheckTests — plan capture, plan
    shape, CLI host checks; nothing that touches Wait Stats, Perfmon, or the name-list cache). A first
    run (before a fix below) additionally failed DocCommentHygieneTests and NOT
    DarlingCliCommandsHostCheckTests — the failure set changed between two back-to-back runs on
    unchanged code, which is the signature of environmental flakiness (likely rig contention: the first
    run overlapped with the measurement seeding/EXPLAIN queries below on the same PostgreSQL instance),
    not a deterministic regression. Cross-checked against dev's latest COMPLETED CI Build run
    (36156144337): it fails a different, also-unrelated test (PgTargetBlockingTests), so none of my 4
    are freshly confirmed by CI either way — flagged here for the coordinator's census rather than
    filed as issues from this lane.
  • Fixed in passing: DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks caught two stacked
    <summary> blocks T1 left behind in both WaitTrendsSql and PerfmonTrendsSql (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
    summary never merged with the one added for bucketing). Merged into one with <para> breaks.

Note on scope

I did not add a raw-vs-bucketed SQL switch, because none exists anywhere in this family today — the MCP
twins (get_wait_trend, get_perfmon_trend, the duration-trend raw tier since #3897) all stamp
date_bin bucket starts unconditionally, at every width, and carry no "don't bucket" branch. Item 3 is
met here by reusing the twins' own first_collection_time metadata column as the point's stamp exactly
when nothing was merged, which is the smallest change that satisfies the ruling's literal wording without
inventing a mechanism the rest of the codebase doesn't have.

CHANGELOG entry

SECTION: Fixed
ENTRY:

🤖 Generated with Claude Code

https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ

erikdarlingdata and others added 6 commits September 25, 2026 11:27
Part of #4234. WaitTrendsSql and PerfmonTrendsSql now bucket every
selected series to TrendBudget.Chart points via date_bin, mirroring
the MCP get_wait_trend/get_perfmon_trend twins generalized to many
series in one query. The picker name lists (DistinctWaitTypesSql,
DistinctPerfmonCountersSql) are now memoized per (server, window
length) for 15 minutes through a new ViewerNameListCache, so the
full-window DISTINCT no longer reruns on every 1-minute auto-refresh.

Builds with 0 warnings. Live pins, EXPLAIN measurements and the full
suite run are still outstanding (see PR body).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…list cache window-end check

Two defects the coordinator found in PR #4304 (#4234 lane 1):

1. Item 3 of the ruling ("when the budget covers every collection in the window, the
   chart gets the raw points unchanged") was not met: the bucketed SQL always stamped
   points at their date_bin bucket start, never the raw collection_time, even when a
   bucket held exactly one physical collection. Both WaitTrendsSql and PerfmonTrendsSql
   now also project first_collection_time (MIN(collection_time), mirroring
   DurationTrendRouting.BuildBucketedRawTrendSql's own column) and collection_count
   (COUNT(*)) per bucket. The two C# readers buffer every row and, only when every
   bucket the whole call returned is a singleton, stamp each point at its bucket's
   first_collection_time instead of bucket_start - exactly reproducing the pre-#4234
   read's timestamps when nothing was merged, and falling back to bucket_start (what
   the MCP twins always serve) the moment any bucket in the call merged two or more
   collections.

2. ViewerNameListCache keyed a hit on (server, window length) alone, so a 7-day custom
   range from last month could reuse this week's 7-day list. A hit now also requires
   the cached entry's window end to sit within the 15-minute TTL of the requested end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…L, MCP-twin equality

Darling/Darling.Tests/ViewerTrendBucketingReviewTests.cs, covering the #4234 ruling's four
required tests plus one extra equality pin:
- ViewerTrendBucketWidthSqlTests: source check that WaitTrendsSql/PerfmonTrendsSql carry a
  bucket width and the singleton-detection columns.
- ViewerNameListCacheTests: unit tests against ViewerNameListCache directly (hit within TTL,
  miss at 15 minutes, and the defect-2 pin - miss when the cached window's end is far from
  the requested end despite a fresh fetch, plus the two-sided early-end case).
- ViewerTrendBucketingLiveTests (gated on DARLING_TEST_PG): the 7-day row-budget cap, point
  equality when the budget covers every collection (off-grid seconds, so the old date_bin-floor
  behavior would fail this), and an equality pin against DarlingDataReader.GetWaitBucketsAsync /
  DarlingTrendReader.GetPerfmonBucketsAsync at a shared explicit width.

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

WaitTrendsSql_KeepsLitesPerTypeLagPerSecondMath and
PerfmonTrendsSql_SumsValueAndDelta_ButTakesTheIntervalAsMax predate #4234 and pinned the
old per-collection SQL's exact text (a per-row division, ORDER BY .. collection_time, no
SUM of sample_interval_seconds anywhere). T1's original #4234 commit changed both
statements' shape without running the suite, so these two silently broke. Updated both to
pin the new bucketed shape: the rated CTE's SUM/SUM ratios, avg_ms_per_wait's one
deliberate explicit "ELSE 0" (division_by_zero guard), the outer query's SUM(...)
FILTER(...) over already-per-collection-MAX'd intervals, and ORDER BY .. 2.

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

WaitTrendsSql and PerfmonTrendsSql each carried two stacked <summary> blocks (the
pre-#4234 summary immediately followed by a second one added for the bucketing change,
never merged into one). DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks
caught this on the first full-suite run. Merged into one summary with <para> breaks.

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 16:55
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 25, 2026 16:55
@erikdarlingdata
erikdarlingdata merged commit c51b43f into dev Sep 25, 2026
18 of 20 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4234-wpf-wait-perfmon-trend-buckets branch September 25, 2026 16:59
erikdarlingdata added a commit that referenced this pull request Sep 25, 2026
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
erikdarlingdata added a commit that referenced this pull request Sep 25, 2026
The clerk trend read (up to 20 clerks) returned every collection; it now buckets as
a gauge (average memory_mb per bucket), same sizing as the MCP trend reads and the
File I/O reads just bucketed. A bucket holding exactly one physical collection keeps
that collection's own raw timestamp, matching WaitTrendsSql. DistinctMemoryClerkTypesSql
(the clerk picker's population read) now sits behind ViewerNameListCache, same as
DistinctWaitTypesSql in PR #4304, so the full-window DISTINCT runs at most once per
15 minutes instead of on every auto-refresh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
erikdarlingdata added a commit that referenced this pull request Sep 25, 2026
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
erikdarlingdata added a commit that referenced this pull request Sep 25, 2026
…4234) (#4329)

* Bucket the File I/O latency and throughput viewer trend reads (#4234)

Both reads used to return every collection (measured 85,010 rows at 7 days for one
store on the File I/O tab). Groups now by TrendBudget.Chart-sized date_bin buckets,
same sizing as the MCP trend reads: latency averages summed stall-ms over summed
read/write count deltas per bucket, throughput sums byte deltas over summed interval
seconds. A bucket holding exactly one physical collection (rated or not) keeps that
collection's own raw timestamp instead of the bucket_start grid line, matching
WaitTrendsSql's pattern from PR #4304. Also pulls in the InternalsVisibleTo grant and
ViewerNameListCache dependency shared with that PR's pattern.

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

* Bucket the memory clerk viewer trend read and cache its picker (#4234)

The clerk trend read (up to 20 clerks) returned every collection; it now buckets as
a gauge (average memory_mb per bucket), same sizing as the MCP trend reads and the
File I/O reads just bucketed. A bucket holding exactly one physical collection keeps
that collection's own raw timestamp, matching WaitTrendsSql. DistinctMemoryClerkTypesSql
(the clerk picker's population read) now sits behind ViewerNameListCache, same as
DistinctWaitTypesSql in PR #4304, so the full-window DISTINCT runs at most once per
15 minutes instead of on every auto-refresh.

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

* Pin the File I/O and memory clerk bucketing with source and live tests (#4234)

Updates the existing SQL-text assertions that the bucketing rewrite made stale
(GROUP BY/ORDER BY shape, the rated-CTE column names, the CAST(memory_mb...) literal),
and adds the ruling's four required tests per read: a bucket-width source check, a
live 7-day-window row-budget cap, live point equality with the old shape when every
bucket holds one collection (seeded off the minute grid at :37 seconds), and for the
memory clerk picker, a live proof that a second call inside 15 minutes answers from
cache and that a window end past the TTL misses.

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

* Fix a miscalibrated byte constant in the new FileIo throughput singleton test

The singleton test's t1..t3 are 5 minutes apart, but the byte delta was calibrated
for a 60-second interval (copied from the other throughput test, which is 60-second
spaced) -- it read 0.2 MB/s instead of the intended 1.0. Caught by actually running
the new test.

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>
erikdarlingdata added a commit that referenced this pull request Sep 25, 2026
…4331)

* 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

* Fix #4331 CI: reposition displaced doc blocks, update the pin count to 3

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

---------

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