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
Original file line number Diff line number Diff line change
@@ -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,
);
});
31 changes: 31 additions & 0 deletions packages/platform-apple/src/core/perf-process-identity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
40 changes: 6 additions & 34 deletions packages/platform-apple/src/core/perf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1054,40 +1060,6 @@ async function runAppleSimulatorProcessCommand(args: string[]): Promise<ExecResu
});
}

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 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;
}

function readProcessCommandToken(command: string): string {
const [token = ''] = command.trim().split(/\s+/, 1);
return token;
}

function buildAppleMemoryPerfSample(args: {
residentMemoryKb: number;
measuredAt: string;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/perf/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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.',
},
Expand Down
1 change: 1 addition & 0 deletions website/docs/docs/debugging-profiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ agent-device perf trace stop --kind perfetto --out app.perfetto-trace
- For React Native JavaScript heap leaks, use `agent-device cdp` against the Metro CDP target instead of native/process memory samples; see the CDP section above.
- Heap and memgraph artifacts are returned as paths plus compact metadata. Example default output: `Memory artifact (android-hprof): /tmp/app.hprof (42MB)`. They are not printed or embedded in JSON by default. heapprofd/native allocation tracing is deferred until Perfetto plumbing is available.
- `perf cpu profile ... --kind xctrace` collects an Apple native `.trace`; `report` aggregates every run, returns at most ten weighted top functions in JSON, and prints five. `perf trace ... --kind xctrace` keeps trace data as an artifact.
- On iOS simulators and macOS, process sampling and captures target the resolved app executable. Other running copies with the same executable name are excluded, including copies installed on another simulator.
- Android native profiling uses `perf cpu profile ... --kind simpleperf`; its report likewise returns at most ten top functions and prints five. Android native trace capture uses `perf trace ... --kind perfetto`. These commands require an active Android app session and return artifact paths/summaries instead of dumping profile or trace contents.
- Use the compact native perf result as agent evidence. For example, a successful Perfetto stop may return `state: "stopped"`, `outPath: "/tmp/app.perfetto-trace"`, `sizeBytes: 5392410`, and `method: "adb-shell-perfetto"` while the 5.3 MB raw trace remains on disk as the artifact.
- Memory and Android frame-health availability depend on platform and whether the active session is bound to an app/package. HarmonyOS reports process RSS through HDC; CPU profiling, frame sampling, and memory-snapshot artifacts remain unavailable on the public HDC surface.
Expand Down
Loading