Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,25 @@ Live slimming only moves toward more-disabled. Managed daemons already disabled
beyond the profile are left alone, because starting a daemon again live would
need a per-label bootstrap; `simslim off` restores them with a reboot.

### Slimming parallel-testing clones

`xcodebuild` parallel testing creates a fresh clone per worker in its own device
set, runs the tests, and deletes the clones when the run ends. The clones are
created stock, and disable overrides do not survive `simctl clone`, so there is
no device to slim ahead of time. `simslim watch` closes that gap: it polls the
device sets and slims each simulator no-reboot the moment it boots, then leaves
it alone. A clone slimmed a few seconds into a multi-minute run keeps its freed
memory for the rest of the run.

```sh
simslim watch --except web,store &
xcodebuild test -scheme App -parallel-testing-enabled YES -parallel-testing-worker-count 4 ...
```

It takes the same `--profile`/`--except`/`--keep` selection as `on`, and scans
the default and `testing` sets plus any passed with `--set`. Point `--set` at a
custom parallel-testing device set if your Xcode uses one. It runs until Ctrl-C.

## Disk cleanup

Disk cleanup is permanent and separate from service slimming. `disk-plan` is
Expand Down
6 changes: 6 additions & 0 deletions cmd/simslim/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ func newApp() *cli.Command {
{Name: "off", Flags: []cli.Flag{
preserveBootStateFlag("return an initially shutdown simulator to shutdown after reconfiguration"),
}, Action: cmdOff},
{Name: "watch", Flags: []cli.Flag{
&cli.StringFlag{Name: "profile", Usage: "apply a JSON profile file (mutually exclusive with --except/--keep)"},
&cli.StringFlag{Name: "except", Usage: "comma-separated category IDs to leave fully enabled (see `simslim profiles`)"},
&cli.StringFlag{Name: "keep", Usage: "comma-separated launchd labels to keep running"},
&cli.DurationFlag{Name: "interval", Usage: "how often to rescan for booting simulators", Value: simslim.DefaultWatchInterval},
}, Action: cmdWatch},
}

onUsageError := func(_ context.Context, _ *cli.Command, err error, _ bool) error {
Expand Down
24 changes: 24 additions & 0 deletions cmd/simslim/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,21 @@ func onNoReboot(ctx context.Context, device simslim.Device, p simslim.Profile, r
return nil
}

// cmdWatch slims every simulator no-reboot as it boots, until Ctrl-C. It is the
// hook for xcodebuild parallel-testing clones, which are created stock and
// deleted per run: point --set at the parallel-testing device set and run this
// alongside the test invocation.
func cmdWatch(ctx context.Context, cmd *cli.Command) error {
p, err := simslim.BuildProfile(cmd.String("profile"), cmd.String("except"), cmd.String("keep"))
if err != nil {
return err
}
report := simslim.Reporter(func(msg string) { fmt.Fprintln(os.Stderr, msg) })
sets := append([]string{"default", "testing"}, simslim.ExtraDeviceSetTokens()...)
fmt.Fprintf(os.Stderr, "Watching device sets [%s]; slimming each simulator no-reboot as it boots. Ctrl-C to stop.\n", strings.Join(sets, ", "))
return simslim.Watch(ctx, p, cmd.Duration("interval"), report)
}

func cmdOff(ctx context.Context, cmd *cli.Command) error {
preserveBootState := cmd.Bool("preserve-boot-state")
udid, err := oneUDID(cmd.Args().Slice())
Expand Down Expand Up @@ -955,6 +970,15 @@ COMMANDS
off <udid> Restore a simulator to stock (re-enable + reboot)
--preserve-boot-state
Return an initially shutdown simulator to shutdown
watch Slim every simulator no-reboot as it boots, until Ctrl-C.
The hook for xcodebuild parallel-testing clones, which are
created stock and deleted per run; point --set at the
parallel-testing device set
--profile path Apply a committed JSON profile (see below); mutually
exclusive with --except/--keep
--except ids Leave these categories enabled (comma-separated)
--keep labels Keep these individual daemons running (comma-separated)
--interval dur How often to rescan for booting simulators (default 3s)
status <udid> Report how slim a booted simulator is
--dropped Also list the disabled daemons grouped by category
verify <udid> Check that a booted simulator's disable overrides still
Expand Down
67 changes: 67 additions & 0 deletions watch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package simslim

import (
"context"
"fmt"
"sort"
"time"
)

// DefaultWatchInterval is how often Watch rescans the device sets.
const DefaultWatchInterval = 3 * time.Second

// slimStrategy is the per-device slimming applied by Watch. It is a field so
// tests can substitute a fake for EnableSlimNoReboot.
type slimStrategy func(ctx context.Context, set, udid string, p Profile, report Reporter) (bool, error)

// Watch slims each simulator no-reboot as it boots, then leaves it alone.
// xcodebuild creates parallel-testing clones stock and deletes them per run, so
// no device exists to pre-slim: the only hook is to catch each clone once it
// boots. A clone slimmed a few seconds into a multi-minute run frees its
// background daemons for the rest of the run. Watch runs until ctx is cancelled
// (Ctrl-C), scanning the default, testing, and any --set device sets. It slims
// each device in its own goroutine so one slow reconfigure does not delay
// catching the next clone.
func Watch(ctx context.Context, p Profile, interval time.Duration, report Reporter) error {
return watch(ctx, p, interval, report, EnableSlimNoReboot)
}

func watch(ctx context.Context, p Profile, interval time.Duration, report Reporter, slim slimStrategy) error {
if interval <= 0 {
interval = DefaultWatchInterval
}
seen := map[string]bool{} // UDIDs already slimmed or in flight; scan loop is single-threaded
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
scanAndSlim(ctx, p, report, slim, seen)
select {
case <-ctx.Done():
return nil
case <-ticker.C:
}
}
}

func scanAndSlim(ctx context.Context, p Profile, report Reporter, slim slimStrategy, seen map[string]bool) {
devices, err := ListDevices(ctx)
if err != nil {
report.report(fmt.Sprintf("watch: list devices: %v", err))
return
}
sort.Slice(devices, func(i, j int) bool { return devices[i].UDID < devices[j].UDID })
for _, d := range devices {
if d.State != "Booted" || seen[d.UDID] {
continue
}
seen[d.UDID] = true
go func(d Device) {
report.report(fmt.Sprintf("watch: slimming %s (set %s)", d.UDID, d.Set))
if _, err := slim(ctx, d.Set, d.UDID, p, nil); err != nil {
report.report(fmt.Sprintf("watch: %s: %v", d.UDID, err))
return
}
report.report(fmt.Sprintf("watch: slimmed %s", d.UDID))
}(d)
}
}
66 changes: 66 additions & 0 deletions watch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package simslim

import (
"context"
"os"
"path/filepath"
"sync"
"testing"
"time"
)

// TestWatchSlimsEachBootedDeviceOnce verifies the scan slims a booted device
// exactly once and never touches a shutdown one, so a steady poll does not
// re-slim clones it already handled.
func TestWatchSlimsEachBootedDeviceOnce(t *testing.T) {
dir := t.TempDir()
xcrunPath := filepath.Join(dir, "xcrun")
script := `#!/bin/sh
if [ "$*" = "simctl list devices -j" ]; then
printf '%s\n' '{"devices":{"com.apple.CoreSimulator.SimRuntime.iOS-26-1":[{"udid":"AAAA","name":"clone-1","state":"Booted","isAvailable":true,"dataPath":"/tmp/a"},{"udid":"BBBB","name":"clone-2","state":"Shutdown","isAvailable":true,"dataPath":"/tmp/b"}]}}'
exit 0
fi
exit 0
`
if err := os.WriteFile(xcrunPath, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))

var mu sync.Mutex
slimmed := map[string]int{}
fake := func(_ context.Context, _, udid string, _ Profile, _ Reporter) (bool, error) {
mu.Lock()
slimmed[udid]++
mu.Unlock()
return true, nil
}

seen := map[string]bool{}
p := Profile{}
for i := 0; i < 3; i++ { // repeated scans must not re-slim
scanAndSlim(context.Background(), p, nil, fake, seen)
}
waitFor(t, func() bool { mu.Lock(); defer mu.Unlock(); return slimmed["AAAA"] == 1 })

mu.Lock()
defer mu.Unlock()
if slimmed["AAAA"] != 1 {
t.Fatalf("booted device slimmed %d times, want 1", slimmed["AAAA"])
}
if slimmed["BBBB"] != 0 {
t.Fatalf("shutdown device slimmed %d times, want 0", slimmed["BBBB"])
}
}

func waitFor(t *testing.T, cond func() bool) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if cond() {
return
}
time.Sleep(5 * time.Millisecond)
}
t.Fatal("condition not met before deadline")
}