Skip to content

refactor(platform-apple): dedupe xctrace logic behind perf-xctrace and split perf.ts - #2823

Merged
thymikee merged 4 commits into
mainfrom
refactor/2795-perf-xctrace-dedupe
Sep 24, 2026
Merged

thymikee merged 4 commits into
mainfrom
refactor/2795-perf-xctrace-dedupe

Conversation

@thymikee

@thymikee thymikee commented Sep 23, 2026

Copy link
Copy Markdown
Member

Summary

Closes #2795. No change to perf output shapes, messages or error details.

  • perf.ts (1,111 → 690 lines) hands timed xctrace recording, table export and the trace-has-data check to perf-xctrace.ts. That file now has one record-argv builder, one retry loop (used by both timed and background recording), one exportAppleXctraceData, and one data check.
  • Process-target resolution moved to the new perf-target.ts, with its parseApplePsOutput test moved unchanged into perf-target.test.ts. This removes the perf.tsperf-xctrace.ts import cycle. The @agent-device/platform-apple/perf facade exports the same names as before.
  • perf.ts and perf-frame.ts share one XML ref resolver in perf-xml.ts.
  • physical-device-coredevice.ts now calls runIosDevicectlJsonRequest. That helper now returns the failure payload, which keeps tunnelState on not-ready errors, and removes its temp file when the command throws.

Where the issue doesn't match current code: the four export wrappers were split two in perf-xctrace.ts and two in perf.ts, and line numbers have moved.

11 files. Gross diff is 1,253 lines: about 400 are the required perf-target move and the net change is −131.

Validation

Tested at c5681af5e (rebased onto origin/main fa3b7291f for the #2832 Smoke fix).

  • pnpm check:affected --run: passed. .fallowrc.json fails open to the full local set; vitest-related ran 419 files and 2,961 tests.
  • pnpm check:fallow --base origin/main: no issues. The three removed forwarders' entries were deleted by hand; baselines are unchanged.
  • The unimported facade forwarders readAppleProcessSamples, resolveAppleExecutable and resolveIosDevicePerfTarget are removed. @agent-device/platform-apple is private, and the async forwarders came from 07c5a537b, which no tag contains.
  • CoreDevice readiness argv, with real devicectl 651.13.4 on Xcode 27.0 (27A5252f):
    • Old: devicectl device info details --device <id> --json-output <path> --timeout 15.
    • New: devicectl device info details --device <id> --timeout 15 --json-output <path>. This is the order devicectl device info details --help gives in USAGE.
    • With a bogus id (00000000-…), both orders exit 1 with The specified device was not found (CoreDeviceError 1000). Both write the same JSON; only the order of info.arguments differs. For comparison, an unknown option exits 64 with Unknown option.
    • Running ensureCoreDeviceReady from origin/main and from this branch through real devicectl (argv captured by an xcrun shim): a bogus id gives identical not_ready details on both, and a simulator visible to CoreDevice returns ready on both. Temp JSON files were removed afterwards.
  • Simulator perf (iPhone 17 Pro, Settings):
    • perf memory sample returned ps-process-snapshot (lazy perf-target path).
    • perf cpu profile start/stop --kind xctrace recorded a trace (shared record argv, retry loop, trace-data check).
    • perf cpu profile report exported --toc and the time-profile table, then ended with Apple xctrace CPU report contained no samples. origin/main parses the same XML to the same zero, because Xcode 27 emits <tagged-backtrace> and the parser only reads <backtrace>. That is a pre-existing issue and not part of this PR.
    • This does not cover the physical-device-only paths: recordAppleXctraceTimedTrace (Animation Hitches / Activity Monitor with --time-limit), the device target resolution through devicectl, and perf frames.
  • Adversarial review (fresh-context agent, asked to refute argv and behavior preservation): no blocking findings. Three nits, all more lenient than before and already noted in review: trim before stripping the pid suffix, the optional display-info read shares its catch, and nested ref ids are indexed.
  • Outstanding: no physical iOS device is attached to this host. Still to do on a physical iPhone: open <app> --debug, then perf frames and perf memory sample in the same session.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 23, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.79 MB 4.79 MB -1.6 kB
Package (unpacked) 4.79 MB 4.79 MB -1.6 kB
Package (download) 1.43 MB 1.43 MB -507 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.6 ms 26.3 ms -0.3 ms
CLI --help 77.3 ms 77.6 ms +0.3 ms

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at 709524a. The refactor looks sound, but merge readiness depends on evidence this PR does not yet have.

I looked for a smaller design and did not find one. The perf-target move looks like the minimum split needed to get perf.ts under 1,000 lines and break the perf.ts/perf-xctrace.ts cycle, the record-attempt callback looks like the smallest seam that lets the timed and background retry loops share one implementation, and the devicectl change is in scope because issue #2795 requires it. Does that match the intent, or is there a narrower cut I'm missing?

Not blocking: readDirectXmlProcess is not byte-equivalent to either old resolver (trim-before-suffix-strip changes 'Foo (12) ' to 'Foo', a placeholder child element now resolves to null instead of the old fmt name, and empty pid text now gives null instead of 0), runIosDevicectlJsonRequest's cleanup-on-throw and failure-payload paths have no test coverage, and the readiness probe's reordered --json-output/--timeout argv is only exercised by mocked tests — these can be taken or left.

Two things still need a live run on a physical iOS device. One call through ensureCoreDeviceReady (for example open <app>) that returns ready would confirm the reordered devicectl device info details ... --timeout N --json-output <path> argv is accepted. A perf frame or memory sample on the same device would confirm recordAppleXctraceTimedTrace and exportAppleXctraceData work end to end, though since the xctrace argv is byte-identical to before, that second check can be waived if the mocked-xcrun tests are accepted as sufficient.

No live device was available for this review, so the reordered devicectl argv is not confirmed to work. I did not run the test suite myself; the PR reports it passed at 709524a. CI was still pending at review time: all non-passing jobs were queued or in progress, none had failed, and the diff touches perf and devicectl code that Typecheck, Coverage, Integration, and Bundle Size exercise, while Smoke Tests do not drive physical-device perf or CoreDevice readiness.

The next step is a live run on a physical iOS device showing ensureCoreDeviceReady return ready with the reordered probe, followed by green CI.

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at 07c5a53. The CoreDevice readiness probe now goes through runIosDevicectlJsonRequest, which changes the argv order to devicectl device info details ... --timeout N --json-output <path> (https://github.com/callstack/agent-device/blob/07c5a53/packages/platform-apple/src/core/physical-device-coredevice.ts). Only mocked tests cover this. If real devicectl rejects the new order, every physical iOS device session fails readiness. Can you post a run on a physical iOS device of a command that goes through ensureCoreDeviceReady (for example open <app> with --debug), showing it reports ready and the new argv? A perf frame or memory sample on the same device would also cover the xctrace record and export path.

Not blocking: the three new forwarders in packages/platform-apple/src/perf-facade.ts:9 (readAppleProcessSamples, resolveAppleExecutable, resolveIosDevicePerfTarget) have no importer and stay only because .fallowrc.json lists them. They are unreleased, so you can delete them and their entries.

The Smoke Tests failure at live-snapshot-depth-frontier.ts:126 looks unrelated. It also fails on main, #2832 fixes it, and the simulator snapshot route does not reach the perf or CoreDevice code this diff changes.

The next step is the physical-device run above.

thymikee and others added 4 commits September 23, 2026 20:40
…checks behind perf-xctrace

perf.ts now delegates timed xctrace recording, table export and the
trace-has-data check to perf-xctrace.ts, which owns one record-argv
builder, one retry loop, one export implementation and one data check.
Process-target resolution moves to perf-target.ts so perf-xctrace no
longer imports perf.ts, bringing perf.ts under 1,000 lines.

perf.ts and perf-frame.ts share one xctrace XML reference resolver in
perf-xml.ts. physical-device-coredevice.ts reads device details through
the shared devicectl --json-output helper, which now carries the failure
payload and cleans up its temp file when the command throws.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hared process-name reader

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… facade

perf-facade.ts, perf.ts, and perf-xctrace.ts all statically imported the
newly split perf-target.ts, adding it as a genuinely new eager module to
the facade's closure. Load it on demand at each call site instead, the
same pattern already used by simctl-facade.ts and
runner-operations-facade.ts, keeping the facade's process-target path
implementation-lazy.
…from the perf facade

readAppleProcessSamples, resolveAppleExecutable and resolveIosDevicePerfTarget
had no importer through @agent-device/platform-apple/perf; perf.ts and
perf-xctrace.ts load perf-target.ts directly. The package is private and the
async forwarders were added on this unreleased branch, so remove them and
their .fallowrc.json entries instead of keeping them alive by allowlist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@thymikee
thymikee force-pushed the refactor/2795-perf-xctrace-dedupe branch from 07c5a53 to c5681af Compare September 23, 2026 18:49
@thymikee

thymikee commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

Addressed at c5681af5e, rebased onto fa3b7291f so Smoke picks up #2832.

1. CoreDevice readiness argv (blocking). A physical-device run is still outstanding: this host has no physical iOS device attached, only simulators. To close it on a physical iPhone, run agent-device open <app> --platform ios --udid <device> --debug, check that readiness passes and that the daemon log shows the probe argv, then run agent-device perf frames --json and agent-device perf memory sample --json in the same session.

What I could check without a device (Xcode 27.0, devicectl 651.13.4):

  • Old argv: devicectl device info details --device <id> --json-output <path> --timeout 15. New argv: ... --device <id> --timeout 15 --json-output <path>. The new order is the one devicectl device info details --help gives in USAGE: [--timeout <seconds>] [--json-output <path>].
  • With a bogus id, both orders exit 1 with The specified device was not found (CoreDeviceError 1000) and write the same JSON; only the order in info.arguments differs. A control run with an unknown option exits 64 with Unknown option '--bogus-flag', so a rejected parse would have looked different.
  • I ran ensureCoreDeviceReady from origin/main and from this branch through real devicectl, capturing argv with an xcrun shim. A bogus id gives identical not_ready details on both. A simulator visible to CoreDevice (info details succeeds, tunnelState: disconnected) returns ready on both. Temp JSON files were cleaned up.
  • I kept the new order. Both options are named options that take a value, and devicectl parsed both orders the same way. Restoring the old order would mean a caller-positioned --json-output option on the shared helper, and every other caller already puts it last.

2. Facade forwarders (non-blocking). Removed readAppleProcessSamples, resolveAppleExecutable, resolveIosDevicePerfTarget and their .fallowrc.json entries. @agent-device/platform-apple is private, and the async forwarders came from 07c5a537b, which no tag contains. pnpm check:fallow --base origin/main is clean, with no baseline changes.

Simulator perf, partial xctrace coverage. On the iPhone 17 Pro simulator with Settings:

  • perf memory sample passes.
  • perf cpu profile start/stop --kind xctrace records a trace through the shared record argv, retry loop and trace-data check.
  • perf cpu profile report runs both exports and then reports no samples. That is pre-existing: origin/main parses the same XML to zero, because Xcode 27 emits <tagged-backtrace>. It needs a separate issue, which I have not filed.
  • Not covered: the timed --time-limit recording, device target resolution through devicectl, and frames. Those need the physical run above.

pnpm check:affected --run passes (full set, 2,961 tests). A fresh-context adversarial review found nothing blocking, only the three already-known leniency nits.

🤖 Generated with Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

This is clean at c5681af. The Smoke failure I saw at the earlier head is fixed by #2832, and this branch is now rebased onto fa3b729 so it picks that fix up. All 18 checks are green, and the facade deletion here does not touch any route that was failing.

The devicectl argv question from the earlier review is closed on your host evidence: real devicectl returned ready on a CoreDevice-visible simulator, parsing matched for bogus ids, and an unknown-option control failed differently. That does not cover a physical iPhone, where the only different payload content is tunnelState, and its parser is unchanged, so the residual risk there is small.

The timed --time-limit xctrace recording and the frames export were not run live; the earlier review accepted this because the xctrace argv is byte-identical and is covered by the mocked-xcrun tests.

I did not run tests or fallow, and I did not check git tag --contains on 07c5a53.

Nothing here needs to happen before merge. If you want extra confidence, a maintainer could still run open --debug plus perf frames once on a physical iPhone, but that's optional.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 23, 2026
@thymikee
thymikee merged commit fd8277e into main Sep 24, 2026
18 checks passed
@thymikee
thymikee deleted the refactor/2795-perf-xctrace-dedupe branch September 24, 2026 05:48
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-24 05:48 UTC

@thymikee

thymikee commented Sep 24, 2026

Copy link
Copy Markdown
Member Author

Physical-device run at main fd8277e39 (includes this PR). Device: thymikee-iphone (iPhone18,1), 00008150-001849640CF8401C, paired over Wi-Fi. Xcode 27.0 beta 6, xctrace 27.0. An isolated daemon ran behind an xcrun shim that logs full argv.

Readiness (ensureCoreDeviceReady). open com.apple.Preferences --platform ios --udid … --debug reported Opened. The daemon sent the new argv to the real device, and it succeeded:

devicectl device info details --device 00008150-001849640CF8401C --timeout 15 --json-output <tmp>/agent-device-coredevice-info-….json
devicectl device info details --device 00008150-001849640CF8401C --timeout 10 --json-output <tmp>/agent-device-coredevice-info-….json

open then launched the app. No temp JSON files were left behind.

perf (same session)

  • perf memory sample --json, Settings: available: true, residentMemoryKb 155024 and 138336 on two runs, method xctrace-activity-monitor. This covers xctrace record --all-processes plus xctrace export.
  • perf frames --json, Agent Device Tester and Settings: available: true, method xctrace-animation-hitches, 120 Hz, deadline 8.3 ms. Both screens were idle, so 0 frames were counted. The record argv matches the pre-PR builder exactly.

One caveat: the phone was locked. The first two perf frames attempts failed. xctrace exited 21 with Cannot find process for provided pid. The phone had auto-locked about a minute after open. I reproduced it deliberately:

  • Locked: open still reports Opened but does not bring the app forward (same stale pid). perf frames fails with that same pid error, and perf memory sample times out (xcrun timed out after 60000ms).
  • Unlocked, same session: perf frames gives available: true, 122 frames, 0 dropped, 120 Hz. perf memory sample gives 62384 kB for AgentDeviceTester.

The AgentDeviceTester numbers come from its process showing the Expo dev-client launcher, because Metro on localhost is unreachable from the device. They prove the attach, record and export plumbing, not the app's own frame or memory profile.

The argv is identical in the locked and unlocked runs, so this PR is not involved. devicectl device info lockState reported passcodeRequired: false while the phone was locked, so it cannot detect this state. Surfacing a clear "device locked" refusal would be a separate improvement.

Session closed and daemon stopped with --clean.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(platform-apple): dedupe xctrace logic between perf.ts and perf-xctrace.ts; split perf.ts under 1,000 lines

1 participant