Skip to content

Commit c9d14e6

Browse files
committed
refactor: migrate perf to device runtime
1 parent 8af2660 commit c9d14e6

81 files changed

Lines changed: 2404 additions & 3297 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.

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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,18 @@
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-plan": {
303+
"types": "./src/perf-runtime-plan.ts",
304+
"default": "./src/perf-runtime-plan.ts"
305+
},
294306
"./progress": {
295307
"types": "./src/facades/progress.ts",
296308
"default": "./src/facades/progress.ts"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { DeviceInfo } from '@agent-device/kernel/device';
2+
import {
3+
type RuntimeOperationFact,
4+
type RuntimeOwnerRef,
5+
whenAdmitted,
6+
} from './platform-runtime.ts';
7+
import type { PerfRuntimeOperations } from './perf-runtime.ts';
8+
9+
export type PerfRuntimeHost = Readonly<{
10+
bind(
11+
input: Readonly<{
12+
device: DeviceInfo;
13+
owner: RuntimeOwnerRef;
14+
signal?: AbortSignal;
15+
}>,
16+
): Promise<Readonly<Partial<PerfRuntimeOperations>>>;
17+
}>;
18+
19+
/** Publishes admitted operation closures without loading or binding a native collector. */
20+
export function bindPerfRuntimeOperations(
21+
input: Readonly<{
22+
host: PerfRuntimeHost;
23+
device: DeviceInfo;
24+
owner: RuntimeOwnerRef;
25+
signal?: AbortSignal;
26+
facts: Readonly<{ [Key in keyof PerfRuntimeOperations]: RuntimeOperationFact }>;
27+
}>,
28+
): Readonly<Partial<PerfRuntimeOperations>> {
29+
type Operation<Key extends keyof PerfRuntimeOperations> = Extract<
30+
PerfRuntimeOperations[Key],
31+
(...args: never[]) => unknown
32+
>;
33+
const invoke = async <Key extends keyof PerfRuntimeOperations>(
34+
key: Key,
35+
operationInput: Parameters<Operation<Key>>[0],
36+
): Promise<Awaited<ReturnType<Operation<Key>>>> => {
37+
const operations = await input.host.bind({
38+
device: input.device,
39+
owner: input.owner,
40+
signal: input.signal,
41+
});
42+
const operation = (operations as Readonly<Record<string, unknown>>)[key] as
43+
| Operation<Key>
44+
| undefined;
45+
if (!operation) throw new TypeError(`Perf host omitted admitted operation ${String(key)}`);
46+
const call = operation as (
47+
value: Parameters<Operation<Key>>[0],
48+
) => Promise<Awaited<ReturnType<Operation<Key>>>>;
49+
return await call(operationInput);
50+
};
51+
return Object.freeze({
52+
...whenAdmitted(input.facts.perfFrames, () => ({
53+
perfFrames: async (value: Parameters<PerfRuntimeOperations['perfFrames']>[0]) =>
54+
await invoke('perfFrames', value),
55+
})),
56+
...whenAdmitted(input.facts.perfMemorySample, () => ({
57+
perfMemorySample: async (value: Parameters<PerfRuntimeOperations['perfMemorySample']>[0]) =>
58+
await invoke('perfMemorySample', value),
59+
})),
60+
...whenAdmitted(input.facts.perfMemorySnapshot, () => ({
61+
perfMemorySnapshot: async (
62+
value: Parameters<PerfRuntimeOperations['perfMemorySnapshot']>[0],
63+
) => await invoke('perfMemorySnapshot', value),
64+
})),
65+
...whenAdmitted(input.facts.perfNativeCaptureStart, () => ({
66+
perfNativeCaptureStart: async (
67+
value: Parameters<PerfRuntimeOperations['perfNativeCaptureStart']>[0],
68+
) => await invoke('perfNativeCaptureStart', value),
69+
})),
70+
...whenAdmitted(input.facts.perfNativeCaptureReattach, () => ({
71+
perfNativeCaptureReattach: async (
72+
value: Parameters<PerfRuntimeOperations['perfNativeCaptureReattach']>[0],
73+
) => await invoke('perfNativeCaptureReattach', value),
74+
})),
75+
...whenAdmitted(input.facts.perfNativeCaptureCleanup, () => ({
76+
perfNativeCaptureCleanup: async (
77+
value: Parameters<PerfRuntimeOperations['perfNativeCaptureCleanup']>[0],
78+
) => await invoke('perfNativeCaptureCleanup', value),
79+
})),
80+
...whenAdmitted(input.facts.perfProfileReport, () => ({
81+
perfProfileReport: async (value: Parameters<PerfRuntimeOperations['perfProfileReport']>[0]) =>
82+
await invoke('perfProfileReport', value),
83+
})),
84+
});
85+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { AppError } from '@agent-device/kernel/errors';
2+
import type { PerfKind } from './facades/observability.ts';
3+
import {
4+
perfFramesUse,
5+
perfMemorySampleUse,
6+
perfMemorySnapshotUse,
7+
perfNativeCaptureRecoveryUse,
8+
perfNativeCaptureStartUse,
9+
perfProfileReportUse,
10+
} from './platform-runtime-operations.ts';
11+
12+
export type PerfRuntimeRequest =
13+
| Readonly<{ area: 'frames'; action: 'sample' }>
14+
| Readonly<{
15+
area: 'memory';
16+
action: 'sample' | 'snapshot';
17+
kind?: PerfKind;
18+
outPath?: string;
19+
}>
20+
| Readonly<{
21+
area: 'cpu';
22+
subject: 'profile';
23+
action: 'start' | 'stop' | 'report';
24+
kind: 'xctrace' | 'simpleperf';
25+
template?: string;
26+
outPath?: string;
27+
tracePath?: string;
28+
}>
29+
| Readonly<{
30+
area: 'trace';
31+
action: 'start' | 'stop';
32+
kind: 'xctrace' | 'perfetto';
33+
template?: string;
34+
outPath?: string;
35+
}>;
36+
37+
export type PerfRuntimePlan =
38+
| Readonly<{
39+
kind: 'frames';
40+
request: Extract<PerfRuntimeRequest, { area: 'frames' }>;
41+
use: typeof perfFramesUse;
42+
}>
43+
| Readonly<{
44+
kind: 'memory-sample';
45+
request: Extract<PerfRuntimeRequest, { area: 'memory' }>;
46+
use: typeof perfMemorySampleUse;
47+
}>
48+
| Readonly<{
49+
kind: 'memory-snapshot';
50+
request: Extract<PerfRuntimeRequest, { area: 'memory' }>;
51+
use: typeof perfMemorySnapshotUse;
52+
}>
53+
| Readonly<{
54+
kind: 'capture-start';
55+
request: Extract<PerfRuntimeRequest, { area: 'cpu' | 'trace' }>;
56+
use: typeof perfNativeCaptureStartUse;
57+
}>
58+
| Readonly<{
59+
kind: 'capture-stop';
60+
request: Extract<PerfRuntimeRequest, { area: 'cpu' | 'trace' }>;
61+
}>
62+
| Readonly<{
63+
kind: 'profile-report';
64+
request: Extract<PerfRuntimeRequest, { area: 'cpu' }>;
65+
use: typeof perfProfileReportUse;
66+
}>;
67+
68+
export function resolvePerfRuntimePlan(request: PerfRuntimeRequest): PerfRuntimePlan {
69+
if (request.area === 'frames') {
70+
return Object.freeze({ kind: 'frames', request, use: perfFramesUse });
71+
}
72+
if (request.area === 'memory') {
73+
return request.action === 'snapshot'
74+
? Object.freeze({ kind: 'memory-snapshot', request, use: perfMemorySnapshotUse })
75+
: Object.freeze({ kind: 'memory-sample', request, use: perfMemorySampleUse });
76+
}
77+
if (request.action === 'start') {
78+
return Object.freeze({ kind: 'capture-start', request, use: perfNativeCaptureStartUse });
79+
}
80+
if (request.action === 'stop') return Object.freeze({ kind: 'capture-stop', request });
81+
if (request.area !== 'cpu') {
82+
throw new AppError('INVALID_ARGS', 'perf trace action must be start or stop');
83+
}
84+
return Object.freeze({ kind: 'profile-report', request, use: perfProfileReportUse });
85+
}
86+
87+
export { perfNativeCaptureRecoveryUse };
88+
89+
export function parsePerfRuntimeRequest(
90+
input: Readonly<{
91+
positionals?: readonly string[];
92+
kind?: string;
93+
outPath?: string;
94+
}>,
95+
): PerfRuntimeRequest {
96+
const values = input.positionals ?? [];
97+
const area = values[0]?.toLowerCase();
98+
if (area === 'frames') {
99+
const action = values[1]?.toLowerCase();
100+
if ((action !== undefined && action !== 'sample') || values.length > 2) {
101+
throw new AppError(
102+
'INVALID_ARGS',
103+
'perf action must be frames, memory sample|snapshot, cpu profile start|stop|report, or trace start|stop',
104+
);
105+
}
106+
return Object.freeze({ area, action: 'sample' });
107+
}
108+
if (area === 'memory') return parseMemoryRequest(values, input);
109+
if (area === 'cpu') return parseCpuRequest(values, input.outPath);
110+
if (area === 'trace') return parseTraceRequest(values, input.outPath);
111+
throw new AppError('INVALID_ARGS', 'perf requires frames, memory, cpu, or trace');
112+
}
113+
114+
// This is the finite CLI grammar boundary; each branch rejects one unsupported surface form.
115+
// fallow-ignore-next-line complexity
116+
function parseMemoryRequest(
117+
values: readonly string[],
118+
input: Readonly<{ kind?: string; outPath?: string }>,
119+
): PerfRuntimeRequest {
120+
const action = (values[1] ?? 'sample').toLowerCase();
121+
if (action !== 'sample' && action !== 'snapshot') {
122+
throw new AppError('INVALID_ARGS', 'perf memory requires sample or snapshot');
123+
}
124+
if (input.kind !== undefined && action !== 'snapshot') {
125+
throw new AppError('INVALID_ARGS', '--kind is only supported with perf memory snapshot');
126+
}
127+
const kind = input.kind ?? values[2];
128+
if (kind !== undefined && kind !== 'android-hprof' && kind !== 'memgraph') {
129+
throw new AppError(
130+
'INVALID_ARGS',
131+
'perf memory snapshot --kind must be android-hprof or memgraph',
132+
);
133+
}
134+
return Object.freeze({
135+
area: 'memory',
136+
action,
137+
...(kind === undefined ? {} : { kind: kind as PerfKind }),
138+
...(input.outPath === undefined ? {} : { outPath: input.outPath }),
139+
});
140+
}
141+
142+
// This is the finite CLI grammar boundary; each branch rejects one unsupported surface form.
143+
// fallow-ignore-next-line complexity
144+
function parseCpuRequest(
145+
values: readonly string[],
146+
flaggedOut: string | undefined,
147+
): PerfRuntimeRequest {
148+
if (values[1]?.toLowerCase() !== 'profile') {
149+
throw new AppError('INVALID_ARGS', 'perf cpu requires profile');
150+
}
151+
const action = values[2]?.toLowerCase();
152+
if (action !== 'start' && action !== 'stop' && action !== 'report') {
153+
throw new AppError('INVALID_ARGS', 'perf cpu profile action must be start, stop, or report');
154+
}
155+
const kind = values[3]?.toLowerCase();
156+
if (kind !== 'xctrace' && kind !== 'simpleperf') {
157+
throw new AppError('INVALID_ARGS', 'perf cpu profile requires --kind xctrace or simpleperf');
158+
}
159+
const outPath = flaggedOut ?? values[5];
160+
return Object.freeze({
161+
area: 'cpu',
162+
subject: 'profile',
163+
action,
164+
kind,
165+
...(values[4] ? { template: values[4] } : {}),
166+
...(outPath ? { outPath } : {}),
167+
...(values[6] ? { tracePath: values[6] } : {}),
168+
});
169+
}
170+
171+
function parseTraceRequest(
172+
values: readonly string[],
173+
flaggedOut: string | undefined,
174+
): PerfRuntimeRequest {
175+
const action = values[1]?.toLowerCase();
176+
if (action !== 'start' && action !== 'stop') {
177+
throw new AppError('INVALID_ARGS', 'perf trace action must be start or stop');
178+
}
179+
const kind = values[2]?.toLowerCase();
180+
if (kind !== 'xctrace' && kind !== 'perfetto') {
181+
throw new AppError('INVALID_ARGS', 'perf trace requires --kind xctrace or perfetto');
182+
}
183+
const outPath = flaggedOut ?? values[4];
184+
return Object.freeze({
185+
area: 'trace',
186+
action,
187+
kind,
188+
...(values[3] ? { template: values[3] } : {}),
189+
...(outPath ? { outPath } : {}),
190+
});
191+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { PendingTransferGuard } from './async-lifecycle.ts';
2+
import type { CleanupOutcome, LiveResourceHandle, ReattachOutcome } from './durable-resource.ts';
3+
import type { DurableResourceEnvelope } from './durable-resource-envelope.ts';
4+
import type { PerfKind } from './facades/observability.ts';
5+
import type { ResourceOwnershipFence, RuntimeOperationFact } from './platform-runtime.ts';
6+
7+
export const PERF_CAPTURE_RESOURCE_KIND = 'perf-capture' as const;
8+
export type PerfData = Readonly<Record<string, unknown>>;
9+
export type PerfObservationInput = Readonly<{ appId?: string }>;
10+
export type PerfMemorySnapshotInput = Readonly<{
11+
appId?: string;
12+
kind?: PerfKind;
13+
outPath?: string;
14+
artifactsDir: string;
15+
}>;
16+
export type PerfNativeCaptureMode = 'cpu-profile' | 'trace';
17+
export type PerfNativeCaptureKind = 'xctrace' | 'simpleperf' | 'perfetto';
18+
export type PerfNativeCaptureStartInput = Readonly<{
19+
sessionId: string;
20+
appId: string;
21+
mode: PerfNativeCaptureMode;
22+
kind: PerfNativeCaptureKind;
23+
template?: string;
24+
outPath: string;
25+
fence: ResourceOwnershipFence;
26+
}>;
27+
export type PerfNativeCaptureCompletion = PerfData;
28+
export type PerfNativeCaptureLiveHandle = LiveResourceHandle<PerfNativeCaptureCompletion> &
29+
Readonly<{
30+
inspect(): PerfData;
31+
setOutputPath(outPath: string): void;
32+
}>;
33+
export type PerfNativeCaptureStartResult = Readonly<{
34+
pendingHandle: PendingTransferGuard<PerfNativeCaptureLiveHandle>;
35+
envelope: DurableResourceEnvelope<typeof PERF_CAPTURE_RESOURCE_KIND>;
36+
response: PerfData;
37+
}>;
38+
export type PerfNativeCaptureRecoveryInput = Readonly<{
39+
envelope: DurableResourceEnvelope<typeof PERF_CAPTURE_RESOURCE_KIND>;
40+
}>;
41+
export type PerfProfileReportInput = Readonly<{
42+
appId?: string;
43+
kind: 'xctrace' | 'simpleperf';
44+
tracePath: string;
45+
outPath: string;
46+
template?: string;
47+
profile?: PerfData;
48+
}>;
49+
export type PerfRuntimeOperations = Readonly<{
50+
perfFrames(input: PerfObservationInput): Promise<PerfData>;
51+
perfMemorySample(input: PerfObservationInput): Promise<PerfData>;
52+
perfMemorySnapshot(input: PerfMemorySnapshotInput): Promise<PerfData>;
53+
perfNativeCaptureStart(input: PerfNativeCaptureStartInput): Promise<PerfNativeCaptureStartResult>;
54+
perfNativeCaptureReattach(
55+
input: PerfNativeCaptureRecoveryInput,
56+
): Promise<ReattachOutcome<PerfNativeCaptureLiveHandle, PerfNativeCaptureCompletion>>;
57+
perfNativeCaptureCleanup(input: PerfNativeCaptureRecoveryInput): Promise<CleanupOutcome>;
58+
perfProfileReport(input: PerfProfileReportInput): Promise<PerfData>;
59+
}>;
60+
export function perfRuntimeOperationFacts(
61+
cells: Readonly<{
62+
frames: RuntimeOperationFact;
63+
memorySample: RuntimeOperationFact;
64+
memorySnapshot: RuntimeOperationFact;
65+
nativeCapture: RuntimeOperationFact;
66+
profileReport: RuntimeOperationFact;
67+
}>,
68+
): Readonly<{ [Key in keyof PerfRuntimeOperations]: RuntimeOperationFact }> {
69+
return Object.freeze({
70+
perfFrames: cells.frames,
71+
perfMemorySample: cells.memorySample,
72+
perfMemorySnapshot: cells.memorySnapshot,
73+
perfNativeCaptureStart: cells.nativeCapture,
74+
perfNativeCaptureReattach: cells.nativeCapture,
75+
perfNativeCaptureCleanup: cells.nativeCapture,
76+
perfProfileReport: cells.profileReport,
77+
});
78+
}

0 commit comments

Comments
 (0)