Repository navigation
The daily summary's month read probes each day once and hashes its query count, and its coverage gate is cached, so the server page's Overview stops waiting on it (#3905) - #3950
Merged
Conversation
…shing, and reads rollup coverage from the shared cache (#3905) The month read's cost was the routed queries CTE, not the raw CTEs. Its not-carried probes (#3653 A6) were planned as a semi-join and an anti-join over the server's whole history in the probed relation, rescanned once per day and spilled past work_mem. Each probe now carries an OFFSET 0 fence, so it runs as a per-day index probe, and the source is probed only on the days the rollup probe found empty. Every tier counts distinct (day, hash) pairs instead of COUNT(DISTINCT), which PostgreSQL can hash instead of sort. Results are identical: a live test compares every tier against the old statement row for row. DarlingHealthReader reads rollup coverage through ComposeStoreAvailability instead of probing on every call. That cache is now single-flight, so concurrent readers share one probe. The web server page's Overview tab draws today's tile from the range read's anchor-day row: one fetch for both panels. Rig (one busy server): month 3,278 -> 186 ms, 120-day 12,359 -> 161 ms, single day 146 -> 45 ms. DARLING01 busiest server: month 729 -> 253 ms. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z4T51J4E6L8fjriPHpsif
…on its own connection (#3905) ViewerDailySummarySqlTests asserted COUNT(DISTINCT query_hash) in the shared statement; it now asserts the distinct-pair shape. The read-shape live seed enables TimescaleDB through LiveTimescaleProbe (#1922), and disposes its scratch database on a skip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z4T51J4E6L8fjriPHpsif
…ack, and each Overview panel renders on its own (#3905) A failed shared probe is cached as RollupAvailability.None, which routes every window to raw. That is the compose panels' accepted fallback, but on a store whose raw purges are armed it prints unique_queries = 0 for every purged day of a calendar. Before #3905 the daily reader's per-call probe failed the read instead. The cache now marks a failed answer, and the daily reader's accessor (GetMeasuredRollupsAsync) probes again on its own token rather than route on it, so a second failure surfaces as the tool's error. The Overview composite renders each panel through its own guard, so a render fault cannot leave both on their loading strips, and today's tile gets its own sentence for an empty range. Found in review. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z4T51J4E6L8fjriPHpsif
erikdarlingdata
enabled auto-merge (squash)
September 23, 2026 00:18
This was referenced Sep 23, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 23, 2026
…3920, #3927, #3931, #3932, #3940, #3942, #3946, #3947, #3950, #3952, #3955, #3956, #3957, #3964, #3965, #3966, #3968, #3972, #3975, #3979, #3980, #3981, #3983, #3984, #3985) (#3989) The wave's fix PRs deliberately carried no CHANGELOG edits (parallel-agent hot-spot protocol); each agent reported its entry and this commit lands them together, byte-verified against origin/dev. Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
Closes #3905.
Why
On the largest production store the web server page's two daily-summary reads were 26 of the Overview tab's 28 seconds, and one of them ran into the
viewerrole's 15 s timeout. The issue named two causes. Measurement confirmed one and corrected the other.The per-call coverage probe: confirmed.
DarlingHealthReader.GetDailySummaryRangeAsyncprobed rollup availability and coverage on every call, uncached, and the page calls it twice per load. Production measured the probe at ≥ 1.7 s. On DARLING01 it is 1–2 ms of execution plus 12 ms of planning, because eachmin()touches only the oldest chunk, which is small there."Route the other eight CTEs to a rollup tier": no such tier exists. The aggregates over those sources are the anomaly baseline's per-collection supplies and the fleet card's
collection_health_hourly, and none of them answers its CTE exactly:collection_health_hourlykeeps 8 of the month's 30 days;The issue's premise that "the daily rollups already exist for several of these sources" is wrong: daily rollups exist only for the query families.
What actually made the month read slow was the
queriesCTE, which is already routed. There were two causes:EXISTSandNOT EXISTS, and PostgreSQL pulls both up into a semi-join and an anti-join. Neither join can push its per-day range into the scan, so the planner materialized the server's entire history in the probed relation (four days of raw on the hourly tier's source probe, the hourly's 90 days on the daily tier's) and rescanned it once per day of the window. Cost grows with days × retained rows, and pastwork_memit spills and rereads a temp file per day. On DARLING01's busiest server this member was 629 of 873 ms. On the rig, seeded with one busy server, it was 4.5 s of a 6.1 s month read, with 32,678 temp blocks read. The comment above it promised "one index range per day"; the plan did not deliver that.COUNT(DISTINCT query_hash). PostgreSQL evaluates a DISTINCT aggregate by sorting every input row of its group. For a busy server's month of hourly rollup rows that sort was 1.1 s of the rig's remaining time.What changes
DailySummarySql, results identical on every tier:OFFSET 0. PostgreSQL'ssimplify_EXISTS_queryrefuses to pull up a sub-select with an OFFSET, so each probe stays a SubPlan that runs once per candidate day: an index range with run-time chunk exclusion.(day, query_hash)pairs instead of usingCOUNT(DISTINCT).COUNTskips a NULL hash exactly as the DISTINCT aggregate does, and the planner can hash aSELECT DISTINCT.DarlingHealthReaderreads coverage throughComposeStoreAvailability(viaGetMeasuredRollupsAsync, below) instead of probing on every call. That is the gate the compose panels already read, and the one Erik's comment pointed at: cached per data source forReprobeInterval(5 min). The viewer's calendar has cached the same gate for the same interval all along.RetentionTierRouter's one-dayRouteMarginkeeps every age-routed window clear of that.RollupAvailability.None, which routes every window to raw. That is the compose panels' accepted fallback. But on a store whose raw purges are armed, it would printunique_queries = 0for every purged day of the calendar: not NULL, and not indays_missing. Before this change, the per-call probe failed the read instead.GetMeasuredRollupsAsync, which probes again on its own token rather than route on a cached failure, so a second failure surfaces as the tool's error, as before.ComposeStoreAvailabilityis now single-flight. Agents race their daily reads, and the compose panels, trend tools and custom alerts read the same gate. It used to be "benignly racy": every stale caller probed. Now one probe runs per data source at a time, with theDarlingFleetReader.CollectionHealthMemoshape (get_fleet_overview's collection-health rollup times out at :35 on the largest store — phase-grid signature, discriminate compression-band contention vs plan before fixing #3735):CancellationToken.None, so a caller's token releases only that caller's wait;That gives "at most once per cache period" literally, and it also covers the compose panels, custom alerts and trend tools that read the same gate.
Web server page, Overview tab: today's tile and the Daily Health Calendar now render from one fetch of
get_daily_summary_range. The tile is the row whose date is the read's ownto_date, and there is no separateget_daily_summaryread.fanout()guards its specs, so a render fault in one cannot leave both on their loading strips.emptyrange the tile says nothing has been collected today, instead of the range's MCP-parameter advice.MCP tools: no payload, description or parameter change.
get_daily_summaryandget_daily_summary_rangeget the faster statement and the cached gate through the reader.WPF viewer: it reads the same
DailySummarySql, so its calendar gets the statement fix. Its gate was already cached.Lite parity: nothing to change. Lite's calendar reads raw DuckDB: no rollup tier, no not-carried member, no coverage probe. DuckDB hashes
COUNT(DISTINCT)natively, and Lite's calendar already reads one range per month.No store migration.
Test plan
DARLING01 (9 servers, 17 days;
EXPLAIN (ANALYZE, TIMING OFF), median of 3; old and new statements interleaved in one session):query_stats)The not-carried member alone on SQL2025's month read went from 629 ms to 1.4 ms. What remains, about 200 ms, is the raw wait, collection-log and CPU CTEs, which have no tier (see Why).
Rig at production scale for one server (5.3M raw rows seeded over 45 days, 571K retained over 4 days, 881K hourly rollup rows;
work_mem16 MB; median of 3):MCP, DARLING01, the deployed dev build (before), median of 5:
get_daily_summary_range639 / 372 / 369 ms andget_daily_summary137 / 79 / 80 ms for SQL2025 / SQL2022 / SQL2016. The "after" needs the deploy.New tests:
DailySummaryReadShapeSqlTests: no tier uses a DISTINCT aggregate; the reader routes on the cached gate; the Overview tab draws both panels from one fetch.ComposeStoreAvailabilitySingleFlightTests:DailySummaryReadShapeLiveTests(rig):Materializeand no semi/anti join in thequeriesCTE. The rollup probe runs once per candidate day (27) and the source probe once per rollup-empty day (23).Red on the old shape: with
DailySummarySql.csandDarlingHealthReader.csreverted, the plan test, the probe-count seam test and both SQL pins fail. On the tiny seed the plan test sees the old semi-join plusMaterializewith loops=27, and the seam test counts 0 probes.Existing pins updated deliberately (text, not intent):
DailySummaryNotCarriedTests(probe texts, plus assertions that the fences are present and in the right place),RetentionTierRouterTests,IntervalHonestHourlyRollupTests,ViewerDailySummarySqlTests,ServerPageTabsTests(composite map),DarlingMcpHealthToolsTests(comment).Overview tab in a node DOM shim, six response states:
/api/read/get_daily_summary_rangefetch;to_daterow;emptygives the tile its own sentence;unavailableand an error reach both panels;Independent code review (subagent) before opening: one should-fix, the cached-failure fallback, fixed as above; four nits, fixed (stale comments, per-panel render guards, the tile's empty sentence, the NULL-hash-only seed day).
Full
Darling.Testsagainst the rig withDARLING_TEST_PG: 12,535 total, 0 failed, 22 skipped (the env-gated managed-runtime, PostgreSQL-target and symlink tests), on the branch rebased onto dev at9f476174After deploy: rerun the MCP timing on DARLING01, and the production Overview tab.
Filed from this: #3933. The start-up materialization-hole scan has the same pulled-up-EXISTS shape, and it runs at every service start.
🤖 Generated with Claude Code
https://claude.ai/code/session_018Z4T51J4E6L8fjriPHpsif