Repository navigation
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
Description
Activity
- addedenhancementNew feature or requestNew feature or requestclient-siteOwned by the client-site agents (other laptop). Local sessions never pick these up.Owned by the client-site agents (other laptop). Local sessions never pick these up.in-progressActively being worked by a local session or its agents (PR open or in flight)Actively being worked by a local session or its agents (PR open or in flight)
on Sep 25, 2026 The second pass measured the rest of the FinOps tab. The "take it off the 30 s timer" point should cover the whole tab, not only Server Inventory.
Utilization, the sub-tab FinOps opens on, runs six reads every 30 s while visible. Store B, the busiest server,
EXPLAIN (ANALYZE, BUFFERS), first run:read ( FinOpsTab.Loaders.cs~:187–215)exec planning buffers UtilizationEfficiencySql(24 h)172 ms 12 ms 2.3 k DatabaseSizeLatestSql2.6 ms 148 ms 80 TopResourceConsumersByTotalSql(24 h, raw tier)875 ms (620 ms I/O) 17 ms 27.1 k TopResourceConsumersByAvgSql(24 h, raw tier)175 ms 0.8 ms the same 27.1 k DatabaseSizeSummarySql4.1 ms 15 ms 62 ProvisioningTrendSql(7 d)241 ms 36 ms 4.9 k - That's about 1.5 s of execution plus 0.23 s of planning per refresh cold, for figures that move hourly (utilization) or daily (sizes).
- The two top-consumer grids aggregate the same 24 h of raw
query_statstwice (oneGROUP BY database_namewould feed both, since the average is total ÷ executions). The Performance Trends tab has the same double read (findings, first pass). - Planning is a real share on a store with 71
database_size_statschunks: 148 ms to plan a 2.6 ms latest-snapshot read.
Storage Growth (
StorageGrowthSql, the threeMAX(collection_time)lookups): 20.6 ms + 45.5 ms planning. The lookups are one-chunk DeferredChunkAppend probes. Not a finding.Storage Growth → database drill (object growth, 30 d, the largest database):
ObjectGrowthSummarySql: 191 ms + 138 ms planning, 22.7 k buffers.ObjectGrowthSeriesSql: 650 ms + 49 ms planning, 27.9 k buffers.- Both recompute the same
boundsCTE (MAX/MIN(collection_time)for server + database). That alone is 89 ms and 21.3 k buffers each time, because no index leads(server_id, collection_time). That's Anomaly detector's "latest two object-stats snapshots" read has no usable index: ~350 MB read per server per pass (15.9 M blocks in 5 h on one store) #4196's class; V142 (add the supporting index instead of scanning the whole fleet's newest chunk (#4196) #4216) adds the index, so re-measure after it deploys. - Computing
boundsonce and passing both instants to both statements halves the rest. - The drill also refreshes every 30 s while open.
Fix-shape addition: refresh FinOps on activation and on its Refresh button (the Recommendations rule), and fold the two top-consumer reads into one aggregate.
Lane V1 (fix/4226-viewer-timer-reads, PR #4236) took #4226 and read this issue closely but did not implement
it: ran out of budget before building. Handing off the diagnosis so the next pass does not have to re-derive
it.The existing stitched-routing pattern in
ViewerDataService.FinOps.Workload.cs/
ViewerDataService.QueryTrends.cs(RollupCoverage.StitchedRelationSql, built by #4182/#4184) is not a
drop-in fix here.query_stats_db_hourlyand its successorquery_stats_db_interval_hourlyare
materialized_only = true(TimescaleDB's post-2.13 default;TimescaleSupport's own remarks on the
query_stats family confirm this is deliberate), unlikecollection_health_hourly(#4226's rollup), which is
materialized_only = false. A plain stitched read misses whatever the aggregate has not refreshed yet. The two
existing stitched readers tolerate that on a 24-hour CPU/IO summary; a boolean "did this database run anything
at all" check should not, since a database active only in the last refresh cycle would read as idle.So the fix needs a raw supplement on both edges of the routed window: the partial leading hour the bucket grain
cannot slice (the same problem #4226'sCollectionHealthRollupSupport.ComposeFleetSqlalready solves, and
reusable as a pattern), and a trailing unmaterialized-hours supplement that no existing reader in this codebase
has needed yet. That second piece is real design work, not a mechanical port.The rest of the issue's fix shape (one fleet statement instead of 43 per-server ones; taking Server Inventory
off the 30s timer onto activation-refresh, matching Recommendations) is unaffected by this and should still be
straightforward.Recommend re-queuing as its own lane with a fresh budget rather than folding into another issue's lane.
- added 7 commits that reference this issue
on Sep 25, 2026 Closed by #4240 (merged).
- removedin-progressActively being worked by a local session or its agents (PR open or in flight)Actively being worked by a local session or its agents (PR open or in flight)
on Sep 25, 2026 - added a commit that references this issue
on Sep 25, 2026
Problem
FinOps → Server Inventory loads the fleet list (
GetServerInventoryAsync, cheap). It then callsGetServerMetricsAsynconce per server, in pool-wide lanes, to overlay CPU, storage, idle-database count and provisioning status. The FinOps tab is an aggregate tab, so the fleet timer re-runs all of it everyNocRefreshIntervalSeconds(default 30 s) while the sub-tab is visible.ServerMetricsSqlbundles five CTEs per server. The expensive one isidle_dbs:That's a scan of every raw
query_statsrow the server has, to learn which databases executed anything. Across the fleet, one refresh reads about the whole rawquery_statstable.It also doesn't measure what it says. Raw
query_statsretention on this store is 4 days. So "no executions in 7 days" is really "none in the ~4 days raw still holds", and a database that ran on day 5 or 6 is reported idle.query_stats_db_hourly(and_daily) keep the per-database execution sums for the full window.Measured
Production SQL Server store A (43 servers, raw
query_stats18 GB across 6 chunks), 2026-09-25,EXPLAIN (ANALYZE, BUFFERS), read-only, one busy server (~1.2 Mquery_statsrows in the window):ServerMetricsSql, whole statement, one server (cold)idle_dbsarm alone (warm)query_stats_db_hourly(execution_count_sum > 0, materialized buckets)ServerInventorySql(the fleet list)Per refresh: 43 × ~44 k buffers ≈ 1.9 M buffers (~15 GB of buffer traffic) every 30 s, from the idle arm alone. That's a projection from the per-server cost × the fan-out, not an observed load: nobody runs this sub-tab on the store today.
Where (origin/dev)
Darling/PerformanceMonitor.Darling.Viewer/FinOpsTab.Loaders.cs:LoadFinOpsServerInventoryAsync~:560–600, with the per-serverGetServerMetricsAsync~:582 insideViewerReadFanOut.Lanes.Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.FinOps.Inventory.cs:ServerMetricsSql~:31 (idle_dbs~:81–101);GetServerMetricsAsyncpassesidleCutoff = UtcNow.AddDays(-7).Darling/PerformanceMonitor.Darling.Viewer/MainWindow.xaml.cs:1173: FinOps refreshes on the fleet timer. Recommendations is exempted, as refresh-on-activation (:611–620); FinOps is not.Darling/PerformanceMonitor.Darling.Storage/TimescaleSupport.cs~:1981:query_stats_db_hourlycarriesexecution_count_sumper (server, database, hour). The FinOps Workload reads already stitch it (ViewerDataService.FinOps.Workload.cs~:122–136).Fix shape
query_stats_db_hourlystitched to its interval-honest successor throughRollupCoverage, asFinOps.Workload.csalready does, plus a raw head for the unmaterialized hours (the aggregate ismaterialized_only). The window then means what it says, and the cost is per database-hour, not per query-snapshot.server_id: 24 h CPU, the latest memory and size snapshots via per-serverLATERAL … LIMIT 1(The fleet overview reads each server's newest sample instead of every retained row, and the web viewer asks for it once per refresh (#3895) #3947's shape), grants, and idle databases. One round-trip per refresh, whose cost doesn't depend on lane scheduling.ServerMetricsSql(or its successor) doesn't readv_query_stats/query_statsfor a multi-day window.