Skip to content

Commit 66bb3bc

Browse files
committed
fix: clean up Android native perf on session close
1 parent 5795c94 commit 66bb3bc

6 files changed

Lines changed: 156 additions & 1 deletion

File tree

‎src/daemon/handlers/__tests__/session-close-shutdown.test.ts‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ vi.mock('../../../platforms/ios/perf-xctrace.ts', async (importOriginal) => {
2222
const actual = await importOriginal<typeof import('../../../platforms/ios/perf-xctrace.ts')>();
2323
return { ...actual, cleanupAppleXctracePerfCapture: vi.fn(async () => ({})) };
2424
});
25+
vi.mock('../../../platforms/android/perf.ts', async (importOriginal) => {
26+
const actual = await importOriginal<typeof import('../../../platforms/android/perf.ts')>();
27+
return { ...actual, cleanupAndroidNativePerfSession: vi.fn(async () => {}) };
28+
});
2529
vi.mock('../../../platforms/ios/macos-helper.ts', async (importOriginal) => {
2630
const actual = await importOriginal<typeof import('../../../platforms/ios/macos-helper.ts')>();
2731
return { ...actual, runMacOsAlertAction: vi.fn(async () => {}) };
@@ -44,10 +48,12 @@ import { teardownSessionResources } from '../session-close.ts';
4448
import { shutdownSimulator } from '../../../platforms/ios/simulator.ts';
4549
import { runCmd } from '../../../utils/exec.ts';
4650
import { cleanupAppleXctracePerfCapture } from '../../../platforms/ios/perf-xctrace.ts';
51+
import { cleanupAndroidNativePerfSession } from '../../../platforms/android/perf.ts';
4752

4853
const mockShutdownSimulator = vi.mocked(shutdownSimulator);
4954
const mockRunCmd = vi.mocked(runCmd);
5055
const mockCleanupAppleXctracePerfCapture = vi.mocked(cleanupAppleXctracePerfCapture);
56+
const mockCleanupAndroidNativePerfSession = vi.mocked(cleanupAndroidNativePerfSession);
5157

5258
const noopInvoke = async (_req: DaemonRequest): Promise<DaemonResponse> => ({ ok: true, data: {} });
5359

@@ -291,6 +297,87 @@ test('daemon session teardown stops active Apple xctrace perf capture', async ()
291297
expect(session.applePerf?.active).toBeUndefined();
292298
});
293299

300+
test('close stops active Android native perf capture before deleting session', async () => {
301+
const sessionStore = makeSessionStore();
302+
const sessionName = 'android-active-native-perf-session';
303+
const activeCapture = {
304+
type: 'trace',
305+
kind: 'perfetto',
306+
packageName: 'com.example.app',
307+
appPid: '1234',
308+
profilerPid: '5678',
309+
remotePath: '/data/misc/perfetto-traces/app.perfetto-trace',
310+
outPath: '/tmp/app.perfetto-trace',
311+
startedAt: Date.now(),
312+
state: 'running',
313+
};
314+
const session = {
315+
...makeSession(sessionName, {
316+
platform: 'android',
317+
id: 'emulator-5554',
318+
name: 'Pixel',
319+
kind: 'emulator',
320+
booted: true,
321+
}),
322+
appBundleId: 'com.example.app',
323+
nativePerf: {
324+
android: activeCapture,
325+
},
326+
} as unknown as SessionState;
327+
sessionStore.set(sessionName, session);
328+
329+
const response = await handleSessionCommands({
330+
req: {
331+
token: 't',
332+
session: sessionName,
333+
command: 'close',
334+
positionals: [],
335+
flags: {},
336+
},
337+
sessionName,
338+
logPath: path.join(os.tmpdir(), 'daemon.log'),
339+
sessionStore,
340+
invoke: noopInvoke,
341+
});
342+
343+
expect(response?.ok).toBe(true);
344+
expect(mockCleanupAndroidNativePerfSession).toHaveBeenCalledWith(session.device, activeCapture);
345+
expect(sessionStore.get(sessionName)).toBeUndefined();
346+
});
347+
348+
test('daemon session teardown stops active Android native perf capture', async () => {
349+
const sessionName = 'android-active-native-perf-teardown-session';
350+
const activeCapture = {
351+
type: 'cpu-profile',
352+
kind: 'simpleperf',
353+
packageName: 'com.example.app',
354+
appPid: '1234',
355+
profilerPid: '5678',
356+
remotePath: '/data/local/tmp/cpu.perf.data',
357+
outPath: '/tmp/cpu.perf.data',
358+
startedAt: Date.now(),
359+
state: 'running',
360+
};
361+
const session = {
362+
...makeSession(sessionName, {
363+
platform: 'android',
364+
id: 'emulator-5554',
365+
name: 'Pixel',
366+
kind: 'emulator',
367+
booted: true,
368+
}),
369+
appBundleId: 'com.example.app',
370+
nativePerf: {
371+
android: activeCapture,
372+
},
373+
} as unknown as SessionState;
374+
375+
await teardownSessionResources(session, sessionName);
376+
377+
expect(mockCleanupAndroidNativePerfSession).toHaveBeenCalledWith(session.device, activeCapture);
378+
expect(session.nativePerf?.android).toBeUndefined();
379+
});
380+
294381
test('close --shutdown is ignored for Android devices', async () => {
295382
const sessionStore = makeSessionStore();
296383
const sessionName = 'android-device-shutdown-session';

‎src/daemon/handlers/session-close.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { SessionStore } from '../session-store.ts';
88
import { stopAppLog } from '../app-log.ts';
99
import { stopIosRunnerSession } from '../../platforms/ios/runner-client.ts';
1010
import { cleanupAppleXctracePerfCapture } from '../../platforms/ios/perf-xctrace.ts';
11+
import { cleanupAndroidNativePerfSession } from '../../platforms/android/perf.ts';
1112
import { clearRuntimeHintsFromApp, hasRuntimeTransportHints } from '../runtime-hints.ts';
1213
import { cleanupRetainedMaterializedPathsForSession } from '../materialized-path-registry.ts';
1314
import {
@@ -72,6 +73,13 @@ async function stopSessionApplePerfCapture(session: SessionState): Promise<void>
7273
session.applePerf = { ...(session.applePerf ?? {}), active: undefined };
7374
}
7475

76+
async function stopSessionAndroidNativePerfCapture(session: SessionState): Promise<void> {
77+
const active = session.nativePerf?.android;
78+
if (!active) return;
79+
await cleanupAndroidNativePerfSession(session.device, active);
80+
session.nativePerf = { ...(session.nativePerf ?? {}), android: undefined };
81+
}
82+
7583
export async function teardownSessionResources(
7684
session: SessionState,
7785
sessionName: string,
@@ -80,6 +88,7 @@ export async function teardownSessionResources(
8088
await stopAppLog(session.appLog);
8189
}
8290
await stopSessionApplePerfCapture(session);
91+
await stopSessionAndroidNativePerfCapture(session);
8392
if (isApplePlatform(session.device.platform)) {
8493
await stopAppleRunnerForClose(session);
8594
}
@@ -101,6 +110,7 @@ export async function handleCloseCommand(params: {
101110
await stopAppLog(session.appLog);
102111
}
103112
await stopSessionApplePerfCapture(session);
113+
await stopSessionAndroidNativePerfCapture(session);
104114
if (req.positionals && req.positionals.length > 0) {
105115
if (shouldStopAppleRunnerBeforeTargetedClose(session)) {
106116
await stopAppleRunnerForClose(session);

‎src/platforms/android/__tests__/perf.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { test } from 'vitest';
66
import type { AndroidAdbExecutor } from '../adb-executor.ts';
77
import {
88
captureAndroidHeapSnapshot,
9+
cleanupAndroidNativePerfSession,
910
parseAndroidFramePerfSample,
1011
parseAndroidMemInfoSample,
1112
startAndroidPerfettoTrace,
@@ -443,6 +444,46 @@ test('stopAndroidSimpleperfProfile fails before pull when remote artifact never
443444
);
444445
});
445446

447+
test('cleanupAndroidNativePerfSession stops profiler and removes remote artifact without pulling', async () => {
448+
const session: AndroidNativePerfSession = {
449+
type: 'trace',
450+
kind: 'perfetto',
451+
packageName: 'com.example.app',
452+
appPid: '1234',
453+
profilerPid: '8765',
454+
remotePath: '/data/misc/perfetto-traces/app.perfetto-trace',
455+
outPath: '/tmp/app.perfetto-trace',
456+
startedAt: Date.now() - 1000,
457+
state: 'running',
458+
};
459+
const calls: string[][] = [];
460+
const adb: AndroidAdbExecutor = async (args) => {
461+
calls.push(args);
462+
if (args[0] === 'shell' && args[1]?.includes('kill -INT')) {
463+
return { exitCode: 0, stdout: '', stderr: '' };
464+
}
465+
if (args[0] === 'shell' && args[1]?.includes('stat -c %s')) {
466+
return { exitCode: 0, stdout: '5\n', stderr: '' };
467+
}
468+
if (args[0] === 'shell' && args[1]?.includes('rm -f')) {
469+
return { exitCode: 0, stdout: '', stderr: '' };
470+
}
471+
if (args[0] === 'pull') {
472+
throw new Error('cleanup must not pull artifacts');
473+
}
474+
throw new Error(`Unexpected adb call: ${args.join(' ')}`);
475+
};
476+
477+
await cleanupAndroidNativePerfSession(ANDROID_EMULATOR, session, { adb });
478+
479+
assert.ok(findCallIndex(calls, 'kill -INT') < findCallIndex(calls, 'stat -c %s'));
480+
assert.ok(findCallIndex(calls, 'rm -f') > findCallIndex(calls, 'stat -c %s'));
481+
assert.equal(
482+
calls.some((args) => args[0] === 'pull'),
483+
false,
484+
);
485+
});
486+
446487
test('start and stop Android Perfetto trace use perfetto trace storage and cleanup remote artifact', async () => {
447488
const tmpDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'agent-device-perfetto-test-'));
448489
const outPath = path.join(tmpDir, 'app.perfetto-trace');

‎src/platforms/android/perf-native.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,22 @@ export async function stopAndroidPerfettoTrace(
249249
return await stopAndroidNativePerfSession(device, { ...session, outPath }, options);
250250
}
251251

252+
export async function cleanupAndroidNativePerfSession(
253+
device: DeviceInfo,
254+
session: AndroidNativePerfSession,
255+
options: AndroidNativePerfOptions = {},
256+
): Promise<void> {
257+
const adb = resolveAndroidAdbExecutor(device, options.adb);
258+
try {
259+
if (session.state === 'running') {
260+
await stopAndroidBackgroundTool(adb, session);
261+
await waitForAndroidNativeArtifact(adb, session).catch(() => {});
262+
}
263+
} finally {
264+
await cleanupAndroidRemotePath(adb, session.remotePath);
265+
}
266+
}
267+
252268
async function stopAndroidNativePerfSession(
253269
device: DeviceInfo,
254270
session: AndroidNativePerfSession,

‎src/platforms/android/perf.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export {
1616
type AndroidFramePerfSample,
1717
} from './perf-frame.ts';
1818
export {
19+
cleanupAndroidNativePerfSession,
1920
startAndroidPerfettoTrace,
2021
startAndroidSimpleperfProfile,
2122
stopAndroidPerfettoTrace,

‎website/docs/docs/client-api.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ Additional CLI-backed methods are exposed on their domain groups with typed opti
263263
- `client.recording.record()` and `client.recording.trace()`
264264
- `client.settings.update()`
265265

266-
`client.observability.perf()` returns daemon-shaped JSON so local and remote transports expose the same metrics payload. Pass `{ area: 'metrics' }` for the broad startup/CPU/memory/frame first pass, `{ area: 'frames' }` for a focused frame/jank-health payload, or `{ area: 'memory', action: 'sample' }` for a compact memory-only sample. Use `{ area: 'memory', action: 'snapshot', kind: 'android-hprof', out: 'app.hprof' }` on Android or `{ area: 'memory', action: 'snapshot', kind: 'memgraph', out: 'app.memgraph' }` on supported Apple simulator/macOS app sessions to write large memory artifacts to disk. Android native artifacts use `{ area: 'cpu', mode: 'profile', action: 'start' | 'stop' | 'report', kind: 'simpleperf', out }` and `{ area: 'trace', action: 'start' | 'stop', kind: 'perfetto', out }`; these Android-only commands return artifact paths and compact summaries, not trace/profile contents. Physical iOS device memgraph capture reports unavailable with a reason/hint. heapprofd allocation tracing is deferred until Perfetto plumbing is available. On Android and supported Apple targets, `data.metrics.fps.droppedFramePercent` is the primary frame-smoothness value. Android derives it from the current `adb shell dumpsys gfxinfo <package> framestats` window; connected iOS devices derive it from `xcrun xctrace` Animation Hitches for the active app process. Frame samples include `windowStartedAt`, `windowEndedAt`, and `worstWindows` so agents can correlate dropped-frame clusters with logs, network entries, and their own session actions. A successful Android read resets Android frame stats; `open <app>` resets the Android frame window too, so agents can call `perf({ area: 'frames' })`, perform a transition or gesture, then call it again to inspect that focused window. iOS simulator and macOS app sessions report frame health as unavailable rather than inventing FPS or dropped-frame values.
266+
`client.observability.perf()` returns daemon-shaped JSON so local and remote transports expose the same metrics payload. Pass `{ area: 'metrics' }` for the broad startup/CPU/memory/frame first pass, `{ area: 'frames' }` for a focused frame/jank-health payload, or `{ area: 'memory', action: 'sample' }` for a compact memory-only sample. Use `{ area: 'memory', action: 'snapshot', kind: 'android-hprof', out: 'app.hprof' }` on Android or `{ area: 'memory', action: 'snapshot', kind: 'memgraph', out: 'app.memgraph' }` on supported Apple simulator/macOS app sessions to write large memory artifacts to disk. Android native artifacts use `{ area: 'cpu', subject: 'profile', action: 'start' | 'stop' | 'report', kind: 'simpleperf', out }` and `{ area: 'trace', action: 'start' | 'stop', kind: 'perfetto', out }`; these Android-only commands return artifact paths and compact summaries, not trace/profile contents. Physical iOS device memgraph capture reports unavailable with a reason/hint. heapprofd allocation tracing is deferred until Perfetto plumbing is available. On Android and supported Apple targets, `data.metrics.fps.droppedFramePercent` is the primary frame-smoothness value. Android derives it from the current `adb shell dumpsys gfxinfo <package> framestats` window; connected iOS devices derive it from `xcrun xctrace` Animation Hitches for the active app process. Frame samples include `windowStartedAt`, `windowEndedAt`, and `worstWindows` so agents can correlate dropped-frame clusters with logs, network entries, and their own session actions. A successful Android read resets Android frame stats; `open <app>` resets the Android frame window too, so agents can call `perf({ area: 'frames' })`, perform a transition or gesture, then call it again to inspect that focused window. iOS simulator and macOS app sessions report frame health as unavailable rather than inventing FPS or dropped-frame values.
267267

268268
For Apple native profiling, call `perf({ area: 'cpu', subject: 'profile', action: 'start', kind: 'xctrace', template: 'Time Profiler', out: 'app.trace' })`, then stop with the same trace path and write a compact report with `action: 'report'`. `area: 'trace'` supports xctrace templates such as `Animation Hitches`. Responses include artifact paths and compact metadata only.
269269

0 commit comments

Comments
 (0)