Skip to main content

Reducing iOS Simulator CPU Overhead on macOS Executors

Reducing iOS Simulator CPU Overhead on macOS Executors

When running iOS E2E tests on CircleCI's macOS executors, freshly created simulators trigger a first-boot indexing storm that can push CPU load averages above 80 and delay test execution by several minutes. This article explains two techniques to dramatically reduce simulator CPU overhead: caching a warm simulator snapshot, and disabling the diagnosticd logging daemon.

Part 1: Cache and Restore a Warm Simulator Snapshot

The Problem

Every time you create and boot a fresh iOS simulator in CI, the simulator runs first-boot initialization: Spotlight indexing, widget extension setup, media extraction, launch services database construction, and more. This spawns 100+ background daemons, many of which are completely unnecessary for testing.


The worst offenders on a cold boot:

Process

Peak Typical CPU

What it does

STExtractionService

90-150%

Media/streaming extraction indexing

assetsd

30-65%

Asset catalog indexing

Widget extensions (10+)

5-30% each

CalendarWidget, FitnessWidget, NewsTag, etc.

searchd

15-18%

Spotlight search indexing


Combined, these can consume over 300% CPU (3 vcpu worth) for the first 1-2 minutes after boot. On a shared CI executor, this starves your actual tests of resources and causes timeouts and flaky failures.

The Solution

Boot a simulator once, let it fully settle, snapshot the device data directory, and cache it. On subsequent runs, create a fresh simulator and overlay the warm data before booting. The simulator starts with all indexing already complete.

version: 2.1workflows:
  ios-e2e:
    jobs:
      - warm-simulator
      - run-tests:
          requires:
            - warm-simulatorjobs:
  warm-simulator:
    macos:
      xcode: "16.4.0"
    resource_class: m4pro.medium
    steps:
      - restore_cache:
          keys:
            - ios-sim-v1-
      - run:
          name: Create warm snapshot
          command: |
            [ -f /tmp/warm-sim-snapshot.tar.gz ] && exit 0
            RUNTIME=$(xcrun simctl list runtimes -j | python3 -c "import json,sys;r=[x for x in json.load(sys.stdin)['runtimes'] if x['platform']=='iOS' and x['isAvailable']];print(r[-1]['identifier'])")
            UDID=$(xcrun simctl create Warm "iPhone 15" "$RUNTIME")
            xcrun simctl boot "$UDID"
            sleep 60
            xcrun simctl shutdown "$UDID" && sleep 5
            cd ~/Library/Developer/CoreSimulator/Devices
            tar czf /tmp/warm-sim-snapshot.tar.gz "$UDID"
            echo "$UDID" > /tmp/warm-sim-udid.txt
      - save_cache:
          key: ios-sim-v1-{{ arch }}
          paths:
            - /tmp/warm-sim-snapshot.tar.gz
            - /tmp/warm-sim-udid.txt  run-tests:
    macos:
      xcode: "16.4.0"
    resource_class: m4pro.medium
    steps:
      - checkout
      - restore_cache:
          keys:
            - ios-sim-v1-
      - run:
          name: Boot from snapshot and run tests
          command: |
            RUNTIME=$(xcrun simctl list runtimes -j | python3 -c "import json,sys;r=[x for x in json.load(sys.stdin)['runtimes'] if x['platform']=='iOS' and x['isAvailable']];print(r[-1]['identifier'])")
            UDID=$(xcrun simctl create E2E "iPhone 15" "$RUNTIME")
            if [ -f /tmp/warm-sim-snapshot.tar.gz ]; then
              TEMPLATE=$(cat /tmp/warm-sim-udid.txt)
              tar xzf /tmp/warm-sim-snapshot.tar.gz -C /tmp
              rsync -a /tmp/$TEMPLATE/data/ ~/Library/Developer/CoreSimulator/Devices/$UDID/data/
            fi
            xcrun simctl boot E2E
            # your tests here

Part 2: Disable diagnosticd to Eliminate Persistent CPU Drain

The Problem

Even after caching eliminates the first-boot indexing storm, two diagnosticd processes continue consuming significant CPU for the entire lifetime of the simulator:

Process

User

Steady-state CPU

diagnosticd (simulator)

distiller

40-50%

diagnosticd (host)

root

20-25%

diagnosticd is Apple's unified logging daemon. It collects os_log messages, signposts, and activity traces from every running process.

The Solution

Unload the host-side diagnosticd via launchctl and kill it inside the simulator. Add these lines after booting:

# Unload host diagnosticd (prevents launchd from respawning it)
sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.diagnosticd.plist# Kill diagnosticd inside the simulator
xcrun simctl spawn "$UDID" killall diagnosticd

Simply killing diagnosticd is not enough — launchd will immediately respawn it. The launchctl unload -w command tells launchd to stop managing the service entirely.


Below is the before and after of the same test run:

Before optimizations:


After optimizations:

Did this answer your question?