Skip to content

Commit d437efe

Browse files
authored
feat: generalize perf sampling for Apple app sessions (#351)
* feat: generalize perf sampling for Apple app sessions * fix: report physical ios perf as unsupported * refactor: simplify Apple perf sampling flow * docs: refresh debugging skill guidance
1 parent f2b0262 commit d437efe

9 files changed

Lines changed: 730 additions & 43 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ In non-JSON mode, core mutating commands print a short success acknowledgment so
6565

6666
- Startup timing is available on iOS and Android from `open` command round-trip sampling.
6767
- Android app sessions also sample CPU (`adb shell dumpsys cpuinfo`) and memory (`adb shell dumpsys meminfo <package>`) when the session has an active app package context.
68-
- Android CPU data is a recent process snapshot and may read as `0` for idle apps; Android memory values are reported in kilobytes from `dumpsys meminfo`.
68+
- Apple app sessions on macOS and iOS simulators also sample CPU and memory from process snapshots resolved from the active app bundle ID.
69+
- Physical iOS devices still report CPU and memory as unavailable in this release.
6970

7071
## Where To Go Next
7172

skills/agent-device/references/debugging.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Logging is off by default. Enable it only when you need a debugging window.
3636
- Default app logs live under `~/.agent-device/sessions/<session>/app.log`.
3737
- `logs clear --restart` is the fastest clean repro loop.
3838
- `network dump [limit] [summary|headers|body|all]` parses recent HTTP(s) entries from the same session app log.
39+
- On macOS, `network dump` is app-scoped and only sees Unified Logging associated with the active session app.
3940
- On iOS simulators, `network dump` can recover recent app log history with `simctl log show` when the live session stream is sparse, so check the returned notes before assuming the repro window was empty.
4041
- On iOS, `network dump` is still limited to what Unified Logging exposes for the app process. If the app does not emit request metadata there, `network dump` can legitimately return no HTTP entries even during a real repro.
4142
- Summary output already shows timestamp, status, and duration when the log backend exposes them.
@@ -44,6 +45,7 @@ Logging is off by default. Enable it only when you need a debugging window.
4445
- `logs doctor` checks backend and runtime readiness for the current session and device.
4546
- `logs mark "before tap"` inserts a timestamped marker into the app log.
4647
- Android `network dump` surfaces timestamps from logcat-style prefixes and can backfill status and request/response duration from adjacent GIBSDK packet lines, so check it before dumping raw log windows.
48+
- Android app-log streaming rebinds to the current app PID after relaunches, so rerun the repro window before assuming the last log slice is stale.
4749
- Marker lines are emitted with the `[agent-device][mark][...]` prefix. When you grep later, prefer a narrow pattern such as `grep -n -E "agent-device.*mark|before tap" <path>`.
4850
- Session app logs can contain runtime data, headers, or payload fragments. Review them before sharing.
4951
- `logs start` requires an active app session and appends to `app.log`.
@@ -78,16 +80,16 @@ If the app showed a visible warning or error overlay during the flow:
7880

7981
## Alerts and permissions
8082

81-
Use `alert` for iOS simulator permission dialogs instead of tapping coordinates.
83+
Use `alert` for iOS simulator permission dialogs and macOS desktop alerts instead of tapping coordinates.
8284

8385
```bash
8486
agent-device alert wait 5000
8587
agent-device alert accept
8688
```
8789

88-
- `alert` is only supported on iOS simulators.
90+
- `alert` is supported on iOS simulators and macOS desktop targets.
8991
- `alert accept` and `alert dismiss` retry internally for a short window, so you usually do not need manual sleeps.
90-
- If a permission sheet is visible in `snapshot` or `screenshot` but `alert accept` says no alert was found, treat it as normal tappable UI for that run: take a scoped `snapshot -i -s "<visible label>"` and `press @ref` instead of looping on `alert`.
92+
- If a permission sheet or modal is visible in `snapshot` or `screenshot` but `alert accept` says no alert was found, treat it as normal tappable UI for that run: take a scoped `snapshot -i -s "<visible label>"` and `press @ref` instead of looping on `alert`.
9193
- iOS 16+ "Allow Paste" prompts are suppressed under XCUITest. Use `xcrun simctl pbcopy booted` when you need to seed simulator clipboard content directly.
9294

9395
## Setup problems worth recognizing early

skills/agent-device/references/verification.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,5 @@ agent-device perf --json
107107
- `startup` is command round-trip timing around `open`.
108108
- It is not true first-frame or first-interactive telemetry.
109109
- Android app sessions also expose `memory` (`dumpsys meminfo`) and `cpu` (`dumpsys cpuinfo`) snapshots when the session has an app package context.
110-
- iOS still reports `fps`, `memory`, and `cpu` as unavailable placeholders.
110+
- Apple app sessions on macOS and iOS simulators also expose `memory` and `cpu` process snapshots when the session has an app bundle ID.
111+
- `fps` is still unavailable, and physical iOS devices still leave `memory` and `cpu` unavailable in this release.

src/daemon/handlers/__tests__/session.test.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,198 @@ test('perf preserves successful metrics and normalizes per-metric Android sampli
20202020
}
20212021
});
20222022

2023+
test('perf samples Apple cpu and memory metrics on macOS app sessions', async () => {
2024+
const sessionStore = makeSessionStore();
2025+
const sessionName = 'perf-session-macos';
2026+
sessionStore.set(sessionName, {
2027+
...makeSession(sessionName, {
2028+
platform: 'macos',
2029+
id: 'host-mac',
2030+
name: 'Host Mac',
2031+
kind: 'device',
2032+
target: 'desktop',
2033+
booted: true,
2034+
}),
2035+
appBundleId: 'com.example.mac',
2036+
});
2037+
mockRunCmd.mockImplementation(async (cmd, _args) => {
2038+
if (cmd === 'mdfind') {
2039+
return { stdout: '/Applications/Example.app\n', stderr: '', exitCode: 0 };
2040+
}
2041+
if (cmd === 'plutil') {
2042+
return { stdout: 'ExampleExec\n', stderr: '', exitCode: 0 };
2043+
}
2044+
if (cmd === 'ps') {
2045+
return {
2046+
stdout: [
2047+
'111 7.5 4096 /Applications/Example.app/Contents/MacOS/ExampleExec',
2048+
'222 0.5 1024 /Applications/Example.app/Contents/MacOS/ExampleExec --flag',
2049+
'333 5.0 2048 /Applications/Other.app/Contents/MacOS/OtherExec',
2050+
].join('\n'),
2051+
stderr: '',
2052+
exitCode: 0,
2053+
};
2054+
}
2055+
return { stdout: '', stderr: '', exitCode: 0 };
2056+
});
2057+
2058+
const response = await handleSessionCommands({
2059+
req: {
2060+
token: 't',
2061+
session: sessionName,
2062+
command: 'perf',
2063+
positionals: [],
2064+
flags: {},
2065+
},
2066+
sessionName,
2067+
logPath: path.join(os.tmpdir(), 'daemon.log'),
2068+
sessionStore,
2069+
invoke: noopInvoke,
2070+
});
2071+
2072+
expect(response?.ok).toBe(true);
2073+
if (!response?.ok) throw new Error('Expected perf response to succeed for macOS session');
2074+
const memory = (response.data?.metrics as any)?.memory;
2075+
const cpu = (response.data?.metrics as any)?.cpu;
2076+
expect(memory?.available).toBe(true);
2077+
expect(memory?.residentMemoryKb).toBe(5120);
2078+
expect(cpu?.available).toBe(true);
2079+
expect(cpu?.usagePercent).toBe(8);
2080+
expect(cpu?.matchedProcesses).toEqual(['ExampleExec']);
2081+
});
2082+
2083+
test('perf samples Apple cpu and memory metrics on iOS simulator app sessions', async () => {
2084+
const sessionStore = makeSessionStore();
2085+
const sessionName = 'perf-session-ios-sim';
2086+
sessionStore.set(sessionName, {
2087+
...makeSession(sessionName, {
2088+
platform: 'ios',
2089+
id: 'sim-1',
2090+
name: 'iPhone 17 Pro',
2091+
kind: 'simulator',
2092+
booted: true,
2093+
}),
2094+
appBundleId: 'com.example.sim',
2095+
});
2096+
mockRunCmd.mockImplementation(async (cmd, args) => {
2097+
if (cmd === 'xcrun' && args.includes('get_app_container')) {
2098+
return { stdout: '/tmp/Example.app\n', stderr: '', exitCode: 0 };
2099+
}
2100+
if (cmd === 'plutil') {
2101+
return { stdout: 'ExampleSimExec\n', stderr: '', exitCode: 0 };
2102+
}
2103+
if (cmd === 'xcrun' && args.includes('spawn') && args.includes('ps')) {
2104+
return {
2105+
stdout: ['111 11.0 6144 ExampleSimExec', '222 2.0 2048 SpringBoard'].join('\n'),
2106+
stderr: '',
2107+
exitCode: 0,
2108+
};
2109+
}
2110+
return { stdout: '', stderr: '', exitCode: 0 };
2111+
});
2112+
2113+
const response = await handleSessionCommands({
2114+
req: {
2115+
token: 't',
2116+
session: sessionName,
2117+
command: 'perf',
2118+
positionals: [],
2119+
flags: {},
2120+
},
2121+
sessionName,
2122+
logPath: path.join(os.tmpdir(), 'daemon.log'),
2123+
sessionStore,
2124+
invoke: noopInvoke,
2125+
});
2126+
2127+
expect(response?.ok).toBe(true);
2128+
if (!response?.ok) throw new Error('Expected perf response to succeed for iOS simulator session');
2129+
const memory = (response.data?.metrics as any)?.memory;
2130+
const cpu = (response.data?.metrics as any)?.cpu;
2131+
expect(memory?.available).toBe(true);
2132+
expect(memory?.residentMemoryKb).toBe(6144);
2133+
expect(cpu?.available).toBe(true);
2134+
expect(cpu?.usagePercent).toBe(11);
2135+
expect(cpu?.matchedProcesses).toEqual(['ExampleSimExec']);
2136+
});
2137+
2138+
test('perf degrades Apple cpu and memory metrics on physical iOS devices', async () => {
2139+
const sessionStore = makeSessionStore();
2140+
const sessionName = 'perf-session-ios-device';
2141+
sessionStore.set(sessionName, {
2142+
...makeSession(sessionName, {
2143+
platform: 'ios',
2144+
id: 'ios-device-1',
2145+
name: 'iPhone Device',
2146+
kind: 'device',
2147+
booted: true,
2148+
}),
2149+
appBundleId: 'com.example.device',
2150+
});
2151+
2152+
const response = await handleSessionCommands({
2153+
req: {
2154+
token: 't',
2155+
session: sessionName,
2156+
command: 'perf',
2157+
positionals: [],
2158+
flags: {},
2159+
},
2160+
sessionName,
2161+
logPath: path.join(os.tmpdir(), 'daemon.log'),
2162+
sessionStore,
2163+
invoke: noopInvoke,
2164+
});
2165+
2166+
expect(response?.ok).toBe(true);
2167+
if (!response?.ok) throw new Error('Expected perf response to succeed for physical iOS session');
2168+
const memory = (response.data?.metrics as any)?.memory;
2169+
const cpu = (response.data?.metrics as any)?.cpu;
2170+
expect(memory?.available).toBe(false);
2171+
expect(memory?.reason).toMatch(/not yet implemented for physical iOS devices/i);
2172+
expect(cpu?.available).toBe(false);
2173+
expect(cpu?.reason).toMatch(/not yet implemented for physical iOS devices/i);
2174+
});
2175+
2176+
test('perf reports physical iOS cpu and memory as unsupported even without an app bundle id', async () => {
2177+
const sessionStore = makeSessionStore();
2178+
const sessionName = 'perf-session-ios-device-no-bundle';
2179+
sessionStore.set(sessionName, {
2180+
...makeSession(sessionName, {
2181+
platform: 'ios',
2182+
id: 'ios-device-2',
2183+
name: 'iPhone Device',
2184+
kind: 'device',
2185+
booted: true,
2186+
}),
2187+
});
2188+
2189+
const response = await handleSessionCommands({
2190+
req: {
2191+
token: 't',
2192+
session: sessionName,
2193+
command: 'perf',
2194+
positionals: [],
2195+
flags: {},
2196+
},
2197+
sessionName,
2198+
logPath: path.join(os.tmpdir(), 'daemon.log'),
2199+
sessionStore,
2200+
invoke: noopInvoke,
2201+
});
2202+
2203+
expect(response?.ok).toBe(true);
2204+
if (!response?.ok) {
2205+
throw new Error('Expected perf response to succeed for physical iOS session without bundle id');
2206+
}
2207+
const memory = (response.data?.metrics as any)?.memory;
2208+
const cpu = (response.data?.metrics as any)?.cpu;
2209+
expect(memory?.available).toBe(false);
2210+
expect(memory?.reason).toMatch(/not yet implemented for physical iOS devices/i);
2211+
expect(cpu?.available).toBe(false);
2212+
expect(cpu?.reason).toMatch(/not yet implemented for physical iOS devices/i);
2213+
});
2214+
20232215
test('open URL on existing iOS session clears stale app bundle id', async () => {
20242216
const sessionStore = makeSessionStore();
20252217
const sessionName = 'ios-session';

0 commit comments

Comments
 (0)