Repository navigation
Job History: GROUP BY join replaces the per-job window function, tab off the 30s timer (#4229) - #4256
Merged
Merged
Conversation
…, take the tab off the 30s fleet timer (#4229) The window function recomputed AVG/MAX success duration and last-success time over every step row the 24h window matched, before the ORDER BY/LIMIT could trim to the newest 2,000. job_stats now computes that once per (server_id, job_id) with a GROUP BY over just the step_id-0 success rows, joined to the already- limited newest rows. Job History also moves off the 30s fleet timer (#4227/#4240's mechanism): it refreshes on tab activation and its Refresh button, matching job-run cadence instead of polling every tick. Lite's LocalDataService.GetJobHistoryAsync carried the identical window-function pattern; same fix there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
# Conflicts: # Darling/PerformanceMonitor.Darling.Viewer/MainWindow.xaml.cs
…st (#4229) PR #4256 rewrote the Job History read on both products, but the Lite side (LocalDataService.JobHistory.cs) was only ever compiled -- no test ran the new job_stats GROUP BY/join statement against DuckDB. Adds JobHistoryGroupedStatsMatchWindowFunctionTests, comparing the new statement field-by-field and in order against the pre-fix window-function statement copied verbatim from origin/dev, through a tie at the LIMIT boundary and a NULL-average job, plus a two-server/shared-job_id case proving the join keys on (server_id, job_id). Verified the second case catches a regression: dropping server_id from the join turned Assert.Equal(2, mine.Count) into Actual: 4; restored, green again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
erikdarlingdata
marked this pull request as ready for review
September 25, 2026 07:44
This was referenced Sep 25, 2026
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 #4229.
Why
PR #4235 (merged) gave Job History's event-windowed read a
collection_timefloor so TimescaleDB skips out-of-window chunks. That left the window's own row cost. The per-job success average and max ran asAVG/MAXOVER (PARTITION BY server_id, job_id)against every step row the 24h window matched. That was all rows, not just the 2,000 the tab shows. Postgres sorted and aggregated the whole windowed set before the finalORDER BY ... LIMITtrimmed it. That ran every 30 seconds while the tab was open.What changes
ViewerDataService.BuildJobHistorySql(Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.JobHistory.cs)A new
job_statsCTE computes the average and max once per(server_id, job_id). It uses a plainGROUP BYover the window's step_id-0 SUCCESS rows.baseselects only the raw columns and runsORDER BY run_datetime_utc DESC, instance_id DESC LIMITwith no aggregation. The planner picks a top-N sort instead of materializing the full windowed set. The finalSELECTleft-joinsjob_statsonto the rowsbasealready picked.Row selection is unchanged. The old
ORDER BY/LIMITnever read the analytic columns. Trimming to the newest rows first and joining per-job stats after picks the same rows, order, and values. A job with no successful step-0 row in the window gets NULL avg and last-success from the left join. That matches whatAVG/MAXreturned for an all-NULL partition.This is a two-scan shape now:
job_statsandbaseboth scanjob_history, each independently floored. That is the same family asBlockingDurationStatsSql. The floor-count pin inEventWindowedReadsCarryTheFloorTestsforBuildJobHistorySqlmoves from 1 to 2. The live chunk-reduction margin for job history drops from 3 to 2. Job history's oracle is single-scan on the old side. Doubling the scan count narrows the absolute reduction even though each scan is still floored.30s fleet timer (
MainWindow.xaml.cs,OnRefreshTimerTick)Job History joins Recommendations, FinOps, and Overview in the tab-early-return. The mechanism matches #4227/#4240, applied independently here. The tab already refreshed on activation through the existing
LoadVisibleTabAsync/JobHistoryContent.RefreshJobsAsync()wiring and has its own Refresh button. Only the 30s poll needed removing. Job history changes at job-run cadence, not every 30 seconds.Lite parity (
Lite/Services/LocalDataService.JobHistory.cs)GetJobHistoryAsynccarried the same window-function-over-every-row pattern (DuckDB,v_job_history) and got the same rewrite. Lite's Job History tab has no fleet-timer poll. Its only timer (_staleDataTimer, 10s) updates a "Refreshed Xs ago" label and never re-runs the query. No refresh-mechanism change was needed.No MCP tool duplicates this per-job average computation. The only MCP job-history references are store-metrics evidence checks. Those are unrelated.
Test plan
Measured on a seeded probe database: 250,000
job_historyrows across 13 daily chunks, 200,000 in the 24h window. This scales down from the issue's 21.7M-row/0.9M-window production numbers at the same ratios.EXPLAIN (ANALYZE, BUFFERS), fleet-wide, no server filter,LIMIT 2000:The old plan's
WindowAggforced a sort over all 200,000 windowed rows and spilled to disk. The new plan'sHashAggregatescans only the step-0/success subset and never spills. The double scan ofjob_historyraises shared-buffer touches but removes the disk-spilling sort and wins on wall time.Darling.Testsbuilds, 0 warnings.Lite.Testsbuilds, 0 warnings.JobHistoryGroupedStatsMatchWindowFunctionLiveTests(two facts):Fact 1 drives
GetJobHistoryAsyncon a scoped server with 9 jobs tied on onerun_datetime_utc.8 successes and 1 job whose only step-0 row failed (NULL average),
limit = 5.Compares every field row-for-row against the pre-fix SQL from
origin/dev.Crosses the LIMIT boundary through the tie.
Confirms the NULL-average job's
IsLongRunningstays false andLastSuccessfulRunUtcstays null.Fact 2 proves the join keys on
(server_id, job_id), notjob_idalone:two servers share a
job_idwith different durations on a fleet-wide unscoped call.server_idhalf fanned onebaserow to both servers'job_stats.Assert.Equal(2, mine.Count)failed withActual: 4. Restored the join, reran, green.EventWindowedReadsCarryTheFloorTests'BuildJobHistorySqlfloor-count pin from 1 to 2and
EventWindowedReadsAreBoundedLivePostgresTests' chunk-reduction margin for job history from 3 to 2.Both have an inline reason.
Darling.Testssuite (13,845 tests) run once, aftergit merge origin/devand a freshdarlingtest:4 failures on the first pass.
Two (
DocCommentHygieneTests.TheBoundedSetIsComparedForEquality,...EveryCrefTargetResolvesOrIsBounded)were mine: a
<see cref="Tied">pointed at a local variable. Fixed to<c>tied</c>.The other two (
ServerListAndSummaryPlanShapeTests,CaptureDownChunkOrderTests) are unrelatedcollection_logchunk-plan-shape tests that do not touch job history or MainWindow.Re-run alone on a freshly recreated
darlingtest, all 81 passed. Shared-store cross-test pollution,not this change.
green and is the evidence for this PR. CI decides whether a second full Darling run is needed.
Lite.Tests/JobHistoryGroupedStatsMatchWindowFunctionTests(2 facts, real DuckDBvia
SharedDuckDbFixture).GroupByJoin_MatchesTheOldWindowFunction_ThroughATieAndANullAverage_AtTheLimitBoundarycompares the newjob_statsstatement against the pre-fix window-function text fromorigin/dev, field-for-field, in order.Seed: eight jobs tied on one
run_datetime, one job with a NULL average,limit = 5, crosses the boundary.GroupByJoin_KeysOnServerIdAndJobId_NotJobIdAloneproves the join on(server_id, job_id), notjob_idalone: two servers share a
job_idwith different durations on a fleet-wide call.Proved it red once: dropping
server_idturnedAssert.Equal(2, mine.Count)intoActual: 4.Full
Lite.Testssuite: 5,341 tests, 0 failures.CHANGELOG entry
SECTION: Fixed
ENTRY:
over every job step in the 24h window. It then sorted and trimmed to 2,000 rows. On a large store this
processed far more rows than the tab showed, every 30 seconds. The average and max are now computed once
per job from its successful step outcomes. The result joins onto the newest rows after row selection.
The tab now refreshes on open, focus, or user click. Lite's job history read had the same pattern and
got the same fix.
REF:
[Job History: GROUP BY join replaces the per-job window function, tab off the 30s timer (#4229) #4256]: Job History: GROUP BY join replaces the per-job window function, tab off the 30s timer (#4229) #4256
Generated with Claude Code
https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ