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:
ProcessPeak Typical CPU What it does 
STExtractionService90-150%Media/streaming extraction indexing
assetsd30-65%Asset catalog indexing
Widget extensions (10+) 5-30% eachCalendarWidget, FitnessWidget, NewsTag, etc.
searchd15-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.1

workflows:
  ios-e2e:
    jobs:
      - warm-simulator
      - run-tests:
          requires:
            - warm-simulator

jobs:
  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:

ProcessUserSteady-state CPU
diagnosticd (simulator)distiller40-50%
diagnosticd (host)root20-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:

 
Was this article helpful?
1 out of 1 found this helpful

Comments

0 comments

Please sign in to leave a comment.