Repository navigation
FinOps refresh: take the FinOps tab off the 30s timer, merge its doubled query_stats and bounds reads (#4227) - #4240
Merged
Merged
Conversation
…d bounds reads (#4227) FinOps aggregated the same 24h of raw query_stats twice (top-consumers-by-total and by-average) and recomputed the same MAX/MIN(collection_time) bounds twice (the Storage Growth object drill's summary and series reads), all on a 30s poll even though the underlying figures move hourly at the fastest. Exempt FinOps from the fleet timer (matches Recommendations: refresh on tab activation, sub-tab switch, and the Refresh button only), merge the two top-consumer statements into one grouped by database_name with ranking moved client-side, and compute the object growth drill's bounds once and pass both instants to both statements. DatabaseSizeLatestSql's planning-time fix (job 4) and Lite parity for the two merges are not done here — left for a follow-up, with the reason in the PR body. Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… merges (#4227) Two live-postgres classes, both passing against a real rig: the merged TopResourceConsumersSql plus client-side ranking returns exactly what the old ByTotal/ByAvg statements (copied verbatim from origin/dev) returned, covering a NULL database_name, a zero-execution database, and a genuine cpu_time_ms tie. The object-growth bounds-computed-once form matches the old per-statement bounds pair, including the empty-window (collection stopped before the window) case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
This was referenced Sep 25, 2026
Lane-orders: keep the UTC rule without per-test claims, and set config before starting the rig
#4244
Merged
#4227 merged the three FinOps RetentionTierRouter.Resolve calls into two (WorkloadTopConsumersSql and WorkloadBoundsSql each carry one .For( lookup after the fleet statement merged them). Total production callers: 5. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
erikdarlingdata
marked this pull request as ready for review
September 25, 2026 06:56
This was referenced Sep 25, 2026
Job History: GROUP BY join replaces the per-job window function, tab off the 30s timer (#4229)
#4256
Merged
erikdarlingdata
added a commit
that referenced
this pull request
Sep 25, 2026
…off the 30s timer (#4229) (#4256) * Job History: replace the per-job window function with a GROUP BY join, take the tab off the 30s fleet timer (#4229) The window function recomputed AVG/MAX success duration and last-success time over every step row the 24h window matched, before the ORDER BY/LIMIT could trim to the newest 2,000. job_stats now computes that once per (server_id, job_id) with a GROUP BY over just the step_id-0 success rows, joined to the already- limited newest rows. Job History also moves off the 30s fleet timer (#4227/#4240's mechanism): it refreshes on tab activation and its Refresh button, matching job-run cadence instead of polling every tick. Lite's LocalDataService.GetJobHistoryAsync carried the identical window-function pattern; same fix there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * Lite parity: Job History group-by/join now has a DuckDB regression test (#4229) PR #4256 rewrote the Job History read on both products, but the Lite side (LocalDataService.JobHistory.cs) was only ever compiled -- no test ran the new job_stats GROUP BY/join statement against DuckDB. Adds JobHistoryGroupedStatsMatchWindowFunctionTests, comparing the new statement field-by-field and in order against the pre-fix window-function statement copied verbatim from origin/dev, through a tie at the LIMIT boundary and a NULL-average job, plus a two-server/shared-job_id case proving the join keys on (server_id, job_id). Verified the second case catches a regression: dropping server_id from the join turned Assert.Equal(2, mine.Count) into Actual: 4; restored, green again. 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>
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.
Part of #4227.
Why
The Mac's second-pass field measurements (issue comment) showed the FinOps tab's cost was not limited to
Server Inventory's idle-database read. That read belongs to lane V3a, on its own branch, and is untouched here.
The whole tab ran on the 30 second fleet timer while visible. Every figure on it moves hourly (utilization) or
daily (sizes, inventory, growth) at the fastest, so that timer bought nothing. Two of its reads also duplicated
work. The two top-consumer grids aggregated the same 24h of raw
query_statsindependently, and the StorageGrowth object drill's summary and series reads each recomputed the same
MAX/MIN(collection_time)bounds.What changes
FinOpsTabjoinsRecommendationsTabandOverviewTabinOnRefreshTimerTick's tabearly-return (
MainWindow.xaml.cs). Every FinOps sub-tab already reloads on tab activation, on its ownsub-tab switch, and on the FinOps Refresh button. That is
FinOpsTab.xaml.cs's existingRefreshActiveSubTabAsync,FinOpsSubTabs_SelectionChanged, andFinOpsRefresh_Click, none of whichchanged. Only the 30 second poll is removed.
TopResourceConsumersByTotalSqlandTopResourceConsumersByAvgSql(
ViewerDataService.FinOps.Workload.cs) are replaced by oneTopResourceConsumersSql, grouped bydatabase_name, computing both the total and theCASE WHEN execution_count > 0average in one pass. A newGetTopResourceConsumersAsyncreads it once and ranks and limits both grids client-side, withOrderByDescending(a stable sort) plusTake(topN). One statement can onlyORDER BY/LIMITone column,and the two grids rank by different ones, so that ranking step moved to C#. The tiered-retention routing
(
TopResourceConsumersSqlForfor raw/hourly/daily, viaRouteOrThrow/ConsumerCteForCagg) keeps its shape. Itit now runs once instead of twice, because the merged query's
workloadCTE is byte-identical to thefragment the routing already swaps.
ObjectGrowthSummarySqlandObjectGrowthSeriesSql(
ViewerDataService.FinOps.Storage.cs) no longer each carry their ownboundsCTE. A newObjectGrowthBoundsSqlcomputesMAX/MIN(collection_time)once.GetObjectGrowthHeatmapDataAsyncreadsit first and passes both instants as plain parameters to both statements. A window with nothing in it
(collection never started, or stopped before the window) now short-circuits to an empty result without
issuing either statement, instead of both independently matching zero rows the long way.
Darling/Darling.Tests/FinOpsMergedReadsLiveTests.cs:two
[Collection("live-postgres")]classes that seed real data and compare the new reads against the exactold SQL text (copied verbatim from
origin/dev, since the old constants no longer exist in source). See"Diagnosis" and "Test plan" below.
DatabaseSizeLatestSqlplanning time) is not done. Diagnosed but not implemented: see "What's left".for this PR explicitly excludes it. V3b's inspection notes (cadence needs no fix, the two double-reads are
present in Lite in the same shape) are left below for that lane's reference.
Behavior notes worth a second look
TopResourceConsumersByTotalSqlexcluded a NULL (unattributed)database_name. The oldTopResourceConsumersByAvgSqlnever did. The merged statement bakes in neither filter: it returns everydatabase, NULL name included, and
GetTopResourceConsumersAsyncapplies each grid's own old filterclient-side (skip NULL name for ByTotal; skip zero-execution, via a NULL
avg_cpu_ms, for ByAvg). The twogrids keep their previous, different inclusion rules. Live-pinned below.
ORDER BY <metric> DESC LIMIT $3with no secondary key, which was never a documented contract. The newbehavior is database name ascending: the merged query's
ORDER BY c.database_name, broken by a stableOrderByDescendingin C#.cpu_time_msandavg_cpu_msare sums of independent per-database deltas, so anexact tie is not expected in practice, but the live pin below seeds one deliberately and asserts same rows /
same values (canonicalized), rather than asserting an old order that was never a real contract.
Diagnosis
Confirmed on a rig this lane stood up (port 55973): PostgreSQL 18.6 + TimescaleDB 2.30.1,
darlingtestseededwith 40 servers (
server_id90001-90040), the target server (90001) carrying 24h ofquery_stats/file_io_statsat 5-minute density across 30 databases (NULL name, zero-execution, and a forced tie baked in), and a
GrowthDbwith 31 days of dailyindex_object_statsacross 50 tables.EXPLAIN (ANALYZE, BUFFERS), one runeach (not averaged: see caveat below):
Utilization top-consumers (server 90001, 24h raw window):
ByTotalByAvg~2.1x less execution time, ~16x less planning time (one statement planned instead of two), same execution
buffers as the more expensive of the two old statements (not doubled), ~37x fewer planning buffers.
Storage Growth drill bounds (server 90001,
GrowthDb, 30-day window, topN 20):ObjectGrowthSummarySql(own bounds CTE)ObjectGrowthSeriesSql(own bounds CTE)ObjectGrowthBoundsSql(computed once)ObjectGrowthSummarySql(bounds as params)ObjectGrowthSeriesSql(bounds as params)~14% less execution time, ~3.7x less planning time, ~14x fewer planning buffers (bounds priced once instead
of embedded in each of two statements).
Next to the issue's own Mac field numbers (#4227's comment, a real fleet, not this rig's synthetic seed):
Utilization was ~1.5 s execution + 0.23 s planning per refresh; each drill
boundspass was 89 ms and 21.3kbuffers. This rig's absolute numbers are far smaller (single-digit ms, tens to low hundreds of buffers) because
the seed is a fraction of a real fleet's scale (8,640
query_statsrows for the target server vs. a realserver's weeks of raw retention). The point proved here is the shape of the fix: one statement instead of
two, one bounds pass instead of two: not a claim of matching the Mac's absolute numbers. A single
EXPLAIN ANALYZErun each, not averaged over repeated runs, so treat the precise digits as indicative, not abenchmark-grade measurement.
What is left
DatabaseSizeLatestSqlplanning time). Diagnosed only, not implemented. Skipped per thislane's brief ("skip if your context is past 150k"): this lane's context watchdog fired at 200k before
reaching it. See V3b's diagnosis (still accurate, unchanged by this lane): a
collection_time >=probebound,
WatermarkPolicy.RecentWatermarkWindow-style, as a second statement tried first, falling back totoday's unbounded statement on a miss (a server whose collection stopped longer ago than the probe window).
against WPF FinOps Server Inventory re-runs a 7-day raw query_stats DISTINCT for every server on the 30 s timer (~43 × 44 k buffers per refresh); query_stats_db_hourly answers it in 32 ms / 175 buffers #4227. V3b's inspection notes: cadence (job 1) needs no Lite fix (Lite's only fleet timer never
touches
FinOpsTab). The top-consumer double read and the object-growth bounds double read are bothpresent in Lite in the same shape as the pre-fix Darling code.
ConsumerCteRaw/ConsumerCteForCaggand theRouteOrThrowsubstitution are byte-identical toorigin/dev(diffed by hand) and are already live-pinnedat the SQL-text level by
RetentionTierRouterTests/ViewerFinOpsSqlTests. This rig'sdarlingtesthas thetimescaledbextension but migrations did not materialize any continuous aggregates/hypertables on it (norows in
timescaledb_information.hypertables), so seeding a live hourly-tier comparison would have neededstanding up that provisioning path first: out of this lane's remaining time. Only the raw-tier merge and
the new client-side ranking are new risk surface versus
origin/dev, and both are fully live-pinned below.Test plan
dotnet build Darling/PerformanceMonitor.Darling.Viewer/PerformanceMonitor.Darling.Viewer.csproj: 0Warning(s), 0 Error(s).
dotnet build Darling/Darling.Tests/Darling.Tests.csproj: 0 Warning(s), 0 Error(s) (both before and aftergit merge origin/dev).FinOpsMergedReadsLiveTests.cs), against a real rig (PostgreSQL 18.6 +TimescaleDB 2.30.1,
DARLING_TEST_PG):FinOpsTopConsumersMergeLiveTests: the mergedTopResourceConsumersSqlplusGetTopResourceConsumersAsync's client-side ranking returns the same rows as the oldByTotal/ByAvgstatements (copied verbatim from
origin/dev), covering a NULLdatabase_name, a zero-execution database,and a genuine
cpu_time_ms/avg_cpu_mstie between two other databases, on the raw tier. Ties are comparedcanonicalized (metric DESC, name ASC) rather than by raw returned order, since the old tie order was never a
documented contract (see "Behavior notes" above): same rows, same values, proven either way. Pass.
FinOpsObjectGrowthBoundsMergeLiveTests: the bounds-computed-once form returns exactly what the oldper-statement
boundsCTE pair returned, including exact row order (this read'sORDER BYhas a fulltie-break, so no canonicalization needed), for both a real two-object growth/shrink window and the
empty-window case (a database whose only data predates the window). Pass.
git show origin/dev:...copies of the retired SQL constants, since thoseconstants no longer exist in current source.
ViewerFinOpsSqlTests,RetentionTierRouterTests,ViewerFleetTimerFanOutPositionTests,FinOpsTopConsumersMergeLiveTests,FinOpsObjectGrowthBoundsMergeLiveTests.ViewerFleetTimerGuardTests,ViewerFinOpsRecommendationsTests,McpPayloadContractCensusTests,StartupCommandTimeoutTests,StorageCommandTimeoutTests,AlertReadFailureSurfaceTests,DocCommentHygiene,LivePostgresCollectionHygieneTests,LiveCleanupConversionRatchetTests. 278/278 passed, 0 failed.FinOpsTab_IsInTheRefreshTicksTabEarlyReturn(ViewerFleetTimerFanOutPositionTests.cs).Proven red by hand against the pre-fix source by V3b (reverted the exemption locally, confirmed the new
fact failed, then restored it).
git merge origin/dev: merged cleanly, no conflicts (19 files, mostly MCP fleet-response-budget workunrelated to FinOps).
darlingtestdropped and recreated first): 13,801 total, 0 errors, 12 failed,47 skipped, 1 not run. None of the 12 failures are in a file this PR (or V3b's commit) touches. They
span
RollupCoverageRoutingTests,PgTargetWaitLiveTests,ViewerCalendarRetentionLivePostgresTests,TopCpuQueriesTextLiveTests,ViewerSessionStatsLivePostgresTests,CaptureDownChunkOrderTests,DefaultTraceEventFrameLivePostgresTests(3 of its offset cases), andPgTargetTileBehaviourIoReplicationBlockingTests(2 cases): all unrelated subsystems, and most are namedaround UTC offsets/shifts, consistent with this rig's
initdbdefaulting toAmerica/New_Yorkfor part ofthe run (see next item). Not independently re-verified against a fresh
dev+ CI per lane-orders' fullpre-existing-failure protocol: flagged here rather than silently dropped. This needs a coordinator
double-check, not a claim of "pre-existing" on my say-so alone.
initdbtook the machine's local zone(
America/New_York); CI runs UTC, and this lane appendedtimezone = 'UTC'/log_timezone = 'UTC'topostgresql.confand restarted (
pg_ctl restart -m fast) partway through the full-suite run above (confirmed via thepost-restart log switching to UTC timestamps); the in-flight run's connection pool reconnected and kept
going, so the run mixes a short pre-restart window with a UTC majority. Of the three tests the coordinator
named,
ServerListAndSummaryPlanShapeTestsandViewerW2aLivePostgresTests.ServerSummary_...did notappear in the failure list.
CaptureDownChunkOrderTestsdid. Re-ran it alone afterward, confirmedserver-side UTC by then: it still failed alone. Per the brief, not chased further. It is in
CaptureDownChunkOrderTests.cs, a collection-log chunk-ordering test this PR's diff does not touch.darlingtestwas NOT re-dropped/recreated after the restart before the run above finished (the run wasalready past its halfway point when the restart landed, and redoing a ~13-minute full run risked missing
this lane's deadline): a clean full run entirely under UTC is worth a coordinator re-check.
CHANGELOG entry
SECTION: Fixed
ENTRY:
Storage Growth, and other sub-tabs re-ran their reads on the fleet's 30 second timer, even though the
underlying numbers move hourly at the fastest. It now refreshes only when you open it, switch its sub-tabs,
or click Refresh, the same as the Recommendations tab already did. The Utilization tab's two "top resource
consumer" grids, and the Storage Growth drill's summary and trend views, also stopped scanning the same data
twice to build what used to be two separate reads.
REF:
[FinOps refresh: take the FinOps tab off the 30s timer, merge its doubled query_stats and bounds reads (#4227) #4240]: FinOps refresh: take the FinOps tab off the 30s timer, merge its doubled query_stats and bounds reads (#4227) #4240
🤖 Generated with Claude Code
https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3