From 726eeffd23bcdd6919c47f35e9034ecda3a34d77 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Wed, 9 Sep 2026 00:37:36 +0100 Subject: [PATCH 1/3] fix(apple): bind perf process selection to the resolved executable --- .../__tests__/perf-process-identity.test.ts | 56 +++++++++++++++++++ .../src/core/perf-process-identity.ts | 36 ++++++++++++ packages/platform-apple/src/core/perf.ts | 35 +----------- src/commands/perf/index.ts | 2 +- website/docs/docs/debugging-profiling.md | 1 + 5 files changed, 95 insertions(+), 35 deletions(-) create mode 100644 packages/platform-apple/src/core/__tests__/perf-process-identity.test.ts create mode 100644 packages/platform-apple/src/core/perf-process-identity.ts diff --git a/packages/platform-apple/src/core/__tests__/perf-process-identity.test.ts b/packages/platform-apple/src/core/__tests__/perf-process-identity.test.ts new file mode 100644 index 0000000000..f2866067fc --- /dev/null +++ b/packages/platform-apple/src/core/__tests__/perf-process-identity.test.ts @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; +import { matchesAppleExecutableProcess } from '../perf-process-identity.ts'; + +const executable = { + executableName: 'Example', + executablePath: '/Devices/selected/data/Example.app/Example', +}; + +test('a resolved executable path excludes the same app on another simulator', () => { + const processes = [ + { pid: 11, command: executable.executablePath }, + { pid: 22, command: '/Devices/another/data/Example.app/Example' }, + { pid: 33, command: '/Applications/Example.app/Example' }, + { pid: 44, command: 'Example' }, + ]; + assert.deepEqual( + processes + .filter(({ command }) => matchesAppleExecutableProcess(command, executable)) + .map(({ pid }) => pid), + [11], + ); +}); + +test('exact paths accept arguments and spaces without accepting a neighboring executable', () => { + const target = { + executableName: 'Example App', + executablePath: '/Apps/Example App.app/Example App', + }; + assert.equal(matchesAppleExecutableProcess(`${target.executablePath} --argument`, target), true); + assert.equal(matchesAppleExecutableProcess(`${target.executablePath}-helper`, target), false); +}); + +test('the private var alias preserves the resolved app identity', () => { + const target = { executableName: 'Example', executablePath: '/private/var/app/Example' }; + assert.equal(matchesAppleExecutableProcess('/var/app/Example --argument', target), true); + assert.equal(matchesAppleExecutableProcess('/var/other/Example', target), false); + assert.equal( + matchesAppleExecutableProcess('/private/var/app/Example', { + ...target, + executablePath: '/var/app/Example', + }), + true, + ); +}); + +test('name-only matching applies when no executable path is known', () => { + assert.equal( + matchesAppleExecutableProcess('/Apps/Example --argument', { executableName: 'Example' }), + true, + ); + assert.equal( + matchesAppleExecutableProcess('/Apps/Different', { executableName: 'Example' }), + false, + ); +}); diff --git a/packages/platform-apple/src/core/perf-process-identity.ts b/packages/platform-apple/src/core/perf-process-identity.ts new file mode 100644 index 0000000000..ad8b0d1580 --- /dev/null +++ b/packages/platform-apple/src/core/perf-process-identity.ts @@ -0,0 +1,36 @@ +import path from 'node:path'; + +export function matchesAppleExecutableProcess( + command: string, + executable: { executableName: string; executablePath?: string }, +): boolean { + const token = readProcessCommandToken(command); + if (executable.executablePath) { + for (const executablePath of buildAppleExecutablePathAliases(executable.executablePath)) { + if ( + command === executablePath || + token === executablePath || + command.startsWith(`${executablePath} `) + ) { + return true; + } + } + return false; + } + return path.basename(token) === executable.executableName; +} + +function buildAppleExecutablePathAliases(executablePath: string): string[] { + const aliases = [executablePath]; + if (executablePath.startsWith('/private/var/')) { + aliases.push(executablePath.replace('/private/var/', '/var/')); + } else if (executablePath.startsWith('/var/')) { + aliases.push(executablePath.replace('/var/', '/private/var/')); + } + return aliases; +} + +export function readProcessCommandToken(command: string): string { + const [token = ''] = command.trim().split(/\s+/, 1); + return token; +} diff --git a/packages/platform-apple/src/core/perf.ts b/packages/platform-apple/src/core/perf.ts index d73ba40860..e52d44fe01 100644 --- a/packages/platform-apple/src/core/perf.ts +++ b/packages/platform-apple/src/core/perf.ts @@ -30,6 +30,7 @@ import { resolveIosPhysicalDeviceControl } from './physical-device-control.ts'; import { readInfoPlistString } from './plist.ts'; import { buildSimctlArgsForDevice } from './simctl.ts'; import { runAppleToolCommand, runXcrun } from './tool-provider.ts'; +import { matchesAppleExecutableProcess, readProcessCommandToken } from './perf-process-identity.ts'; import { findAllXmlNodes, findFirstXmlNode, @@ -1054,40 +1055,6 @@ async function runAppleSimulatorProcessCommand(args: string[]): Promise Date: Wed, 9 Sep 2026 00:55:14 +0100 Subject: [PATCH 2/3] docs(perf): clarify that executable scoping includes sampling --- src/commands/perf/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/perf/index.ts b/src/commands/perf/index.ts index 810eb310d3..0af6029634 100644 --- a/src/commands/perf/index.ts +++ b/src/commands/perf/index.ts @@ -73,7 +73,7 @@ export const perfCommandFacet = defineCommandFacet({ text: { summary: 'Check frames, memory, or native profiles', cliDetail: - 'Use perf frames for bounded frame-health evidence and perf memory sample for a compact process-memory reading. On iOS simulators and macOS, process captures target the resolved app executable and exclude other copies with the same name. Apple xctrace and Android Simpleperf/Perfetto captures keep raw artifacts on disk; report produces bounded agent-readable evidence. For React render internals, use agent-device react-devtools.', + 'Use perf frames for bounded frame-health evidence and perf memory sample for a compact process-memory reading. On iOS simulators and macOS, process sampling and captures target the resolved app executable and exclude other copies with the same name. Apple xctrace and Android Simpleperf/Perfetto captures keep raw artifacts on disk; report produces bounded agent-readable evidence. For React render internals, use agent-device react-devtools.', mcpDetail: 'For CPU profiles, start and stop write the raw artifact while report writes a compact summary; request the report when the task needs readable native CPU evidence. Profiling output is evidence only: compact state, artifact path, and size.', }, From 33fbe8dc806c23152c2824177e34dbc6ab4cdae1 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Wed, 9 Sep 2026 01:06:50 +0100 Subject: [PATCH 3/3] fix(apple): load perf process identity only when sampling --- packages/platform-apple/src/core/perf-process-identity.ts | 7 +------ packages/platform-apple/src/core/perf.ts | 7 ++++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/platform-apple/src/core/perf-process-identity.ts b/packages/platform-apple/src/core/perf-process-identity.ts index ad8b0d1580..f608e26d6e 100644 --- a/packages/platform-apple/src/core/perf-process-identity.ts +++ b/packages/platform-apple/src/core/perf-process-identity.ts @@ -4,7 +4,7 @@ export function matchesAppleExecutableProcess( command: string, executable: { executableName: string; executablePath?: string }, ): boolean { - const token = readProcessCommandToken(command); + const [token = ''] = command.trim().split(/\s+/, 1); if (executable.executablePath) { for (const executablePath of buildAppleExecutablePathAliases(executable.executablePath)) { if ( @@ -29,8 +29,3 @@ function buildAppleExecutablePathAliases(executablePath: string): string[] { } return aliases; } - -export function readProcessCommandToken(command: string): string { - const [token = ''] = command.trim().split(/\s+/, 1); - return token; -} diff --git a/packages/platform-apple/src/core/perf.ts b/packages/platform-apple/src/core/perf.ts index e52d44fe01..5b68e16eb8 100644 --- a/packages/platform-apple/src/core/perf.ts +++ b/packages/platform-apple/src/core/perf.ts @@ -30,7 +30,6 @@ import { resolveIosPhysicalDeviceControl } from './physical-device-control.ts'; import { readInfoPlistString } from './plist.ts'; import { buildSimctlArgsForDevice } from './simctl.ts'; import { runAppleToolCommand, runXcrun } from './tool-provider.ts'; -import { matchesAppleExecutableProcess, readProcessCommandToken } from './perf-process-identity.ts'; import { findAllXmlNodes, findFirstXmlNode, @@ -965,11 +964,17 @@ export async function readAppleProcessSamples( const result = isMacOs(device) ? await runAppleToolCommand('ps', args, { timeoutMs: APPLE_PERF_TIMEOUT_MS }) : await runAppleSimulatorProcessCommand(args); + const { matchesAppleExecutableProcess } = await import('./perf-process-identity.ts'); return parseApplePsOutput(result.stdout).filter((processInfo) => matchesAppleExecutableProcess(processInfo.command, executable), ); } +function readProcessCommandToken(command: string): string { + const [token = ''] = command.trim().split(/\s+/, 1); + return token; +} + async function resolveAppleMemorySnapshotProcess( device: DeviceInfo, appBundleId: string,