Feed Supervisor.KnownTestDurations from the previous main run's ledger data - #4111
Conversation
…r data
Bobcat balances lanes longest-processing-time-first, but a run with no
duration data falls back to test COUNT — measured on PersistenceTests
as lanes finishing at 101.5s and 11.4s, a quarter of the fleet idle.
The durations were on every run's SupervisorResults and thrown away.
The loop (build/TestDurations.cs):
- every supervised run writes {Job}.{Project}.{Framework}.durations.json
(first-attempt ms per uid; unmeasured omitted, never zero-filled)
beside its ledger, so the existing per-job artifact carries it;
- the flakiness roll-up copies every job's durations into the
test-ledger-run baseline it already publishes from main (and
excludes *.durations.json from its ledger-entry fold);
- each test job fetches that baseline into previous-durations/ before
./build.sh runs (build/fetch-duration-baseline.sh — repo root, not
artifacts/, because Clean wipes artifacts/ on every invocation);
- runSupervised feeds the matching file into KnownTestDurations.
Every hop is best-effort: no baseline, a stale one, or a renamed test
degrades to today's count balancing, with unknown tests charged the
median of what is known. The test job gains an explicit permissions
block (actions: read) because the fetch crosses runs.
Verified locally: run 1 wrote 2633 durations; seeded back, run 2
logged "balancing lanes with 2633 test duration(s)" and the two runs'
uid sets matched 2633/2633 — the hashed xUnit v3 uids are stable, so
LPT balancing genuinely matches. Honest scope: balancing only bites
where workers > 1 (CISqlServer today, plus local --test-workers), but
the recorded data is also the groundwork for duration trend reporting
(Bobcat #56).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CISqlServer A/B, as promisedTwo consecutive main runs of the same code — the only difference being whether a durations baseline existed. Control: run 32786336368 (first run post-merge, no baseline). Treatment: run 32791986856 (dispatched after the control published its baseline).
The mechanism works — the treatment fetched the baseline, fed 405 durations in, and produced the deliberately uneven 140/265 plan that is LPT's signature. The verdict is a no-win on this suite, and the durations data itself explains why: the baseline's total measured test time is 1370s, so the perfect-balance floor at 2 lanes is ~685s — and both runs sit essentially on it (626s and 669s, single samples on different runners). Count balancing already saturated both lanes to the final heartbeat; there was never imbalance here to recover. The 43s delta is within runner noise, and the 2026-08-01 profile this target's comment cites (931s total, ~470s LPT bound) describes a suite that has since grown to 405 post-filter tests and ~1370s. What the loop is still worth: insurance for suites where counts don't align with cost — the motivating PersistenceTests measurement (count-balanced lanes at 101.5s vs 11.4s) is exactly the shape this fixes, and CISqlServer today just doesn't have it; plus the per-job durations corpus now accumulating on every main run, which is the input for duration trend reporting (Bobcat #56) and for re-checking this table when the suite's shape changes. Cost of the loop when it can't help: one artifact download and a dictionary. Side-findings from the control run, for the record: its CIKafka job hit the 20-minute cap wedged on 🤖 Generated with Claude Code |
The follow-up queued since the supervised-CI work landed: Bobcat balances lanes longest-processing-time-first, but with no duration data it falls back to test count — measured on PersistenceTests as lanes finishing at 101.5s vs 11.4s, a quarter of the fleet idle. Every run's
SupervisorResultsalready carried per-test durations; they were being thrown away.The loop
build/TestDurations.cs): every supervised run writes{Job}.{Project}.{Framework}.durations.jsonbeside its ledger — first-attempt milliseconds per uid (a retry-amplified total would overweight exactly the flaky tests), unmeasured tests omitted rather than zero-filled. Rides the existing per-jobtest-ledger-*artifact upload untouched.test-ledger-runbaseline artifact it already publishes frommain(and excludes*.durations.jsonfrom its ledger-entry fold, which would otherwise trip its field assert).aggregate.jsonstays at the artifact root, so the existing baseline walk is unchanged.build/fetch-duration-baseline.sh): each test job downloads the last completed main run's baseline intoprevious-durations/before./build.shruns — repo root, notartifacts/, becauseCleanwipesartifacts/on every invocation. Same walk-a-few-runs-back logic as the flakiness baseline. The test job gains an explicitpermissions: actions: readblock because the download crosses runs.runSupervisedfeeds the matching file intoSupervisor.KnownTestDurations.Every hop is best-effort by design: no baseline (including the very first run after this merges), a stale one, or a renamed test degrades to exactly today's count balancing, with unknown tests charged the median of what is known — Bobcat's own rule.
Verified
previous-durations/, run 2 loggedbalancing lanes with 2633 test duration(s) from the previous main runand passed 2633/2633.entries.json(no assert trip), copied into the publisheddurations/directory.Honest scope
Lane balancing only bites where
MaxParallelWorkers > 1— today that isCISqlServer(workers: 4) and local--test-workersruns. Everything else records the data for free, which is also the groundwork for duration trend reporting (Bobcat #56's committed-ledger layer).🤖 Generated with Claude Code