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..f608e26d6e --- /dev/null +++ b/packages/platform-apple/src/core/perf-process-identity.ts @@ -0,0 +1,31 @@ +import path from 'node:path'; + +export function matchesAppleExecutableProcess( + command: string, + executable: { executableName: string; executablePath?: string }, +): boolean { + const [token = ''] = command.trim().split(/\s+/, 1); + 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; +} diff --git a/packages/platform-apple/src/core/perf.ts b/packages/platform-apple/src/core/perf.ts index d73ba40860..5b68e16eb8 100644 --- a/packages/platform-apple/src/core/perf.ts +++ b/packages/platform-apple/src/core/perf.ts @@ -964,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, @@ -1054,40 +1060,6 @@ async function runAppleSimulatorProcessCommand(args: string[]): Promise