Repository navigation
perf: slim booted simulators offline and run launchctl transitions in parallel - #51
Conversation
… 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.
|
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. |
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
runBatchspawned/bin/sh -c batchScriptinside the simulator to walk a chunk of labels. No iOS runtime ships a shell —RuntimeRoot/bincontains exactlydfandlaunchctl, from iOS 18.3 through 27.1:So
parseBatchOKalways returned empty, every label landed inremaining, 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.
ensurespends them up front and hands off toensureOffline, which replaces everylaunchctlspawn 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, anderrOfflineIneffectivestill falls back to thelaunchctlpath.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.BatchProgresscarries 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.on --no-reboot(thelaunchctlfallback, 340 spawns)onstarting from Bootedoffstarting from Bootedonwith the profile already applied, BootedThe 2m14.3s baseline reproduces the report's ~144s almost exactly.
Verification
make checkgreen;go test -race ./...green.TestApplyDeltaRunsTransitionsConcurrentlyButBounded(peak concurrency is >1 and never exceedsspawnWorkers; fails with the pool set to 1),TestEnsureSlimsABootedDeviceThroughTheOfflineStore(asserts the exactxcruncall list — nolaunchctltransition at all),TestEnsureLeavesAnAlreadySlimBootedDeviceAlone(one read, no shutdown, no reboot). The fakesimctlnow tracks device state so a shutdown/boot the code issues is visible to the nextlist.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
doctorCI preflight,top, and a dependency-free importable package, which an Xcode-only target would drop.