Skip to content
Merged
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
34 changes: 0 additions & 34 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -338,23 +338,6 @@ jobs:
with:
xcode-version: ${{ env.XCODE_VERSION }}

# Starts the simulator boot now so it overlaps the network-bound install
# steps below instead of the Metro bundle build. Harness reuses a booted
# or booting simulator; if this fails, Harness boots it itself.
- name: Boot iOS simulator
env:
SIMULATOR_NAME: iPhone 17 Pro
SIMULATOR_RUNTIME: iOS-26-4
run: |
udid="$(xcrun simctl list devices --json | jq -r --arg name "$SIMULATOR_NAME" --arg runtime "$SIMULATOR_RUNTIME" \
'.devices | to_entries[] | select(.key | endswith($runtime)) | .value[] | select(.name == $name) | .udid' | head -n 1)"
if [ -z "$udid" ]; then
echo "::warning::No $SIMULATOR_NAME simulator for $SIMULATOR_RUNTIME; Harness will report it."
exit 0
fi
echo "Booting $SIMULATOR_NAME ($udid)"
xcrun simctl boot "$udid" || echo "::warning::Could not pre-boot $udid; Harness will boot it."

- name: Install dependencies
run: |
pnpm install
Expand Down Expand Up @@ -619,23 +602,6 @@ jobs:
with:
xcode-version: ${{ env.XCODE_VERSION }}

# Starts the simulator boot now so it overlaps the network-bound install
# steps below instead of the Metro bundle build. Harness reuses a booted
# or booting simulator; if this fails, Harness boots it itself.
- name: Boot iOS simulator
env:
SIMULATOR_NAME: iPhone 17 Pro
SIMULATOR_RUNTIME: iOS-26-4
run: |
udid="$(xcrun simctl list devices --json | jq -r --arg name "$SIMULATOR_NAME" --arg runtime "$SIMULATOR_RUNTIME" \
'.devices | to_entries[] | select(.key | endswith($runtime)) | .value[] | select(.name == $name) | .udid' | head -n 1)"
if [ -z "$udid" ]; then
echo "::warning::No $SIMULATOR_NAME simulator for $SIMULATOR_RUNTIME; Harness will report it."
exit 0
fi
echo "Booting $SIMULATOR_NAME ($udid)"
xcrun simctl boot "$udid" || echo "::warning::Could not pre-boot $udid; Harness will boot it."

- name: Install dependencies
run: |
pnpm install
Expand Down
5 changes: 5 additions & 0 deletions .nx/version-plans/ios-simulator-slim-before-boot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
__default__: patch
---

On CI hosts with 3 cores or less than 8 GB of memory, Harness now keeps the iOS simulator's unused background services from starting when it boots the simulator, instead of stopping them after the boot has already paid for them. It also turns off more of them, including wallpapers, Spotlight and on-device intelligence. Push notifications, StoreKit, in-app purchases and universal links keep working, and services the simulator needs to boot or to launch apps under XCTest are left on. A simulator that is already booted when Harness starts is now left as it is.
50 changes: 48 additions & 2 deletions packages/platform-ios/src/__tests__/instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,17 @@ vi.mock('../simulator-settle.js', () => ({
const slimMocks = vi.hoisted(() => ({
shouldSlimSimulator: vi.fn(),
slimSimulator: vi.fn(),
seedSimulatorSlim: vi.fn(),
}));

vi.mock('../simulator-slim.js', () => ({
slimSimulator: slimMocks.slimSimulator,
}));

vi.mock('../simulator-slim-seed.js', () => ({
seedSimulatorSlim: slimMocks.seedSimulatorSlim,
}));

vi.mock('../startup-strategy.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../startup-strategy.js')>()),
shouldWaitForSimulatorToSettle: settleMocks.shouldWaitForSimulatorToSettle,
Expand Down Expand Up @@ -80,8 +85,15 @@ describe('iOS platform instance dependency validation', () => {
slimMocks.shouldSlimSimulator.mockReset();
slimMocks.shouldSlimSimulator.mockReturnValue(false);
slimMocks.slimSimulator.mockReset();
slimMocks.seedSimulatorSlim.mockReset();
slimMocks.seedSimulatorSlim.mockReturnValue({
seeded: true,
path: '/private/var/tmp/com.apple.CoreSimulator.SimDevice.sim-udid/disabled.plist',
added: 1,
});
slimMocks.slimSimulator.mockResolvedValue({
stopped: ['com.apple.mediaanalysisd'],
alreadyStopped: [],
quieted: [],
skipped: [],
timedOut: false,
Expand Down Expand Up @@ -456,11 +468,13 @@ describe('iOS platform instance dependency validation', () => {
expect(shutdownSimulator).toHaveBeenCalledWith('sim-udid');
});

it('slims the simulator after boot and before installing the app when asked to', async () => {
it('seeds the overrides before boot and slims before installing the app when asked to', async () => {
slimMocks.shouldSlimSimulator.mockReturnValue(true);
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Shutdown');
vi.spyOn(simctl, 'bootSimulator').mockResolvedValue(undefined);
const bootSimulator = vi
.spyOn(simctl, 'bootSimulator')
.mockResolvedValue(undefined);
const waitForBoot = vi
.spyOn(simctl, 'waitForBoot')
.mockResolvedValue({ lastStatusLine: 'Finished', elapsedSeconds: 6 });
Expand All @@ -487,6 +501,10 @@ describe('iOS platform instance dependency validation', () => {
);

expect(slimMocks.shouldSlimSimulator).toHaveBeenCalledWith(true);
expect(slimMocks.seedSimulatorSlim).toHaveBeenCalledWith('sim-udid');
expect(
slimMocks.seedSimulatorSlim.mock.invocationCallOrder[0],
).toBeLessThan(bootSimulator.mock.invocationCallOrder[0]);
expect(slimMocks.slimSimulator).toHaveBeenCalledWith(
'sim-udid',
expect.objectContaining({ signal: expect.any(AbortSignal) }),
Expand All @@ -496,6 +514,33 @@ describe('iOS platform instance dependency validation', () => {
expect(slimOrder).toBeLessThan(isAppInstalled.mock.invocationCallOrder[0]);
});

it('leaves an already booted simulator alone', async () => {
slimMocks.shouldSlimSimulator.mockReturnValue(true);
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted');
vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true);
vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue(
undefined,
);

await getAppleSimulatorPlatformInstance(
{
name: 'ios',
device: {
type: 'simulator',
name: 'iPhone 16 Pro',
systemVersion: '18.0',
},
bundleId: 'com.harnessplayground',
},
harnessConfig,
init,
);

expect(slimMocks.seedSimulatorSlim).not.toHaveBeenCalled();
expect(slimMocks.slimSimulator).not.toHaveBeenCalled();
});

it('does not slim the simulator by default outside CI', async () => {
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted');
Expand All @@ -518,6 +563,7 @@ describe('iOS platform instance dependency validation', () => {
init,
);

expect(slimMocks.seedSimulatorSlim).not.toHaveBeenCalled();
expect(slimMocks.slimSimulator).not.toHaveBeenCalled();
});

Expand Down
143 changes: 143 additions & 0 deletions packages/platform-ios/src/__tests__/simulator-slim-seed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
getLaunchdOverridesPath,
parseLaunchdOverrides,
seedSimulatorSlim,
serializeLaunchdOverrides,
SIMULATOR_SLIM_LABELS,
} from '../simulator-slim-seed.js';

const FORBIDDEN_LABELS = [
// simslim: these wedge a simulator or break its boot when disabled.
'com.apple.nanoregistryd',
'com.apple.nanoregistrylaunchd',
'com.apple.nanoprefsyncd.2',
'com.apple.nanotimekitcompaniond',
'com.apple.nanobackupd',
'com.apple.sleepd',
'com.apple.appprotectiond',
'com.apple.ManagedSettingsAgent',
'com.apple.managedconfiguration.profiled',
'com.apple.mobiletimerd',
'com.apple.routined',
'com.apple.biomed',
'com.apple.biomesyncd',
'com.apple.dmd',
'com.apple.donotdisturbd',
// simslim: XCUITest app launches stall without gamed; Spotlight leaks
// memory without assistantd.
'com.apple.gamed',
'com.apple.assistantd',
// Harness itself depends on these.
'com.apple.SpringBoard',
'com.apple.backboardd',
'com.apple.runningboardd',
'com.apple.tccd',
'com.apple.testmanagerd',
'com.apple.mobile.installd',
'com.apple.ReportCrash',
];

describe('SIMULATOR_SLIM_LABELS', () => {
it('never includes a service a simulator or Harness needs', () => {
expect(SIMULATOR_SLIM_LABELS).not.toEqual(
expect.arrayContaining([expect.toBeOneOf(FORBIDDEN_LABELS)])
);
});

it('has no duplicates', () => {
expect(new Set(SIMULATOR_SLIM_LABELS).size).toBe(
SIMULATOR_SLIM_LABELS.length
);
});
});

describe('seedSimulatorSlim', () => {
let root: string;

beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-slim-seed-'));
});

afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});

it('writes every label as disabled for a never-booted simulator', () => {
const result = seedSimulatorSlim('SIM-1', { root });

expect(result).toEqual({
seeded: true,
path: getLaunchdOverridesPath('SIM-1', root),
added: SIMULATOR_SLIM_LABELS.length,
});
const entries = parseLaunchdOverrides(
fs.readFileSync(getLaunchdOverridesPath('SIM-1', root), 'utf8')
);
expect(entries.size).toBe(SIMULATOR_SLIM_LABELS.length);
expect([...entries.values()].every(Boolean)).toBe(true);
});

it('keeps the entries the runtime wrote and is idempotent', () => {
const overridesPath = getLaunchdOverridesPath('SIM-1', root);
fs.mkdirSync(path.dirname(overridesPath), { recursive: true });
fs.writeFileSync(
overridesPath,
serializeLaunchdOverrides(
new Map([
['com.apple.runtime.enabled', false],
['com.apple.chronod', false],
])
)
);

expect(seedSimulatorSlim('SIM-1', { root })).toMatchObject({
seeded: true,
added: SIMULATOR_SLIM_LABELS.length,
});
const entries = parseLaunchdOverrides(fs.readFileSync(overridesPath, 'utf8'));
expect(entries.get('com.apple.runtime.enabled')).toBe(false);
expect(entries.get('com.apple.chronod')).toBe(true);

expect(seedSimulatorSlim('SIM-1', { root })).toMatchObject({
seeded: true,
added: 0,
});
});

it('leaves overrides it cannot read untouched', () => {
const overridesPath = getLaunchdOverridesPath('SIM-1', root);
fs.mkdirSync(path.dirname(overridesPath), { recursive: true });
fs.writeFileSync(overridesPath, 'bplist00');

expect(seedSimulatorSlim('SIM-1', { root })).toMatchObject({
seeded: false,
});
expect(fs.readFileSync(overridesPath, 'utf8')).toBe('bplist00');
});

it('reports a failure instead of throwing', () => {
const file = path.join(root, 'not-a-directory');
fs.writeFileSync(file, '');

expect(seedSimulatorSlim('SIM-1', { root: file })).toMatchObject({
seeded: false,
});
});
});

describe('serializeLaunchdOverrides', () => {
it('round-trips labels that need escaping', () => {
const entries = new Map([
['com.example.a&b', true],
['com.example.c', false],
]);

expect(parseLaunchdOverrides(serializeLaunchdOverrides(entries))).toEqual(
entries
);
});
});
Loading
Loading