Skip to content

The server page labelled page sums as totals, drew one instant as a trend, and four tools called their capture time by another name: get_pg_database_stats' totals ride the row statement, the memory-grant pair renders window[] beside its newest snapshot, get_pg_io_stats' page count is combinations_returned, and four latest reads stamp captured_at on both SKUs (partial #3653) - #3697

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/3653-wwwroot-twins
Sep 19, 2026

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

The server page labelled page sums as totals, drew one instant as a trend, and four tools called their capture time by another name

Partial for #3653 — the wwwroot twins item (all three clauses) and the StampedUnderCollectionTime clause of A15/A16. Does not close the issue.

Three of the payload-contract censuses that landed in the #3541 campaign each carried exactly ONE stated allowance, and each allowance pointed at the same file: Darling/PerformanceMonitor.Darling.Service/wwwroot/js/pages/server-tabs.js, the web server page, which sat outside those lanes' boundaries. This PR is the lane that owns that file, so it retires the allowances from the tool side and the page side in one diff — which is the only way a rename of a wire key can land without a release where one half lies.

1. get_pg_database_stats: the totals were the page's, honestly labelled as such — now they are the window's

The tool fetched its limit rows, rows.Sum(...)'d them, and published the sums as total_temp_files / total_temp_bytes / total_deadlocks. #3613 (the A7 percent lane) found this and — because relabelling the web tile was outside its boundary — carried it honestly by disclosure: a limit_reached flag, a note saying the totals covered only the databases returned, a database_count the tile labelled "Databases returned", and a ratio already spelled cache_hit_pct_of_returned. The A7 census (McpPageContractTests.NoPagedTool_DividesByOrPublishesAPageSumAsATotal_OnEitherSku) named those three keys as its one stated allowance, with a control that fails the day the allowance stops being used.

The window's figures are free on the statement the read already runs. DarlingPgDatabaseReader.PgDatabaseSql gains seven columns on its final select list — CAST(SUM(coalesce(SUM(d_temp_files), 0)) OVER () AS bigint) AS window_total_temp_files and its siblings for bytes, deadlocks, blks_hit, blks_read; COUNT(*) OVER () as window_database_count; and SUM(count(*) FILTER (WHERE reset_here) + count(*) FILTER (WHERE rewound_here)) OVER () as window_reset_count. PostgreSQL evaluates a window aggregate over the grouped result after GROUP BY / HAVING and before ORDER BY / LIMIT, so each sums every database the window holds under the SAME "moved or was reset" filter the rows are drawn from, cannot drift from them (one statement), and costs one pass over a result the query has already grouped and is about to sort. The reader returns a page record (PgDatabasePage: rows + the window figures) — the #3613 shape, chosen for the same reason: these are facts about the window, and the Viewer's projection census maps every ROW property to a display column. The Viewer's GetPgDatabaseStatsAsync keeps its signature and returns .Rows.

The tool, in the A3 dialect, through a new BuildDatabaseStatsJson split-out (the BuildIoJson precedent, so the wire shape is assertable without a store):

PG_DATABASE_STATS on the page: "Databases active" (database_count) beside "Databases returned" (databases_returned), the total tiles' labels now true, "Cache hit %" reads cache_hit_pct, "Window held more" reads truncated; the panel subtitle is "totals over every database that moved". The allowance set in the census is now empty — kept as an empty set rather than deleted, with the paragraph saying why, so the next tool that needs one has to write its reason there and the still-used control keeps refusing a stale one.

2. get_pg_io_stats.combination_count → combinations_returned

#3613 kept combination_count only because the web I/O tile read it by key. It IS a page count (the description said so, truncated sat beside it) — but a bare _count beside two window totals reads as the window's third, and every other paged tool spells its page count <noun>s_returned (queries_returned, wait_events_returned, waits_returned, settings_returned, …). Renamed to the dialect; the "kept for the JS" caveat is gone from the tool's comment; the tile reads the new key and is labelled "Combinations returned"; the two pins (DarlingMcpPgPercentDenominatorTests, McpPageContractLivePostgresTests) read the new key.

3. The Memory Grants LINE chart drew one instant as a trend

server-tabs.js drew get_memory_grants.grants[] as a line chart keyed on collection_time under the page's range label. Since #3637 grants[] is the NEWEST snapshot in the window — one row per pool, every row carrying the same stamp — so the "series" was one x-position with a dot per pool, captioned as if it spanned the window. The Resource Semaphore table one panel down told the same lie more quietly: grants[] rows under a window subtitle.

A brief claim found false while doing this: the item says "chart the window[] block #3637 added" and the lane brief describes window[] as a "per-collection series across the window". It is not. window[] is ONE row per pool (per semaphore and pool on the semaphore lens) aggregating every snapshot in the window — snapshots_in_window, peak_waiter_count + peak_waiters_at, peak_granted_memory_mb, min_available_memory_mb, timeout_errors_in_window, forced_grants_in_window, first/last stamps (DarlingMcpMemoryGrantTools.WindowShape; #3637's own body says "per-pool aggregate over EVERY snapshot"). It cannot be a line chart. The honest rendering is what this PR does: each read becomes a fanout (one fetch, two panels — the no-double-fetch pin holds) of two TABLES, the window first ("Memory Grant Pressure", subtitled "every snapshot in the window, per resource pool") and the newest snapshot second ("Memory Grants", subtitled "newest snapshot in , per resource pool - a moment, not the window") — the reading order the read's own description gives ("read window[] first for 'was there pressure', grants[] for 'is there pressure now'"). Same for the semaphore pair. GRANT_SERIES is deleted; GRANT_WINDOW_COLUMNS, SEMAPHORE_WINDOW_COLUMNS and GRANT_COLUMNS are the new column sets. A per-collection memory-grant TREND would need a read this service does not serve (there is none in DarlingMemoryGrantReader), and is not something this lane should invent to satisfy a sentence that assumed one existed. The Memory Pressure Events panel moves up one slot so the two span-1 panels (clerks, pressure events) still pair on the grid.

4. Four tools said collection_time where the census says captured_at

get_database_sizes, get_running_jobs, get_server_properties, get_session_stats stamped their latest snapshot truthfully, but under the pre-#3637 spelling, and were held out of the roster as McpLatestSnapshotStampTests.StampedUnderCollectionTime because the web's SESSION_STATS "Collected" tile read get_session_stats by the old key. The tile moves in the same PR, so all four cut over to captured_at on BOTH SKUs (Darling: DarlingMcpObjectStatsTools / DarlingMcpJobTools / DarlingMcpDataTools / DarlingMcpSessionTools; Lite: McpServerInfoTools / McpJobTools / McpSessionTools) with a "LATEST IS A TIME … captured_at is the instant it was collected" sentence in each description, in the Stamped dialect's voice. No alias: the census is the contract, the only reader was the tile edited here, and a second key for one instant is the drift the vocabulary exists to refuse (said in each tool's comment). get_running_jobs' description also stops saying start_time matches "the collection_time on this payload".

The census: the four join LatestTools as Shape.Stamped rows (so the captured_at / description / no-knob / same-parameters-on-both-SKUs assertions hold them like every other latest read); StampedUnderCollectionTime and its sweep arm and control are gone; the old indentation-keyed TopLevelCollectionTimeKey discriminator is replaced by one anchored on JsonSerializer.Serialize(new { and now runs as a NEGATIVE sweep over every latest-reading Darling body (a renamed tool cannot keep both spellings; a new one cannot revive the old) — witnessed both ways, including against the memory-grant shape whose per-row collection_time sits at the exact indent the old form keyed on; the stamp-column Theory gains the four readers' SQL consts, and its select-list slice is re-anchored on the main statement's column-zero FROM because RunningJobsSql opens with a CTE whose own FROM the old first-FROM slice stopped at. Lite's roster in Lite.Tests/McpLatestSnapshotStampTests gains the same four rows.

Blast radius

  • Payload keys, get_pg_database_stats: added databases_returned, truncated, order, returned_temp_files, returned_temp_bytes, returned_deadlocks, cache_hit_pct; removed limit_reached; database_count and the three total_* keep their names and change meaning from page to window — which is what the names said. cache_hit_pct_of_returned unchanged.
  • get_pg_io_stats: combination_count → combinations_returned.
  • The four latest reads, both SKUs: top-level collection_time → captured_at. Per-row collection_time columns elsewhere are untouched.
  • Reader API: DarlingPgDatabaseReader.PgDatabasePage + GetPgDatabaseStatsPageAsync; GetPgDatabaseStatsAsync keeps its signature (.Rows). PgDatabaseSql's select list grows 14 → 21 columns (positional reader updated).
  • Web: Memory tab panel order (pressure events up one), two fanouts replacing a line chart and a table, tile relabels on the PostgreSQL Activity and I/O tabs, SESSION_STATS key. /api/read/* dispatch unchanged (DarlingWebEndpoints.cs untouched).
  • Swept wwwroot/js: no other reader of combination_count, limit_reached on this tool, database_count on this tool, or the four tools' collection_time. Swept both READMEs and docs/: none name the changed keys.

What it does NOT do

  • Does not close Brains-review campaign: deferred structural residue (from #3538 / #3539 / #3540 / #3541) #3653. Does not touch DarlingMcpTools.cs, DarlingMcpServerAdminTools.cs, the deadlock / plan / index PG tool files, McpHelpers.cs, DarlingWebEndpoints.cs or any PgTarget* file.
  • Does not add a memory-grant trend read (see §3). Does not touch the three other PostgreSQL tiles that still read an inferred limit_reached (get_pg_table_bloat, get_pg_session_states, get_pg_index_usage) — separate tools, other lanes' files; reported.
  • No migration rung, no CHANGELOG edit (entry returned to the coordinator), no deprecated/Dashboard change.

Test plan

New / changed, executed here (the whole Darling.Tests.dll runs in-process on this Mac once Microsoft.WindowsDesktop.App is dropped from its runtimeconfig; Lite's class through a selective net10.0-windows harness):

  • McpPageContractTests — the A7 allowance set is EMPTY (the still-used control passes over an empty set; population floor 60 bodies unchanged); new TheDatabaseStatsRead_CarriesItsWindowTotalsOnTheSameStatement_AboveTheCap (each window_total_* column is the window aggregate of the SAME grouped sum the row reports, window_database_count, window_reset_count, the WindowTotalColumn shape, parameterised LIMIT, OVER () below the HAVING and above the LIMIT) and TheDatabaseStatsTool_PublishesTheWindowAsTotal_AndThePageAsReturned (observed truncation off limit + 1, no TruncationInferred, no limit_reached, every key sourced from page.Window*, no page-sum local reaches a total_* or a share, the description and limit sentences).
  • DarlingMcpPgPercentDenominatorTests — new DatabaseStats_TotalsAreOfTheWindow_NotOfThePage (the cut/whole pair: at limit = 1 the totals are the window's 1,000 / 3 / 36.67% while returned_* is 600 / 1 / 90%; at limit = 3 they meet) and DatabaseStats_PublishesThePagesWindowFigures_NeverASumOfTheRows (a page handed window figures that disagree with its rows reports what it was handed; the reset flag is true off the window with a never-reset row on the page; a window with no block access reports a null ratio, not 0). combination_count → combinations_returned in the I/O pair.
  • McpLatestSnapshotStampTests — roster +4; sweep with the negative collection_time arm; discriminators witnessed against the shipped get_session_stats shape, the fixed shape, and the memory-grant shape; stamp-column Theory +4 with the re-anchored slice; McpLatestSnapshotStampLivePostgresTests seeds an older and a newer row for each of the four tables and asserts captured_at is the newer stamp and the retired key is ABSENT.
  • DarlingPgDatabaseStatsLiveTests (DARLING_TEST_PG) — a second, smaller spiller seeded so a one-row page leaves temp files and a deadlock OFF the page: at limit = 1 total_* is 12 files / 93 MB / 3 deadlocks over 5 databases while returned_* is 9 / 90 MB / 2, cache_hit_pct is the window's 22,400 / 22,600 against the page's 20,000 / 20,100; at limit = 2 both spillers are on the page and returned_* meets total_*; the unlimited call is not truncated and every returned_* equals its total_*; limit_reached absent, cache_hit_pct_of_returned present.
  • Lite.Tests/McpLatestSnapshotStampTests — roster +4. ConsumedTimestampFrameDisciplineTests — one census reason string names the new key.

Executed: 347/347 across the 24 Darling test classes touching these surfaces (including the four *LivePostgres* / *Live* classes against a throwaway timescale/timescaledb:2.28.1-pg18 on Docker context desktop-linux, migrated by PgMigrations); the whole Darling.Tests.dll (11,003 tests) with only platform reds (WPF PresentationCore/PresentationFramework, Microsoft.Data.SqlClient on macOS, Windows install paths — none in a touched class); Lite's McpLatestSnapshotStampTests 12/12 through the harness; DarlingPgReadSqlParsesLiveTests parses the reshaped PgDatabaseSql against live PG. The statement was also run raw through psql with three planted databases and LIMIT 2 (tool limit 1 + sentinel): the page holds two rows, the window columns on every row read 3 databases / 13 files / 94,000,000 bytes / 3 deadlocks / 1 reset — the reset on the database that is NOT on the page. node --check on server-tabs.js, and the shipped modules were executed under a throwaway DOM shim with a stubbed fetch: the Memory tab fetches get_memory_grants and get_resource_semaphore exactly once each, renders the window tables from window[] (peak 7,777 MB → "7.6 GB", min 111 MB, the peak instant), the latest tables from grants[], no line chart; SESSION_STATS renders captured_at as a relative time; the PostgreSQL tiles carry the new labels and none of the old ones; the I/O tile reads combinations_returned. Container torn down. Five projects build with 0 warnings (Darling.Service, Darling.Storage, Lite, Darling.Tests; Lite.Tests carries dev's one pre-existing xUnit2000 in LogTailOverlapThresholdPinTests.cs:95, untouched here).

Which component(s) does this affect?

  • Lite
  • Darling
  • Lite Tests
  • Darling Tests
  • SQL collection scripts
  • Documentation
  • Full Dashboard (deprecated)
  • CLI Installer (deprecated)

Checklist

  • I have read the contributing guide
  • My code builds with zero warnings (dotnet build -c Debug)
  • I have tested my changes against at least one SQL Server version (store-side and web-side change; verified against PG18.4 / Timescale 2.28.1 for the Darling store and DuckDB for the Lite tools)
  • I have not introduced any hardcoded credentials or server names

…rend, and four tools called their capture time by another name: get_pg_database_stats' totals ride the row statement as window aggregates, the memory-grant pair renders its window[] beside its newest snapshot, get_pg_io_stats' page count is combinations_returned, and get_database_sizes / get_running_jobs / get_server_properties / get_session_stats stamp captured_at on both SKUs (partial #3653)
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 19, 2026 06:26
@erikdarlingdata
erikdarlingdata merged commit e6b1ff5 into dev Sep 19, 2026
7 of 8 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3653-wwwroot-twins branch September 19, 2026 06:26
erikdarlingdata added a commit that referenced this pull request Sep 20, 2026
…ption names its reader's two documented traps, the web catalogue's duration-trend line says rates and never percentiles, and server-tabs.js reads every Stamped tool by captured_at (partial #3653 A15/A16) (#3801)

The six drifts item 17 still listed were all delivered before this branch (#3665, #3696, #3697); three of the fixed texts had nothing holding them. Pins only, no wire change:

- DarlingMcpAgToolsSurfaceTests.Description_SurfacesTheReadersTwoDocumentedTraps anchors on DarlingAgReader's own bolded paragraph headers, then holds the description and the instructions paragraph to each trap's mechanism, consequence and tell (NO row for a server with no AGs / last non-empty snapshot / collection_time; lag and queue depth NOT banded / secondary_lag_seconds / is_suspended / 0 lag while suspended).
- DarlingMcpTrendToolsSurfaceAndSqlTests.QueryDurationTrend_CatalogueLine_NamesTheRatesThePayloadCarries_NotPercentiles holds the catalogue line to ms/sec + executions/sec + 'not percentiles', SerializeTrend to the two rates it projects, and the whole catalogue to 'percentile' only ever negated.
- ServerPageTabsTests.TheStampedReads_AreReadByTheirStamp_NeverTheRetiredKey derives the Stamped + SearchBound roster from McpLatestSnapshotStampTests.LatestTools, slices every stat/table/fanout call by bracket balance, and refuses key: "collection_time" in any descriptor array bound to one of them; the four named reads must be reached; SESSION_STATS must show captured_at.

Each red against its defect, then green, in the Mac harness.
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