Skip to content

perf: slim booted simulators offline and run launchctl transitions in parallel - #51

Merged
Interlap01 merged 2 commits into
mainfrom
perf/offline-booted-and-parallel-launchctl
Sep 24, 2026
Merged

Interlap01 merged 2 commits into
mainfrom
perf/offline-booted-and-parallel-launchctl

Conversation

@Interlap01

@Interlap01 Interlap01 commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Implements the three backend improvements reported in #50 by @Anywhere-Music-Player. Thanks for the detailed writeup and measurements — they held up, and one of them turned out to be broader than reported.

1. The shell batching never ran, on any runtime

runBatch spawned /bin/sh -c batchScript inside the simulator to walk a chunk of labels. No iOS runtime ships a shell — RuntimeRoot/bin contains exactly df and launchctl, from iOS 18.3 through 27.1:

$ xcrun simctl spawn <udid> /bin/sh -c 'echo ok'
domain=com.apple.CoreSimulator.LaunchdSimError, code=111
Invalid or missing Program/ProgramArguments

So parseBatchOK always returned empty, every label landed in remaining, and all 170 went through the serial per-label retry pass — which is the "retrying N failed services (pass 2/3)…" line in the report's log. The batching added in #25 has never batched anything.

The fix is to delete the script and its helpers and run the direct spawns through a pool of spawnWorkers (8). Less code and the actual speedup.

2. A booted device now takes the offline store too

An override only takes effect at the next boot, so a booted device already owed a shutdown and a boot. ensure spends them up front and hands off to ensureOffline, which replaces every launchctl spawn for the same two state changes. It also reads the live state first and returns early when the profile already matches, so re-applying an unchanged profile costs one read and no reboot.

The offline store stays best-effort: the booted device is still read back with print-disabled, and errOfflineIneffective still falls back to the launchctl path.

3. The GUI no longer freezes on a global busy flag

Batches reserve every device up front and run two at a time (heavyOperationLimit). Controls gate on the devices they would act on — isBusy(device), isSelectionBusy — instead of "anything is running", so work on one simulator leaves the others live. Profile-editing and whole-table controls keep the global flag, since they feed in-flight operations. BatchProgress carries the names in flight rather than a single current name.

Measurements

Apple M1 Pro, macOS 27.0 / Xcode 27.0. Freshly erased iPhone 17 Pro, iOS 26.5, 170 labels, same binary except spawnWorkers.

Operation before after
on --no-reboot (the launchctl fallback, 340 spawns) 2m14.3s 37.5s
on starting from Booted ~2m14s + reboot 16.8s
off starting from Booted ~2m14s + reboot 15.0s
on with the profile already applied, Booted boot + read 0.7s, no reboot

The 2m14.3s baseline reproduces the report's ~144s almost exactly.

Verification

  • make check green; go test -race ./... green.
  • New tests: TestApplyDeltaRunsTransitionsConcurrentlyButBounded (peak concurrency is >1 and never exceeds spawnWorkers; fails with the pool set to 1), TestEnsureSlimsABootedDeviceThroughTheOfflineStore (asserts the exact xcrun call list — no launchctl transition at all), TestEnsureLeavesAnAlreadySlimBootedDeviceAlone (one read, no shutdown, no reboot). The fake simctl now tracks device state so a shutdown/boot the code issues is visible to the next list.
  • Live: slimmed and restored a real simulator through every path above, and drove a two-device batch through the built app — both simulators reconfigure at once, the banner reads "Applying iPhone Duo, iPhone Duo…", and the toolbar dims only for the busy selection. Both ended Fully slim (170/170), then restored to stock.

Closes #50 on the backend items. The Swift app itself is not adopted: the Go library is also the CLI, the doctor CI preflight, top, and a dependency-free importable package, which an Xcode-only target would drop.

… parallel

Three changes from the SwiftSimSlim report in #50, all in the Go backend and
the SwiftUI front end.

**The shell batching never ran.** `runBatch` spawned `/bin/sh -c` inside the
simulator to walk a chunk of labels, but no iOS runtime ships a shell —
`RuntimeRoot/bin` holds exactly `df` and `launchctl`, from iOS 18.3 through
27.1. Every spawn failed with LaunchdSimError 111, `parseBatchOK` returned
nothing, and all 170 labels fell through to the serial per-label retry pass.
Delete the script and its helpers; run the direct spawns through a pool of 8
instead. That is both less code and the actual speedup: `on --no-reboot` over
170 labels on a freshly erased iPhone 17 Pro / iOS 26.5 goes 2m14s -> 37.5s.

**A booted device now takes the offline store too.** An override only takes
effect at the next boot, so a booted device already owed a shutdown and a
boot. `ensure` spends them up front and hands off to `ensureOffline`, which
replaces every launchctl spawn for the same two state changes. Measured 16.8s
to slim a booted simulator, 15.0s to restore one. It also reads the live state
first and returns early when the profile already matches, so re-applying an
unchanged profile costs one read and no reboot at all (0.7s).

**The GUI no longer freezes on a global busy flag.** Batches reserve every
device up front and run two at a time, and controls gate on the devices they
would act on rather than on "anything is running", so work on one simulator
leaves the others live. `BatchProgress` carries the names in flight instead of
a single current name.
Four defects found reviewing the previous commit.

**Batch completion could erase a newer reservation.** `slim` and `restore`
cleared their own reservation in a `defer`, and `runConcurrently` cleared it
again when the result arrived. Those run in different MainActor tasks, so
another operation could claim the device in the gap and have its reservation
wiped by the batch's stale clear — leaving the device shown as free while work
ran on it. The runner is now the sole owner.

**Overlapping operations could lose their final refresh.** `refresh()` dropped
any request made while one was in flight. That was safe when every mutation was
globally exclusive; now that two devices finish independently, the second one's
refresh could be dropped after the first had already read the device list, and
its row would stay stale indefinitely. Requests made during a refresh are
coalesced into a follow-up run, mirroring `diskReloadRequested`.

**Batch controls were enabled but silently did nothing.** Relaxing the view
gating left `runBatch`, `cleanDisk`, `analyzeDisk` and `runManagementBatch`
guarding on `batchProgress == nil`, so Slim/Unslim/Erase/Delete/Clean on an
idle device during a batch returned without doing anything. They now gate on
`canStartBatchOnSelection` / `isBatchRunning`; per-device actions (boot,
shutdown, clone, rename, measure) stay live as intended.

**`ensure` read a booted device without waiting for boot.** `simctl list`
reports Booted from the moment a boot starts, so `readDisabled` on a device
that is still coming up would fail the whole command instead of waiting. The
booted branch calls `BootAndWait` first; on a device that really is booted
that costs ~0.2s.

Also assert full transition coverage in the applyDelta concurrency test — it
pinned the bound but would have passed an implementation that ran only the
first `spawnWorkers` labels.
@Anywhere-Music-Player

Copy link
Copy Markdown

Thanks for picking up the backend improvements and validating them!

Regarding the Swift app not being adopted because of the CLI and library use cases: Swift can support those too. The simulator backend could be shared by the SwiftUI app and a separate command-line executable, with a reusable Swift module/package for other Swift callers. The current Xcode-only layout is how I packaged this app, not a limitation of Swift. A Swift package would not be a drop-in replacement for existing Go importers, of course.

I do not personally need the CLI, doctor, top, or watch workflows, so I have not implemented that interface or feature parity, and I am not offering to maintain a full replacement. If a native Swift direction interests you, you are welcome to reuse the app/backend and add the command-line features you need under the MIT license. That would take additional implementation and testing, but the CLI requirement itself would not prevent a Swift version.

Either way, I am glad the improvements are useful upstream. Thanks also for catching the Booted-versus-ready case; I have recorded that for our Swift backend.

@Interlap01
Interlap01 merged commit e752a72 into main Sep 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SwiftSimSlim: native Swift/SwiftUI app, comparison, and offer to adopt the code

2 participants