Skip to content

Commit cb8d45e

Browse files
committed
perf: cache successful device readiness checks
1 parent d1a7641 commit cb8d45e

2 files changed

Lines changed: 107 additions & 2 deletions

File tree

src/daemon/__tests__/device-ready.test.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,77 @@
1-
import { test } from 'vitest';
1+
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
22
import assert from 'node:assert/strict';
3-
import { parseIosReadyPayload, resolveIosReadyHint } from '../device-ready.ts';
3+
import type { DeviceInfo } from '../../utils/device.ts';
4+
5+
vi.mock('../../platforms/ios/index.ts', () => ({
6+
ensureBootedSimulator: vi.fn(async () => {}),
7+
}));
8+
vi.mock('../../platforms/android/devices.ts', () => ({
9+
waitForAndroidBoot: vi.fn(async () => {}),
10+
}));
11+
12+
import { waitForAndroidBoot } from '../../platforms/android/devices.ts';
13+
import { ensureBootedSimulator } from '../../platforms/ios/index.ts';
14+
import { ANDROID_EMULATOR, IOS_SIMULATOR } from '../../__tests__/test-utils/index.ts';
15+
import {
16+
clearDeviceReadyCacheForTests,
17+
DEVICE_READY_CACHE_TTL_MS,
18+
ensureDeviceReady,
19+
parseIosReadyPayload,
20+
resolveIosReadyHint,
21+
} from '../device-ready.ts';
22+
23+
const mockEnsureBootedSimulator = vi.mocked(ensureBootedSimulator);
24+
const mockWaitForAndroidBoot = vi.mocked(waitForAndroidBoot);
25+
26+
beforeEach(() => {
27+
vi.useFakeTimers();
28+
vi.setSystemTime(new Date('2026-04-01T10:00:00.000Z'));
29+
clearDeviceReadyCacheForTests();
30+
mockEnsureBootedSimulator.mockReset();
31+
mockEnsureBootedSimulator.mockResolvedValue(undefined);
32+
mockWaitForAndroidBoot.mockReset();
33+
mockWaitForAndroidBoot.mockResolvedValue(undefined);
34+
});
35+
36+
afterEach(() => {
37+
clearDeviceReadyCacheForTests();
38+
vi.useRealTimers();
39+
});
40+
41+
test('ensureDeviceReady caches successful simulator readiness checks', async () => {
42+
const device: DeviceInfo = { ...IOS_SIMULATOR, simulatorSetPath: '/tmp/simset-a' };
43+
44+
await ensureDeviceReady(device);
45+
await ensureDeviceReady({ ...device });
46+
47+
expect(mockEnsureBootedSimulator).toHaveBeenCalledTimes(1);
48+
});
49+
50+
test('ensureDeviceReady includes simulator set path in the cache key', async () => {
51+
await ensureDeviceReady({ ...IOS_SIMULATOR, simulatorSetPath: '/tmp/simset-a' });
52+
await ensureDeviceReady({ ...IOS_SIMULATOR, simulatorSetPath: '/tmp/simset-b' });
53+
54+
expect(mockEnsureBootedSimulator).toHaveBeenCalledTimes(2);
55+
});
56+
57+
test('ensureDeviceReady expires cached readiness checks after the ttl', async () => {
58+
await ensureDeviceReady(ANDROID_EMULATOR);
59+
vi.setSystemTime(new Date(Date.now() + DEVICE_READY_CACHE_TTL_MS - 1));
60+
await ensureDeviceReady({ ...ANDROID_EMULATOR });
61+
vi.setSystemTime(new Date(Date.now() + 1));
62+
await ensureDeviceReady({ ...ANDROID_EMULATOR });
63+
64+
expect(mockWaitForAndroidBoot).toHaveBeenCalledTimes(2);
65+
});
66+
67+
test('ensureDeviceReady does not cache failed readiness checks', async () => {
68+
mockEnsureBootedSimulator.mockRejectedValueOnce(new Error('boot failed'));
69+
70+
await expect(ensureDeviceReady(IOS_SIMULATOR)).rejects.toThrow('boot failed');
71+
await ensureDeviceReady(IOS_SIMULATOR);
72+
73+
expect(mockEnsureBootedSimulator).toHaveBeenCalledTimes(2);
74+
});
475

576
test('parseIosReadyPayload reads tunnelState from direct connectionProperties', () => {
677
const parsed = parseIosReadyPayload({

src/daemon/device-ready.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,59 @@ const IOS_DEVICE_READY_TIMEOUT_MS = resolveTimeoutMs(
1313
1_000,
1414
);
1515
const IOS_DEVICE_READY_COMMAND_TIMEOUT_BUFFER_MS = 3_000;
16+
export const DEVICE_READY_CACHE_TTL_MS = 5_000;
17+
18+
const readyCache = new Map<string, number>();
1619

1720
export async function ensureDeviceReady(device: DeviceInfo): Promise<void> {
21+
const cacheKey = deviceReadyCacheKey(device);
22+
const cachedUntil = readyCache.get(cacheKey);
23+
if (cachedUntil !== undefined) {
24+
if (cachedUntil > Date.now()) {
25+
return;
26+
}
27+
readyCache.delete(cacheKey);
28+
}
29+
1830
if (device.platform === 'ios') {
1931
if (device.kind === 'simulator') {
2032
const { ensureBootedSimulator } = await import('../platforms/ios/index.ts');
2133
await ensureBootedSimulator(device);
34+
markDeviceReady(cacheKey);
2235
return;
2336
}
2437
if (device.kind === 'device') {
2538
await ensureIosDeviceReady(device.id);
39+
markDeviceReady(cacheKey);
2640
return;
2741
}
2842
}
2943
if (device.platform === 'android') {
3044
const { waitForAndroidBoot } = await import('../platforms/android/devices.ts');
3145
await waitForAndroidBoot(device.id);
46+
markDeviceReady(cacheKey);
3247
}
3348
}
3449

50+
export function clearDeviceReadyCacheForTests(): void {
51+
readyCache.clear();
52+
}
53+
54+
function markDeviceReady(cacheKey: string): void {
55+
readyCache.set(cacheKey, Date.now() + DEVICE_READY_CACHE_TTL_MS);
56+
}
57+
58+
function deviceReadyCacheKey(device: DeviceInfo): string {
59+
const simulatorSetPath = device.kind === 'simulator' ? (device.simulatorSetPath ?? '') : '';
60+
return JSON.stringify([
61+
device.platform,
62+
device.kind,
63+
device.id,
64+
device.target ?? '',
65+
simulatorSetPath,
66+
]);
67+
}
68+
3569
async function ensureIosDeviceReady(deviceId: string): Promise<void> {
3670
const jsonPath = path.join(
3771
os.tmpdir(),

0 commit comments

Comments
 (0)