Skip to content

Commit 9f16fc8

Browse files
authored
refactor: migrate perf to device runtime (#2061)
1 parent a904ef0 commit 9f16fc8

102 files changed

Lines changed: 3197 additions & 3351 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.fallowrc.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@
6060
"@arethetypeswrong/cli"
6161
],
6262
"ignoreExports": [
63+
{
64+
"comment": "Android perf mechanics are selected through the lazy platform host so importing agent-device does not eagerly load adb mechanics. Fallow cannot follow the dynamic property read in src/platform-runtime-perf-host.ts.",
65+
"file": "src/platforms/android/perf.ts",
66+
"exports": ["sampleAndroidMemoryPerf"]
67+
},
6368
{
6469
"comment": "Daemon route handlers are reached only through the dynamic `import()` table in request-handler-chain.ts, which --production analysis cannot follow to a consumer.",
6570
"file": "src/daemon/handlers/{lease,session,snapshot,react-native,record-trace,find,interaction}.ts",

packages/capture-kit/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export {
1212
export { createAppLogLiveHandle, createAppLogLiveHandleFromFinish } from './app-log-live-handle.ts';
1313
export { createHostAudioProbeCaptureOperations } from './audio-probe-runtime.ts';
1414
export { hostAudioProbeDescriptorCodec } from './audio-probe-descriptor.ts';
15+
export { decodeDurableDescriptor } from './durable-descriptor-codec.ts';
1516
export { createScreenRecordingLiveHandle } from './screen-recording-live-handle.ts';
1617
export { createScreenRecordingCompletion } from './screen-recording-completion.ts';
1718
export { assertScreenRecordingOptionsSupported } from './screen-recording-options.ts';

packages/contracts/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,22 @@
291291
"types": "./src/platform-runtime-unavailable.ts",
292292
"default": "./src/platform-runtime-unavailable.ts"
293293
},
294+
"./perf-runtime": {
295+
"types": "./src/perf-runtime.ts",
296+
"default": "./src/perf-runtime.ts"
297+
},
298+
"./perf-runtime-host": {
299+
"types": "./src/perf-runtime-host.ts",
300+
"default": "./src/perf-runtime-host.ts"
301+
},
302+
"./perf-runtime-operation-builder": {
303+
"types": "./src/perf-runtime-operation-builder.ts",
304+
"default": "./src/perf-runtime-operation-builder.ts"
305+
},
306+
"./perf-runtime-plan": {
307+
"types": "./src/perf-runtime-plan.ts",
308+
"default": "./src/perf-runtime-plan.ts"
309+
},
294310
"./progress": {
295311
"types": "./src/facades/progress.ts",
296312
"default": "./src/facades/progress.ts"
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { DeviceInfo } from '@agent-device/kernel/device';
2+
import type { RuntimeOwnerRef } from './platform-runtime.ts';
3+
import type {
4+
PerfData,
5+
PerfNativeCaptureRecoveryInput,
6+
PerfNativeCaptureStartInput,
7+
PerfNativeCaptureStartResult,
8+
PerfProfileReportInput,
9+
PerfRuntimeOperations,
10+
} from './perf-runtime.ts';
11+
12+
type NativeCaptureHost = Readonly<{
13+
start(
14+
device: DeviceInfo,
15+
owner: RuntimeOwnerRef,
16+
input: PerfNativeCaptureStartInput,
17+
): Promise<PerfNativeCaptureStartResult>;
18+
reattach(
19+
device: DeviceInfo,
20+
input: PerfNativeCaptureRecoveryInput,
21+
): ReturnType<PerfRuntimeOperations['perfNativeCaptureReattach']>;
22+
cleanup(
23+
device: DeviceInfo,
24+
input: PerfNativeCaptureRecoveryInput,
25+
): ReturnType<PerfRuntimeOperations['perfNativeCaptureCleanup']>;
26+
}>;
27+
28+
export type ApplePerfHost = NativeCaptureHost &
29+
Readonly<{
30+
sampleFrames(device: DeviceInfo, appId: string): Promise<object>;
31+
frameSampling(device: DeviceInfo): Promise<PerfData>;
32+
sampleMemory(device: DeviceInfo, appId: string): Promise<object>;
33+
memorySampling(device: DeviceInfo): Promise<PerfData>;
34+
memorySnapshotSupport(device: DeviceInfo): Promise<PerfData>;
35+
captureMemorySnapshot(device: DeviceInfo, appId: string, outputPath: string): Promise<PerfData>;
36+
writeProfileReport(input: PerfProfileReportInput): Promise<PerfData>;
37+
}>;
38+
39+
export type AndroidPerfHost = NativeCaptureHost &
40+
Readonly<{
41+
sampleFrames(device: DeviceInfo, appId: string): Promise<object>;
42+
sampleMemory(device: DeviceInfo, appId: string): Promise<object>;
43+
captureMemorySnapshot(device: DeviceInfo, appId: string, outputPath: string): Promise<PerfData>;
44+
writeProfileReport(device: DeviceInfo, input: PerfProfileReportInput): Promise<PerfData>;
45+
}>;
46+
47+
export type HarmonyPerfHost = Readonly<{
48+
sampleMemory(device: DeviceInfo, appId: string): Promise<object>;
49+
}>;
50+
51+
/** Family-specific native mechanics; platform packages own policy and operation construction. */
52+
export type PerfRuntimeHost = Readonly<{
53+
apple: ApplePerfHost;
54+
android: AndroidPerfHost;
55+
harmony: HarmonyPerfHost;
56+
}>;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { expect, test, vi } from 'vitest';
2+
import type { PerfRuntimeOperations } from './perf-runtime.ts';
3+
import {
4+
createPerfNativeOperations,
5+
missingPerfAppMetric,
6+
missingPerfSnapshotAppError,
7+
settlePerfMetric,
8+
unsupportedPerfMemoryArtifact,
9+
} from './perf-runtime-operation-builder.ts';
10+
11+
test('builds compact unavailable observations and memory-artifact guidance', async () => {
12+
await expect(settlePerfMetric(Promise.reject(new Error('sample failed')))).resolves.toMatchObject(
13+
{
14+
available: false,
15+
reason: 'sample failed',
16+
},
17+
);
18+
expect(missingPerfAppMetric('Apple', 'bundle ID')).toMatchObject({
19+
available: false,
20+
reason: expect.stringContaining('Run open <app> first'),
21+
});
22+
expect(unsupportedPerfMemoryArtifact('Apple', 'android-hprof', {}, 'Use memgraph')).toEqual({
23+
artifact: {
24+
available: false,
25+
kind: 'android-hprof',
26+
reason: 'Apple perf memory snapshot does not support android-hprof.',
27+
hint: 'Use memgraph',
28+
support: {},
29+
},
30+
support: {},
31+
});
32+
expect(missingPerfSnapshotAppError()).toMatchObject({ code: 'INVALID_ARGS' });
33+
});
34+
35+
test('delegates a valid native profile report', async () => {
36+
const writeProfileReport = vi.fn(async () => ({ report: true }));
37+
const unused = vi.fn() as unknown as PerfRuntimeOperations['perfNativeCaptureStart'];
38+
const operations = createPerfNativeOperations({
39+
platform: 'Apple',
40+
expectedProfileKind: 'xctrace',
41+
start: unused,
42+
reattach: vi.fn() as unknown as PerfRuntimeOperations['perfNativeCaptureReattach'],
43+
cleanup: vi.fn() as unknown as PerfRuntimeOperations['perfNativeCaptureCleanup'],
44+
writeProfileReport,
45+
});
46+
47+
await expect(
48+
operations.perfProfileReport({
49+
kind: 'xctrace',
50+
tracePath: '/tmp/profile.trace',
51+
outPath: '/tmp/report.json',
52+
}),
53+
).resolves.toEqual({ report: true });
54+
expect(writeProfileReport).toHaveBeenCalledOnce();
55+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { PerfData, PerfRuntimeOperations } from './perf-runtime.ts';
2+
import { AppError, normalizeError } from '@agent-device/kernel/errors';
3+
4+
export async function settlePerfMetric(promise: Promise<object>): Promise<PerfData> {
5+
try {
6+
return { available: true, ...(await promise) };
7+
} catch (error) {
8+
const normalized = normalizeError(error);
9+
return { available: false, reason: normalized.message, error: normalized };
10+
}
11+
}
12+
13+
export function missingPerfAppMetric(platform: string, appIdentity: string): PerfData {
14+
return {
15+
available: false,
16+
reason: `No ${platform} app ${appIdentity} is associated with this session. Run open <app> first.`,
17+
};
18+
}
19+
20+
export function unsupportedPerfMemoryArtifact(
21+
platform: string,
22+
kind: string,
23+
support: PerfData,
24+
hint: string,
25+
): PerfData {
26+
return {
27+
artifact: {
28+
available: false,
29+
kind,
30+
reason: `${platform} perf memory snapshot does not support ${kind}.`,
31+
hint,
32+
support,
33+
},
34+
support,
35+
};
36+
}
37+
38+
export function missingPerfSnapshotAppError(): AppError {
39+
return new AppError('INVALID_ARGS', 'perf memory snapshot requires an active app session', {
40+
hint: 'Run open <app> first so perf memory snapshot can resolve the app process.',
41+
});
42+
}
43+
44+
export function createPerfNativeOperations(
45+
params: Readonly<{
46+
platform: string;
47+
expectedProfileKind: 'xctrace' | 'simpleperf';
48+
start: PerfRuntimeOperations['perfNativeCaptureStart'];
49+
reattach: PerfRuntimeOperations['perfNativeCaptureReattach'];
50+
cleanup: PerfRuntimeOperations['perfNativeCaptureCleanup'];
51+
writeProfileReport: PerfRuntimeOperations['perfProfileReport'];
52+
}>,
53+
): Pick<
54+
PerfRuntimeOperations,
55+
| 'perfNativeCaptureStart'
56+
| 'perfNativeCaptureReattach'
57+
| 'perfNativeCaptureCleanup'
58+
| 'perfProfileReport'
59+
> {
60+
return Object.freeze({
61+
perfNativeCaptureStart: params.start,
62+
perfNativeCaptureReattach: params.reattach,
63+
perfNativeCaptureCleanup: params.cleanup,
64+
perfProfileReport: async (input) => {
65+
if (input.kind !== params.expectedProfileKind) {
66+
throw new AppError(
67+
'INVALID_ARGS',
68+
`${params.platform} native perf requires --kind ${params.expectedProfileKind}, not ${input.kind}`,
69+
);
70+
}
71+
return await params.writeProfileReport(input);
72+
},
73+
});
74+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, test } from 'vitest';
2+
import { parsePerfRuntimeRequest, resolvePerfRuntimePlan } from './perf-runtime-plan.ts';
3+
4+
describe('perf runtime grammar', () => {
5+
test('rejects flags and positionals that do not belong to frame sampling', () => {
6+
expect(() => parsePerfRuntimeRequest({ positionals: ['frames'], kind: 'perfetto' })).toThrow(
7+
'perf action must be frames',
8+
);
9+
expect(() => parsePerfRuntimeRequest({ positionals: ['frames', 'sample', 'extra'] })).toThrow(
10+
'perf action must be frames',
11+
);
12+
});
13+
14+
test('keeps memory sample and snapshot grammars disjoint', () => {
15+
expect(() =>
16+
parsePerfRuntimeRequest({ positionals: ['memory', 'sample', 'memgraph'] }),
17+
).toThrow('does not accept additional positionals');
18+
expect(() =>
19+
parsePerfRuntimeRequest({ positionals: ['memory', 'sample'], kind: 'memgraph' }),
20+
).toThrow('--kind is only supported');
21+
expect(
22+
parsePerfRuntimeRequest({ positionals: ['memory', 'snapshot'], kind: 'memgraph' }),
23+
).toEqual({ area: 'memory', action: 'snapshot', kind: 'memgraph' });
24+
expect(() =>
25+
parsePerfRuntimeRequest({ positionals: ['memory', 'snapshot'], kind: 'perfetto' }),
26+
).toThrow('--kind must be android-hprof or memgraph');
27+
});
28+
29+
test('maps every accepted request to its single runtime use', () => {
30+
expect(resolvePerfRuntimePlan(parsePerfRuntimeRequest({ positionals: ['frames'] })).kind).toBe(
31+
'frames',
32+
);
33+
expect(
34+
resolvePerfRuntimePlan(parsePerfRuntimeRequest({ positionals: ['memory', 'snapshot'] })).kind,
35+
).toBe('memory-snapshot');
36+
expect(
37+
resolvePerfRuntimePlan(
38+
parsePerfRuntimeRequest({ positionals: ['cpu', 'profile', 'report', 'xctrace'] }),
39+
).kind,
40+
).toBe('profile-report');
41+
});
42+
});

0 commit comments

Comments
 (0)