Repository navigation
pg_database_stats stores numbackends and pg_wait_sampling stores sampled_ms (Darling V133), so a PostgreSQL operator's saturation fraction has a numerator and the sampler arm's wait rate has its duty-cycle denominator (partial #3691) - #3716
Merged
Merged
Conversation
…led_ms (Darling V133), so the saturation fraction has a numerator and the sampler arm's wait rate has its duty-cycle denominator (partial #3691) The ONE migration rung of the #3691 v2 wave: two nullable integer columns, written by their collectors from this rung on and read by nothing yet. numbackends: pg_stat_database's client-only connection count per database, a level sampled every minute, universal since 8.x - the honest numerator for "how full is max_connections", which pg_session_states could not supply because it captures only sessions holding an old open transaction. sampled_ms: the milliseconds the service-side wait sampler actually observed in each collection (snapshots read x period, 30,000 on a full window). The arm stores profile_period_ms = 1000 and a cumulative tally but watches 30 s of every 300 s, so samples x period over the interval understates its rate ~10x and nothing in the row said so. The extension arm writes NULL, having watched the whole interval; NULL means today's arithmetic. Both nullable, no default, no backfill, appended last; no view to refresh (the PG collector tables are view-less); V83/V96 CREATE texts carry the columns for a fresh store per the V101 rule. Lite stores no pg_* table, so no DuckDB bump. Viewer probe gains the V133 sentinel and top arm; V132's test hands the top-rung claims to PgNumbackendsAndSampledMsRungTests.
This was referenced Sep 19, 2026
Closed
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.
The saturation fraction had no numerator and the sampler arm's wait rate had no denominator
Partial for #3691 — the ONE migration rung for this wave; V134 is free after merge. Darling rung V133 (
pg-numbackends-and-sampled-ms). Numberedmax(dev) + 1at open and re-checked againstorigin/devimmediately before push (V132 was still the top; nothing else is in flight). Completes these #3691 checklist lines VERBATIM:numbackendsonpg_database_stats— a RUNG (universal 1-min client-only numerator for saturation instead of exception-capture peaks). Ladder-sequenced with the other seat.sampled_mshalf of Stock PostgreSQL's sampled waits get no anomaly baseline … "or storesampled_msper row (rung)" — the rung half only; thecollector_statewhitelist alternative is not taken and the consumer's arithmetic is not changed.Does not close the issue.
The two lies the store told
Connections. The v1 saturation read had
pg_session_states.total_sessionsand nothing else to put overmax_connections. That collector stores the sessions holding an open transaction old enough to matter — the exceptions, not the population — so a quiet server has no row, a busy one has whatever crossed the capture rule, and "count / max_connections" is not a saturation fraction.pg_stat_database.numbackendsis the number the question wanted all along: client backends connected per database at the instant of the read, a LEVEL sampled every minute with the rest of the row, present since PostgreSQL 8.x (the oldest column in the view; the collector's version audit records it and the AppliesTo stays unconditional). It was never selected.Waits. The #3604 service-side sampler stores
profile_period_ms = 1000and a cumulative tally, exactly like the extension arm — but it observesSamplerSnapshotsPerCycleone-second snapshots (30 s) of every 300 s cycle. A rate read over the interval naturally writesΔsamples × period / interval, which on that arm is a ~10× understatement, and nothing in the row said so: the instrument token that discloses the arm lives incollector_state, outside the analysis FROM/JOIN whitelist, and the period alone cannot say how much of the interval was watched.The row contract
pg_database_statsnumbackends integer NULLd.numbackends, selected LAST) or NULL where the engine reports none (the shared-relations row)pg_wait_samplingsampled_ms integer NULLSamplerPeriodMs— 30,000 on a full window, counted by result-set shape like the tally itselfNULL means "not sampled", never 0. Zero connections is a measurement; a pre-rung row never took one. NULL
sampled_msmeans "the whole interval was observed", which is exactly today'ssamples × periodarithmetic — so the extension arm and every historical row read as they do now the moment a consumer divides by it.sampled_msis per row, not a cumulative in state — the one design choice worth arguing. A cumulative observed-ms counter would mirrorsample_count's reset-aware differencing, but it needs a third state key kept in step with the tally, and the tally's cap-drop "start over" semantics would decouple one key's samples from the shared denominator. The observed window of one collection is a fact only that collection knows and nothing can recover afterwards; a per-row value survives a tally reset or a cap-drop with no second counter; every row of a collection carries the same figure (likeprofile_period_ms), so a consumer wanting a window's total sums one value percollection_timebetween its endpoints. Stated in the rung doc, the collector's column comment andDarlingPgWaitSamplingReader.EstimatedWaitMs's doc, which now namessampled_msas the denominator a rate read must use and says plainly that this reader does NOT yet make the correction.What changed, and why this shape
Schema.
PgMigrationsV133:ALTER TABLE collect.pg_database_stats ADD COLUMN IF NOT EXISTS numbackends integer;+ the same forcollect.pg_wait_sampling.sampled_ms. Nullable, no DEFAULT, no backfill (V127/V128/V132's shape; both tables are compressed hypertables on the fleet and a DEFAULT is the compressed-segment rewrite trap). No view to refresh — the PostgreSQL collector tables have nov_passthrough, so unlike V132 this rung is two ALTERs and nothing else (pinned: noVIEWin the SQL, neither table inAllPassthroughViews). The V101 rule:V83SqlandV96SqlCREATE texts carry the new columns too, becausePgSchemaGeneratorTests.EveryPostgresRung_IsIdenticalToTheGeneratedSchemaholds every collector rung column-for-column equal to the generator's CURRENT output — the fresh population builds the table from the generator at V1, the upgraded population gets it from this rung's ALTER; neither text is redundant, and both rung docs say so.StorageVersion133.Lite is unchanged, and that is a checked claim. Lite stores no
pg_*table (DuckDbSchemaGenerator.StoredCollectorsis the SQL Server set); V128's Lite bump was for the SQL Server families it also dressed, not forpg_wait_stats. Pinned byLiteStoresNeitherTable_SoTheRungHasNoDuckDbTwin(both collectors'TargetEngineis PostgreSQL).Collectors.
PgDatabaseStatsCollector: selectsd.numbackendsLAST (afterstats_reset, so the reader ordinal 9 and the appended column agree),Rowgainsint? Numbackends,PayloadColumnsappendsnumbackends Integer,WritePayloadwrites it tenth; the type header gains the "one level in a row of counters" paragraph and the version audit records the column.PgWaitSamplingCollector:Rowgainsint? SampledMs,PayloadColumnsappendssampled_ms Integer,ReadSamplerAsynccounts four-column result sets and stampssnapshots × SamplerPeriodMson every emitted row, the extension arm'sReadAsyncstampsnull,WritePayloadwrites it seventh; the FLOOR paragraph in the type header says why.Viewer probe (
ViewerDataService.cs, the boundary extension every rung makes): the V133 COLUMN sentinel (pg_database_stats/numbackends— the rung's first ALTER, stated so nobody looks for a significance), ordinal 108,hasPgNumbackendsAndSampledMstop arm returning 133, V132's arm re-worded as one rung behind. The arm's prose states that the gate rests on the standing invariant alone (no viewer read names either column yet). Without itRequiredStoreSchemaVersion == StorageVersion.SchemaVersionwould map every current store one rung low and the connect-time gate would refuse it.Readers: doc only.
DarlingPgWaitSamplingReader.EstimatedWaitMs— the addendum above, arithmetic untouched.DarlingPgDatabaseReader— a class-doc paragraph namingnumbackendsas the level it does not yet select and what a consumer must do (never difference it; NULL is "not sampled"; sum across databases for the cluster figure).Docs.
Darling/README.md: a V133 row in the migration table; thepg_database_statsandpg_wait_samplingrows in the collector table gain one sentence each ending "read by nothing yet".Blast radius
pg_database_statsrow gains the level (or NULL on the shared-relations row); every sampler-armpg_wait_samplingrow gains 30,000 (or the snapshots actually read × 1,000); every extension-arm row gains NULL. No existing column changes meaning or position.NoReaderSelectsEitherColumnYet_…greps both readers' SQL constants). ThePgTarget*analysis files are not touched; the existing differencing read over rows carrying the new columns is exercised live below and reports what it always did.Rowrecords gain a trailing nullable field; the two Lite fixtures that constructedPgDatabaseStatsCollector.Rowpositionally are updated.What this PR does NOT do
numbackendsonget_pg_database_stats, no Compose measure. Those are the consumer lanes once the fleet has a day of rows.collector_statefor analysis reads (the checklist line's other alternative) — the row now carries what the token would have said.pg_session_states, changeEstimatedWaitMs's arithmetic, or touch Lite,deprecated/Dashboard,install/or anyPgTarget*file.Live verification
PostgreSQL 18 + TimescaleDB 2.28.1 (
timescale/timescaledb:2.28.1-pg18, throwaway container on Docker contextdesktop-linux, torn down):PgNumbackendsAndSampledMsLivePostgresTests(gated, new):MigrateAsyncto 133; then the store is put one rung short (both columns dropped, the 133 stamp deleted) andMigrateAsyncapplies exactly one rung and then zero, stamping 133; both columns readinteger / YES / no defaultand are the LAST column of their tables; one populated row and one NULL row per table through each collector's realWritePayloadover a realNpgsqlBinaryImporter(DarlingCollectorRunner's COPY loop in shape) read back verbatim (("appdb", 37),(NULL, NULL);Lockrows 30,000 andIOrows NULL across two cycles);DarlingPgWaitSamplingReader.GetPgWaitSamplingPageAsyncover those rows differences as before (Lock Δ7 →EstimatedWaitMs7,000; IO Δ1,000 → 10,000) and publishes neither column.PgWaitSamplerLiveTests(gated, changed): the REAL sampler batch through the real runner against a live target — every stored row now also assertssampled_ms == SamplerSnapshotsPerCycle × SamplerPeriodMs(30,000). 1/1.MigrationUpgradeLadderLiveTests(release fixtures v3.3.0 / 3.5.0 / 3.6.0 / 3.7.0 climbed to 133): 5/5.PerfmonCounterTypeLivePostgresTests,DarlingPgDatabaseStatsLiveTests,DarlingAnalysisStoreTests(max stamped version ==StorageVersion.SchemaVersion),StoreCopyPhaseLivePostgresTests: all green against the migrated store.Test plan
New —
Darling.Tests/PgNumbackendsAndSampledMsRungTests(5 + 1 live): the rung at the top of a dense ladder; two nullable integer ADD COLUMNs in each collector's generator-rendered type, no VIEW / DEFAULT / NOT NULL / UPDATE / INSERT / DELETE / DROP / INDEX / TABLE, neither table inAllPassthroughViews, both generated CREATE TABLEs and COPY lists carry the column LAST, the rung doc's phrases,HypertableCount71, payload counts 10 and 7; Lite stores neither table; the probe's three sites + top arm (all-true →StorageVersion.SchemaVersion, one behind → 132, arm order, neither table named in the arm's prose); no reader SQL names either column and both readers' docs say what a consumer must do.PgNumbackendsAndSampledMsLivePostgresTests(gated, above).Changed —
Darling.Tests/PerfmonCounterTypeRungTestshands its top-rung claims to V133's file (the standing handoff idiom; the ordinal-107 pin becomes "fixed, not last").Darling.Tests/PgWaitSamplerLiveTestsreads and assertssampled_ms.Lite.Tests/PgDatabaseStatsCollectorDefinitionTests:d.numbackendsin the select and selected afterstats_reset; 10 payload columns withnumbackends IntegerLAST; the populated-row fixture reads 37, the shared-relations fixture reads NULL as null; the twoRowconstructions carry the tenth value;WritesEveryPayloadColumnInOrderasserts slot 9; newWritesANullNumbackendsAsNull.Lite.Tests/PgWaitSamplerArmTests: three snapshots → every rowSampledMs3,000 (the 100%-vs-1% comment); the carried-tally cycle → 1,000 on every re-emitted row (per-row, not cumulative); the extension arm →null.Lite.Tests/PgWaitSamplingCollectorDefinitionTests: newTheObservedWindow_IsTheLastPayloadColumn_AndTheExtensionQueryDoesNotSelectIt(7 columns,sampled_ms Integerlast afterbackend_count, absent from the extension query).Executed here — Darling.Tests on the Mac (WindowsDesktop stripped from the runtimeconfig,
DARLING_TEST_PGon the rig):PgNumbackendsAndSampledMsRungTests5/5 + live 1/1,PerfmonCounterTypeRungTests,NotificationRoutesRungTests,MigrationLadderPins,MigrationDataMovingRungCensusPins,PgSchemaGeneratorTests(the ladder-generator diff — the one census that fired during the lane and drove the V83/V96 CREATE edits),PgLogEventsPipelineTests, bothDeltaFamilyInterval*RungTests,PgCopyWriterTests,ReadmeDerivedCountPinTests,StoreSqlClockDisciplineTests,AuroraOnlySqlIsGatedTests,DocCommentHygieneTests,RepoFileAdoptionTests,CommentFilterAdoptionTests,RefreshCeilingProvenancePinTests,PgExtensionDependencyContractTests,MigrationUpgradeLadderLiveTests,PgWaitSamplerLiveTests,DarlingMcpPgWaitToolsTests,PgWaitInstrumentDisclosureTests,LiveCleanupConversionRatchetTests— all green;LivePostgresCollectionHygieneTests.EveryClassUsingTheSharedStore_…reds on the Mac withReflectionTypeLoadException(the WPF platform artefact; the new live class carries[Collection("live-postgres")]and theLiveStoreCleanupfinally). Lite through a net10.0 harness over the built Collectors library (AssemblyName Lite.Tests):PgDatabaseStatsCollectorDefinitionTests,PgWaitSamplerArmTests,PgWaitSamplingCollectorDefinitionTests— 55/55.First executed in CI, stated plainly: the Windows
buildjob is the first execution of every other Lite/Viewer census; nothing here is SQL Server-side.Builds,
-c Debug -p:EnableWindowsTargeting=true:PerformanceMonitor.Collectors,PerformanceMonitor.Darling.Storage,PerformanceMonitor.Darling.Viewer,PerformanceMonitor.Darling.Service,PerformanceMonitorLite,Darling.Tests— 0 warnings;Lite.Tests— dev's one pre-existing xUnit2000 inLogTailOverlapThresholdPinTests.cs:95, untouched.Which component(s) does this affect?
Checklist
dotnet build -c Debug)