perf(ci): Bigger install chunks, a shard resource sampler, and lower-priority checks - #2852
Merged
Conversation
A chunk pays one resolution whether or not it installs anything. Run 31930350338's preflight logged `80 pkgs + 214 deps: kept 294 [44s]` for a chunk that built nothing at all -- 44 seconds of pak, start to finish, for no work. At 100, the 4406-package universe is 45 chunks, so something like half an hour of resolution before a single build starts, and all of it on the critical path: every shard waits for the preflight. At 400 it is 12 chunks. The size was 100 because one pak call for the whole universe is what killed run 31270092803 -- ten minutes inside pak, no install started, then the runner went away. That resolution was a few thousand refs. 400 is an order of magnitude below it, and still small enough that a chunk which dies costs a chunk rather than the job. What it does cost: a failed chunk is four times as much to redo. `REVDEP2_INSTALL_CHUNK` is still the knob, so a preflight under pressure can be turned back down without a code change. Chunking is unchanged otherwise: 1200 packages come out as 3 chunks of 400 instead of 12 of 100, and every strong dependency inside the set still precedes the package that needs it -- 0 out of order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D1xpHRV7yVfgtJg4vp9P7z
The load test captured everything and printed it once at the end, sorted slowest first. That group answers "what was slow", which arrival order cannot, so it stays -- but a sweep of 1173 packages taking half an hour said nothing at all while it ran, except through the resource sampler. `load-test.sh` now writes each verdict twice: the machine-readable line to stdout, as before, and a human one to stderr as it happens, with a running count: ``` [load 123/1173] OK red 19s ``` The caller captures stdout and inherits stderr, so the second copy reaches the job log live and the first is still the data the summary is built from. The count is kept without a lock. Each finished package appends one byte to a temp file and reads the size back; single-byte appends to an O_APPEND descriptor do not interleave, so the number is exact. It would be cosmetic if it were not -- a progress indicator, not data. The verdict word is `OK`, `TIMEOUT` or `ERROR`, which distinguishes the two failure kinds on the live line; stdout keeps its `FAIL <pkg> timeout|error <secs>` shape untouched, so nothing downstream changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D1xpHRV7yVfgtJg4vp9P7z
Shard 16 of run 31951756102 died with ``` The hosted runner lost communication with the server. Anything in your workflow that terminates the runner process, starves it for CPU/Memory, or blocks its network access can cause this error. ``` and there was not one number from that job to say which of those it was. The preflight has sampled memory, disk and load every 30 seconds since the incident that motivated it; the shards, which run for hours rather than minutes, had nothing. A runner that stops answering takes its `if: always()` steps with it, so the only record that survives is what was already streamed to the log -- which is the whole reason the sampler exists, and it was in the wrong job. It is backgrounded once, before the install: a process started in one step outlives it, and Actions reaps it with the job. `RESOURCE_LOG` also puts the series in the shard artifact, for the jobs that do reach their upload. A final `always()` step adds the last sample and asks dmesg whether the kernel killed anything, which is the difference between running out of memory and the host going away. And the checks now run under `nice -n 10`, plus `ionice -c3` where it exists. Two `R CMD check` processes at once, each with children of its own -- a test suite that opens a PSOCK cluster, a vignette that knits -- can take every core the runner has. The runner agent is a process on that machine too, and it has to reach the service regularly or the job dies. `nice` costs nothing when there is headroom: the scheduler only consults priority when there is more work than cores, which is exactly the case worth protecting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D1xpHRV7yVfgtJg4vp9P7z
Two things the shard's new sampler made obvious about the preflight's. It was started and killed inside the "Install and load every dependency" step, so the two artifact uploads after it went unsampled. This job's library artifact is gigabytes -- 4.1 GB in run 31951756102 -- and packing and uploading it is exactly the kind of work that runs a runner out of disk or stalls it. "The runner stopped answering while packing" would have been as unanswerable as it was for shard 16. It is backgrounded and left running now, as the shard's is. And its label was fixed when it started, so it said `installing` for the whole job. It is not a lie for the first ten minutes and it is one for the next thirty: ``` Preflight: load-testing 1173 of 2775 installed package(s) ... [resources] 14:40:12 installing -- mem 1.8/15.6G used ... ``` `watch-resources.sh watch` now reads its label from $RESOURCE_PHASE_FILE when that is set, and `preflight.R` writes the phase it is in -- installing, surveying system requirements, load-testing, packing the library. Fixed labels still work; the file is optional and the shard passes none, its `shard <n>` being phase-neutral already. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D1xpHRV7yVfgtJg4vp9P7z
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.
Three changes to what the preflight and the shards spend, and to what they say while spending it.
1. A shard samples its resources, and checks at a lower priority
Shard 16 of run 31951756102 died with
and there was not one number from that job to say which of those it was.
The preflight has sampled memory, disk, load and the three largest processes every 30 seconds since the incident that motivated
watch-resources.sh. The shards, which run for hours rather than minutes, had nothing. A runner that stops answering takes itsif: always()steps with it, so the only record that survives is what was already streamed to the log — which is the entire reason the sampler exists, and it was in the wrong job.It is backgrounded once, before the install: a process started in one step outlives it, and Actions reaps it with the job.
RESOURCE_LOGalso puts the series in the shard artifact for the jobs that do reach their upload. A finalalways()step adds the last sample and asksdmesgwhether the kernel killed anything — the difference between running out of memory and the host going away.And the checks now run under
nice -n 10, plusionice -c3where it exists. TwoR CMD checkprocesses run at once, each with children of its own — a test suite that opens a PSOCK cluster, a vignette that knits — and together they can take every core the runner has. The runner agent is a process on that machine too, and it has to reach the service regularly or the job dies.nicecosts nothing when there is headroom: the scheduler only consults priority when there is more work than cores, which is exactly the case worth protecting.ionicedoes the same for the disk, where a check writing its.Rcheckdirectory competes with the agent writing logs.This does not prove starvation was the cause. It means the next one says so.
2. 400 packages per pak call, not 100
A chunk pays one resolution whether or not it installs anything. Run 31930350338's preflight, verbatim:
44 seconds of pak for a chunk that built nothing. At 100 per chunk the 4406-package universe is 45 chunks — on the order of half an hour of resolution before a single build starts, all of it on the critical path, because every shard waits for the preflight. At 400 it is 12 chunks.
Why it was 100. One pak call for the whole universe killed run 31270092803 — ten minutes inside pak, not one install started, then the runner was shut down. That resolution was a few thousand refs; 400 is an order of magnitude below it, and still small enough that a chunk which dies costs a chunk rather than the job. The cost is that a failed chunk is four times as much to redo.
REVDEP2_INSTALL_CHUNKremains the knob.Chunking is otherwise identical: on 1200 CRAN packages, 3 chunks of 400 instead of 12 of 100, and every strong dependency still precedes the package that needs it — 0 out of order.
3. The load test says what it is doing
It captured everything and printed it once at the end, sorted slowest first. That group answers "what was slow", which arrival order cannot, so it stays — but a sweep of 1173 packages taking half an hour said nothing while it ran.
Each verdict goes to stdout (the caller's data, unchanged) and to stderr (the live log); the caller captures the first and inherits the second. The running count needs no lock — each finished package appends one byte and reads the size back, and single-byte
O_APPENDwrites do not interleave.Verified
nice -n 10+ionice -c3prefix applies: the child reports niceness10and I/O classidle. Theionicehalf is probed before use, so an image without it degrades tonicealone.SHARD=16: a labelled "before the install" line, thenshard 16samples every interval, both to the log and toRESOURCE_LOGin the results directory.ERRORon the live line,FAIL nosuchpackage error 0on stdout, temp file removed on exit.revdep2.yamlparses; the two shell scripts passbash -n.Complements #2854, which bounds what a lost runner costs; this one is about seeing it coming and making it less likely.
🤖 Generated with Claude Code
https://claude.ai/code/session_01D1xpHRV7yVfgtJg4vp9P7z