mirror of
https://github.com/MobAI-App/simslim.git
synced 2026-10-19 10:18:32 +02:00
358 lines
15 KiB
Markdown
358 lines
15 KiB
Markdown
# simslim
|
|
|
|
Run a lot more iOS simulators on one Mac by turning off the background daemons a simulator doesn't need.
|
|
|
|
A freshly booted iOS simulator starts around 180 background services: Siri, Spotlight indexing, photo analysis, News, wallpaper posters, iCloud sync, and so on. None of it matters when you're using the simulator for development, testing, or CI. simslim switches those services off, which cuts each simulator's memory roughly 4x. On the same laptop you go from a handful of simulators to a screenful.
|
|
|
|
https://github.com/user-attachments/assets/f4665e41-43b4-49cd-9388-3da533e9fd7b
|
|
|
|
*12 slim simulators on a 16 GB M1 Pro, each driven by its own Claude Code agent through MobAI. Stock simulators start thrashing at around 5.*
|
|
|
|
## Numbers
|
|
|
|
One simulator, booted stock and then slimmed, same device and settle time (M1 Pro, 16 GB):
|
|
|
|
| | Stock | Slim |
|
|
|---|---|---|
|
|
| Processes | 258 | 70 |
|
|
| Memory | 4.0 GB | 0.9 GB |
|
|
|
|
Memory here is phys_footprint, the figure Activity Monitor shows, which counts compressed and swapped pages. That's what decides how many simulators fit before the machine starts swapping. Run `simslim measure <udid>` to see it for any booted simulator.
|
|
|
|
`status` and `measure` answer different questions. `status` counts only the
|
|
allowlisted launchd labels SimSlim manages; it is not a live-process count. A
|
|
slim boot still runs required core services plus system apps and extensions.
|
|
`measure` walks every process under that simulator's `launchd_sim` and sums
|
|
`phys_footprint`. A sum of `ps` RSS values is not comparable because RSS counts
|
|
shared mappings once for every process that maps them.
|
|
|
|
## Install
|
|
|
|
```sh
|
|
brew install mobai-app/tap/simslim
|
|
```
|
|
|
|
or
|
|
|
|
```sh
|
|
go install github.com/mobai-app/simslim/cmd/simslim@latest
|
|
```
|
|
|
|
macOS only, and you need Xcode with an iOS Simulator runtime, since simslim
|
|
drives simulators through `xcrun simctl`.
|
|
|
|
## macOS app
|
|
|
|
The SwiftUI app bundles the CLI and adds:
|
|
|
|
- Searchable simulator status, disk-size, and live RAM columns.
|
|
- Searchable service profiles with per-daemon controls and purpose summaries.
|
|
- Read-only disk analysis plus confirmed cleanup of allowlisted data.
|
|
- Clone, rename, erase, delete, and Finder shortcuts.
|
|
|
|
Build it locally with Go and Xcode:
|
|
|
|
```sh
|
|
make app
|
|
open build/SimSlim.app
|
|
```
|
|
|
|
Memory estimates are guidance rather than additive savings; see the
|
|
[measurement method](docs/category-memory.md). SimSlim recommends cloning before
|
|
service or disk changes so the copy can serve as a backup.
|
|
|
|
## Usage
|
|
|
|
```sh
|
|
simslim list # simulators and their slim status (--booted to filter)
|
|
simslim profiles # what a slim boot turns off
|
|
simslim profiles <id> # the launchd labels in one category
|
|
simslim on <udid> # slim a simulator and reboot it slim
|
|
simslim off <udid> # put it back to stock
|
|
simslim status <udid> # managed launchd-label state (not a process count)
|
|
simslim verify <udid> --profile ci.json # exact profile match; non-zero on drift
|
|
simslim doctor <udid> --requires push,storekit,universal-links
|
|
simslim measure <udid> # a booted simulator's memory footprint
|
|
simslim top # live fleet monitor; enter a sim for per-daemon RAM/CPU
|
|
simslim size <udid> # total allocated simulator size
|
|
simslim disk-plan <udid> # measure reclaimable data; read-only
|
|
simslim disk-clean --categories caches,logs --confirm <udid>
|
|
simslim clone <udid> <name>
|
|
simslim repair-clone <source-udid> <clone-udid>
|
|
simslim rename <udid> <name>
|
|
simslim boot <udid> # boot a simulator and wait for its services
|
|
simslim shutdown <udid> # shut down a booted simulator
|
|
simslim erase <udid> # erase apps, data, settings, and slimming overrides
|
|
simslim delete <udid> # permanently delete a simulator
|
|
```
|
|
|
|
Read-only and simulator-management commands accept `--json` for integrations
|
|
and the macOS app.
|
|
|
|
Keep a category you actually need, like Spotlight search:
|
|
|
|
```sh
|
|
simslim on <udid> --except search
|
|
```
|
|
|
|
Or keep one specific daemon, like push notifications:
|
|
|
|
```sh
|
|
simslim on <udid> --keep com.apple.apsd
|
|
```
|
|
|
|
<details>
|
|
<summary>All slimming categories</summary>
|
|
|
|
| ID | Category | Turns off | ~MB |
|
|
|---|---|---|---|
|
|
| `widgets` | Widgets & Wallpaper | Home and lock screen posters, widgets, and Live Activities. | 675 |
|
|
| `siri` | Siri & Intelligence | Siri, Apple Intelligence, speech, and on-device ML model services. | 265 |
|
|
| `search` | Spotlight & Search | On-device Spotlight and in-Settings search services. | 50 |
|
|
| `icloud` | iCloud & Apple Account | iCloud sync, Apple Account, keychain, and backup services. | 100 |
|
|
| `store` | App Store, Push & Media | App Store, push notification, StoreKit, and media services. | 80 |
|
|
| `pim` | Mail, Calendar & Contacts | Mail, Calendar, Contacts, Reminders, and related sync services. | 80 |
|
|
| `web` | Safari Sync & Web Services | Safari sync, web push, privacy, and universal-link services. | 50 |
|
|
| `family` | Family & Screen Time | Family Sharing, Screen Time, and usage tracking. | 65 |
|
|
| `health` | Health, Home & Fitness | HealthKit, HomeKit, and Fitness services. | 135 |
|
|
| `photos` | Photos & Media Analysis | Photos library, photo analysis, and media analysis services. | 60 |
|
|
| `apps` | News, Weather, Maps & Games | News, Weather, Maps, Tips, and game services. | 90 |
|
|
| `messaging` | Messaging & FaceTime | iMessage, FaceTime, call, and identity services. | 60 |
|
|
| `connectivity` | Sharing & Device Connectivity | AirDrop, Continuity, CarPlay, Watch, and Find My services. | 65 |
|
|
| `telemetry` | Ads, Diagnostics & Telemetry | DeviceCheck, ad privacy, analytics, diagnostics, and feedback services. | 105 |
|
|
| `other` | Other Background Services | Wallet, business services, assets, and miscellaneous background daemons. | 195 |
|
|
|
|
Memory figures are iOS 26.5 clean-boot medians and are not additive; see the
|
|
[measurement method](docs/category-memory.md). Run `simslim profiles <id>` to
|
|
see the daemons in a category.
|
|
|
|
</details>
|
|
|
|
### Slow CI runners
|
|
|
|
`simslim on` boots the simulator, disables ~170 daemons one `launchctl` call at a
|
|
time, then reboots — all under a single 10-minute deadline. Shared CI runners (like
|
|
GitHub-hosted macOS runners) are slower and less predictable, and can blow that
|
|
deadline mid-reconfigure with `context deadline exceeded` errors. Raise it with the
|
|
global `--boot-timeout` flag or the `SIMSLIM_BOOT_TIMEOUT` environment variable:
|
|
|
|
```sh
|
|
simslim on <udid> --boot-timeout 15m
|
|
# or, for the whole job:
|
|
export SIMSLIM_BOOT_TIMEOUT=15m
|
|
```
|
|
|
|
Each individual `launchctl` transition is also bounded by its own 2-minute
|
|
timeout, and the first transitions after a cold boot can exceed that on slow
|
|
hosts (failed ones are retried automatically). Raise it with the global
|
|
`--spawn-timeout` flag or the `SIMSLIM_SPAWN_TIMEOUT` environment variable:
|
|
|
|
```sh
|
|
simslim on <udid> --spawn-timeout 5m
|
|
# or, for the whole job:
|
|
export SIMSLIM_SPAWN_TIMEOUT=5m
|
|
```
|
|
|
|
### Profile files
|
|
|
|
For a repeatable setup, commit a JSON profile alongside your project and apply it
|
|
per run. A `ci.json` and a `dev.json` can slim differently for each purpose:
|
|
|
|
```json
|
|
{
|
|
"name": "ci",
|
|
"description": "UI test runs",
|
|
"except": ["search", "store"],
|
|
"keep": ["com.apple.apsd"]
|
|
}
|
|
```
|
|
|
|
```sh
|
|
simslim on <udid> --profile ci.json
|
|
```
|
|
|
|
`except` and `keep` mirror the flags of the same name; `name` and `description`
|
|
are for whoever reads the file. Unknown fields, unknown category IDs, and labels
|
|
that no category disables are rejected, so a typo fails loudly. `--profile` is the
|
|
single source of truth for its run and cannot be combined with `--except` or
|
|
`--keep`.
|
|
|
|
To build one interactively, run `simslim profile ci.json`: name it, then use the
|
|
arrow keys and space to tick whole features to keep enabled, or press `→` to open
|
|
a feature and keep individual daemons within it. Point it at a directory to save
|
|
`<name>.json` there, or omit the path to print to stdout.
|
|
|
|
### Checking a simulator with doctor
|
|
|
|
Slimming a simulator turns features off on purpose, so a test suite that needs
|
|
one of them wants a fast way to catch a mis-slimmed simulator before it runs.
|
|
`doctor` checks a booted simulator against the features you name and exits
|
|
non-zero if any of them are broken, which makes it a natural CI preflight:
|
|
|
|
```sh
|
|
simslim doctor <udid> --requires push,storekit,universal-links
|
|
```
|
|
|
|
```
|
|
<udid>: 2/3 required features OK
|
|
ok push
|
|
ok universal-links
|
|
BROKEN storekit — com.apple.storekitd disabled
|
|
```
|
|
|
|
Feature IDs are finer-grained than the slimming categories: each maps to just
|
|
the daemons that back one capability. Run `simslim doctor --list` to see them
|
|
all. Both the check and the list support `--json`.
|
|
|
|
### Verifying a profile still holds
|
|
|
|
The disable overrides are per-simulator state that is easy to lose without
|
|
noticing: a deleted-and-recreated device or a simulator from a new runtime
|
|
comes up stock, and nothing else fails loudly when that happens — the simulator
|
|
just quietly runs heavy again. `verify` compares a booted
|
|
simulator's overrides against a profile — the same `--profile`/`--except`/`--keep`
|
|
you passed to `on` — and exits non-zero listing the drift, so a script or CI
|
|
step can catch it and re-run `simslim on` (idempotent: it only applies the
|
|
missing delta) to repair:
|
|
|
|
```sh
|
|
simslim verify <udid> --profile ci.json || simslim on <udid> --profile ci.json
|
|
```
|
|
|
|
Where `doctor` answers "do the features my tests need still work?", `verify`
|
|
answers "is this simulator in exactly the slim state I configured?". Supports
|
|
`--json`.
|
|
|
|
## Disk cleanup
|
|
|
|
Disk cleanup is permanent and separate from service slimming. `disk-plan` is
|
|
read-only. `disk-clean` shuts down the exact simulator, clears only allowlisted
|
|
per-device directories, and refuses to run without `--confirm`.
|
|
|
|
```sh
|
|
simslim disk-categories
|
|
simslim disk-plan <udid>
|
|
simslim disk-clean --categories caches,logs,temporary --confirm <udid>
|
|
# Optional: also remove on-demand language models
|
|
simslim disk-clean --categories linguistic-data --confirm <udid>
|
|
```
|
|
|
|
Built-in apps and core OS language resources are part of a signed iOS runtime
|
|
shared by every simulator using that version, so simslim never modifies them.
|
|
Required Siri assets are measured only because iOS restores them on launch;
|
|
on-demand language data is opt-in and may download again when needed.
|
|
|
|
`disk-plan` also reports a read-only storage breakdown for installed app bundles,
|
|
Documents, app data, and user media. Those durable rows are never eligible for
|
|
cleanup. See the [disk cleanup safety model](docs/disk-cleanup.md) for recovery
|
|
behavior, safeguards, and Xcode 26.6 validation results.
|
|
|
|
## Use as a Go library
|
|
|
|
The repo root is an importable package with no external dependencies, so you can
|
|
drive simulators from your own tooling instead of shelling out to the CLI:
|
|
|
|
```sh
|
|
go get github.com/mobai-app/simslim
|
|
```
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/mobai-app/simslim"
|
|
)
|
|
|
|
func main() {
|
|
ctx := context.Background()
|
|
|
|
devices, err := simslim.ListDevices(ctx)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Slim every booted simulator, keeping Siri and search enabled.
|
|
profile, err := simslim.BuildProfile("", "siri,search", "")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
for _, d := range devices {
|
|
if d.State != "Booted" {
|
|
continue
|
|
}
|
|
changed, err := simslim.EnableSlim(ctx, d.Set, d.UDID, profile, func(msg string) {
|
|
fmt.Println(d.UDID, msg)
|
|
})
|
|
fmt.Println(d.UDID, "changed:", changed, "err:", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
The package never writes to stdout — it returns values and reports progress
|
|
through the `simslim.Reporter` callback you pass in. `simslim.Categories`,
|
|
`simslim.Features`, and `simslim.SlimmableSet()` expose the same allowlist the
|
|
CLI uses. macOS only, since everything runs through `xcrun simctl`.
|
|
|
|
## How it works
|
|
|
|
`simslim on` writes persistent `launchctl disable` entries for the chosen launchd labels into the simulator's own launchd database, then reboots it. The entries stick across reboots, so the simulator comes up slim in a single boot from then on. `simslim off` clears them and reboots back to stock. Your Mac is never touched, only the simulator you point it at, and only services that are safe to disable. Core workflow services such as `sharingd`, plus the handful that wedge a simulator when turned off, are left running.
|
|
|
|
The earliest runtime with verified persistence is iOS 18.5. iOS 17.x and 18.3
|
|
accept each `launchctl disable`, but the simulator comes back stock after a
|
|
reboot. `simslim on` rejects runtimes older than 18.5 before booting or changing
|
|
launchd state. Supported runtimes are still read back after the reboot, and the
|
|
command fails instead of claiming success if any requested override was lost.
|
|
|
|
This is per-simulator state, not a global setting. The daemon disables live in
|
|
that one simulator's launchd database and nowhere else. `simslim clone` copies
|
|
the source's exact SimSlim-managed profile along with its installed apps, app
|
|
data, and settings. It also rebases simulator-local symlinks, regenerates app
|
|
registration databases, and clears copied system caches and temporary files.
|
|
Before finishing, it boots the clone and audits its running processes for open
|
|
paths into the source. The clone finishes shutdown, and the source returns to
|
|
the boot state it had before cloning.
|
|
|
|
Clones created by an older SimSlim build can be repaired in place without
|
|
deleting their installed apps or app data:
|
|
|
|
```sh
|
|
simslim repair-clone <source-udid> <clone-udid>
|
|
```
|
|
|
|
The repair preserves the clone's current SimSlim profile and boot state. The
|
|
source is kept shutdown while copied paths are rebuilt; if direct links or open
|
|
files into that source remain, the clone stays shutdown instead of being
|
|
restarted. CoreSimulator does not expose clone lineage, so pass the exact source
|
|
UDID: SimSlim verifies references to the source you specify.
|
|
|
|
`erase`, `delete` and recreate, and "Erase All Content and Settings" still
|
|
produce stock service state, so the simulator's memory climbs back until you
|
|
rerun `simslim on`. A simulator created from a new or updated runtime also
|
|
starts stock. A tar or filesystem snapshot also preserves the profile. Run
|
|
`simslim list` to see current state, or `simslim verify` to check one against a
|
|
specific profile.
|
|
|
|
## What you lose
|
|
|
|
Turning services off is fine for most development, UI automation, and CI, but some features genuinely stop working. The ones worth knowing:
|
|
|
|
- Spotlight and in-Settings search return nothing (`search`).
|
|
- Push notifications need `apsd`, StoreKit testing needs `storekitd` (`store`).
|
|
- Universal links need `swcd` (`web`).
|
|
- The Contacts, Photos, and Calendar pickers can act up without their categories.
|
|
|
|
`simslim profiles` lists every category, so you can keep a category with `--except` or individual daemons with `--keep`.
|
|
|
|
## Why
|
|
|
|
Testing is shifting. Once agents are writing apps, you want agents running them too, and the place an iOS app runs is a simulator. One agent, one simulator. So how much work you get through at once comes down to how many simulators a machine can hold, and stock simulators are heavy enough that a laptop fills up fast. Slimming them is the cheapest way to raise that ceiling: more simulators on the box means more agents working in parallel on it.
|
|
|
|
Built for [MobAI](https://mobai.run) to run more simulators on one machine.
|
|
|
|
## License
|
|
|
|
MIT, copyright Interlap.
|