Skip to content

The I/O-latency baseline re-reads 30 days of raw per-file rows every hour for every server and spills ~50 MB to disk each time (store B: ~50–95 GB/day of temp writes) #4248

Closed

Description

@erikdarlingdata

Related: #2820 (the MAD-tier join fix on this arm), #3941 (baseline cache sharing), #1757

Problem

PgBaselineProvider's MetricNames.IoLatency arm reads the raw file_io_stats hypertable at per-file grain over the full 30-day baseline window (BaselineMath.BaselineWindowDays). It feeds the rows through RobustTierScaffold:

  • GROUPING SETS over hour × day-of-week, hour, and all;
  • a keyed_tiers UNION ALL that triples the rows;
  • percentile_cont twice (median, then MAD).

Those sorts don't fit in work_mem, so each call writes a temp file. The window ends on the analysis hour (#3941), so every server's baseline is recomputed once an hour. A 30-day baseline moves by about 1/720 per hour.

Measured

Production SQL Server stores, build 3.8.0-nightly.20260924.467, 2026-09-25, pg_stat_statements (owner role). work_mem is 64 MB on both stores.

calls temp written per call per day
Store B (42 servers), 21:59–03:57Z 262 13.5 GB ~52 MB ~54 GB
Store B, window 03:43–04:38Z 78 3.63 GB ~47 MB ~95 GB
Store B, steady window 04:45–05:41Z 42 2.00 GB ~48 MB ~52 GB
Store A (43 servers), 22:09–03:57Z 274 6.0 GB ~22 MB ~25 GB
Store A, windows 03:43–04:38Z / 04:45–05:41Z 54 / 43 0.26 / 0.26 GB ~5–6 MB ~7 GB
  • On store B this one statement was the largest temp writer:
    • 13.5 of 22.1 GB of all temp since the stats reset;
    • 3.6 of 6.0 GB in the first window;
    • 86% of temp in the steady window.
  • Does it reach the disk? Store B's volume took 440 GB of writes on Sep 24 (CloudWatch). The store-side sum of WAL, data, extend and temp writes came to 98% and 111% of the volume's writes in the two windows, so on that store the temp files do reach the disk, give or take ~10%.
  • Share of disk writes: in the steady window this read was ~16% of store B's volume writes (1.5–2 GB of 9.8 GB), and ~10–20% across all the readings.

Where

  • Darling/PerformanceMonitor.Darling.Analysis/PgBaselineProvider.cs:
    • :1249-1256, the IoLatency arm (raw file_io_stats, per-file rows);
    • :977-1015, RobustTierScaffold;
    • :805-807, the window windowEnd.AddDays(-BaselineWindowDays) on the rounded hour;
    • :133, CacheTtl, 1 h.
  • PerformanceMonitor.Analysis/Baselines/BaselineMath.cs:26: BaselineWindowDays = 30.

Fix shape

Any one of these removes most of it. They compose.

  • Recompute less often. A 30-day baseline doesn't need an hourly recompute. Key the cache on the day, or on the hour-of-week bucket, for the raw-reading arms. Check the Every scheduled analysis pass recomputes every 30-day baseline: a fresh DarlingAnalysisService per pass never reuses (or shares with MCP) the bucket cache #3941 sharing semantics: every caller in the period still gets the same rows.
  • Stop the spill. SET LOCAL work_mem sized for this statement: the measured spill is ~50 MB, so the in-memory sort needs roughly 2–4× that. Add a live rig pin that EXPLAIN (ANALYZE) of the arm over a seeded 30-day, per-file dataset reports no external merge.
  • Shrink the input. Collapse per-file rows to one ratio per collection (sum of stall over sum of reads) before the scaffold, if the per-file grain isn't load-bearing for the anomaly semantics. This changes Lite parity, so it's the maintainer's call.
  • Pin: the arm's pg_stat_statements.temp_blks_written over a full analysis pass on the rig is zero, or under a stated bound.

Activity

  1. added
    enhancementNew feature or request
    client-siteOwned by the client-site agents (other laptop). Local sessions never pick these up.
    on Sep 25, 2026
  2. erikdarlingdata commented on Sep 25, 2026

    @erikdarlingdata
    OwnerAuthor

    Ruled: how this gets built

    • Recompute less often. Key the cache for the arms that read raw data over the 30-day window on the UTC day, not the analysis hour. That covers the I/O-latency arm, and any other arm that reads raw data over the full window. A 30-day baseline moves by about 1/720 per hour, so a daily recompute loses nothing. Every caller in the same day must still get the same rows (Every scheduled analysis pass recomputes every 30-day baseline: a fresh DarlingAnalysisService per pass never reuses (or shares with MCP) the bucket cache #3941's sharing).
    • No work_mem change. With a daily recompute the spill happens once a day per server, not 24 times. That is enough, and a larger per-statement work_mem risks memory on a busy store.
    • No change to the per-file grain. Collapsing it changes what the baseline means, and Lite needs the same change.
    • Lite: check whether Lite recomputes the same baseline every hour from raw data. If it does, give it the same daily key, so both products flag the same anomalies.
    • Pins:
      • Two calls in the same UTC day share one computation, and a call the next day recomputes.
      • On the rig, temp_blks_written for the arm across a day of hourly passes shows one spill, not 24.

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3

  3. erikdarlingdata commented on Sep 25, 2026

    @erikdarlingdata
    OwnerAuthor

    Addendum to the ruling above

    Two things the daily key needs, or it saves nothing:

    1. The window ends at the day boundary. For these arms, the window ends at 00:00 UTC, not at the analysis hour. Otherwise two calls on one day ask for different windows under one key. That breaks Every scheduled analysis pass recomputes every 30-day baseline: a fresh DarlingAnalysisService per pass never reuses (or shares with MCP) the bucket cache #3941's promise: every caller for a key gets the same rows. The baseline then leaves out up to a day of the newest data, out of 30 days.
    2. The cache entry lasts until the day ends. CacheTtl is 1 hour. If these entries keep that TTL, each one expires after an hour and the arm recomputes anyway. Give these arms' entries a lifetime that runs to the end of their key's day. Check that the shared tier's sweep honors it.

    The pins stand. The first pin must span more than an hour inside one day.

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3

  4. added
    in-progressActively being worked by a local session or its agents (PR open or in flight)
    on Sep 25, 2026
  5. erikdarlingdata commented on Sep 25, 2026

    @erikdarlingdata
    OwnerAuthor

    Fixed by #4291, merged to dev (ff376dc).

  6. removed
    in-progressActively being worked by a local session or its agents (PR open or in flight)
    on Sep 25, 2026
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

    client-siteOwned by the client-site agents (other laptop). Local sessions never pick these up.enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions