Repository navigation
perf(ci): skip LLVM AOT on the iOS gate, cache packages, drop the redundant main build - #531
Merged
Merged
Conversation
…undant main build
A pull request took ~7m30s wall clock and the iOS head was ~7m04s of it; every
other job finished inside 3m40s. So the iOS job was the only thing that decided
how long CI took, and inside it a single phase decided the job: 312 of the build
step's 354 seconds passed between "Optimizing assemblies for size" and the next
line of output.
That phase is LLVM. The iOS SDK enables it for every Release mobile build, and
the comment sitting directly above the property that does so claims the value
"is ignored when using the simulator" — which is wrong for this build. A default
simulator build writes aot-instances.dll.s.bc (61 MB), .s.opt.bc (58 MB) and
.llvm.o (30 MB), and pushing that bitcode through LLVM is where the five minutes
went.
Passing -p:MtouchUseLlvm=false in the workflow, measured on clean builds on one
machine (rm -rf bin obj on all three projects, RID-less restore, then the exact
command the workflow runs):
default (LLVM on) 162.7 s
-p:MtouchUseLlvm=false 43.1 s
AOT itself still runs, which is the coverage this job exists for (#105): the Mono
backend still emits aot-instances.dll.o and System.Private.CoreLib.dll.o, only
the LLVM optimisation pipeline is skipped. Both builds produce 148 warnings and
0 errors, a 136 MB .app, a 254 KB Assets.car and 6 CFBundleIcon* keys, so the
asset-catalogue assertion still proves what it proved before, and the lock files
are untouched. What is given up is LLVM's optimisation of native code this job
compiles and throws away — a 32.7 MB app binary instead of 35.5 MB. The flag is
on the command line rather than in the csproj so local and device builds keep
LLVM.
Two smaller things, neither on the critical path:
Nothing in this repo cached NuGet packages — all four jobs logged cache: false
and re-downloaded the whole graph every run, and the macos and ios jobs did it
twice because their from-scratch-derivation guard restores again with
--force-evaluate. The eight committed packages.lock.json files make the cache key
exact rather than approximate, since locked mode already pins every resolved
version to those same files.
The push trigger on main rebuilt a tree this workflow had already built. The
ruleset on main carries merge_queue with max_entries_to_merge 1 and SQUASH, so
the merge_group run builds main-plus-that-one-PR, which is the commit that lands;
over one week that was 13 push runs against 13 merge_group runs, perfectly
paired, each re-spending ~10 minutes of macOS at this repository's 10x billing.
What makes dropping it safe is specifically that bypass_actors is empty, so
nothing can reach main outside the queue — noted in both places, because if a
bypass actor is ever added the trigger has to come back. The cost is that commits
on main no longer carry their own check status.
The merge-queue README still said the queue was blocked on plan; it has been live
since September. Replaced the enabling instructions with the settings actually in
force, and recorded which two of them the push-trigger removal depends on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Why
A pull request took ~7m30s wall clock and the
iOS headjob was ~7m04s of it. Every other job finished inside 3m40s, so iOS alone decided how long CI took — and inside it, one phase decided the job: 312 of the build step's 354 seconds passed betweenOptimizing assemblies for sizeand the next line of output.Runner queue waits were 2–3s, so contention was never the problem.
The main change: no LLVM on the simulator gate
The iOS SDK enables LLVM for every Release mobile build, and the comment sitting directly above the property that does so claims the value "is ignored when using the simulator". That is wrong for this build — a default simulator build writes
aot-instances.dll.s.bc(61 MB),.s.opt.bc(58 MB) and.llvm.o(30 MB), and pushing that bitcode through LLVM is where the five minutes went.Measured on clean builds on one machine (
rm -rf bin objon all three projects, RID-less restore, then the exact command the workflow runs):-p:MtouchUseLlvm=falseAOT itself still runs, which is the coverage this job exists for (#105) — the Mono backend still emits
aot-instances.dll.oandSystem.Private.CoreLib.dll.o; only the LLVM optimisation pipeline is skipped. Both builds produce 148 warnings / 0 errors, a 136 MB.app, a 254 KBAssets.carand 6CFBundleIcon*keys, so the asset-catalogue assertion still proves what it proved before. Lock files untouched.What is given up: LLVM's optimisation of native code this job compiles and throws away — a 32.7 MB app binary instead of 35.5 MB. Stated plainly in the workflow comment, because it means CI's configuration now differs from the shipping one on a second axis (codegen backend, on top of the trimming difference the csproj already records). If an iOS bug ever turns out to be LLVM-specific, that comment is where to look. The flag is on the command line rather than in the csproj so local and device builds keep LLVM.
Two smaller things, neither on the critical path
NuGet packages were never cached. All four jobs logged
cache: falseand re-downloaded the whole graph every run — and themacosandiosjobs did it twice, because their from-scratch-derivation guard restores again with--force-evaluate. The eight committedpackages.lock.jsonfiles make the cache key exact rather than approximate, since locked mode already pins every resolved version to those same files.The
pushtrigger onmainrebuilt a tree this workflow had already built. The ruleset onmaincarriesmerge_queuewithmax_entries_to_merge: 1andSQUASH, so themerge_grouprun builds main-plus-that-one-PR — the commit that lands. Over one week: 58pull_request, 13merge_group, 13push— perfectly paired, each push run re-spending ~10 minutes of macOS at this repository's 10× private-repo billing for no new information.What makes dropping it safe is specifically that
bypass_actorsis empty, so nothing can reachmainoutside the queue. That is noted in both the workflow and the README, because if a bypass actor is ever added the trigger has to come back. The cost: commits onmainno longer carry their own check status — the green tick lives on the queue entry that produced them.Checked and rejected
AotCompileMaxDegreeOfParallelismis never set by the SDK, and the task does run strictly serially — onemono-aot-crossacross 110 consecutive 1s samples on a 12-core box. But there are only 2 AOT units andaot-instancesdominates utterly (61 MB of bitcode vs 19 KB for CoreLib), so parallelism buys nothing.~/.dotnet, brittle against the SDK pin, and not worth it once the LLVM change lands.merge_group-only. Would halve PR feedback, but an iOS break would arrive as an ejected queue entry rather than a red PR. Left as a deliberate non-change.Expected effect
iOS ~7m → ~2.5–3m, and the whole workflow ~7m30s → ~4m, with
Desktop head on macOSbecoming the critical path. Roughly a third of this workflow's spend goes away with thepushtrigger.Verification
actionlintclean onbuild.yml..github/scriptsself-tests pass.merge_group/pull_request/workflow_dispatch,cache: trueon all foursetup-dotnetsteps, and the iOS build carrying-p:MtouchUseLlvm=false.Assets.car254168 bytes, 6CFBundleIconkeys, no lock-file drift.Also refreshed
.github/merge-queue/README.md, which still said the queue was "blocked on plan" — it has been live since September. Replaced the enabling instructions with the settings actually in force and recorded which two of them thepush-trigger removal depends on.🤖 Generated with Claude Code