Skip to content

compare_analysis sigma-bands the v2 PostgreSQL baselined metrics — I/O read latency, replay lag, WAL volume — so a 16× latency move stops reading by the ladder alone; the write family has one definition of "WAL tracked" (partial #3691) - #3748

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/3691-compare-v2-banding
Sep 19, 2026

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

compare_analysis on a PostgreSQL target now sigma-bands the three v2 baselined metrics — PG_IO_READ_LATENCY_MS, PG_REPLICATION_LAG and PG_WAL_VOLUME_SHIFT — against the server's own hour-of-week bucket, and the write family has ONE definition of "WAL tracked" that both its facts apply. Two residues from the v2 exit check. PARTIAL for #3691 — do not close. Checklist line completed verbatim: compare_analysis bands the v2 baselined metrics absolutely

Item 1 — the W1 gap, repeated for the v2 baselines

#3713 closed W1 by mapping the four v1 pg_ buckets in ComparisonBanding.BaselinedMetricFor and adding the BaselinedValueFor unit-reconciliation seam. The v2 lanes then stored three more buckets (pg_io_read_latency, #3726/#3737; pg_replay_lag_bytes, #3727; pg_wal_bytes_per_sec, #3728) with detectors that judge against them — and nobody extended the seam. On the exit check's planted store PG_IO_READ_LATENCY_MS (1.55 → 25 ms), PG_REPLICATION_LAG (1 → 200 MB) and PG_WAL_VOLUME_SHIFT (1 → 9.7 MB/s) came back band_source: absolute, baseline_metric: null in the same window their detectors called 25σ.

Before (the exit check's shape, reconstructed for the live class's planted I/O series: the absolute arm's relative_move / ladder_position are the very figures the after-payload still carries, and with no mapped metric the row could only have taken that arm — not a separately executed pre-change binary):

{ "key": "PG_IO_READ_LATENCY_MS", "baseline_value": 1.5, "comparison_value": 25, "value_delta": 23.5,
  "relative_move": 0.94, "ladder_position": 0.875, "status": "worse", "band_source": "absolute",
  "delta_sigma": null, "baseline_sigma": null, "baseline_median": null, "baseline_metric": null }

(I/O happens to clear the absolute rule because its ladder moves; the lag and WAL facts on the exit-check store did not — PG_WAL_VOLUME_SHIFT has base 0 by design, exactly PG_TPS's trap.)

After, the same store through the REAL DarlingMcpTools.CompareAnalysis, read off the executed live class:

{ "key": "PG_IO_READ_LATENCY_MS", "baseline_value": 1.499848, "comparison_value": 25, "value_delta": 23.500152,
  "relative_move": 0.94, "ladder_position": 0.875, "status": "worse", "band_source": "baseline",
  "delta_sigma": 25, "baseline_sigma": 0.0207, "baseline_median": 1.5017, "baseline_confidence": 0.85,
  "baseline_tier": "Full", "baseline_metric": "pg_io_read_latency", "beyond_anomaly_cutoff": true }

The three arms, each verified against its collector's unit before mapping:

  • PG_IO_READ_LATENCY_MS (PgTargetFactCollector.Io.cs): Value = window read ms ÷ reads; pg_io_read_latency = the same quotient per quarter-hour sample. Same unit, the averaged-key case (per-sample sigma bounds the average's dispersion; the band is conservative) — banded on Value. BaselinedValueFor withholds a fact that makes no latency claim (latency_measured = 0: no pg_stat_io on the major, or track_io_timing off — both unavailable shapes) and one whose quotient the scorer states but does not grade (insufficient_ops = 1); the scorer's own vocabulary, no new flag.
  • PG_REPLICATION_LAG (.Replication.cs): Value = the window's peak replay_bytes_behind (one fact, the worst standby); pg_replay_lag_bytes = MAX(replay_bytes_behind) per collection. Same bytes — banded on Value. No unavailable shape exists (a server with no replication emits nothing).
  • PG_WAL_VOLUME_SHIFT (.Write.cs): Value = the window's MEAN bytes/s across rated collections; the bucket is per-collection bytes/s and PgTargetAnomalyDetector.Wal.cs judges the window's PEAK against it. So this is the second key (after PG_CONNECTION_SATURATION) whose Value is not the reading banded: the row bands on the fact's peak_wal_bytes_per_sec metadata via the compare_analysis sigma-bands the PostgreSQL baselined metrics, so a 6× TPS surge on a PostgreSQL target stops reading "stable" (#3691 W1/W2) #3713 seam — the reading whose verdict beyond_anomaly_cutoff claims to agree with — and value_delta stays the mean, stated in BandBySigma. The unavailable shape (Aurora, pre-14, an all-zero series) carries no peak, so it is withheld by the metadata's absence and no second flag is consulted here; two unavailable sides read stable by the absolute rule with no sigma and baseline_metric still naming the bucket (pinned). Rejected: banding the mean (it would disagree with the detector on the same window) and inverting beyond_anomaly_cutoff's meaning for one key.

The compare_analysis description enumerated the PostgreSQL keys ("transactions/sec, session count, deadlocks/hour, CPU as % of capacity"), so it now names all seven on both SKUs; PgTargetBetweenWavesV2Tests' parity pin follows deliberately and additionally pins that exactly seven PgTargetFactKeys constants map to a pg_ metric — a new key silently mapped, or a described one silently dropped, fails it.

Item 2 — one definition of "WAL tracked"

Lane 15 (#3728) shipped two: ReadCheckpointPressureAsync decided from NULL-ness alone (bool_or(wal_bytes IS NOT NULL)) and stamped wal_tracked 1, wal_bytes 0, wal_bytes_per_sec 0; ReadWalVolumeAsync, over the SAME table and window a moment later, also required a non-zero byte OR record sum and emitted unavailable / reason_wal_stats_not_reported. On a window whose wal_bytes is 0-not-NULL the two facts contradicted each other on one pass. That shape is a planted store's — the real collector types the WAL columns NULL where pg_stat_wal is not implemented (Aurora) or does not exist (below 14), so on the fleet it is unreachable — but a read that can contradict its sibling on any input is two definitions, and PgTargetAdvice.Write.cs reads the pressure fact's flag to decide whether to state WAL figures at all.

PgTargetFactCollector.WalIsTracked(anyNonNull, walBytes, walRecords) — anyNonNull && (walBytes > 0 || walRecords > 0) — is now the one predicate, documented once (mechanism, the exit-check evidence, why one, what it does not change), applied by both reads. The checkpoint read's SQL gains wal_records as its appended last column (ordinal 11, after sample_count) so the ten ordinals lane 2 and lane 15 read did not move; the differencing is the WAL read's, verbatim. Untracked leaves the pressure fact's WAL figures ABSENT, not zero — the posture it had. Bytes moving with records at 0 (the PgTargetKnobsTests fixture) stays tracked: either counter moving is the counter being populated.

Blast radius

  • SQL Server: byte-identical. Every SQL Server key's BaselinedValueFor is its value (the _ => fact.Value arm), the four SQL Server metric arms are untouched, DispersionMetricsFor over a SQL Server set names no pg_ metric — ComparisonBandingTests.ASqlServerFactSet_NeverReachesThePostgresArms and the 18 SQL Server scenarios pass unchanged.
  • Lite: shares ComparisonBanding; never asked for a pg_ metric because it has no pg_-sourced facts. Its compare_analysis description changes by the same seven words as Darling's (parity).
  • PostgreSQL: three more keys can take the sigma path; LookUpDispersionAsync issues up to three more windowed bucket reads per compare (the same reads the anomaly pass already makes; not on the alert path). Payload shape unchanged. The write family's two facts now agree on trackedness on every input; on the fleet (all-NULL on Aurora) both said untracked before and say so now.

What it does NOT do

Does not band PG_IO_WRITE_LATENCY_MS (no write-latency bucket), PG_WAIT_PROFILE, or the buffer / checkpoint / vacuum / temp / config readings (no bucket). Does not give any key a two-sided reading (v3). Does not make band_rules.baseline engine-aware (its sentence is shared; noted on #3713). Does not touch PgTargetBaselineProvider, MetricNames, any scorer, detector or advice file, or PgTargetKnobsTests (its (wal_bytes IS NOT NULL) AS wal_tracked pin still holds — the SQL keeps that column). No rung, no key, no knob.

Every file, one line

File Why
PerformanceMonitor.Analysis/ComparisonBanding.cs three arms in BaselinedMetricFor; two in BaselinedValueFor (I/O withheld shapes, WAL peak); PgWalPeakBytesPerSecKey; the class summary / Compare / BandBySigma doc lines the change falsified
Darling/…/PgTargetFactCollector.Write.cs WalIsTracked; wal_records appended to PgTargetCheckpointSql; both reads call the predicate; doc
Lite.Tests/ComparisonBandingTests.cs one new test (the three keys, their withheld shapes, the seven-metric DispersionMetricsFor); four collector-shaped builders
Darling/Darling.Tests/CompareAnalysisPgBandingLiveTests.cs the I/O series planted beside the TPS one; the PG_IO_READ_LATENCY_MS row asserted through the real tool; cleanup covers pg_io_stats
Darling/Darling.Tests/PgTargetWriteTests.cs WalIsTracked theory (six shapes); the checkpoint read's column pin; a gated e2e planting the 0-not-NULL fixture and asserting both facts untracked
Darling/…/Mcp/DarlingMcpTools.cs, Lite/Mcp/McpAnalysisTools.cs the description names the seven PostgreSQL baselined keys (boundary extension the STEP brief names: "update … ONLY if it enumerates keys")
Darling/Darling.Tests/PgTargetBetweenWavesV2Tests.cs the description parity pin follows; plus the exactly-seven-mapped-keys pin

Which component(s) does this affect?

  • Lite (shared PerformanceMonitor.Analysis; description parity)
  • Darling
  • Lite Tests
  • Darling Tests
  • SQL collection scripts
  • Documentation
  • Full Dashboard (deprecated)
  • CLI Installer (deprecated)

How was this tested?

EXECUTED here — two net10.0 harnesses compiling the real test files against this tree (first execution of everything by CI on Windows):

Builds, zero warnings of ours: PerformanceMonitor.Analysis 0/0; PerformanceMonitor.Darling.Analysis 0/0; PerformanceMonitor.Darling.Service 0/0; Lite/PerformanceMonitorLite (-p:EnableWindowsTargeting=true) 0/0; Darling/Darling.Tests 0/0; deprecated/Dashboard.Tests 0/0; Lite.Tests 1 warning = dev's pre-existing xUnit2000 at LogTailOverlapThresholdPinTests.cs:95.

No production store or fleet server was read.

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 (the SQL Server pass is unreachable by every new arm; the routing pin and 18 SQL Server scenarios pass unchanged)
  • I have not introduced any hardcoded credentials or server names

Partial for #3691 (step 19). CHANGELOG deliberately untouched — the coordinator splices this wave's entries. Not armed (review-dark window).

… definition of WAL tracked (partial #3691)

ComparisonBanding maps PG_IO_READ_LATENCY_MS -> pg_io_read_latency (value),
PG_REPLICATION_LAG -> pg_replay_lag_bytes (value) and PG_WAL_VOLUME_SHIFT ->
pg_wal_bytes_per_sec (its peak_wal_bytes_per_sec, the reading the detector
judges; value_delta stays the mean). Unavailable shapes are withheld through
BaselinedValueFor and take the absolute rule with the bucket still named.

PgTargetFactCollector.Write.cs: WalIsTracked(anyNonNull, bytes, records) is
the one trackedness decision for both the checkpoint and WAL-volume reads;
the checkpoint read gains wal_records as its appended last column.

compare_analysis description names the seven PostgreSQL baselined keys on
both SKUs; the parity pin follows.
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