Give a PostgreSQL target PostgreSQL tabs in the WPF viewer (#2530) - #2555
Conversation
The WPF viewer cannot reference the service, so the PostgreSQL reads it needs for #2530 had to be reachable from somewhere both front ends see. They move to PerformanceMonitor.Darling.Storage, which the service, the viewer and the tests all already reference. Namespace change only: no query text, no ordinal mapping and no signature is touched, and every existing reader test still pins the same constants. Copying them into the viewer instead would have meant a second copy of, among others, a 200-line recursive blocking walk whose revisit guard, root attribution and truncation flag were each a separate review finding. Two copies of a query like that diverge, and the copy that diverges is never the one being read - the same reasoning CollectorEngineCapability's own comment gives for keeping one copy of a sentence both SKUs print. Storage gains InternalsVisibleTo("Darling.Tests") because the ordinal pins reach the internal Map*Row helpers, which followed the readers; the service keeps its own for everything that stayed behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`git grep IsPostgres` across the viewer returned nothing, so an Aurora target opened on the SQL Server correlated-lane Overview and got the whole nineteen-tab strip - tempdb, Trace Flags, Query Store, Plan Viewer, Always On - nearly all of it permanently empty and several tabs meaningless for the engine. It now gets six: Overview, Activity, Vacuum, Waits, I/O and Replication, the same six #2547 chose for the web, with the same ids and the same grouping so the two front ends do not teach one engine two shapes. Placement is DERIVED from CollectorCatalog in both directions rather than enumerated. All nine PostgreSQL collectors reach exactly one tab; a tenth turns the pin red naming itself, which is the check that did not exist while eight of them shipped MCP-only for three releases. The ViewerCollectorCoverageTests allow-list, which carried all nine as tracked debt, is now empty - and its reader-layer scan now follows the shared store readers the viewer NAMES, one hop outwards, so coverage still means "the viewer reads it" without demanding a duplicate copy of the SQL to satisfy a text match. Both Aurora-only panels are SHOWN on stock PostgreSQL, not hidden. Each prints CollectorEngineCapability.NotCollectedMessage - the same sentence the MCP surface and the web print, naming the server, the engine, the collector and the exact aurora_stat_* surface, ending "and never will". The defect here is unexplained emptiness, not emptiness; hiding would also make the tab strip a different shape on two PostgreSQL servers in one fleet. For the same reason the Overview grid is built from the CATALOG rather than from collection_log: a gated-off collector writes no log row at all, so a log-driven grid would drop precisely the row an operator most needs explained. Mechanics worth knowing: - The six TabItems CONTINUE the same TabControl at indices 19-24 rather than living in a second one. For a PostgreSQL server the nineteen SQL Server tabs are collapsed and these six shown, so both sets keep fixed indices and every drill-down that navigates by an index constant is untouched. - Each PostgreSQL index gets its OWN dispatch arm. Falling through to `default:` would run the SQL Server overview lanes - four collectors that cannot run on this engine - at a PostgreSQL server. - engine_kind and sql_engine_edition are selected by BOTH server reads. The sidebar uses ManagedServersSql on any seeded store, i.e. every real deployment, so a discriminator on only ServersSql would have left every PostgreSQL target on the SQL Server tabs while a unit test passed. - Only a POSITIVE claim switches. A null or unrecognised token keeps the SQL Server tabs and shows no engine badge at all, because the tabs such a server gets are a default rather than a finding. - The display projections keep the store's sentinels out of the cells: -1 is not a number, an untracked pg_stat_io write counter is not 0, a NULL recurrence is "cannot tell" and not "once", temp blocks are not bytes, and every timestamp goes through ViewerTimeHelper. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ReviewThis is a large, well-scoped change (WPF viewer parity with the web dashboard's PostgreSQL tabs from #2547), and the mechanics are sound: tab ids/grouping match the web registry exactly, the index-continuation scheme (19-24) keeps SQL Server drill-downs untouched, every dispatch arm is explicit (no Found two real content-parity gaps versus the web dashboard for the same collectors (left as inline comments), plus one typo. Nothing here is a crash risk; the gaps are silently-dropped diagnostic signal rather than incorrect behavior. Focus was correctness, Lite/Darling(web) parity, and security — no missing-index DMV concerns apply (no new T-SQL; this PR touches C#/XAML and PostgreSQL reads/queries only). Didn't attempt to build/run the WPF app (Windows-only; consistent with the PR's own "no pixels" caveat). |
| internal sealed class SlotRow | ||
| { | ||
| public string SlotName { get; init; } = ""; | ||
| public string SlotType { get; init; } = ""; | ||
| public string ActiveText { get; init; } = ""; | ||
| public string WalStatus { get; init; } = ""; | ||
| public string RetainedWal { get; init; } = ""; | ||
| public string RetainedWalTrend { get; init; } = ""; | ||
| public string SafeWalSize { get; init; } = ""; | ||
| public string XminAge { get; init; } = ""; | ||
| public string CatalogXminAge { get; init; } = ""; | ||
| public string InactiveSince { get; init; } = ""; | ||
| public string DatabaseName { get; init; } = ""; | ||
| public string Plugin { get; init; } = ""; | ||
| public string InvalidationReason { get; init; } = ""; | ||
| public bool IsInvalidated { get; init; } | ||
| public bool IsInactive { get; init; } | ||
| } | ||
|
|
||
| internal static SlotRow Slot(DarlingPgSlotReader.PgSlotRow row) => new() | ||
| { | ||
| SlotName = row.SlotName, | ||
| SlotType = row.SlotType ?? "", | ||
| ActiveText = row.IsActive ? "active" : "INACTIVE", | ||
| WalStatus = row.WalStatus ?? "", | ||
| RetainedWal = Bytes(row.RetainedWalBytes), | ||
| RetainedWalTrend = ByteDelta(row.FirstRetainedWalBytes, row.RetainedWalBytes), | ||
| SafeWalSize = Bytes(row.SafeWalSizeBytes), | ||
| XminAge = Count(row.XminAge), | ||
| CatalogXminAge = Count(row.CatalogXminAge), | ||
| InactiveSince = Timestamp(row.InactiveSince), | ||
| DatabaseName = row.DatabaseName ?? "", | ||
| Plugin = row.Plugin ?? "", | ||
| InvalidationReason = row.InvalidationReason ?? "", | ||
| /* An invalidated slot has already lost its WAL: the replica behind it needs rebuilding, and that | ||
| is a different day from an inactive slot that is merely accumulating. */ | ||
| IsInvalidated = !string.IsNullOrEmpty(row.InvalidationReason) | ||
| || string.Equals(row.WalStatus, "lost", StringComparison.OrdinalIgnoreCase), | ||
| IsInactive = !row.IsActive, | ||
| }; |
There was a problem hiding this comment.
PgSlotRow.Conflicting is never surfaced here or in the XAML's PgReplicationSlotsGrid (no Conflicting property on SlotRow, no column, no row-highlight trigger). This isn't a derived/computed field — it's a raw stored column the shared reader already reads (DarlingPgSlotReader.cs selects conflicting straight off collect.pg_replication_slot_stats), and the web dashboard renders it as its own "Conflicting" column (PG_SLOT_COLUMNS in server-tabs.js, fed from DarlingMcpPgSlotTools.cs's conflicting = r.Conflicting).
A conflicting logical replication slot (one whose retained rows got vacuumed away, breaking it) is exactly the kind of "quiet, slow, predictable failure" this PR's own Overview/Vacuum design philosophy is built around surfacing — but on the WPF viewer this signal is currently invisible. An operator using only the desktop viewer would have no way to see it, while the same operator on the web dashboard would.
Given IsInvalidated/IsInactive already drive row highlighting here, adding Conflicting (property + XAML column, maybe folded into the existing highlight logic or its own trigger) looks like a small, self-contained follow-up rather than something that needs new plumbing — the data is already flowing through PgSlotRow.
| internal static IoRow Io(DarlingPgIoReader.PgIoRow row) => new() | ||
| { | ||
| BackendType = row.BackendType ?? "", | ||
| ObjectType = row.ObjectType ?? "", | ||
| Context = row.Context ?? "", | ||
| Reads = Count(row.Reads), | ||
| ReadTimeMs = Math.Round(row.ReadTimeMs, 1), | ||
| Hits = Count(row.Hits), | ||
| Extends = Count(row.Extends), | ||
| Evictions = Count(row.Evictions), | ||
| Reuses = Count(row.Reuses), | ||
| /* "not tracked", not 0. On Aurora the whole pg_stat_io write side is NULL because backends there | ||
| do not write data files, and the read carries WriteCountersTracked precisely so this cell can | ||
| tell that apart from a server that wrote nothing. */ | ||
| Writes = row.WriteCountersTracked ? Count(row.Writes) : "not tracked", | ||
| WriteTimeMs = row.WriteCountersTracked | ||
| ? Math.Round(row.WriteTimeMs, 1).ToString("N1", CultureInfo.CurrentCulture) | ||
| : "not tracked", | ||
| OpSize = Bytes(row.OpBytes), | ||
| StatsReset = Timestamp(row.StatsReset), | ||
| }; |
There was a problem hiding this comment.
The WPF I/O tab shows noticeably less than the web dashboard's get_pg_io_stats panel for the same collector/tab. DarlingMcpPgIoTools.cs computes context_meaning (what the backend/object/context combination means), avg_read_ms (ReadTimeMs / Reads), hit_pct (Hits / (Hits+Reads) * 100), and pct_of_total_read_time on top of the same PgIoRow, and the web's PG_IO_COLUMNS renders all four — none of them appear here or in IoRow/the XAML grid. ExtendTimeMs (the raw field the reader already returns) isn't projected at all either, unlike Reads's ReadTimeMs.
These are cheap derivations (a couple of divisions, already done once in DarlingMcpPgIoTools.cs) rather than new data, so this looks like it was simply not ported over rather than a deliberate simplification — worth a quick pass to close the gap, or a one-line note if it's intentional scope-trimming for this PR.
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <!-- #2530: the PostgreSQL reads moved here from the service'''s MCP folder so the WPF viewer can run the |
There was a problem hiding this comment.
Typo: service'''s (triple apostrophe) → should be service's.
All three claude[bot] findings were real. Two of them are the same defect - a field the shared reader goes to the trouble of returning that no viewer column shows - so the fix is an invariant rather than two columns, per the rule that the second instance of something is a sign you are fixing the wrong level. EveryFieldTheSharedReadersReturn_ReachesAColumn_OrIsFoldedWithAReason reflects over the ten reader row records against their display classes: every field must reach a column, or appear in FoldedFields naming the column that carries the fact and why that shape reads better. Derived both ways - a new reader field turns it red until something happens, and an exemption for a field that no longer exists is deleted the moment the reader stops returning it. Proved red by deleting exactly the two columns review found, each naming itself. It found three more on its first run, which is the point: PctTowardMultixactWraparound had no column at all (the multixact side has its own shutdown ceiling and a subtransaction-heavy workload reaches it first, with nothing on the XID columns to say so), and the two freeze_max_age settings were renamed rather than shown. The findings themselves: - Replication gains a Conflicting column, folded into the invalidated row highlight. A logical slot whose needed rows were vacuumed away by a recovery conflict is a different failure from invalidation-by-WAL-size, with a different cause (hot_standby_feedback, not max_slot_wal_keep_size), so it is its own column rather than folded into the invalidation reason. It was visible on the web and invisible on the desktop. - I/O gains ExtendTimeMs plus the three derivations the MCP tool already computed - avg read ms, a per-combination hit ratio, and each row's share of the window's read time (which is how the grid's order was decided, made legible). ContextMeaning moved to DarlingPgIoReader, beside the query that produces the value it explains, so the MCP surface and the viewer print one copy; it rides as the Context cell's tooltip because it is a paragraph. - The csproj comment's triple apostrophe, from my own shell escaping. Also filled while the pin was pointing at them: the autovacuum grid's ANALYZE half (a table can be vacuumed on schedule and still give the planner stale row counts), live tuples and the dead-tuple SHARE, the manual-vs-automatic run split, and autovacuum_count - zero being the classic wraparound route, since relfrozenxid never advances on a table autovacuum has never processed. Statements gain the database OID (half the read's grain), one cache-hit ratio over both Aurora cache tiers, and temp blocks READ beside written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…onitor into feat/2530-viewer-pg-tabs
The merge itself was clean, but it carried a SEMANTIC conflict a text merge cannot see: PgInt64IdentityWireShapeTests arrived on dev naming DarlingPgStatementReader and DarlingPgBlockingReader, which this branch had already moved to PerformanceMonitor.Darling.Storage. One using. Nothing about #2548's fix changes here — queryid was already rendered as a string in the viewer's statement projection, for the same reason it is now a string on the wire: a PostgreSQL int8 queryid routinely exceeds 2^53, and the one field whose entire purpose is joining back to pg_stat_statements must not go through anything that rounds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three note assignments indexed All[0..2], which couples them to the registry's STRIP order - a list whose whole job is to be reorderable. The first reorder would have put the Vacuum note above the Activity grids, silently and correctly-compiling. NoteFor(id) instead, returning empty for an id the registry does not carry, because a missing note must never take a tab down with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
IsPostgres is only true for a token DescribeEngineKind recognises, so inside that branch EngineDescription always has words - the local function offering 'an engine the store has not recorded' was describing a state this code path excludes, which is the kind of defensive branch that later reads as a real case somebody should handle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two small ones the prose already promised and the markup did not deliver. The Query Shapes tab gave the per-database counters TWICE the height of the statement grid they exist to explain, which is backwards: they are the follow-up question (did this spill?), not a second list of equal standing. Swapped. And an unrecognised engine token reaches the badge raw so an operator can search their store for it; raw did not have to mean untrimmed, and a hand-edited row with surrounding whitespace would have rendered it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| string[] units = { "B", "KB", "MB", "GB", "TB", "PB" }; | ||
| double scaled = value; | ||
| var unit = 0; | ||
| while (scaled >= 1024 && unit < units.Length - 1) | ||
| { | ||
| scaled /= 1024; | ||
| unit++; | ||
| } | ||
|
|
||
| return unit == 0 | ||
| ? $"{value:N0} B" | ||
| : string.Create(CultureInfo.CurrentCulture, $"{scaled:N1} {units[unit]}"); |
There was a problem hiding this comment.
Bytes() picks the display unit by comparing the unrounded scaled value against 1024, but then formats with N1 (one decimal). That means a value whose unrounded quotient sits just under 1024 in a given unit renders as 1024.0 in that unit instead of bumping to the next one:
PgDisplay.Bytes(1048575) // 1 MB - 1 byte → "1024.0 KB", not "1.0 MB"
PgDisplay.Bytes(1073741823) // 1 GB - 1 byte → "1024.0 GB"... wait, "1024.0 MB", not "1.0 GB"This affects every column built on Bytes()/ByteDelta() — RetainedWal, SafeWalSize, TotalSize, OpSize, PeakMemory, WalWritten, TempBytes — whenever the underlying byte count lands just below a power-of-1024 boundary. Only Bytes(-1) and Bytes(0) are pinned in ViewerPostgresTabsTests, so this boundary case isn't caught.
Fix: re-check after rounding, e.g.
while (scaled >= 1024 && unit < units.Length - 1)
{
scaled /= 1024;
unit++;
}
if (unit < units.Length - 1 && Math.Round(scaled, 1) >= 1024)
{
scaled /= 1024;
unit++;
}| <DataTrigger Binding="{Binding IsInvalidated}" Value="True"> | ||
| <Setter Property="Background" Value="#33FF6B6B"/> | ||
| </DataTrigger> | ||
| <DataTrigger Binding="{Binding IsInactive}" Value="True"> | ||
| <Setter Property="Background" Value="#33FFB86B"/> | ||
| </DataTrigger> |
There was a problem hiding this comment.
Both DataTriggers set the same Background property, and WPF applies the last matching trigger when more than one condition is true simultaneously. IsInactive is declared after IsInvalidated, so it wins when both are true.
Per ViewerPostgresDisplay.cs's Slot() projection, these two flags are not mutually exclusive — IsInvalidated is set for an invalidated/lost/conflicting slot regardless of IsActive, and an invalidated slot is commonly also inactive (its subscriber has disconnected). So the common case of "this slot is invalidated" renders with the merely-informational orange "inactive" highlight instead of the red "invalidated" one, understating severity to an operator scanning row colors by background.
Swapping the declaration order fixes it (last-wins then favors the more severe state):
<DataTrigger Binding="{Binding IsInactive}" Value="True">
<Setter Property="Background" Value="#33FFB86B"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsInvalidated}" Value="True">
<Setter Property="Background" Value="#33FF6B6B"/>
</DataTrigger>(Note: the same ordering pattern already exists pre-PR elsewhere in this file for IsAborted/IsAttention, so this isn't a novel mistake — but it's new code in this PR and the two flags here are more likely to co-occur than most other pairs in the file.)
Review summaryThis PR is C#/WPF (the Darling desktop viewer's new PostgreSQL tabs) — no T-SQL install scripts touched, so the T-SQL style checklist doesn't apply here. Lite ships no PostgreSQL support at all (no Reviewed in depth: the shared-reader move to Two real findings, posted as inline comments:
Both are narrow and non-blocking, but real bugs in new code from this PR. |
**The CI red is mine, and the guard that caught it did its job.** DarlingPgReadSqlParsesLiveTests discovers the shipped PostgreSQL reads by reflection, filtered on a HARDCODED namespace string — and this branch moved the readers, so the filter matched nothing and its own anti-vacuity floor failed the build saying exactly that: "the reflection filter has stopped matching, so this test is no longer checking anything". That assertion is the reason a silent pass was not what happened instead. The filter now takes the namespace from a reader TYPE it has to resolve anyway, so it follows the readers wherever they live. Verified the fix rather than assuming it: on macOS that test SKIPS before reaching its own count (no DARLING_TEST_PG), so "it passed" proves nothing — a throwaway host invoked the shipped private ShippedReadSql() directly and it discovers 12 constants across the nine readers, which is the number its own doc comment names. Proved red by restoring the literal: 0. **Two review findings, both real.** Bytes() chose the display unit against the UNROUNDED quotient and then formatted with N1, so one byte short of a megabyte rendered "1024.0 KB" — a number expressed in its own next unit, which reads as a typo rather than a size, on every column built over bytes. Re-checked after rounding. WPF applies the LAST matching DataTrigger, and IsInvalidated/IsInactive are not exclusive: an invalidated replication slot is usually also inactive, because its subscriber has already gone. Declaring inactive last painted the informational amber over the red on exactly the rows that needed the red. Swapped — and asserted as a RULE over every row style on these tabs rather than fixed on the one grid review found, since the same ordering pattern is one edit away from recurring: each flag carries a severity and a milder one may not follow a severer one. The Overview grid's pair is mutually exclusive today and is ordered that way anyway, so one rule covers the tab set instead of a rule and an exception. Both proved red by reverting only the thing under test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review summaryReviewed the full diff (46 files, ~2.8k additions) against Parity check. The relevant parity boundary here is Darling's WPF viewer vs. Darling's web dashboard (not Lite — Lite has no PostgreSQL target, confirmed in Correctness.
Minor observation (not blocking): No missing-index DMV suggestions, no T-SQL in this diff (Darling stores to PostgreSQL, and object names are appropriately unqualified per this codebase's existing Nice work — this is an unusually well self-verified PR (the coverage/registry/dispatch/XAML consistency pins in |
Closes the WPF viewer half of #2530. With #2547 (web) already on
dev, this is the rest of the issue.What ships
git grep IsPostgresacrossPerformanceMonitor.Darling.Viewerreturned nothing, so an Aurora target opened on the SQL Server correlated-lane Overview and got the whole nineteen-tab strip — tempdb, Trace Flags, Query Store, Plan Viewer, Always On — nearly all of it permanently empty and several tabs meaningless for the engine. It now gets six, the same six #2547 chose for the web, with the same ids and the same grouping so the two front ends do not teach one engine two shapes.collection_logcomposed against the catalog — status, last run, runs, failures, rows, and why for one that cannot run herepg_stat_ioper backend / object / contextAll nine PostgreSQL collectors land on exactly one tab, and that placement is derived from
CollectorCatalogin both directions — a tenth collector turnsEveryPostgresCollector_IsShownOnExactlyOnePostgresTabred naming itself. That is the check that did not exist while eight of them shipped MCP-only for three releases. TheViewerCollectorCoverageTestsallow-list, which carried all nine as tracked debt with "remove each when the PostgreSQL tab ships", is now empty.Six against nineteen is the design. The tabs that are missing (tempdb, Query Store, trace flags, plan cache,
system_health, Always On) have no PostgreSQL analogue to fill, and the signals that matter here — wraparound headroom, the xmin horizon, vacuum backlog, WAL retention — have no SQL Server analogue either.The Aurora-only panels are SHOWN, not hidden
pg_wait_statsandpg_statement_statsboth gate onIsAurora, so on stock PostgreSQL they can never have content. Each panel printsCollectorEngineCapability.NotCollectedMessage— the same sentence the MCP surface and the web dashboard print, naming the server, the engine, the collector and the exactaurora_stat_*surface, ending "and never will". The defect #2530 is about is unexplained emptiness, not emptiness; hiding would also make the tab strip a different shape on two PostgreSQL servers in one fleet, and would re-derive in the viewer a gate the collectors already decide.The Overview grid is catalog-driven for the same reason. A gated-off collector writes no
collection_logrow at all — dispatch filters it out, deliberately, because a fake SUCCESS/0-rows would be ~2,880 rows a day per server of noise — so a grid built from the log alone would drop precisely the row an operator most needs explained. Rows come from the nine catalog collectors and the log only fills them in. Three distinct states, because they need three different responses: a permanent engine gap (nothing to do), a collector that should be running with no rows in the window (something to chase), and one that ran and failed (its own error text).One copy of the SQL
The nine
DarlingPg*Readerclasses move from the service'sMcp/folder intoPerformanceMonitor.Darling.Storage, which the service, the viewer and the tests all already reference. Namespace change only — no query text, no ordinal mapping, no signature is touched, and every existing reader test still pins the same constants.Copying them into the viewer instead would have meant a second copy of, among others, a 200-line recursive blocking walk whose revisit guard, root attribution and truncation flag were each a separate review finding. The copy that drifts is never the one being read — the same reasoning
CollectorEngineCapability's own comment gives for keeping one copy of a sentence both SKUs print.ViewerCollectorCoverageTests.ReaderLayerText()follows that: it now scans the viewer'sViewerDataService*.csplus the shared store readers those files NAME, one hop outwards, never the reverse. Coverage still means "the viewer reads it" — stop callingDarlingPgIoReaderandpg_io_statsloses coverage immediately, proved below — without demanding a duplicate copy of the SQL purely to satisfy a text match.Mechanics worth knowing
TabItems continue the sameTabControlat indices 19–24 rather than living in a second one. For a PostgreSQL server the nineteen SQL Server tabs are collapsed and these six shown, so both sets keep fixed indices and every drill-down that navigates by an index constant is untouched. A pin asserts the PostgreSQL run is a contiguous block at the end, in registry order, with headers matching the XAML.default:— falling through would run the SQL Server overview lanes (CPU %, wait ms/sec, buffer pool, file-I/O latency: four collectors that cannot run on this engine) at a PostgreSQL server, which is the exact defect being closed.engine_kindandsql_engine_editionare selected by BOTH server reads. The sidebar usesManagedServersSqlon any seeded store — i.e. every real deployment — so a discriminator on onlyServersSqlwould have left every PostgreSQL target on the SQL Server tab set while a unit test over the other query passed. Pinned.IsPostgresisMonitoredEngineKind.IsPostgres(EngineKind), false for null, empty, unrecognised andsqlserver. Such a server keeps the SQL Server tabs and gets no engine badge at all rather than one reading "SQL Server" — the tabs it gets are a default, not a finding. An unrecognised token renders as the raw token, because the describer's "an unrecognised engine" is worded to sit mid-sentence in the capability messages and the literal string is what an operator would search their store for.pg_blockingis a periodic sample, not an event log; "two chains" means something different in 60 captures than in 4, and the stored edge list cannot tell an absent capture from a capture that found nothing.-1is the not-applicable sentinel (chosen over0precisely because0reads as "started this instant" / "retains nothing"), an untrackedpg_stat_iowrite counter reads "not tracked" rather than0(Aurora backends do not write data files), a NULL recurrence is "unknown" and not "once",temp_blks_writtenis labelled in blocks because the block size is a compile-time server setting, and every timestamp goes throughViewerTimeHelper.ForDisplay.Verification
Every pin was executed on macOS against the compiled
Darling.Tests.dllthrough a throwawaynet10.0reflection host that loads types by name (enumerating them drags in PresentationFramework, which does not exist here). That is not the xUnit runner; CI is the arbiter for the Windows suites.covering
ViewerPostgresTabsTests(22),ViewerCollectorCoverageTests,ServerPageTabsTests,MonitoredEngineKindStoreTests, all ten movedDarlingPg*ReaderTests,ViewerDataServiceTests,ViewerServerTabCapabilityPinTests,ViewerGridPayloadColumnOrderPinTests,ViewerConfigChanges*Tests,FleetViewTests,DarlingEmptyEnumeration*Tests.Every new guard was proved RED first, by reverting only the thing under test:
DarlingPgIoReaderfrom the viewerEveryCollectorTable_HasAViewerReader_OrIsAllowListed— "pg_io_stats"pg_wait_statsfrom the Waits tab's registry entryEveryPostgresCollector_IsShownOnExactlyOnePostgresTabandTheAuroraOnlyCollectors_AreShown_NotHiddenLoadInnerTabAsyncEveryPostgresTab_HasItsOwnArmInTheDispatchTabItemheader in the XAMLTheRegistry_MatchesTheTabItemsDeclaredInTheXamlPgWaitsNote.TextEveryPostgresPanel_ExplainsItsOwnEmptyStateNoSqlServerTabLoader_CallsAPostgresReadIsPostgres→ "any non-empty token"TheTabSet_SwitchesOnlyOnAPositivePostgresClaim(sqlserver),(mysql)TheOverviewGrid_ShowsAGatedOffCollector_WithTheReason-1render as a numberTheNotApplicableSentinel_NeverRendersAsANumberBothServerReads_CarryTheEngineDiscriminatorAlso: full-solution build clean;
--locked-moderestore passes for bothDarling.Testsand the viewer (the one-line lock addition is the viewer's new explicitPerformanceMonitor.Collectorsreference); the XAML parses as well-formed XML; bare-LF count is 0 across all 41 changed files;CHANGELOG.mdstaged throughhash-object --no-filtersso its diff is 4 lines rather than 5,000; and theFleetIdentifierScrubTestsregex was run by hand over all changed files (0 offenders — its own run is vacuous in a.claude/worktrees/checkout, since it excludes any path containing/.claude/).What could NOT be verified
Said plainly rather than implied:
Expanderfor cycles, the engine badge and the row-highlightDataTriggers are all unexercised. The pins assert structure, placement and prose — never layout.-1sentinel actually appearing inroot_xact_duration_ms/safe_wal_size_bytes, Aurora'swritesarriving as the "not tracked" flag rather than 0,vacuum_thresholdbeing 0 on a never-analyzed table, and whetherPgGridRowLimit(200) is the right ceiling on a real autovacuum backlog.net10.0-windows). The reflection host is not the xUnit runner; CI decides.ApplyEngineTabSet()touchesTabItem.VisibilityandInnerTabs.SelectedIndex, which need a WPF dispatcher. The decision it makes is pinned; the application of it is not.Corrections to #2530's plan
Recorded on the issue as well. The web half found five errors; these are the viewer's:
Darling/README.mdsaid "three of the seven" PostgreSQL collectors are outage predictors. Seven was stale twice over (Darling monitors PostgreSQL and Amazon Aurora PostgreSQL #2213's eighth, Collect pg_stat_database: temp-file spills, cache hit ratio, deadlocks and commit ratio in one read #2539's ninth). Corrected to nine, derived.Darling/README.mdsaid the blocking-chain reads have no PostgreSQL equivalent. They have had one since thepg_blockingcollector landed. Corrected.Not closing #2530 — that is Erik's call.
Review round
Three
claude[bot]findings, all real. Two of them were the same defect, so the fix is an invariant rather than two columns.PgSlotRow.Conflictingreaches no viewer column. A logical slot whose needed rows were vacuumed away by a recovery conflict — visible on the web, invisible on the desktop.hot_standby_feedback, notmax_slot_wal_keep_size, and the two need different responses), plus the invalidated row highlight.ExtendTimeMs, noavg_read_ms,hit_pctorpct_of_total_read_time.ContextMeaningmoved toDarlingPgIoReader, beside the query that produces the value it explains, so the MCP surface and the viewer print one copy; it rides as the Context cell's tooltip, because it is a paragraph and it is the dimension with no SQL Server counterpart.service'''sin the csproj comment — my own shell escaping.The category, not the instances. Two instances of "a field the shared reader goes to the trouble of returning that no screen shows" is the signal to stop fixing instances.
EveryFieldTheSharedReadersReturn_ReachesAColumn_OrIsFoldedWithAReasonreflects over the ten reader row records against their display classes: every field reaches a column, or appears inFoldedFieldsnaming the column that carries the fact and why that shape reads better. Derived both ways — a new reader field turns it red until something happens, andEveryFoldedField_StillExists_AndSaysWhydeletes an exemption the moment its field stops existing.Proved red by deleting exactly the two columns review found, each pin naming itself:
It found three more on its first run, which is the point of writing it instead of two columns:
PctTowardMultixactWraparoundhad no column at all. The multixact side has its own shutdown ceiling, reached independently — a workload heavy on row-level share locks or subtransactions gets there first, and nothing on the XID columns would say so.AutovacuumFreezeMaxAgeandAutovacuumMultixactFreezeMaxAgewere renamed rather than shown; they are now columns, because the percentages beside them are graded against this cluster's settings and two databases at "80% to emergency" are different distances from trouble when theirfreeze_max_agediffers.Filled while the pin was pointing at them: the autovacuum grid's ANALYZE half (a table can be vacuumed on schedule and still hand the planner stale row counts), live tuples and the dead-tuple share, the manual-vs-automatic run split, and
autovacuum_count— zero being the classic wraparound route, sincerelfrozenxidnever advances on a table autovacuum has never processed. Statements gain the database OID (half the read's grain), one cache-hit ratio over both Aurora cache tiers and both miss sources, and temp blocks read beside written.Merge note
devmoved while this was in review and the PR wentCONFLICTING— worth flagging on its own, because a conflicting PR dispatches no workflows, sogh pr checksshows a lone passingcheck-branchesand looks exactly like a settled green. Onlymergeable/mergeStateStatustell the truth. (All six checks had gone green on the first push before that happened.)Merged
devin rather than rebasing — a force-push orphans the inline review anchors above. The text merge was clean, and carried a semantic conflict it could not see: #2548's newPgInt64IdentityWireShapeTestsarrived namingDarlingPgStatementReaderandDarlingPgBlockingReader, which this branch had already moved. Oneusing.Nothing about #2548's fix changes here —
queryidwas already rendered as a string in the viewer's statement projection, for the same reason it is now a string on the wire.Re-verified after the merge: full solution build clean, and 146 pins re-executed through the reflection host including #2548's own new suites — 0 failures.
Three of my own, found re-reading the diff
Not review findings — mine, and each is the same shape: prose the markup did not deliver.
All[0..2]) into a registry whose whole job is to be reorderable. The first reorder would have put the Vacuum note above the Activity grids, silently and correctly-compiling.NoteFor(id)instead.IsPostgresis only true for a tokenDescribeEngineKindrecognises, so "an engine the store has not recorded" described a state that branch excludes. Deleted, rather than left to read later as a real case somebody should handle. (An unrecognised token is now also trimmed before it reaches the badge — raw did not have to mean untrimmed.)Review round two, and the CI red
The red was mine, and the guard that caught it is exactly the kind this PR is arguing for.
DarlingPgReadSqlParsesLiveTests(from #2554's lane, merged in fromdev) discovers the shipped PostgreSQL reads by reflection filtered on a hardcoded namespace string — and this branch moved the readers, so the filter matched nothing. Its own anti-vacuity floor failed the build saying precisely that: "the reflection filter has stopped matching, so this test is no longer checking anything." Without that assertion it would have gone green over an empty list.The filter now takes the namespace from a reader type it has to resolve anyway, so it follows the readers wherever they live. Verified rather than assumed: on macOS that test skips before reaching its own count (no
DARLING_TEST_PG), so "it passed" locally proves nothing — a throwaway host invoked the shipped privateShippedReadSql()directly and it discovers 12 constants across the nine readers, the number its own doc comment names. Proved red by restoring the literal: 0.Two more
claude[bot]findings, both real:Bytes()picks the display unit against the UNROUNDED quotient, then formatsN1— so one byte short of a megabyte rendered"1024.0 KB", a number expressed in its own next unit. Affects every byte column.DataTrigger, andIsInvalidated/IsInactiveare not exclusive — an invalidated slot is usually also inactive, its subscriber having gone. Declaring inactive last painted the informational amber over the red on exactly the rows that needed the red.Both proved red by reverting only the thing under test.
Final state: all six checks green on
1f41186a,mergeable=MERGEABLE,mergeStateStatus=CLEAN, and the review run on that head posted no new findings. Not merging — that is Erik's call.🤖 Generated with Claude Code