Skip to content

FinOps Lite top-consumer and idle-check reads use one statement each (part of #4227) - #4243

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/4227-lite-finops-reads
Sep 25, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
fix/4227-lite-finops-reads

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Part of #4227.

Why

Lane V3b confirmed by inspection that Lite's FinOps tab carries the same two redundant-read patterns #4240 fixed in the Darling viewer. The coordinator also added a third item: V3a's PR #4241 folded Darling's Server Inventory idle-database check from one statement per server into one fleet statement. V3b measured Lite's twin too.

This PR measures all three patterns on a seeded DuckDB before changing anything and ports only the ones with a real, repeatable win. DuckDB is columnar and in-process. A second read over data DuckDB already cached can cost next to nothing. That differs from Postgres/TimescaleDB, where the original #4227/#4240/#4241 wins came from avoiding a second pass over a large, disk-resident hypertable. That difference held for one of the three patterns here.

What I found and measured

Seeded one DuckDB (own isolated instance, not the shared test fixture) with 50 servers and 15,000+ rows across cpu/memory/grant/size/query_stats. One focus server had a heavier load: 4,800 query_stats rows, 960 file_io_stats rows, and 4,500 index_object_stats rows over 30 daily snapshots. Timed the current code against a merged form, 5 iterations each, 3 full runs:

pattern old new verdict
Utilization top-consumers (by total + by average) 132-220 ms/pair (2 statements, 2 connection-opens) 60-112 ms/pair (1 statement, 1 connection-open) ported: consistent ~2x across all 3 runs
Storage Growth database drill (bounds computed once per statement, twice total) 97-209 ms/call 105-137 ms/call not ported: direction flipped between runs (new was slower once, faster once). No reliable win
Server Inventory idle-database check (N per-server calls vs. 1 fleet statement) 128-185 ms wall-clock (50 servers, parallel Task.WhenAll, matches production) 85-99 ms ported: consistent ~2x across all 3 runs

The Utilization and Server Inventory wins both trace mostly to halving or fleet-collapsing the OpenConnectionAsync/read-lock-acquisition cost. Each of those methods opens its own DuckDB connection today. The Storage Growth pattern's redundant work is a cheap MAX/MIN over an already-indexed collection_time column, computed within a single connection either way. There was nothing real to save.

"Lite's top server count"

Not documented anywhere I found (no cap in ServerManager, no stated limit in Lite/README.md). This PR uses 50, matching the production 43-server fleet #4227 used as a round-number ceiling.

Continuous aggregates

Lite has none (no TimescaleDB, no rollup views). The rollup/watermark routing #4241 added for the idle check does not apply here. The merged fleet statement reads raw tables only, the same as the per-server loop it replaces.

What changes

  • Lite/Services/LocalDataService.FinOps.Workload.cs: GetTopResourceConsumersByTotalAsync and GetTopResourceConsumersByAvgAsync become one GetTopResourceConsumersAsync, returning (List<TopResourceConsumerRow> ByTotal, List<TopResourceConsumerRow> ByAvg) from one statement. Each grid keeps its own old filter. ByTotal drops a NULL database name. ByAvg drops a zero-execution database. Both filters now apply client-side from the same row set.
  • Lite/Services/LocalDataService.FinOps.ServerProperties.cs: GetServerMetricsAsync(int serverId) becomes GetServerMetricsAsync(), returning Dictionary<int, ServerMetricsRow> for every server in one statement. A server with no collected rows still gets an entry (all fields null), matching the old per-server statement's anchor row.
  • Lite/Controls/FinOpsTab.xaml.cs: LoadServerInventoryAsync calls the fleet statement once before the per-server fan-out. The fan-out still runs per server for the LIVE SQL query (a real network call per monitored server). Only the DuckDB overlay moved. LoadUtilizationAsync calls the merged consumer read once.
  • No change to LocalDataService.FinOps.IndexObjects.cs (Storage Growth). Measured, not ported. Numbers are in the table above.

Test plan

  • Lite.Tests/FinOpsFleetReadParityTests.cs (new): pins that the merged Utilization read keeps each old grid's NULL-name/zero-execution filter. It also pins that the fleet statement covers every server (a server with zero rows gets a slot) and flags an idle database. Both tests pass.
  • FinOpsVerdictSourcePinTests (existing [BUG] FinOps provisioning status reports UNDER_PROVISIONED for every healthy server: the memory ratio compares Total to Target Server Memory, which converge at 1.0 #2246 source pin) still passes against the rewritten ServerProperties.cs. The pressure-signal columns and the shared ProvisioningVerdict.Evaluate call are still there in the merged SQL/C#.
  • dotnet build Lite.Tests/Lite.Tests.csproj: 0 warnings, 0 errors, both before and after merging origin/dev (fast-forward, no conflicts, touched only Darling files).
  • Full Lite.Tests suite: not run. This lane hit its context checkpoint after the targeted runs above. The coordinator must run a full-suite pass before merging.

CHANGELOG entry

SECTION: Fixed
ENTRY:

Generated with Claude Code

https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3

…fleet statement for Server Inventory idle-database check (#4227)

Part of #4227's Lite parity. Measured on a seeded DuckDB before porting either:
the Utilization top-consumers pair and the Server Inventory per-server loop both
showed a real, repeatable win from merging (roughly 2x); the Storage Growth
bounds-computed-twice pattern did not, so it's left unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
@erikdarlingdata erikdarlingdata changed the title DO NOT MERGE (FinOps Lite): merge Utilization top-consumers, fleet-statement the Server Inventory idle-database check (#4227) FinOps Lite top-consumer and idle-check reads use one statement each (part of #4227) Sep 25, 2026
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 25, 2026 05:47
@erikdarlingdata
erikdarlingdata merged commit 73c1219 into dev Sep 25, 2026
18 of 20 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4227-lite-finops-reads branch September 25, 2026 05:47
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