Repository navigation
ui: aggregate run summaries in SQL and show in-progress runs in the strips (MARSOHS-1649) - #16
Merged
Merged
Conversation
…trips (MARSOHS-1649) ListRunSummariesInRange selected runs.error and the full tests.result jsonb for every test row and unmarshalled it into tester.T only to read .Name and .State. For the dashboard's 30-day call in production that is 94,127 rows and 39 MB of jsonb, three times per page load, and runs.error is now up to ~32 KB of runner output per errored run. The query now selects projected columns only (`error IS NOT NULL`, `result->>'name'`, `result->>'state'`), and is a LEFT JOIN from runs to tests so runs without tests (errored/killed before reporting) appear in ErrorRunIDs instead of vanishing from the strip. The tests of an errored run are excluded in the join condition since they were never counted. In-flight runs are included: the `finished_at IS NOT NULL` filter is gone, PackageSummary gains RunningRunIDs and both summary types NumRunningRuns(). A run is attributed to every bucket in [started_at, coalesce(finished_at, now())), clamped to the range, so a 15-minute run no longer paints one 5-minute cell followed by two blank ones. Its tests are only counted in the bucket it started in. Runs that started up to 24h before `begin` are considered so runs spanning a range boundary are attributed correctly while keeping the query a bounded range scan on runs_started_at_finished_at_idx. The bucket index is now floor-divided and clamped: a run with started_at == end previously indexed one past the slice. Templates: a `summaryState` helper classifies a bucket as results / errored / running / active / empty. Cells with a run in progress and no results render animated grey stripes, cells a finished run was executing through render static stripes, cells with only errored runs render solid red (previously a NaN-height bar, i.e. blank). Finished buckets with results keep the existing pass/skip/fail bar. Tooltips and /run_summary show running and erred counts, and /run_summary links the running and erred runs.
ElanHasson
marked this pull request as ready for review
September 22, 2026 15:09
fumblehool
approved these changes
Oct 13, 2026
fumblehool
left a comment
Collaborator
There was a problem hiding this comment.
lgtm 👍
note for future: The suggested fix was to use GROUP BY with count(*) FILTER (WHERE result->>'state' = 'passed'). The PR instead extracts only the needed fields but still iterates row-by-row. Both approaches dramatically reduce data transfer, but the GROUP BY approach would push more computation to Postgres.
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.
Jira: MARSOHS-1649
Problem
ListRunSummariesInRangeselectedruns.errorand the fulltests.resultjsonb for every test row and unmarshalled it intotester.Tjust to read.Nameand.State. Measured in prod for the dashboard's 30-day call: 94,127 rows, 39 MB ofresultjsonb transferred and unmarshalled, three times per page load (1 h / 1 d / 30 d). Since #10runs.erroris also up to ~32 KB of stdout/stderr per errored run, multiplied by that run's test rows.Separately, the query required
finished_at IS NOT NULLand bucketed bystarted_atonly, so an in-flight run was invisible until it finished (~15 min foragents), and a 15-minute run painted one 5-minute cell followed by two blank ones. Short suites re-enqueued every 5 min fill every cell, which made long suites look broken by contrast.Changes
Payload (
db/pg.go)runs.package, runs.id, runs.started_at, runs.finished_at, (runs.error IS NOT NULL), tests.id, tests.result->>'name', tests.result->>'state'. Scanned intostring/bool/NullUUID, notester.Tunmarshal.FROM runs LEFT JOIN tests ON tests.run_id = runs.id AND runs.error IS NULL. Two behaviour changes:ErrorRunIDs. Previously the inner join fromtestsdropped them entirely, so a killed run /TestMainfailure showed as a blank slot. They now render solid red.continued over).bucketIndex) and clamped. Previously a run withstarted_at == endindexed one past the slice (<= end+ exact-multiple range → panic). Range is now half-open[begin, end).begin/endused for bucket arithmetic are truncated to microseconds to matchtimestamptz/pgx; the old code only got bucket 0 right for a run starting exactly atbeginbecauseint()truncates toward zero.In-progress and spanning runs
finished_at IS NOT NULLfilter dropped.PackageSummary.RunningRunIDsadded (subset ofRunIDswithfinished_at IS NULL);NumRunningRuns()on bothRunSummaryandPackageSummary(plusNumRuns/NumErrorRunsonPackageSummaryso both types satisfy one interface).[started_at, coalesce(finished_at, now())), clamped to the range. Its tests are only counted in the bucket it started in, so nothing is double-counted; spanned buckets carry the run id only. Tests already submitted by a running run are counted as usual (simplest; the cell shows results and the tooltip says "1 running").beginbut were still executing atbegin, the query looks backrunSummaryLookback = 24hbeforebegin. That keeps the query a bounded range scan (the scheduler resets/fails anything past the run timeout, so 24h is a very generous bound) and makes/run_summaryfor a spanned cell consistent with the strip.SpanningRunIDs: colouring only needs "has results / has running / has errored / has runs", all derivable from the existing fields.Templates
summaryStatehelper inhttp/templates.go:results→ existing pass/skip/fail bar (unchanged semantics);errored→ solidbg-danger;running→ animated grey stripes (.summary-running);active(a finished run was executing through this cell; its results are in the cell it started in) → static grey stripes (.summary-active);empty→bg-light./run_summaryshows running/erred counts and lists running and erred runs with links to/runs/<id>.db.DBinterface unchanged;db/db_mock.gois unchanged aftergo generate.EXPLAIN
Local Postgres 16, 103k runs / 1.03M tests over 60 days, default planner settings, 2-day window (≈ prod's 30-day selectivity against the multi-year table):
The plan drives from
runs_started_at_finished_at_idx→tests_run_id_idxas before; thefinished_at IS NULLOR-branch is served by the existingruns_finished_at_idxand costs a handful of index tuples. The 1-hour call runs the same shape in ~1.7 ms.Verification
gofmt -l .,go vet ./...,go generate ./... && git diff --exit-code db/db_mock.go,go test -race ./...(withPG_DSNset) all clean.TestPG_ListRunSummariesInRangeextended: errored run with zero tests appears inErrorRunIDs; errored run's tests are not counted; running run spans buckets withRunningRunIDsset and its test counted once; finished run spans the buckets it executed in with half-open end; run started before the range is attributed but its tests are not; run atendexcluded. PlusTestBucketIndex.http/ui_test.go:summaryStateunit test and a render test throughNewUIHandlerwith the mock for/and/run_summary(running and errored) asserting 200 and the expected markup./,/packages,/packages/{db,http},/run_summaryfor running, errored, and spanned cells: all 200 with the expected states.README has no UI/dashboard section, so no doc change.