Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ jobs:
# fail. Baseline growth vs the PR base is the next step below.
bash scripts/ci-gates.sh rules --baseline scripts/rule-enforcement-baseline.tsv .

# Three legs, SHARD=i/3 like selftests: each runs only the scripts/ci-tests.d registrations that scripts/ci-tests-shard.sh lists
# Four legs, SHARD=i/4 (the i/N spelling selftests uses): each runs only the scripts/ci-tests.d registrations that scripts/ci-tests-shard.sh lists
# for it, so the job takes as long as its slowest slice, not the sum. `gates` needs `perpr`, so it fails unless every leg passes.
perpr:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
shard: [1, 2, 3, 4]
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
Expand All @@ -133,7 +133,7 @@ jobs:
# This leg's registrations (serial and background) are listed once, here, into $RUNNER_TEMP/bg/leg; both steps read that list.
- name: Start background per-PR tests (scripts/ci-tests.d/*.bg.sh)
env:
SHARD: ${{ matrix.shard }}/3
SHARD: ${{ matrix.shard }}/4
run: |
mkdir -p "$RUNNER_TEMP/bg"
date +%s >"$RUNNER_TEMP/bg/started"
Expand All @@ -145,7 +145,7 @@ jobs:
f="$RUNNER_TEMP/bg/$(basename "$t")"
: >"$f.log"
# shellcheck disable=SC2016 # $1 and $2 expand in the child shell, on purpose
nohup bash -c 'timeout 900 bash -e "$1" >"$2.log" 2>&1; echo $? >"$2.rc.tmp"; mv "$2.rc.tmp" "$2.rc"' _ "$t" "$f" >/dev/null 2>&1 &
nohup bash -c 'timeout 900 bash -e "$1" >"$2.log" 2>&1; echo $? >"$2.rc.tmp"; date +%s >"$2.end"; mv "$2.rc.tmp" "$2.rc"' _ "$t" "$f" >/dev/null 2>&1 &
done

# Per-PR tests: a PR registers its test by adding scripts/ci-tests.d/<slug>.sh (one command line),
Expand All @@ -171,6 +171,8 @@ jobs:
end=$(($(cat "$RUNNER_TEMP/bg/started") + ${BG_WAIT_S:-960})) # counted from the background start, not from here
while [ ! -f "$f.rc" ] && [ "$(date +%s)" -lt "$end" ]; do sleep 1; done
cat "$f.log"
took='?'; [ -f "$f.end" ] && took=$(($(cat "$f.end") - $(cat "$RUNNER_TEMP/bg/started")))
echo "bg elapsed: ${f##*/} $took s"
[ -f "$f.rc" ] || { echo "FAIL: $t did not finish in time"; exit 1; }
[ "$(cat "$f.rc")" = 0 ]
done
Expand Down
16 changes: 13 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,24 @@ Parallel lanes must not bump the version: each one claims "main + 1" and all but
or `size-budgets.tsv` rows: committing them makes any two PRs conflict once one merges. CI rebuilds them
for an unlanded PR (`scripts/regen-unlanded.sh`; the size ratchet still compares the rebuilt rows to
main), and the land commit carries them and is checked byte-for-byte. Register a new test by adding
`scripts/ci-tests.d/<slug>.sh` (one command line), not a line in `ci.yml`. CI splits the registrations over 3
parallel legs by sorted position (`SHARD=i/3 bash scripts/ci-tests-shard.sh` lists leg i's), so a test must not rely on
another registration having run first.
`scripts/ci-tests.d/<slug>.sh` (one command line), not a line in `ci.yml`. CI splits the registrations over the
parallel `perpr` legs by recorded seconds (`scripts/ci-durations.tsv`: the longest test goes first, to the least-loaded leg; a
test with no row counts as the median, so adding one needs no table edit; `*.bg.sh` ones go round-robin;
`SHARD=i/N bash scripts/ci-tests-shard.sh` lists leg i's of the N legs), so a test must not rely on another registration having run first.
`bash scripts/run-ci-tests.sh --affected` runs only the registered tests a changed file can reach (the pre-push default),
so run it before pushing instead of just your own new test.
`bash scripts/ci-gates.sh fragments --config scripts/size-budgets.tsv changelog.d` checks the fragment's marker
lines and headings (the pre-commit and pre-push hooks, land and CI run it), so a malformed fragment fails your
commit instead of the release.
- **Test-time table:** refresh `scripts/ci-durations.tsv` when the slowest `perpr` leg's `Per-PR tests` step is more than 20%
over the fastest on a PR run (until then a new test counts as the median, so only the balance drifts). Save the job log of
every `perpr` leg as `leg1.log` ... `legN.log` (`gh api repos/<owner>/<repo>/actions/jobs/<job id>/logs`; the job id is in
the job's URL on the run page), rebuild the rows (the seconds between consecutive `== <registration>` lines, rounded, at least 1)
and put them under the table's header with its run id and date updated:

```bash
awk 'FNR == 1 { p = "" } $2 == "==" && $3 ~ /^scripts\/ci-tests\.d\// { t = substr($1, 12, 2) * 3600 + substr($1, 15, 2) * 60 + substr($1, 18); d = t - q; if (d < 0) d += 86400; if (p != "") print p "\t" (d < 1.5 ? 1 : int(d + 0.5)); p = ($3 ~ /\.bg\.sh$/ ? "" : $3); q = t }' leg*.log | LC_ALL=C sort
```
- **Land:** on the branch, run `bash scripts/land-release.sh`. Under a lock it merges `origin/main`
(generated-file conflicts are regenerated, anything else aborts), stamps the next minor across the
lockstep set, folds the fragments into `CHANGELOG.md`, regenerates, and commits. Push and merge as
Expand Down
4 changes: 4 additions & 0 deletions changelog.d/perpr-shard-balance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Changed
- The CI `perpr` job now runs four legs instead of three (`shard: [1, 2, 3, 4]`, `SHARD=i/4`), and its legs are balanced by recorded test time instead of by sorted position. `scripts/ci-tests-shard.sh` reads `scripts/ci-durations.tsv` (whole seconds per `scripts/ci-tests.d/*.sh` registration: the seconds between consecutive `== <registration>` lines of the `perpr` job logs of public CI run 41148962559 (2026-10-31), rounded, at least 1), takes the serial registrations longest first (ties by name, C locale) and gives each to the least-loaded leg (ties to the lowest index). A registration with no row counts as the table's median, so adding a test never needs a table edit and two PRs never conflict on the table. `*.bg.sh` registrations keep the sorted round-robin, because they run beside the serial ones, so only serial seconds are balanced. The contract is unchanged: the `i/N` slices are disjoint and cover every registration for any N, a malformed `SHARD` exits 2, an empty slice exits 1, the listing is sorted and does not depend on the locale. On the committed table, which counts every test at least 1 s (so these are predictions from the table, not CI measurements), the old helper with three legs on the tree before this change gives legs of 283, 489 and 682 s of serial tests, and the new split over four legs gives 364, 364, 364 and 363 s.
- The `Per-PR tests` step also prints one `bg elapsed: <test> <seconds> s` line per background test (its wall time from the background start, `?` when it left no end time), because the `== <registration>` lines show only how long the step waited for it, not how long it ran.
- Test: `scripts/test-perpr-shard-balance.sh` (registered as `scripts/ci-tests.d/perpr-shard-balance.sh`) checks the committed table's format, that the slices are disjoint and complete for N = 1 to 5, that the slowest of the legs `ci.yml` runs (the N its `perpr` legs pass in `SHARD=i/N`, read from `ci.yml`) is predicted within 1.1x of the mean (it fails on the old split), and, on a fixture, the exact longest-first assignment, the median default for a malformed or missing row, and the round-robin of background tests. `scripts/test-perpr-shard.sh` no longer asserts that the slices hold nearly equal numbers of registrations, which only the old split guaranteed, and now checks that each leg logs its `bg elapsed:` line. `CONTRIBUTING.md` describes the new split, says when to refresh the table (the slowest leg's `Per-PR tests` step more than 20% over the fastest on a PR run) and gives the awk command that rebuilds its rows from the job logs of the legs; the test runs that command on synthetic logs.
Loading
Loading