Repository navigation
Bucket the Darling viewer's TempDB file I/O trend (#4234) - #4353
Merged
Merged
Conversation
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
marked this pull request as ready for review
September 25, 2026 22:45
erikdarlingdata
enabled auto-merge (squash)
September 25, 2026 22:45
erikdarlingdata
disabled auto-merge
September 25, 2026 23:27
This was referenced Sep 25, 2026
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4234.
Why
TempDbFileIoTrendSql(ViewerDataService.TempDb.cs) was the last unbucketed trend read #4234 named: itgrouped
v_file_io_statsby(collection_time, file_name)for tempdb and had no window end, so a widerange 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:TempDbFileIoTrendSqlrewrittenaround a
ratedCTE — the Measurement-layer campaign: the delta honesty contract (11 findings, one keystone) #3540 "no delta knowable" marker (sample_interval_seconds = 0) is nulled outof the sums rather than filtered from the FROM clause, so an unrated row still counts toward
collection_countand a bucket is never mistaken for a true singleton just because its one physicalcollection 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_nameonly (no top-10 ranking pass — tempdb's own file count is already small, matchingLite's read), and projects
first_collection_time/collection_countfor singleton detection.GetTempDbFileIoTrendAsyncnow 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 insteadof the bucket grid when every bucket the call returned holds exactly one physical collection.
Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Charts.cs:LoadTempDbAsyncpassesendUtcinto 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 theGROUP BY/ORDER BYsource pin to the newbucketed 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 assertionthat every returned point's
CollectionTimeequals the raw seeded timestamp), and added a new live testplus a
BulkSeedTempDbFileIoAsynchelper proving the 7-day budget cap.Darling/Darling.Tests/ViewerFileIoBlockingTests.cs: updated the cross-fileIS DISTINCT FROM 0pin tomatch the new CASE-based (not WHERE-filtered) idiom.
Row counts measured
Seeded 7 days of 1-minute
file_io_statsrows for 4 tempdb files (generate_series, a throwawayserver_id) on the rig, then measured both shapes directly:GROUP BY collection_time, file_name):40,324 rows.
GetTempDbFileIoTrendAsync's actual bucket width for this window, auto-picked to 10minutes): 4,036 rows — about a 90% reduction for this seed.
The automated live test (
TempDbFileIo_SevenDayWindow_ReturnsAtMostBudgetTimesFileCount_AgainstDevPostgres)pins the same shape generally: at most
TrendBudget.Chart.AutoPoints * fileCount(1,500 × 4 = 6,000) rowsfor any 7-day/4-file seed, not just this one measurement.
CHANGELOG entry
SECTION: Changed
ENTRY:
REF:
[Bucket the Darling viewer's TempDB file I/O trend (#4234) #4353]: Bucket the Darling viewer's TempDB file I/O trend (#4234) #4353
Test plan
dotnet build Darling/Darling.Tests/Darling.Tests.csproj: 0 Warning(s), 0 Error(s)ViewerCpuTempDbSqlTests,ViewerFileIoBlockingSqlTests,ViewerTrendBucketWidthSqlTests): 35 passedTempDbFileIoTrendSqlbackto the old text, reran — 3 failures (
FileIoLatencyReads_DropTheUnknowableMarker_KeepPreV127Rows,TempDbFileIoTrendSql_FiltersToTempDb_GroupsPerFileAndBucket_CastsStallToDouble,TempDbFileIoTrendSql_CarriesABucketWidth_AndProjectsSingletonDetectionColumns) — then restored andreconfirmed green.
ViewerCpuTempDbLivePostgresTests,ViewerFileIoBlockingLivePostgresTests,ViewerTrendBucketingLiveTests,ViewerNameListCacheTests: 62 passedDocCommentHygieneTests,StorageCommandTimeoutTests,LivePostgresCollectionHygieneTests: 107 passedViewerCommandTimeoutTests(the fan-out census over the editedLoadTempDbAsync): 57 passedDarling.Testssuite on a freshly created database: started aftergit merge origin/dev(no-op, branch was current) and a fresh
darlingtestdatabase. Still running in the background whenthis 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 MCPget_file_io_trendtool inPerformanceMonitor.Darling.Service, a code path this PR does not touch (this PR only changesPerformanceMonitor.Darling.Viewer's WPF-side tempdb read); several other Darling.Tests processeswere 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 aregression 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
TrendPayloadBudgetLiveTests), on a rig not shared with otherconcurrent lanes, to confirm it's unrelated to this PR.