Skip to content

Bucket the Darling viewer's TempDB file I/O trend (#4234) - #4353

Merged
erikdarlingdata merged 2 commits into
devfrom
fix/4234-tempdb-file-io-buckets
Sep 25, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
fix/4234-tempdb-file-io-buckets

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Closes #4234.

Why

TempDbFileIoTrendSql (ViewerDataService.TempDb.cs) was the last unbucketed trend read #4234 named: it
grouped v_file_io_stats by (collection_time, file_name) for tempdb and had no window end, so a wide
range returned one row per collection per file. Lite's twin already bucketed (LocalDataService.FileIo.cs,
#4340), and the File I/O tab's own two reads bucketed first (#4329), so the Darling tempdb file-I/O chart
was the one surface still shipping the pre-#4234 shape.

What changes

  • Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.TempDb.cs: TempDbFileIoTrendSql rewritten
    around a rated CTE — the Measurement-layer campaign: the delta honesty contract (11 findings, one keystone) #3540 "no delta knowable" marker (sample_interval_seconds = 0) is nulled out
    of the sums rather than filtered from the FROM clause, so an unrated row still counts toward
    collection_count and a bucket is never mistaken for a true singleton just because its one physical
    collection happened to be unrated (the same fix Bucket the Darling viewer's File I/O and memory clerk trend charts (#4234) #4329 made for the File I/O tab's reads). The outer query
    buckets on date_bin($4 minutes, collection_time, TrendBucketSql.OriginSql) floored to the window start,
    groups by file_name only (no top-10 ranking pass — tempdb's own file count is already small, matching
    Lite's read), and projects first_collection_time / collection_count for singleton detection.
    GetTempDbFileIoTrendAsync now takes (serverId, startUtc, endUtc) instead of (serverId, sinceUtc),
    computes its bucket width via TrendBuckets.AutoMinutes(windowMinutes, 1, TrendBudget.Chart.AutoPoints)
    exactly like GetFileIoLatencyTrendAsync, and stamps every point at its own raw collection time instead
    of the bucket grid when every bucket the call returned holds exactly one physical collection.
  • Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Charts.cs: LoadTempDbAsync passes endUtc
    into the file-I/O read and drops the now-redundant client-side post-filter on that series (the tempdb
    usage trend stays start-only and keeps its own post-filter, unchanged).
  • Darling/Darling.Tests/ViewerCpuTempDbTests.cs: updated the GROUP BY/ORDER BY source pin to the new
    bucketed shape, added a bucket-width/singleton-column source pin, updated the live per-file test to pass
    endUtc (its single-collection-per-file seed already proves the singleton pass-through: the assertion
    that every returned point's CollectionTime equals the raw seeded timestamp), and added a new live test
    plus a BulkSeedTempDbFileIoAsync helper proving the 7-day budget cap.
  • Darling/Darling.Tests/ViewerFileIoBlockingTests.cs: updated the cross-file IS DISTINCT FROM 0 pin to
    match the new CASE-based (not WHERE-filtered) idiom.

Row counts measured

Seeded 7 days of 1-minute file_io_stats rows for 4 tempdb files (generate_series, a throwaway
server_id) on the rig, then measured both shapes directly:

The automated live test (TempDbFileIo_SevenDayWindow_ReturnsAtMostBudgetTimesFileCount_AgainstDevPostgres)
pins the same shape generally: at most TrendBudget.Chart.AutoPoints * fileCount (1,500 × 4 = 6,000) rows
for any 7-day/4-file seed, not just this one measurement.

CHANGELOG entry

SECTION: Changed
ENTRY:

Test plan

  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj: 0 Warning(s), 0 Error(s)
  • Source-pin classes (ViewerCpuTempDbSqlTests, ViewerFileIoBlockingSqlTests, ViewerTrendBucketWidthSqlTests): 35 passed
  • Proved the new/changed pins 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 text: swapped TempDbFileIoTrendSql back
    to the old text, reran — 3 failures (FileIoLatencyReads_DropTheUnknowableMarker_KeepPreV127Rows,
    TempDbFileIoTrendSql_FiltersToTempDb_GroupsPerFileAndBucket_CastsStallToDouble,
    TempDbFileIoTrendSql_CarriesABucketWidth_AndProjectsSingletonDetectionColumns) — then restored and
    reconfirmed green.
  • Live classes on a PostgreSQL 18 + TimescaleDB rig (port 55983, UTC): ViewerCpuTempDbLivePostgresTests,
    ViewerFileIoBlockingLivePostgresTests, ViewerTrendBucketingLiveTests, ViewerNameListCacheTests: 62 passed
  • DocCommentHygieneTests, StorageCommandTimeoutTests, LivePostgresCollectionHygieneTests: 107 passed
  • ViewerCommandTimeoutTests (the fan-out census over the edited LoadTempDbAsync): 57 passed
  • Full Darling.Tests suite on a freshly created database: started after git merge origin/dev
    (no-op, branch was current) and a fresh darlingtest database. Still running in the background when
    this PR opened — 140+ of the suite's lines logged, one failure seen so far
    (TrendPayloadBudgetLiveTests.EveryDefaultAnswer_StaysNearTheBudget_AndTheLargestAnswerStaysUnderTheCap,
    get_file_io_trend over 4h answered a error instead of data: ... "Exception while reading from stream"). That test exercises the MCP get_file_io_trend tool in
    PerformanceMonitor.Darling.Service, a code path this PR does not touch (this PR only changes
    PerformanceMonitor.Darling.Viewer's WPF-side tempdb read); several other Darling.Tests processes
    were running concurrently on the same box against the same rig port range at the time (seen via
    Get-Process), which is consistent with a transient connection hiccup under shared load rather than a
    regression from this change. Flagging for the coordinator to confirm against a clean run rather than
    asserting it's pre-existing myself, per the lane rule on failures in files I didn't touch.

What the coordinator should double-check

  • The one full-suite failure above (TrendPayloadBudgetLiveTests), on a rig not shared with other
    concurrent lanes, to confirm it's unrelated to this PR.
  • That the CHANGELOG entry's row counts read as measured facts, not a performance claim beyond what was measured.

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.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
@erikdarlingdata
erikdarlingdata merged commit 04e1c92 into dev Sep 25, 2026
15 of 16 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4234-tempdb-file-io-buckets branch September 25, 2026 23:42
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
…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