Parallel xcodebuild lab ======================= Scripts and raw data behind the article "Parallel xcodebuild for agents: DerivedData and Simulators" (https://macswarm.com/blog/parallel-xcodebuild-coding-agents/). Everything runs on a Mac you own and stays inside the directory you put these files in. Nothing is downloaded, no account is needed. Test environment of the published numbers (26 October 2026) ----------------------------------------------------------- Machine MacBook Pro, Apple M2 Pro, 12 cores (8 performance, 4 efficiency), 16 GB, macOS 26.3 (25D125) sysctl -n hw.ncpu hw.perflevel0.physicalcpu hw.perflevel1.physicalcpu -> 12 8 4 Xcode Xcode 26.6 (17F113), Swift 6.3.3. No iOS Simulator runtime installed. Load A working Mac with other sessions running; 21.9 GB of swap already in use at the start. Package ./gen.py package 100: five library targets plus one executable, 501 Swift files, 47,709 lines. Dependency graph: Core <- Model <- (Network, Storage) <- Features <- LabApp (Network and Storage are siblings; both depend on Core and Model.) Build cd wt/agent-N && xcodebuild -scheme XcbLab -destination 'platform=macOS,arch=arm64' \ -derivedDataPath /dd/agent-N build Debug configuration. Clean build = DerivedData folder deleted first. Prerequisites ------------- Xcode with its command-line tools selected (xcode-select), python3, git, jq (only for xcresult-demo.sh). Free disk: about 2 GB for three DerivedData folders, plus about 0.5 GB per failing test run in xcresult-demo.sh. The timing scripts run up to three clean builds at once; expect the Mac to be busy while they run. Get the files ------------- mkdir xcb-lab && cd xcb-lab for f in README.txt gen.py run.py summarize.py setup.sh collide.sh bench.sh mixed.sh spm-demo.sh \ xcresult-demo.sh cleanup.sh helpers.sh sim-pool.sh timings.csv spm-resolves.csv; do curl -fsSO "https://macswarm.com/lab/parallel-xcodebuild/$f" done chmod +x *.sh *.py Procedure --------- 1. ./setup.sh Generates package/ (identical to the measured one with the default 100 files per module), commits it and adds three worktrees: wt/agent-1, wt/agent-2, wt/agent-3. 2. ./collide.sh about 3 minutes Two clean builds into one DerivedData path (one checkout, then two worktrees), then two builds with separate paths. Expected: one build of each colliding pair exits 65 with error: unable to attach DB: error: accessing build database ".../XCBuildData/build.db": database is locked Usually the later build fails; with a 1-second gap on a small package we also saw the earlier one fail. Builds that start at the same instant can instead report "unable to initialize database (disk I/O error)". The separate-path pair exits 0 twice. 3. ./bench.sh about 15 minutes Three interleaved repetitions of 1, 2 and 3 concurrent clean builds, then a no-op build and builds after `touch` on one file. Waits for 65% idle CPU before each batch (GATE_IDLE) and repeats a batch if other processes averaged more than 350 summed %CPU (MAX_OTHER). Prints medians at the end. 4. ./mixed.sh about 7 minutes An incremental build requested 10 s (DELAY) into one or two clean builds. 5. ./spm-demo.sh about 1 minute Concurrent resolves into one shared -clonedSourcePackagesDirPath, empty and seeded, a version flip in a shared checkout, and strict resolution. Expected: cold rounds mostly exit 74, seeded rounds exit 0, strict resolution exits 74. Failed resolves leave ResultBundle_*.xcresult folders in $TMPDIR (xcodebuild's own). 6. ./xcresult-demo.sh A macOS test run with one deliberate failure (exit 65), read back with xcresulttool and jq. 7. ./cleanup.sh Removes package/ wt/ dd/ spm/ results/ logs/ in this directory and nothing else. Exit statuses seen with Xcode 26.6: 0 success; 65 build or test failure (including the DerivedData collision); 74 package resolution failure; 64 usage error, e.g. an existing -resultBundlePath; 70 when no destination matches (for example an iOS destination without the iOS platform installed). run.py itself exits 3 when it rejects a batch for background load. run.py SPEC syntax: WORKTREE:DERIVED_DATA[@START_S][!keep], for example python3 run.py my-test 1 wt/agent-1:dd/agent-1 'wt/agent-2:dd/agent-2@10!keep' Data files ---------- timings.csv one row per build: experiment, rep, start time, builds in the batch, build index, worktree, incremental, start offset s, wall s, exit, batch wall s, peak swift-frontend processes, peak summed RSS of build processes (MiB), mean summed %CPU of build processes and of all other processes, idle % before the batch, status. status "excluded" marks the first matrix attempt, aborted when a video render from another session used about 7.5 cores. One earlier collision attempt (first build 119.6 s under the same kind of load) was run before the runner existed and is not in the file. spm-resolves.csv one row per resolve: run (lab = the published run; script-check = a later run of spm-demo.sh), phase, round, agent, exit, first error line. How to read the resource columns - The published rows were sampled by an earlier run.py that attributed processes by name (every swift-frontend, swift-driver, clang, ld, xcodebuild and build-service process on the Mac). The run.py here attributes by process tree (the launched xcodebuild processes and their descendants) and also records vm_stat pageouts and swapouts per batch. Wall times do not depend on this. - %CPU comes from ps, a decaying average; it is not an exact count of busy cores. - RSS is sampled once a second and summed; it is not total memory demand. Limits ------ One machine, one synthetic package, three batches per configuration, always run in the order 1, 2, 3 builds, on a laptop with background load. The incremental cases change a timestamp, not source code. Treat the results as failure cases to check and as one data point on scaling, not as the best concurrency limit for your project. The Simulator helpers (helpers.sh with_simulator, sim-pool.sh) are documented from `xcrun simctl help` and `man xcodebuild`; they never ran against a real Simulator. Their locking and exit-status handling was tested with stand-in xcrun and xcodebuild functions only.