Skip to content

Tile PG-target TPS, sessions, CPU and Aurora waits detectors (#3653 A8 option B, lane L3a) - #4171

Merged
erikdarlingdata merged 11 commits into
devfrom
feat/3653-b-l3a
Sep 24, 2026
Merged

erikdarlingdata merged 11 commits into
devfrom
feat/3653-b-l3a

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Part of #3653 (A8 option B, lane L3a).

What this PR does

Moves the four PG-target families this lane owns onto per-hour tiles, scored through AnomalyGate.EvaluateTiles against PgTargetBaselineProvider.GetBucketMapAsync, with the never-blind fallback to today's whole-window EvaluateZScore when no tile clears AnomalyThresholds.MinTileSamples or has a non-empty bucket:

  • TPS (DetectTpsAnomalies): new TpsTileWindowSql const. DatabaseCounterWindowSql is left byte-identical — the deadlock-rate detector still reads it, unchanged.
  • Sessions (DetectSessionAnomalies): new SessionTileWindowSql, keeping the per_collection CTE and grouping the outer SELECT by tile.
  • CPU (DetectCpuAnomalies): new CpuTileWindowSql. Peak time moves to a per-tile array_agg(collection_time ORDER BY v DESC)[1]. The raw cpu_percent window scalar rides per tile; the detector reports the worst tile's raw value.
  • Aurora waits (DetectWaitProfileAnomalies): new WaitRateTileWindowSql. Rewritten in this lane to follow the coordinator's ruling on multi-arm families (see below), replacing L3a's original per-tile three-arm decision.

L3a-2 (this lane): work done

Step 1 — Aurora waits conforms to the coordinator's ruling

L3a's original code ran all three trust arms (robust z / classical ratio / absolute fallback) per tile by hand through a private DecideWaitProfileTile/WaitProfileTileDecision, then picked the worst firing tile. That broke the design in the four ways the brief named: no multiplicity correction on the ratio arm, an uncorrected k on the z arm's per-tile mean, a single untrustworthy tile able to switch the whole window to the absolute arm even when the start bucket trusted the z arm, and duplicated gate logic.

Replaced with the ruling's shape:

  • The arm is chosen from the START-hour bucket (GetBaselineAsync at TimeRangeStart), exactly as dev's whole-window body decides it.
  • Only arm 1 (baseline.IsTrustworthy && EffectiveRobustSigma > 0) moves onto tiles: AnomalyGate.EvaluateTiles(tiles, map, HeavyTailModifiedZThreshold, HeavyTailModifiedZThreshold, PgWaitProfileFallbackMsPerSec, PgWaitProfileFallbackMsPerSec, SigmaDisplayCap, window). When it returns null, today's EvaluateZScore runs over WindowTiles.WholeWindow(tiles).
  • Arms 2 and 3 (classical ratio, absolute fallback) stay whole-window and unchanged, fed from WholeWindow(tiles)'s Peak/Mean — no second SQL read. Commented in the source as outside B's scope.
  • DecideWaitProfileTile/WaitProfileTileDecision deleted.
  • Metadata (modified_z, mean_modified_z, ratio, fire_threshold, baseline keys) comes from the bucket and values actually scored — the worst tile's on the tiled path, the start bucket's otherwise. AddTileMetadata runs only on the tiled path.
  • Builds clean (dotnet build Darling/PerformanceMonitor.Darling.Analysis -p:EnableWindowsTargeting=true → 0 errors).

Step 2 — pins and census

  • PgTargetAnomalyTests.s_detectorSql: added TpsTileWindowSql, SessionTileWindowSql, CpuTileWindowSql, WaitRateTileWindowSql (the reflection census requires every public const ending Sql to be listed).
  • Added table/local_hour/dialect pins for each of the four new consts (same file, EveryDetectorRead_... test).
  • Found and fixed a pre-existing pin regression from L3a's earlier commits (not introduced by this lane's step 1, but in scope for "fix every pin failure" — step 2's own instruction): ThePairGate_... test's regex pinned AnomalyGate.EvaluateZScore(baseline, peakTps, avgTps, ... window: context.TimeRangeEnd - context.TimeRangeStart) verbatim, but L3a's TPS/sessions/CPU tiled call-flow refactor introduced a local window variable computed once at the top of each method (also feeding EvaluateTiles), so the call site now reads window: window. Updated the three regexes (TPS/sessions/CPU) to match window: window and added a pin that the local is assigned context.TimeRangeEnd - context.TimeRangeStart. Applied the same fix to the wait-profile detector's own EvaluateZScore pin after step 1's rewrite.
  • LocalClockBucketKeyTests's "$4..$6" census does NOT cover these consts — checked: that census (EveryBucketStatement()) only enumerates PgBaselineProvider/PgTargetBaselineProvider baseline queries, not detector window-SQL consts. Confirmed against lane L3b-2's own step-1 commit (1e9c8ce76), which touched pins for its own tiled families in this same PR wave and made no change to that census either. No action needed there.
  • Grepped Darling/Darling.Tests for the old const names and for DetectTpsAnomalies/DetectSessionAnomalies/DetectCpuAnomalies/DetectWaitProfileAnomalies text pins and window_samples — no other pin needed updating.
  • Verified: dotnet Darling.Tests.dll -class Darling.Tests.PgTargetAnomalyTests → 44 total, 0 failed, 2 skipped (live-PG classes, expected without DARLING_TEST_PG). -class LocalClockBucketKeyTests -class PgTargetClockTests -class WindowTilesTests -class AnomalyGateTilesTests → 41 total, 0 failed.

What is NOT done (ran out of context before steps 3 and 4)

  1. Live PG run — not run. Needs timescale/timescaledb:2.28.1-pg18 on a free 558xx port, CREATE ROLE darling ..., DARLING_TEST_PG set, then *PgTargetAnomaly*, *PgTargetBetweenWaves*, *PgTargetWaitProfile*, *PgTargetClock* run against it (strip Microsoft.WindowsDesktop.App from the runtimeconfig first, restore after). This is the recipe's own "Skipped: N is not a pass" requirement — the tiled SQL for all four families has never executed against a real PostgreSQL. Highest-priority remaining item.
  2. Timing — not run. Seed 24h of one-per-minute session rows, time SessionTileWindowSql vs. dev's SessionWindowSql (5 runs each, median), both numbers in this PR body.
  3. Full Darling suite once with DARLING_TEST_PG unset — not run in this lane's pass (ran only the targeted classes above). dev has 214 Windows-only failures on this Mac per the brief; any failure outside that set needs naming.
  4. Lite build sanity check — not done (out of scope per L3a's own note, this lane didn't touch it either).
  5. New behaviour tests (planted 2h +6σ shift, tile_local_hour assertions) — explicitly out of scope per the brief ("a follow-up lane writes them") unless steps 1-4 finished with room to spare; they did not.

Handoff

The next lane/coordinator pass should, in order: (a) run step 3's live-PG proof — this is the acceptance-blocking item, since the tiled Aurora-waits rewrite in step 1 has never run against real PostgreSQL and the ruling's arm-choice logic (start-bucket decides, only arm 1 tiles) is exactly the kind of thing that looks right by inspection and needs a live check; (b) run step 4's timing and drop both numbers in here; (c) then the full suite once.

CHANGELOG entry

Base branch note

Built on origin/feat/3653-b-bucket-map (#4168's head) because GetBucketMapAsync was not yet on origin/dev at the time this lane started. Per the lane brief, titled DO NOT MERGE (stacked on #4168); the coordinator rebases onto dev once #4168 lands. (#4168 has since merged to dev as 41a10c729 — a rebase may now be straightforward, but this lane did not attempt it, out of scope for L3a-2's brief.)

Behaviour tests

Darling/Darling.Tests/PgTargetTileBehaviourTests.cs: all 7 pass live (timescale/timescaledb:2.28.1-pg18).

  • Sessions_TwoHourShiftInFourHourWindow_FiresPerTile_WhereTheWholeWindowGateWouldStayQuiet — PASS
  • Sessions_TwoHourShiftInTwentyFourHourWindow_Fires_WithTheRaisedCutoff — PASS
  • Sessions_LoneSpikeInFourHourWindow_DoesNotFire — PASS
  • Sessions_SparseWindow_FallsBackToWholeWindowPath — PASS
  • Tps_TwoHourShiftInFourHourWindow_FiresPerTile_WhereTheWholeWindowGateWouldStayQuiet — PASS (fixed this lane)
  • Cpu_TwoHourShiftInFourHourWindow_FiresPerTile_WhereTheWholeWindowGateWouldStayQuiet — PASS
  • WaitProfile_TwoHourShiftInFourHourWindow_FiresPerTile_WhereTheWholeWindowGateWouldStayQuiet — PASS

TPS root cause (diagnosed, then fixed): the fixture's minute-resolution rows only ran across minutes 0–11
of each hour, leaving a ~49-minute gap to the next hour's first sample. The per-collection LAG-rated tps for
that boundary-crossing sample divides one minute-increment's worth of xacts by the ~49-minute gap, so it comes
back near 1–2 tps instead of ~60–76 — a spurious low outlier inside every tile. That single low sample dragged
each tile's AVG below the correctMean-raised cutoff even though its PEAK cleared easily, so no tile's
Decision.Fire was true (EvaluateTiles still returned a non-null, non-firing verdict — the "still returns a
display verdict even on a quiet pass" contract, not the never-blind-null path), and DetectTpsAnomalies
returned with no fact. Diagnosed with a temporary [Fact] (never committed) that ran TpsTileWindowSql's own
6 binds directly and printed the raw per-collection rated rows, the bucket map per window hour, and the fired
fact count.

Fix: the TPS fixture's rows are now spaced every 5 minutes across the full hour (i in 0..11, matching the
sessions/CPU fixtures' own convention) instead of every 1 minute, with the per-step increment scaled from *60
to *300 to keep the seeded μ/σ tps unchanged under the new 300-second step. Two INSERT statements changed
(PlantTpsHistoryAsync, PlantTpsWindowAsync); no product code touched.

Mutation check: AnomalyGate.EvaluateTiles forced to always return null → the 5 tile-shift-dependent
tests (sessions 4h, sessions 24h, CPU, TPS, wait profile) all failed as expected; the 2 non-tile-dependent
tests (Sessions_LoneSpikeInFourHourWindow_DoesNotFire, Sessions_SparseWindow_FallsBackToWholeWindowPath)
stayed green. Reverted; rebuilt; git diff --stat PerformanceMonitor.Analysis/AnomalyGate.cs is empty.

Full suite (DARLING_TEST_PG unset): 13667 total, 214 failed, 737 skipped — matching dev's known
214-Windows-only-failure baseline on this Mac exactly. *PgTargetAnomaly* + *PgTargetTileBehaviour* live:
51/51 pass.

Not verified: CI (Windows) run of these tests — only this Mac's live-PG rig and the local suite.

@erikdarlingdata erikdarlingdata changed the title DO NOT MERGE (stacked on #4168): Tile PG-target TPS, sessions, CPU and Aurora waits detectors (#3653 A8 option B, lane L3a) DO NOT MERGE (rebased onto dev; behaviour tests pending): Tile PG-target TPS, sessions, CPU and Aurora waits detectors (#3653 A8 option B, lane L3a) Sep 24, 2026
@erikdarlingdata

Copy link
Copy Markdown
Owner Author

Behaviour tests (lane T4171-2, tests-only)

Landed T4171's PgTargetTileBehaviourTests.cs (cherry-picked from 8a87e96d7) onto this branch with a plain push, then made it build and iterated it live against timescale/timescaledb:2.28.1-pg18.

Result: 6 of 7 pass live. 1 (TPS scenario 1) is still red — pushed as-is per the brief's "push scenario 1, iterate" rule, but I ran out of budget before root-causing it. Flagging for a fast follow-up, not closing the loop myself.

What I found and fixed

  1. The as-written test asserted the wrong "dilution" baseline. Every scenario-1 test computed wholeWindowPeakZ from the RAW SHIFT VALUE against the start bucket, not from the window's own MEAN (baseline hours + shifted hours). Judging the peak alone is trivially anomalous by construction — it doesn't prove dilution. Fixed all three (Sessions_TwoHourShiftInFourHourWindow, Tps_TwoHourShiftInFourHourWindow, Cpu_TwoHourShiftInFourHourWindow) to compute the actual per-collection window mean (matching the seeded sin-noise shape, rounded where the product rounds) and assert THAT clears the cutoff — the real "two quiet hours dilute two shifted hours" case.
  2. Missing canary gate seed. PgTargetAnomalyDetector.DetectAnomaliesAsync gates on HasBaselineDataAsync, which reads pg_database_stats ALONE (30-day lookback) before ANY detector runs — sessions and CPU tests never seeded that table, so the whole detector returned empty facts regardless of what was in pg_session_states/pg_cpu_utilization. Added PlantCanaryDatabaseStatsRowAsync and wired it into the 4 sessions scenarios and the CPU scenario (TPS and wait-profile already write to pg_database_stats directly, so they didn't need it).
  3. CPU shift magnitude was too large. 90% shift against a 30±2% baseline gave a whole-window mean z of 6.0 (past the 3.5 cutoff) — not diluted by construction. Reduced the shift to 60% (still clears PgCpuFloorPct at 40).

Still red: TPS scenario 1

After fixing the canary gate and re-sizing the shift (76 tps vs 60±3 baseline, chosen so the computed window mean sits below the 3.5σ cutoff against the measured bucket), the detector still returns an EMPTY fact list — Assert.Single(facts, f => f.Key == PgTargetFactKeys.AnomalyTps) fails on an empty collection, same symptom as the missing-canary bug but the canary is present here (TPS seeds pg_database_stats directly via PlantTpsHistoryAsync/PlantTpsWindowAsync). By hand-computing the tile bucket stats (median ≈ 60, robust sigma ≈ 3.1-3.2) the hour-3/hour-4 tiles' peak and mean sigma both come out around 5.0–5.2 — comfortably past the 3.5 cutoff, so it SHOULD fire. Not yet root-caused: could be the LAG-based TpsTileWindowSql's per-database windowing losing a row at an hour boundary, MinTileSamples (3) not being cleared for one tile, or a units/interval mismatch I haven't isolated. This is a test-precondition problem until proven otherwise (per the brief, a live failure after preconditions pass would be a product bug — I have not yet confirmed preconditions pass here).

Live verification

  • timescale/timescaledb:2.28.1-pg18 on port 55890, role darling created, DARLING_TEST_PG set.
  • Build: dotnet build Darling/Darling.Tests/Darling.Tests.csproj -p:EnableWindowsTargeting=true — 0 errors both times.
  • Microsoft.WindowsDesktop.App stripped from Darling.Tests.runtimeconfig.json after every build (never committed).
  • dotnet Darling.Tests.dll -class 'Darling.Tests.PgTargetTileBehaviourTests' (final targeted run): Total: 7, Errors: 0, Failed: 1, Skipped: 0 — the 6 passing: Sessions_TwoHourShiftInFourHourWindow_FiresPerTile…, Sessions_TwoHourShiftInTwentyFourHourWindow_Fires…, Sessions_LoneSpikeInsideAnHour_DoesNotFire, Sessions_UnderMinTileSamplesEveryTile_FallsBackToTheWholeWindowPath…, Cpu_TwoHourShiftInFourHourWindow_FiresPerTile…, WaitProfile_TwoHourShiftInFourHourWindow_FiresPerTile…. Failing: Tps_TwoHourShiftInFourHourWindow_FiresPerTile….
  • Not done (budget ran out before turn 8's follow-through): the mutation check (AnomalyGate.EvaluateTiles → null) and the full *PgTargetAnomaly* live class, plus the full Darling suite with DARLING_TEST_PG unset. Container was removed at end of session.
  • Did NOT touch product code — only the test file, per the tests-only brief.

CHANGELOG entry

(none — tests-only lane; no user-facing change)

Handoff

Next lane should: (1) root-cause the TPS scenario-1 failure (instrument TpsTileWindowSql's tile output directly, e.g. add a temporary diagnostic query against the live rig, before touching test values further); (2) run the mutation check and the two full-suite passes the brief calls for; (3) if TPS turns out to be a genuine product bug, push it as a RED: … commit per the scenario brief instead of tuning the test further.

…on B, lane L3a)

Moves DetectTpsAnomalies, DetectSessionAnomalies and DetectCpuAnomalies onto
per-hour tiles through AnomalyGate.EvaluateTiles: each hour of the analysis
window is scored against its own hour-of-week baseline, and when no tile
scores the family falls back to today's whole-window EvaluateZScore path.

New SQL consts (DatabaseCounterWindowSql left byte-identical for the
deadlock-rate ratio path):
- TpsTileWindowSql
- SessionTileWindowSql (keeps the per_collection CTE, groups the outer select)
- CpuTileWindowSql (per-tile array_agg peak time instead of the ORDER BY/LIMIT
  1 subquery; carries the raw cpu_percent peak per tile)

Aurora waits (WaitRateWindowSql) is NOT yet tiled in this commit -- see the
PR body.
…B, lane L3a)

Adds WaitRateTileWindowSql and moves DetectWaitProfileAnomalies onto
per-hour tiles. This family's own three-armed trust rule (robust pair /
classical ratio pair / absolute fallback) does not fit AnomalyGate's
z-only EvaluateTiles, so each tile is decided by hand through the same
three arms the whole-window body already had (DecideWaitProfileTile),
and the worst FIRING tile is reported. Never-blind fallback: when no
tile scores, today's whole-window logic runs unchanged.
Only the trusted robust arm (baseline.IsTrustworthy && EffectiveRobustSigma > 0)
moves onto tiles now, chosen from the START-hour bucket exactly as dev's
whole-window body decides it. AnomalyGate.EvaluateTiles scores that arm; when
it returns null the never-blind fallback runs today's EvaluateZScore over
WindowTiles.WholeWindow(tiles). The classical-ratio arm and the no-baseline
absolute arm stay whole-window and unchanged, fed from WholeWindow(tiles)'s
Peak/Mean, with no second SQL read.

Deletes the private DecideWaitProfileTile/WaitProfileTileDecision L3a wrote
to run all three arms per tile by hand (no multiplicity correction on the
ratio arm, uncorrected k on the z arm's per-tile mean, and a single
untrustworthy tile could switch the WHOLE window to the absolute arm even
when the start bucket trusted the z arm).
…in regression

s_detectorSql/EveryDetectorRead_* census gets TpsTileWindowSql, SessionTileWindowSql,
CpuTileWindowSql, WaitRateTileWindowSql, plus table/local_hour/dialect pins for each.

Also fixes a pin L3a's TPS/sessions/CPU tiled call-flow refactor already broke:
the never-blind fallback's EvaluateZScore call now passes a local  variable
instead of the inline  expression the
old regex matched verbatim (ThePairGate_TpsSessionsAndCpu... test class's source
pin). Updated the regex to match  and added a pin that the local is
assigned from the same expression. Same fix applied to the wait-profile detector's
own EvaluateZScore pin after step 1's rewrite.

LocalClockBucketKeyTests' census only covers PgBaselineProvider/PgTargetBaselineProvider
baseline queries, not detector window-SQL consts, so the four new TileWindowSql consts
are not in scope for that census (confirmed against lane L3b-2's own step-1 commit,
which touched the same census file for its own tiled families and made no change there
either).
…binds only $1..$3; the detectors' catch turned 'no value for $4' into silence); waits fetches its map before the read; the Aurora waits live test pins the worst tile
… TpsTileWindowSql insertion stacked two summaries)
The TPS scenario 1 fixture's minute-resolution rows (i in 0..11) only spanned minutes 0-11 of each
hour, leaving a ~49-minute gap to the next hour's first sample. The per-collection LAG-rated tps for
that boundary-crossing sample divides one minute-increment's worth of xacts by the whole ~49-minute
gap, producing a spurious near-zero tps sample inside every tile. That dragged each tile's AVG well
below the correctMean-raised cutoff even though its PEAK cleared easily, so EvaluateTiles never fired
any tile, EvaluateTiles returned a non-firing verdict (never null - it still returns a display verdict
even when no tile fires), and the fallback branch was never reached either: the tile path's own
Decision.Fire was false, so DetectTpsAnomalies returned with no fact at all.

Fixed by spacing the TPS fixture's rows every 5 minutes across the full hour (i in 0..11, 5 min apart,
matching the sessions/CPU fixtures' own convention) instead of every 1 minute, and scaling the
per-step increment from *60 to *300 to keep the seeded mu/sigma tps unchanged under the new 300-second
step. Diagnosed with a temporary [Fact] that ran TpsTileWindowSql's own 6 binds directly, printed the
per_collection rated rows, the bucket map per window hour, and the fired fact count - never committed.

Verified live (timescale/timescaledb:2.28.1-pg18): all 7 PgTargetTileBehaviourTests pass; the
*PgTargetAnomaly* + *PgTargetTileBehaviour* live classes are 51/51; the mutation check (AnomalyGate.EvaluateTiles
forced to return null) failed exactly the 5 tile-shift tests and left the 2 non-tile tests (lone spike,
sparse fallback) green, then reverted cleanly (no diff on AnomalyGate.cs). Full suite with
DARLING_TEST_PG unset: 214 failures, matching dev's known Windows-only baseline exactly - no new
failures.
@erikdarlingdata erikdarlingdata changed the title DO NOT MERGE (rebased onto dev; behaviour tests pending): Tile PG-target TPS, sessions, CPU and Aurora waits detectors (#3653 A8 option B, lane L3a) Tile PG-target TPS, sessions, CPU and Aurora waits detectors (#3653 A8 option B, lane L3a) Sep 24, 2026
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 24, 2026 18:03
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 24, 2026 18:03
CommandCapture.IsBaselineRead matched any SQL containing
BaselineLocalClock.LocalCollectionTimeSql, on the doc comment's promise
that "nothing else the pass runs does". B's tiled window reads (this PR:
TPS, sessions, CPU, Aurora waits) key their tiles through
WindowTiles.LocalHourSql, which wraps that same LocalCollectionTimeSql
spelling in date_trunc('hour', ...) — so every tiled window read on every
pass, cached baseline or not, was miscounted as a baseline compute. That's
exactly the 4 the second-pass cache-hit test found where it expected 0.

Excludes any SQL containing WindowTiles.LocalHourSql from the classifier
(loop seat's diagnosis and fix). No baseline arm on dev uses
date_trunc('hour', <LocalCollectionTimeSql>); only WindowTiles.LocalHourSql
does, so this can't misclassify a real baseline compute.
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
Same fix as feat/3653-b-l3a's 584ace6, applied here since #4171 has not
merged yet: CommandCapture.IsBaselineRead matched any SQL containing
BaselineLocalClock.LocalCollectionTimeSql. B's tiled window reads (this
PR: CPU, waits, I/O read and write) key their tiles through
WindowTiles.LocalHourSql, which wraps that same spelling in
date_trunc('hour', ...), so every tiled window read was miscounted as a
baseline compute regardless of cache state.

Excludes any SQL containing WindowTiles.LocalHourSql from the classifier.
No baseline arm on dev uses date_trunc('hour', <LocalCollectionTimeSql>);
only WindowTiles.LocalHourSql does.
@erikdarlingdata
erikdarlingdata merged commit 89447bd into dev Sep 24, 2026
25 of 28 checks passed
@erikdarlingdata
erikdarlingdata deleted the feat/3653-b-l3a branch September 24, 2026 18:39
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
…ectors move onto per-hour tiles (#4172)

* issue-3653 A8 option B (lane L2a): move SQL Server CPU and wait detectors onto per-hour tiles

Part of #3653.

CPU: DetectCpuAnomalies reads CpuTileWindowSql (one row per target-local hour),
scores every tile through AnomalyGate.EvaluateTiles, and falls back to today's
whole-window EvaluateZScore when no tile scores (never-blind rule).

Waits: per coordinator ruling, only the robust-z start-bucket arm moves onto
EvaluateTiles; the classical-ratio and no-baseline arms stay whole-window,
fed from WindowTiles.WholeWindow(tiles), with no second SQL read. total_wait_ms
is summed from the tiled rows since it isn't a WindowTile field.

Added CpuTileWindowSql, WaitRateTileWindowSql, and shared BindTiledWindow /
ReadTilesAsync helpers for reuse by lane L2b (batch, sessions, query duration,
memory) in the same file.

I/O family and test-pin updates are NOT yet in this commit; see the PR body.

* issue-3653 A8 option B (lane L2a): move SQL Server I/O detector onto per-hour tiles

Part of #3653.

ONE tiled read (IoTileWindowSql) feeds both the read-latency and write-latency
gates, each through its own WindowTile list and its own EvaluateTiles call
(each family's fallback re-fetches its own bucket map lookup, since the tile
key is shared but the gate call and floor are per-direction). Never-blind
fallback to today's whole-window EvaluateZScore per direction when no tile
scores.

* 3653 B L2a-2 step 1: fix pins falsified by the CPU/wait/I-O tile move, delete unused old consts

- CpuWindowSql, WaitRateWindowSql, IoWindowSql were dead once L2a's tile move
  landed (no remaining production reader); deleted, with cref updates on the
  two class-doc comments that named them.
- DarlingAnomalyBaselineTests: census list, the I/O peak/mean pair pin, and
  the wait-rate pair pin now point at the Tile consts. The wait-rate pin's
  stale reader-ordinal regexes (which assumed the old single-collapsed-row
  read) are replaced with a direct pin on the new whole.Peak/whole.Mean/
  totalReader.IsDBNull(3) shape; Lite's own single-row ordinals are pinned
  unchanged. The EvaluateZScore baseline-argument capture widened to catch
  readBaseline/writeBaseline (the tiled I/O arms' own locals), and the
  never-blind fallback's window: argument now accepts either window (PG) or
  the inline TimeRangeEnd - TimeRangeStart (Lite, unmoved).
- LatestCpuReadShapeTests' two crefs already point at the still-live
  CpuTileWindowSql text (unchanged, no fix needed there).

Verified: DarlingAnomalyBaselineTests, LatestCpuReadShapeTests,
LocalClockBucketKeyTests, MeasurementContractCensusTests,
CollectorMeasurementSeamTests, DocCommentHygieneTests all green, no PG.

* 3653 B L2a: the I/O and wait-profile live tests pin the WORST TILE (its mean over the hour holding the hot row, located by tile_start_ticks), not the whole-window mean

* WIP: draft behaviour tests for PR #4172 tiled SQL Server detectors (5/6 red, needs fixing)

* T4172-2: size CPU tile fixtures from the family's own magnitude floor

CpuFloorPct is 50.0%. The prior fixture (mu=20, sigmaAmp=2) put mu+6sigma
around 29 -- never clears the floor, so nothing fired in any of the 3
CPU shift/24h/fallback scenarios. Raised to mu=30, sigmaAmp=5 (sample
stddev ~3.69, mu+6sigma ~52.1). Also corrected two assertions that
expected the classical DefaultDeviationThreshold (2.0): this CPU
baseline carries a positive robust sigma, so the tiled gate runs the
modified-z frame (ModifiedZThreshold, 3.5) on both the peak and the
tile-corrected mean, not the classical one.

CPU: 3 of 4 scenarios now pass (shift, lone spike, fallback). The 24h
as_of scenario and both wait/IO scenario-1 tests are still red; next
commit.

* T4172-2: fix 24h scenario's baseline seeding day-of-week gap; document a RED product bug in waits

24h scenario: the window spans two calendar days (Tue 12:00 - Wed
12:00). SeedCpuBaselineAsync anchors every hour it's given to ONE
calendar date, so seeding all 24 hours off windowStart's date alone
left every Wednesday-side tile bucket empty. Now seeds each hour off
the day it actually falls on in the window. Passes.

Waits scenario 1: RED, documented rather than bent. DetectWaitAnomalies
never writes fire_threshold to the fact's metadata on any arm, unlike
every sibling tiled family (CPU, I/O) which stamp it from
decision.ThresholdUsed. The fact still fires correctly (right worst
tile, tiles_scored/tiles_fired) -- asserted -- but the missing key is a
product gap, described in the PR body's Behaviour tests section per
the lane rule (a red test after true preconditions is a product bug,
not something to bend the assertion around).

I/O scenario 1 is still red; not yet diagnosed (context budget). Left
for follow-up, described in the PR body.

* T4172-3 step 1: DetectWaitAnomalies now stamps fire_threshold (coordinator ruling)

The SQL Server-store waits detector never wrote a fire_threshold key on ANY
arm, while every sibling tiled family (CPU, I/O) does. Added:
- robust-z arm: decision.ThresholdUsed (the tiled/whole-window cutoff actually applied)
- ratio arm: DefaultRatioThreshold
- no-baseline arm: 0

Flipped Waits_TwoHourSustainedShift_FiresOnWorstTile_... from asserting the
key's absence to asserting its value (AnomalyThresholds.HeavyTailModifiedZThreshold
on this scenario, since the robust-z tiled arm fires).

Live run (DARLING_TEST_PG, timescale/timescaledb:2.28.1-pg18):
SqlServerStoreTileBehaviourTests: Total 6, Failed 1 (Io_TwoHourSustainedShift_...,
still being diagnosed per step 2), Skipped 0.

* WIP T4172-4: read the I/O start bucket at T (not T-24h) to match the tile map's lookup key

Diagnosed: GetBaselineAsync(serverId, IoLatency, T.AddHours(-24), ...) missed the seeded
Wednesday 10/11 Full bucket (T-24h is a Tuesday) and fell through to a pooled HourOnly
bucket, an under-sized/untrustworthy sigma that mis-sized the shift. Reading at T (the
tile map's own lookup key) now reports the seeded Full bucket and Assert.True(IsTrustworthy)
passes, but the detector still emits no ANOMALY_READ_LATENCY fact. Not yet green -- see
PR body handoff for the next diagnostic step (dump IoTileWindowSql's tile rows + EvaluateTiles
verdict, per lane brief step 1's escape hatch).

* T4172-5 step 1: drop the I/O tiled-shift fixture (coordinator ruling, scope trim)

* 3653 B L2a: exclude tiled window reads from the baseline-read classifier

Same fix as feat/3653-b-l3a's 584ace6, applied here since #4171 has not
merged yet: CommandCapture.IsBaselineRead matched any SQL containing
BaselineLocalClock.LocalCollectionTimeSql. B's tiled window reads (this
PR: CPU, waits, I/O read and write) key their tiles through
WindowTiles.LocalHourSql, which wraps that same spelling in
date_trunc('hour', ...), so every tiled window read was miscounted as a
baseline compute regardless of cache state.

Excludes any SQL containing WindowTiles.LocalHourSql from the classifier.
No baseline arm on dev uses date_trunc('hour', <LocalCollectionTimeSql>);
only WindowTiles.LocalHourSql does.
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
…gap (lane G1-3)

- RunBothWindowsAsync now returns Task<bool> (CommandCapture.CaptureAsync needs
  Func<Task<T>>, not Func<Task>) — the merge with dev (#4171) broke the build.
- AssertEveryWindowSqlRan excludes PgTargetAnomalyDetector's SessionWindowSql,
  CpuWindowSql, WaitRateWindowSql by name: #4171's tiled-window recipe gave those
  arms tile twins the detector actually reads now; the plain consts stay only for
  PgTargetAnomalyTests' pinning. PgAnomalyDetector's SQL Server consts of the same
  names are NOT excluded (still read directly there).
- SeedSqlServerFamilyTablesAsync: added a spiked current-window wait_stats row so
  the wait-profile detector's fallback bar actually fires and WaitContribWindowSql
  runs (a flat series across every seeded row never exceeded the bar).
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
…gap (lane G1-3)

- RunBothWindowsAsync now returns Task<bool> (CommandCapture.CaptureAsync needs
  Func<Task<T>>, not Func<Task>) — the merge with dev (#4171) broke the build.
- AssertEveryWindowSqlRan excludes PgTargetAnomalyDetector's SessionWindowSql,
  CpuWindowSql, WaitRateWindowSql by name: #4171's tiled-window recipe gave those
  arms tile twins the detector actually reads now; the plain consts stay only for
  PgTargetAnomalyTests' pinning. PgAnomalyDetector's SQL Server consts of the same
  names are NOT excluded (still read directly there).
- SeedSqlServerFamilyTablesAsync: added a spiked current-window wait_stats row so
  the wait-profile detector's fallback bar actually fires and WaitContribWindowSql
  runs (a flat series across every seeded row never exceeded the bar).
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
… error (silent-family guard) (#4179)

* Add a live guard: no anomaly detector logs an error over a seeded window (#3653 A8 option B)

Both PgAnomalyDetector (SQL Server store) and PgTargetAnomalyDetector (PG target) swallow their own
per-family exceptions into a logged Error line, so a SQL error or a missing parameter bind
silently kills a family with no fact and no failed test. This adds
Darling/Darling.Tests/AnomalyDetectorErrorGuardLiveTests.cs: a capturing ILogger runs every
family over minimally seeded tables (one row per table each *WindowSql*/*TileWindowSql* const
reads, across a 4h and a 24h window) and fails if any Error-or-above line was captured.

Part of #3653

* issue-3653 A8 option B: make the silent-detector guard non-vacuous (lane G1-2)

Seed 12 same-hour/day-of-week rows per family table (12 weeks back) instead of
1, so every detector's baseline.SampleCount clears BaselineMath.CollapseThreshold
and each family reaches its *WindowSql read instead of returning early on
SampleCount == 0. Capture the SQL Npgsql actually executed via
CommandCapture.CaptureAsync and assert every public const string *WindowSql
field on PgAnomalyDetector / PgTargetAnomalyDetector (reflected, so it covers
every partial-class piece automatically) appears verbatim among the captured
commands, naming any that never ran.

* issue-3653 A8 option B: fix guard build break + wait-profile fixture gap (lane G1-3)

- RunBothWindowsAsync now returns Task<bool> (CommandCapture.CaptureAsync needs
  Func<Task<T>>, not Func<Task>) — the merge with dev (#4171) broke the build.
- AssertEveryWindowSqlRan excludes PgTargetAnomalyDetector's SessionWindowSql,
  CpuWindowSql, WaitRateWindowSql by name: #4171's tiled-window recipe gave those
  arms tile twins the detector actually reads now; the plain consts stay only for
  PgTargetAnomalyTests' pinning. PgAnomalyDetector's SQL Server consts of the same
  names are NOT excluded (still read directly there).
- SeedSqlServerFamilyTablesAsync: added a spiked current-window wait_stats row so
  the wait-profile detector's fallback bar actually fires and WaitContribWindowSql
  runs (a flat series across every seeded row never exceeded the bar).

* issue-3653 B (G1-4): seed PgTarget fixtures for the detector error guard's 4 window consts

WIP: DatabaseCounterWindowSql, WaitContribWindowSql and SampledWaitContribWindowSql now run;
IoLatencyWindowSql still needs a fix (baseline gate: PgIoReadLatency SampleCount==0).

* issue-3653 B (G1-4): attempt IoLatencyWindowSql baseline predecessor row (still gapped)

DatabaseCounterWindowSql, WaitContribWindowSql and SampledWaitContribWindowSql now run live.
IoLatencyWindowSql still 'never ran' -- the baseline arm's per-15-minute date_bin bucket collapses
same-bucket rows via MAX before LAG runs, so a same-bucket predecessor/successor pair does not
give LAG anything to diff against. Needs a predecessor in the PRECEDING 15-minute bucket, not the
same one. Left for a follow-up; documented in the PR body.

* issue-3653 A8 option B (G1-5): seed IoLatencyWindowSql's LAG predecessor in the preceding 15-minute bucket

The current-window pg_io_stats pair sat 30 seconds apart, inside the
SAME date_bin('15 minutes') bucket, so the sampled CTE's MAX()
aggregation collapsed them to one row per bucket before LAG ever ran
-- raw_reads stayed NULL every time and IoLatencyWindowSql never
executed, exactly the silent-family failure this guard exists to
catch. Move the pair 15 minutes apart (one bucket earlier, then the
bucket itself) with an increasing reads/read_time_ms counter so LAG
resolves a real predecessor.

Part of #3653

* 3653 B guard: exclude L3c's two superseded consts; fix stacked doc comments

Two real CI failures after the dev rebase, neither a flake:

1. AssertEveryWindowSqlRan reported CpuBurnWindowSql and WalVolumeWindowSql
   as never-ran. Same situation as the three consts already excluded
   (SessionWindowSql/CpuWindowSql/WaitRateWindowSql, from lane L3a): L3c
   (#4178) declared these two only so the census keeps seeing the same
   SQL text PgTargetFactCollector uses to build the PG_CPU_BURN_CORES/
   PG_WAL_VOLUME_SHIFT facts -- the detector itself only ever calls their
   tile twins, CpuBurnTileWindowSql/WalVolumeTileWindowSql. Added both to
   the existing supersededByTile exclusion set.

2. DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks: two
   <summary> blocks stacked on SeedSqlServerFamilyTablesAsync (247/252)
   and SeedPgTargetFamilyTablesAsync (349/353). Checked each pair first,
   per that test's own warning -- both describe the SAME member, the
   first an earlier/shorter draft ("one row... background history row")
   superseded by a later, more complete one written when a lane raised
   the seeding from 1 row to 12 (explaining the CollapseThreshold floor).
   Deleted the stale first block in each case; nothing lost, the second
   already carries everything the first said and more.

Local: real dotnet build clean; DocCommentHygieneTests class run clean
(0 failed). The guard tests themselves skip locally (no DARLING_TEST_PG)
-- Windows CI's live PG service is the arbiter for the fixture-gap fix.
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