Skip to content

Measurement-layer campaign: the delta honesty contract (11 findings, one keystone) #3540

Description

@erikdarlingdata

Adversarial review of every delta/rate/trend computation across both SKUs, against every discontinuity class (restart, gap, failover, stats reset, chunk boundary, first sample). Verdict: the delta ENGINE (CollectorDeltaCalculator: measured 3600s gap policy, series-age rescue, the (0,0) unknowable marker) is honest and battle-hardened — but the honesty is destroyed at the schema for 4 of 6 delta families, and two unit lies feed the analysis brain. Individually filed: the perfmon per-second divisor, the Lite cadence clamp, the QueryStore ordinal slip.

The keystone — A1: interval columns for the naked delta families. wait_stats, file_io_stats, latch_stats, spinlock_stats persist deltas with NO sample_interval_seconds column, so the calculator's (delta=0, interval=0) "unknowable" marker cannot survive the write, and readers LAG-divide fabricated zeros into confident 0.0 at exactly the moments (restarts) they're unknowable. The calculator's own doc claims "every consumer maps 0 to NULL via NULLIF(sample_interval_seconds, 0)" — false for these four. perfmon/query_stats already have the column (the pattern to copy). Once landed, four findings below become one-line WHERE/NULLIF changes. Schema change -> upgrade-folder rules, both SKUs.

Remaining findings (checklist; verified at path:line; re-verify at pickup):

  • A4 Seeding covers 4 of 10 delta families (PR Every delta family is seeded from the store at service start, and the series-age rescue finally has passes to read, so a restart no longer fabricates one interval of quiet for six families (#3540 A4) #3614, merged 98069fd — query_stats key-seed needs the offsets rung, see comment): latch, spinlock, query_stats, proc_stats, pg_wait_stats, pg_statement_stats unseeded -> one full interval of fabricated quiet after EVERY service restart/deploy. _passes never seeded (the series-age rescue is inert on the exact cycle it exists for). ClearServer has zero callers in Darling.Service (re-onboard <1h fabricates a delta).
  • A5 No identity-epoch detection: sqlserver_start_time collected but consumed only for display; AG failover clears alert state but never delta baselines (listener failover to a hotter replica fabricates a positive storm); pg_stat_statements_reset() fully invisible (pg_stat_statements_info.stats_reset: zero occurrences repo-wide). The deprecated T-SQL collector HAD start-time detection; not carried forward. Fix: per-server epoch check (start_time / stats_reset / server_name) -> ClearServer + a discontinuity marker the read layer renders.
  • A6 CAGGs sum fabricated zeros and count them as samples; a >24h host outage leaves a PERMANENT unmaterialized hourly hole served as covered (RetentionTierRouter self-discloses); daily-tier calendar prints unique_queries=0 beside fresh raw numbers.
  • A7 Viewer perfmon chart plots raw per-interval deltas ("Value", divisor fetched but unplotted); cntr_type not stored so gauges are delta'd as activity (a falling gauge = fake counter reset); viewer line charts interpolate straight across gaps.
  • A8 Rollup rates divide by gap-to-previous-NON-EMPTY bucket (a quiet hour halves the next hour's true rate); first point of every differenced series fabricates 0.0 instead of NULL.
  • A10 Baseline contamination guard is a magnitude heuristic with four escape hatches; perfmon/query baselines don't read the interval they HAVE; the repo's one correct restart predicate (PlanCacheAnomalyDetector.IsRealDeltaRow) is orphaned; UTC-keyed hour-of-week buckets smear under DST. Zeros drag mean down (ratio over-fires) and stddev up (z under-fires).
  • A11 assorted: duration-trend LAG-recomputes a stored denominator (two contracts, one table); overview IO lane averages fabricated 0-latency into the lane mean; pg statement delta key embeds datid OID (DROP/CREATE reuse resumes a dead series); cpu sample_time is target-LOCAL vs UTC windows; utc_offset is offset-not-zone across DST.
  • The contract: pin the 10-rule measurement contract as census tests (every delta family persists interval; readers NULL-not-0 on unknowable; rates divide by measured elapsed; epoch change -> ClearServer + marker; rollups aggregate interval>0 only; per-sec names divided at comparison; gauges never delta'd). Natural homes: SharedCollectorDefaultsPinTests, GoldenCollectorSchema, the delta/seeder test families, TimescaleContinuousAggregateTests.

Credits: the delta calculator itself; NO assumed-cadence divisor anywhere in either SKU's trend path; the QS cumulative-snapshot solution (the reference for cumulative sources); DarlingPgTrendReader as the reference discontinuity-honest reader ("nothing on the SQL Server side matches it — that asymmetry IS the roadmap"); clock-skew immunity by construction; the Azure id-space fix with no live siblings.

From the 2026-09 brains-review campaign.

Activity

  1. erikdarlingdata commented on Sep 18, 2026

    @erikdarlingdata
    OwnerAuthor

    Filed-in-passing from the #3561 fix (PR #3569): the deprecated Dashboard's schema-computed cntr_value_per_second column uses integer division, truncating fractional rates on its chart/MCP display surface. Pre-existing, display-only, deprecated tier - tracked here as a measurement-lane checklist item rather than its own issue.

  2. erikdarlingdata commented on Sep 18, 2026

    @erikdarlingdata
    OwnerAuthor

    Keystone (A1) landed in #3595 — Darling V127 / Lite v60: sample_interval_seconds on wait_stats, file_io_stats, latch_stats, spinlock_stats; the four collectors write it; twenty read sites across service MCP, viewer, Lite, the anomaly detectors and Lite's baseline provider now treat a stored 0 as NULL and a pre-upgrade NULL as "keep the LAG-derived interval". Census: Lite.Tests/DeltaFamilyIntervalColumnTests pins that every delta family in the interval-carrying set persists the column and names the four that still do not. Doc-pointer follow-up in #3600.

    Follow-ups this landing exposes, parked here (each is measurement-lane scope; the checklist items below inherit them):

    • A6 CAGG follow-up — TimescaleSupport.cs:427 CreateWaitStatsBaselineSql still sums the restart zero and counts it as a sample. A CAGG query cannot be altered in place and a rebuild forfeits ~35 days of baseline history that the 30-day raw retention cannot refill (the PERFMON_BATCH_REQ_SEC facts and the anomaly floor read a per-interval delta as per-second #3527 precedent). The documented shape: a new aggregate under a new name with WHERE sample_interval_seconds IS DISTINCT FROM 0, WITH NO DATA + --backfill-rollups, retire the old via RetiredBaselineRelations (Retire the orphaned cpu_utilization_baseline / file_io_baseline continuous aggregates (#1995 cleanup) #2007 shape). This is the A6 item's first concrete step.
    • Four still-naked delta families — ProcedureStatsCollector, MemoryGrantsCollector, PgWaitStatsCollector, PgStatementStatsCollector persist deltas without an interval. Each is a future rung (one un-landed rung at a time); pinned by the census's StillNaked list so the set cannot grow silently. Contract rule 4 is complete when that list is empty.
    • Compose compiler — Darling/PerformanceMonitor.Darling.Service/Compose/MeasureCatalog.cs / ComposeCompiler.cs:660: the Cumulative archetype's AVG/MIN/MAX(delta) counts unknowable rows; now that six families carry the column, the compiler can emit sample_interval_seconds IS DISTINCT FROM 0 for Cumulative measures on interval-carrying sources (touches perfmon/query_stats too — its own small lane).
    • Latch/spinlock snapshot grids (both SKUs) show raw deltas with no interval column, so a (0,0) restart row displays as "delta 0". Cosmetic; surface the interval or render the unknowable row as such (A7's viewer item).
  3. erikdarlingdata commented on Sep 18, 2026

    @erikdarlingdata
    OwnerAuthor

    A4 landed in #3614 — every delta family a host monitors is seeded from the store at service start (latest row per key inside the 15-minute window via DISTINCT ON for collectors that do not write every key every pass; the bounded latest-collection probe for the rest), the per-group pass window that the #2235 series-age rescue reads is seeded for ALL families including the original four, seeding runs under a per-family guard so one slow table cannot cost the others their continuity, and ClearServer is wired into Lite's server-removal cleanup and Darling's reconcile-remove branch. Census Lite.Tests/DeltaFamilySeedingCensusTests reads both hosts' seeders against the calculator's family list and each collector's own delta-group names — an eleventh family cannot ship unseeded (contract rule 7). Measured on a migrated container: a 645k-row pg_statement_stats window seeds in 372 ms, every plan bounded to the window's chunk.

    One residual that is a rung, not a follow-up fix:

  4. erikdarlingdata commented on Sep 18, 2026

    @erikdarlingdata
    OwnerAuthor

    Status 17:45Z — A1 keystone merged (#3595, V127) and A4 seeding merged (#3614). In flight: V128 delta-family-interval-completion lane (interval on the four still-naked families + query_stats statement offsets; PR not yet open). Remaining after that: A5 identity-epoch detection (structural), A6 CAGG follow-up (new wait_stats_baseline aggregate + retirement), A7 viewer perfmon/gauge rendering, A8 rollup rate denominators, A10 baseline contamination guard, A11 assorted, the 10-rule contract census (rules 4 and 7 are now pinned; the rest are not). Issue stays open by design.

  5. erikdarlingdata commented on Sep 18, 2026

    @erikdarlingdata
    OwnerAuthor

    V128 delta-family-interval-completion / Lite v61 landed in #3630 (merged 2a1e367, 19:19:58Z). The last four families (procedure_stats, memory_grant_stats, pg_wait_stats, pg_statement_stats) carry sample_interval_seconds; query_stats carries statement_start_offset / statement_end_offset (byte offsets, -1 = end of batch, stored verbatim) so both hosts' restart seeds rebuild its delta key. Contract rule 4 is complete — DeltaFamilyIntervalColumnTests.StillNaked is empty and asserted empty; #3614's PassWindowOnly exemption is likewise empty and asserted. Verified: fresh ladder V1→V128, a simulated pre-V128 store on compressed hypertables with 20 CAGGs (one rung applied, ALTERs succeed, CAGGs refresh), four release fixtures 3.3.0→3.7.0 climb to 128.

    Residuals from that lane (all parked under the remaining checklist items):

    • A11a — the raw duration-trend SQL in ViewerDataService.QueryTrends, DarlingTrendReader.QueryDurationTrendSql, Lite GetQueryDurationTrendAsync/GetExecutionCountTrendAsync still LAG-derives the interval though query_stats has carried it from the start; the three-state idiom applies; pinned as "reported, not rewritten".
    • OversizedPlanBacklog.QueryStatsFallbackSql (:244) stays hash-keyed — an exact offsets join is possible for V128+ rows; not taken because readers ask at the hash grain and pre-V128 rows are NULL.
    • Compose MeasureCatalog Cumulative archetype (V127's follow-up) — all ten families now carry the column, so the IS DISTINCT FROM 0 guard can be emitted uniformly.
  6. added 3 commits that reference this issue on Sep 18, 2026
  7. erikdarlingdata commented on Sep 18, 2026

    @erikdarlingdata
    OwnerAuthor

    Closing — the keystone and its completion are landed; contract rules 4 and 7 are pinned; the structural remainder moves to #3653 #3653.

    Shipped from this issue (all merged to dev on 2026-09-18):

    Deferred to #3653: A5 identity-epoch detection (the only genuinely structural item), A6 the wait_stats_baseline new-aggregate + retirement, A7 viewer gauge/interpolation rendering + the cntr_type rung, A8 rollup denominators, A10 the baseline contamination guard + orphaned IsRealDeltaRow, A11 assorted (raw duration-trend LAG, datid key, local-time CPU stamps, DST offset, Dashboard integer division), the Compose Cumulative archetype, and the eight unpinned contract rules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions