From 59f666ad13255c418cc0ff1e1f04ff64461cbc37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 31 Jul 2026 13:24:06 +0200 Subject: [PATCH] perf(devices): probe platform inventories concurrently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An inventory lookup with no platform filter awaited each platform's toolchain in turn, so it cost their sum. Opening a physical iPhone by name spent 6.7s in resolve_target_device on a host with the Apple, Android and Vega toolchains installed, most of it enumerating platforms the request could not target (vega device list alone was 2.8s of it). The probes are independent, so run them concurrently and concatenate in selector order — the Linux local device still lands last, where it must be so it does not displace connected Android/Apple devices in implicit selection. Alternating A/B on one host against a cabled iPhone, three runs each: sequential 4065/4015/3912ms, concurrent 2359/2365/2295ms. A platform answering with a non-array still contributes nothing: spreading it used to throw into the per-platform catch, and that is now explicit. --- src/core/platform-inventory.test.ts | 81 +++++++++++++++++++++++++++-- src/core/platform-inventory.ts | 30 +++++++---- 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/src/core/platform-inventory.test.ts b/src/core/platform-inventory.test.ts index d6b85bf02b..11816338a7 100644 --- a/src/core/platform-inventory.test.ts +++ b/src/core/platform-inventory.test.ts @@ -2,14 +2,29 @@ import assert from 'node:assert/strict'; import { test, vi } from 'vitest'; import type { DeviceInfo } from '@agent-device/kernel/device'; -const { listVegaDevices } = vi.hoisted(() => ({ - listVegaDevices: vi.fn(), -})); +const { listVegaDevices, listAndroidDevices, listAppleDevices, listLinuxDevices } = vi.hoisted( + () => ({ + listVegaDevices: vi.fn(), + listAndroidDevices: vi.fn(), + listAppleDevices: vi.fn(), + listLinuxDevices: vi.fn(), + }), +); vi.mock('../platforms/vega/devices.ts', () => ({ listVegaDevices, })); +vi.mock('../platforms/android/devices.ts', () => ({ + listAndroidDevices, +})); +vi.mock('../platforms/apple/core/devices.ts', () => ({ + listAppleDevices, +})); +vi.mock('../platforms/linux/devices.ts', () => ({ + listLinuxDevices, +})); +import { LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS } from '@agent-device/contracts/device'; import { listLocalDeviceInventory } from './platform-inventory.ts'; const VEGA_EMULATOR: DeviceInfo = { @@ -33,3 +48,63 @@ test('explicit Vega inventory delegates to the Vega device module', async () => assert.deepEqual(result, [VEGA_EMULATOR]); assert.deepEqual(listVegaDevices.mock.calls[0], []); }); + +test('probes every platform concurrently and keeps selector order in the result', async () => { + // Sequential awaits made an unfiltered lookup cost the sum of every + // toolchain probe. Each stub records when it starts and resolves only once + // all four have started, so this deadlocks unless they genuinely overlap. + const PLATFORM_COUNT = LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS.length; + let started = 0; + let allStarted!: () => void; + const everyProbeStarted = new Promise((resolve) => { + allStarted = resolve; + }); + const gate = async () => { + started += 1; + if (started === PLATFORM_COUNT) allStarted(); + await everyProbeStarted; + }; + + listAndroidDevices.mockImplementation(async () => { + await gate(); + return [device('android', 'android-1')]; + }); + listAppleDevices.mockImplementation(async () => { + await gate(); + return [device('apple', 'apple-1')]; + }); + listVegaDevices.mockImplementation(async () => { + await gate(); + return [device('vega', 'vega-1')]; + }); + listLinuxDevices.mockImplementation(async () => { + await gate(); + return [device('linux', 'linux-1')]; + }); + + const result = await listLocalDeviceInventory({}); + + assert.equal(started, PLATFORM_COUNT); + assert.deepEqual( + result.map((entry) => entry.id), + ['android-1', 'apple-1', 'vega-1', 'linux-1'], + ); +}); + +test('a failing platform probe does not drop the devices found by the others', async () => { + listAndroidDevices.mockRejectedValue(new Error('adb exploded')); + listAppleDevices.mockResolvedValue([device('apple', 'apple-1')]); + listVegaDevices.mockRejectedValue(new Error('vega exploded')); + listLinuxDevices.mockResolvedValue([device('linux', 'linux-1')]); + + const result = await listLocalDeviceInventory({}); + + assert.deepEqual( + result.map((entry) => entry.id), + ['apple-1', 'linux-1'], + ); +}); + +function device(platform: DeviceInfo['platform'], id: string): DeviceInfo { + return { platform, id, name: id, kind: 'device', booted: true }; +} diff --git a/src/core/platform-inventory.ts b/src/core/platform-inventory.ts index c0c05239cd..f1c96892e1 100644 --- a/src/core/platform-inventory.ts +++ b/src/core/platform-inventory.ts @@ -43,15 +43,27 @@ export async function listLocalDeviceInventory( }); } - const devices: DeviceInfo[] = []; - // Linux local device is appended last so it does not displace - // connected Android/Apple devices in implicit auto-selection. - for (const platform of LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS) { - try { - devices.push(...(await listLocalDeviceInventory({ ...request, platform }))); - } catch {} - } - return devices; + // Probed concurrently: each platform shells out to its own toolchain, and + // awaiting them in turn made an unfiltered lookup cost their sum — measured + // at 6.7s on a host with the Apple, Android and Vega toolchains installed, + // most of it spent enumerating platforms the request could not target. + // + // Results are still concatenated in selector order, so the Linux local device + // stays last and does not displace connected Android/Apple devices in + // implicit auto-selection. + const perPlatform = await Promise.all( + LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS.map(async (platform) => { + try { + const listed = await listLocalDeviceInventory({ ...request, platform }); + // A platform that answers with anything but a list contributes nothing, + // exactly as before: spreading a non-array used to throw into the catch. + return Array.isArray(listed) ? listed : []; + } catch { + return []; + } + }), + ); + return perPlatform.flat(); } export function resolveAndroidDiscoverySerialAllowlist(