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
3 changes: 0 additions & 3 deletions .fallowrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,6 @@
"isRetryableIosDeviceTraceRecordFailure",
"resolveIosDevicePerfHint",
"captureAppleMemorySnapshot",
"readAppleProcessSamples",
"resolveAppleExecutable",
"resolveIosDevicePerfTarget",
"sampleAppleFramePerf",
"sampleAppleMemoryPerf",
"cleanupAppleXctracePerfCapture",
Expand Down
26 changes: 26 additions & 0 deletions packages/platform-apple/src/core/__tests__/perf-target.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test } from 'vitest';
import assert from 'node:assert/strict';
import { parseApplePsOutput } from '../perf-target.ts';

test('parseApplePsOutput reads pid cpu rss and command columns', () => {
const rows = parseApplePsOutput(
['123 12.5 45678 /Applications/Test.app/Contents/MacOS/Test --flag', '456 0.0 2048 Test'].join(
'\n',
),
);

assert.deepEqual(rows, [
{
pid: 123,
cpuPercent: 12.5,
rssKb: 45678,
command: '/Applications/Test.app/Contents/MacOS/Test --flag',
},
{
pid: 456,
cpuPercent: 0,
rssKb: 2048,
command: 'Test',
},
]);
});
24 changes: 0 additions & 24 deletions packages/platform-apple/src/core/__tests__/perf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ vi.mock('@agent-device/host-kit/command', async (importOriginal) => {
import {
buildAppleMemorySnapshotSupport,
captureAppleMemorySnapshot,
parseApplePsOutput,
sampleAppleFramePerf,
sampleAppleMemoryPerf,
} from '../perf.ts';
Expand Down Expand Up @@ -115,29 +114,6 @@ test('buildAppleMemorySnapshotSupport never emits the internal apple platform',
}
});

test('parseApplePsOutput reads pid cpu rss and command columns', () => {
const rows = parseApplePsOutput(
['123 12.5 45678 /Applications/Test.app/Contents/MacOS/Test --flag', '456 0.0 2048 Test'].join(
'\n',
),
);

assert.deepEqual(rows, [
{
pid: 123,
cpuPercent: 12.5,
rssKb: 45678,
command: '/Applications/Test.app/Contents/MacOS/Test --flag',
},
{
pid: 456,
cpuPercent: 0,
rssKb: 2048,
command: 'Test',
},
]);
});

test('parseAppleFramePerfSample summarizes app hitches and worst windows', () => {
const sample = parseAppleFramePerfSample({
hitchesXml: makeAppleHitchesXml(),
Expand Down
41 changes: 25 additions & 16 deletions packages/platform-apple/src/core/devicectl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ export type IosDevicectlJsonOutcome =
ok: false;
reason: IosDevicectlJsonFailureReason;
args: string[];
result?: ExecResult;
result: ExecResult;
/** The JSON a failed command still wrote, when it was readable. */
payload?: unknown;
cause?: string;
};

Expand All @@ -210,28 +212,35 @@ export async function runIosDevicectlJsonRequest(options: {
`${options.jsonPrefix}-${hostProcessId()}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`,
);
const args = [...options.args, '--json-output', jsonPath];
const result = await runXcrun(args, {
allowFailure: true,
signal: options.signal,
timeoutMs: options.timeoutMs ?? IOS_DEVICECTL_TIMEOUT_MS,
});

try {
const result = await runXcrun(args, {
allowFailure: true,
signal: options.signal,
timeoutMs: options.timeoutMs ?? IOS_DEVICECTL_TIMEOUT_MS,
});
if (result.exitCode !== 0) {
if (options.tolerateFailurePayload) {
const failurePayload = await readJsonFile(jsonPath).catch(() => undefined);
if (failurePayload !== undefined && options.tolerateFailurePayload(failurePayload)) {
return { ok: true, payload: failurePayload };
}
const failurePayload = await readJsonFile(jsonPath).catch(() => undefined);
if (failurePayload !== undefined && options.tolerateFailurePayload?.(failurePayload)) {
return { ok: true, payload: failurePayload };
}
return { ok: false, reason: 'command-failed', args, result };
return { ok: false, reason: 'command-failed', args, result, payload: failurePayload };
}
return await readIosDevicectlJsonPayload(jsonPath, args, result);
} finally {
await unlinkHostFile(jsonPath).catch(() => {});
}
}

async function readIosDevicectlJsonPayload(
jsonPath: string,
args: string[],
result: ExecResult,
): Promise<IosDevicectlJsonOutcome> {
try {
return { ok: true, payload: await readJsonFile(jsonPath) };
} catch (error) {
if (error instanceof AppError) throw error;
return { ok: false, reason: 'unreadable-json', args, result, cause: String(error) };
} finally {
await unlinkHostFile(jsonPath).catch(() => {});
}
}

Expand All @@ -251,7 +260,7 @@ async function runIosDevicectlJsonCommand(
const outcome = await runIosDevicectlJsonRequest(options);
if (outcome.ok) return outcome.payload;

if (outcome.reason === 'command-failed' && outcome.result) {
if (outcome.reason === 'command-failed') {
const { stdout, stderr } = outcome.result;
throw new AppError(
'COMMAND_FAILED',
Expand Down
6 changes: 1 addition & 5 deletions packages/platform-apple/src/core/display-inventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,7 @@ export async function queryAppleDisplayInventory(
signal: options.signal,
});
if (!outcome.ok) {
emitUnresolvedDiagnostic(
device,
outcome.reason,
outcome.result ? outcome.result.stderr.trim() : (outcome.cause ?? ''),
);
emitUnresolvedDiagnostic(device, outcome.reason, outcome.result.stderr.trim());
return unresolvedInventory();
}
const displays = parseCoreDeviceDisplays(outcome.payload);
Expand Down
42 changes: 3 additions & 39 deletions packages/platform-apple/src/core/perf-frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { roundOneDecimal, roundPercent } from '@agent-device/kernel/numeric';
import { parseXmlDocumentSync, type XmlNode } from '@agent-device/xml';
import {
findAllXmlNodes,
findFirstXmlNode,
parseDirectXmlNumber,
readSchemaColumns,
rememberXmlReferences,
resolveXmlNumber,
resolveXmlProcess,
type XmlReference,
} from './perf-xml.ts';

const MAX_WORST_WINDOWS = 3;
Expand Down Expand Up @@ -54,11 +55,6 @@ type AppleHitchSchemaIndexes = {
isSystem: number;
};

type XmlReference = {
numberValue?: number | null;
process?: { pid?: number; name?: string } | null;
};

export function parseAppleFramePerfSample(options: {
hitchesXml: string;
frameLifetimesXml: string;
Expand Down Expand Up @@ -236,17 +232,6 @@ function parseTable(xml: string, schemaName: string): { rows: XmlNode[]; schema:
};
}

function rememberXmlReferences(elements: XmlNode[], references: Map<string, XmlReference>): void {
for (const element of elements) {
rememberXmlReferences(element.children, references);
if (!element.attributes.id) continue;
references.set(element.attributes.id, {
numberValue: parseDirectXmlNumber(element),
process: readDirectProcess(element),
});
}
}

function resolveXmlBoolean(
element: XmlNode | undefined,
references: Map<string, XmlReference>,
Expand All @@ -255,24 +240,3 @@ function resolveXmlBoolean(
if (value === null) return null;
return value !== 0;
}

function resolveXmlProcess(
element: XmlNode | undefined,
references: Map<string, XmlReference>,
): { pid?: number; name?: string } | null {
if (!element) return null;
if (element.attributes.ref) return references.get(element.attributes.ref)?.process ?? null;
return readDirectProcess(element);
}

function readDirectProcess(element: XmlNode | undefined): { pid?: number; name?: string } | null {
if (!element || element.children.some((child) => child.name === 'sentinel')) return null;
const pidNode = findFirstXmlNode(element.children, (child) => child.name === 'pid');
const pid = parseDirectXmlNumber(pidNode);
const name = (element.attributes.fmt ?? '').replace(/\s+\(\d+\)$/, '').trim();
if (pid === null && name.length === 0) return null;
return {
pid: pid ?? undefined,
name: name.length > 0 ? name : undefined,
};
}
Loading
Loading