Skip to content

feat(react-devtools): profile React renders through the Metro inspector - #3613

Open
thymikee wants to merge 7 commits into
mainfrom
feat/react-devtools-profiler
Open

thymikee wants to merge 7 commits into
mainfrom
feat/react-devtools-profiler

Conversation

@thymikee

@thymikee thymikee commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

agent-device react-devtools is now React Native performance profiling, built into agent-device. profile start and profile stop attach to the app's JavaScript runtime through its Metro dev server (CDP Runtime.evaluate) and drive the React DevTools backend that development builds already ship. The app, Metro config and Babel config need no changes, and there is no helper to start or wait for.

$ agent-device react-devtools profile start
$ agent-device react-devtools profile stop
React profile 3.0s: 8 commits, 1197ms rendering, 8 over 16ms (worst #0 170ms)
1. memo-defeated ContactRow: memo bypassed on 480/480 updates; new highlight (object) 480x, onSelect (function) 480x from ContactList -> wrap those props in useCallback/useMemo in ContactList

stop prints ranked findings (about 100–200 tokens), saves the profile, and ends with a More: line naming the drill-down views over the saved profile:

agent-device react-devtools profile report ContactRow   # causes, changed props and their kinds, owners, costliest commits
agent-device react-devtools profile commit 6            # one commit's components, grouped, by self time
agent-device react-devtools profile slow --limit 5
agent-device react-devtools profile rerenders --limit 5
agent-device react-devtools profile timeline --limit 20 --sort duration

Tables say how many rows they omit. Each subcommand rejects options it does not read. If one app is connected from several devices, profiling fails closed until --metro-device names one.

Removed:

  • the npm exec agent-react-devtools passthrough (status, start, wait, get tree, find, errors, profile export, profile diff)
  • its 8097 remote tunnel and port reverse
  • the injected-dispatch seam that only that port reverse used

Help and docs now describe profiling only.

Validation

At 639d73d05d, check:affected --run, check:production-exports and check:di-seams all pass.

The Perf lab (#3612) was run through agent-device open/click/wait plus profile start/stop:

RN platform expected first finding
0.74 / 0.76 / 0.80 / 0.83 Android 7/7 each
0.83 iOS simulator 7/7
0.88-rc Android, final build 7/7, plus a smoke of the route rework

Drill-downs, live: on Android RN 0.88-rc after the memo-defeated scenario, profile report ContactRow printed its causes, the changed prop kinds and its costliest commits. On the RN 0.83 iOS simulator, profile report ProductTile printed both of its findings.

Not verified: remote bridges; help and docs say so.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-3613/

Built to branch gh-pages at 2026-10-14 18:14 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.24 MB 5.27 MB +31.7 kB
Package (unpacked) 5.24 MB 5.27 MB +31.7 kB
Package (download) 1.58 MB 1.59 MB +10.9 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 18.9 ms 20.2 ms +1.3 ms
CLI --help 54.0 ms 58.5 ms +4.6 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 25 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread website/docs/docs/debugging-profiling.md Outdated
Comment thread website/docs/docs/debugging-profiling.md Outdated
Comment thread src/cli/react-profiler/react-profile-command.test.ts
Comment thread src/cli/react-profiler/react-profile-analysis.ts Outdated
Comment thread src/cli/react-profiler/react-profiler-runtime.fixtures.ts Outdated
Comment thread src/cli/react-profiler/react-profile-format.ts Outdated
Comment thread src/cli/react-profiler/react-profile-command.ts Outdated
Comment thread src/cli/react-profiler/react-profile-command.ts
Comment thread src/cli/react-profiler/react-profile-command.ts Outdated
Comment thread src/cli/react-profiler/react-profile-command.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

At f87e64a, the Metro-inspector transport and injected runtime look like real new capability, but two problems need fixing before merge.

selectReactNativeTarget ignores the session's device (https://github.com/callstack/agent-device/blob/f87e64a/src/cli/react-profiler/metro-inspector.ts#L124). When one Metro serves the same app on two devices, it picks the last one listed (.at(-1)), and react-profile-command.ts never passes a device to narrow it. So --session qa1 react-devtools profile start can record on qa2's runtime, and qa1's stop can save another device's profile under react-profiles/qa1.json. Nothing errors, so the findings get attributed to the wrong session. The rule: start, status and stop must each resolve to exactly one runtime on the session's device, and ambiguity across devices must fail closed the way ambiguity across apps already does at line 115. Please enforce it in selectReactNativeTarget, the one selector every subcommand uses, by throwing INVALID_ARGS when candidates span more than one device and nothing narrows them. A test with two devices for the same app that expects INVALID_ARGS would cover it.

Help and docs say "a development build on React Native 0.74 or newer" for every platform (https://github.com/callstack/agent-device/blob/f87e64a/src/commands/schema/cli-help.ts#L308), but the PR body says iOS was not verified. The remote-bridge branch at react-profile-command.ts:149 is new code that was never run, and help does not say so, unlike debugging-profiling.md. Please either show an iOS simulator run on the Perf lab through the CLI argv (react-devtools profile start, run the burst, profile stop printing the scenario's expected first finding, then profile report <Component> from the saved profile), or narrow help and docs to Android. For the remote bridge, either show a run with metroPublicBaseUrl where /json/list is fetched from the public URL and stop succeeds, or add the same "not verified" note to help.

Is about 1,970 production lines (3,536 gross) the right shape for one PR, given the 700 net-production and 1,000-line gross budgets in docs/agents/pull-requests.md? The transport and injected runtime (about 600 lines) are the new capability and nothing else owns them, so I would keep those. Could the findings engine instead live upstream, with a Metro/CDP transport mode added to agent-react-devtools, which already owns slow, rerenders, report, timeline, export and diff? If it must live here, could this be stacked: transport, runtime and start/stop with the stop summary first, drill-down formatters and subcommands second, so each slice's exports have consumers? For the upstream route, an ADR or issue should first decide where render-profile analysis belongs. For the stack, only a re-base onto feat/test-app-perf-lab is needed.

Not blocking, and you can take or leave these: the stateDir ?? process.cwd() fallback at react-devtools.ts:202 would write react-profiles/ into the cwd, the as unknown as string[] cast at metro-inspector.ts:158, and stop swallowing describeProps errors at react-profile-command.ts:310.

The Lint & Format failure is the PR's own. The di-seams gate fails on fetchImpl?: typeof fetch at react-profile-command.ts:65, which this diff adds, and the connect and write options are the same kind of test-only seam. Please remove those seams from ReactProfileCommandOptions rather than marking them approved. The Smoke Tests failure is the iOS "Preflight iOS runner" job with daemon_startup_failed. The diff touches no daemon, startup or iOS runner code, so it is likely unrelated. I did not run a device, so iOS behavior, /json/list ordering across devices and the remote-bridge Origin handling are unchecked. No conflicts were reported.

Before merge, the lint failure and the two findings above need fixing.

Of the open inline threads, the P2 threads on #3613 (comment) (context renders), #3613 (comment) (inferred effect chain), #3613 (comment) (start-reset test), #3613 (comment) (duplicate instance counts), #3613 (comment) (emoji fixture length), #3613 (comment) (help "effect sets state"), #3613 (comment) (help "unrelated context"), #3613 (comment) (unused disconnect handle), #3613 (comment) (lastId only), #3613 (comment) (prop candidate cap), #3613 (comment) (timing-only effect claim), #3613 (comment) (top mounts only) and #3613 (comment) (stale react-native topic) still stand. Also, 10 lower-priority P3 threads still apply. No thread is outdated, so none need resolving.

@thymikee

Copy link
Copy Markdown
Member Author

At f25394b07e (this PR) and b11b5ade89 (#3618):

Device ambiguity: selectReactNativeTarget, which every subcommand uses, now fails closed with INVALID_ARGS when candidates span more than one device. The hint lists the device names, and --metro-device <name> narrows to one. Two tests cover it, one in the selector and one through the command.

iOS: shown through the CLI on a bare RN 0.83 iOS simulator. Each scenario ran as profile start, the burst, wait, then profile stop, and each printed its expected first finding (7/7). After that, profile report ProductTile from the saved profile printed both of its findings and its costliest commits. Help and docs now say "verified on Android emulators and iOS simulators".

Remote bridge: not run. Help now says remote bridge sessions are not verified yet, matching the docs.

Lint: removed the fetchImpl, connect and write seams from ReactProfileCommandOptions and the fetch parameter from listMetroInspectorTargets. Tests stub fetch, spy on InspectorSession.connect and capture stdout instead. check:di-seams passes.

Shape: restacked as you suggested. This PR keeps the transport, runtime, analysis and start/stop/status with the stop summary. #3618 adds the drill-down formatters and subcommands (377 lines), and every layer's exports have consumers (check:production-exports passes on both). I kept the analysis here rather than upstream: the summary is the product surface, and it depends on agent-device's own Metro/session resolution. agent-react-devtools' slow/rerenders/report work over its @c tree from its own daemon, which this path doesn't run.

Non-blocking items:

  • A missing stateDir now falls back to the daemon state dir, not the cwd.
  • The WebSocket cast became one named constructor type with a comment saying why.
  • A describeProps failure now shows as a Note: line in the summary instead of being swallowed.

All 25 inline threads are answered and resolved.

Re-validated on the final top-of-stack build: Android RN 0.88-rc, 7/7.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 16 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/cli/react-profiler/metro-inspector.ts Outdated
Comment thread src/cli/react-profiler/react-profile-command.test.ts
Comment thread website/docs/docs/commands.md Outdated
Comment thread src/cli/commands/react-devtools.ts Outdated
Comment thread src/cli/react-profiler/react-profile-analysis.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

I reviewed f25394b again. The code looks good, and the seams that failed Lint & Format (di-seams) at f87e64a are gone from ReactProfileCommandOptions. Most earlier wording, test and flag-parsing items are now fixed. 17 of 20 checks are still queued or running, and none has failed. The PR touches only the React profiler, its command, help, docs and skills, so the daemon and Android-helper files in the delta come from the base move and cannot explain a failure here. No conflicts. I did not run a device. The iOS and Android runs you reported are not verified by me, and no run with two devices on one Metro was reported, so --metro-device is covered only by unit tests over a synthetic /json/list.

Not blocking: when one app runs on several devices, every session sharing that Metro gets INVALID_ARGS until --metro-device is passed (https://github.com/callstack/agent-device/blob/f25394b/src/cli/react-profiler/metro-inspector.ts#L137), so you could default the filter when the session's device name equals exactly one candidate's deviceName and keep failing closed otherwise. You can take or leave this.

Several inline threads from cubic-dev-ai still stand. #3613 (comment) (duplicate-name hint) is the one to fix first: when two devices share a deviceName, the INVALID_ARGS hint must list a value that --metro-device matches uniquely (deviceKey or logicalDeviceId whenever names collide). Please add a test with two same-named devices that passes that value. Next come #3613 (comment) (weak selector test), #3613 (comment) ("over budget" wording), #3613 (comment) (state dir env) and #3613 (comment) (mount cost double-count). The other threads are fixed at this head, or moved to #3618 or judged harmless, and you can resolve them: r4272372132, r4272372164, r4272372176, r4272372179, r4272372188, r4272372199, r4272372206, r4272372231, r4272372240, r4272372246, r4272372259, r4272372269, r4272372288, r4272372296, r4272372312, r4272372331, r4272372386 and r4272372402 are fixed. r4272372222 is deliberate, because a replacement runtime reuses the attached backend. r4272372279, r4272372349, r4272372357, r4272372365 and r4272372393 left this PR for #3618. r4272372326 is harmless, because the docs line is general enough that omitting a zero count does not mislead.

Before merge, fix the duplicate-name hint above, then wait for CI to finish, including check:di-seams and check:production-exports.

Base automatically changed from feat/test-app-perf-lab to main October 14, 2026 17:01
@thymikee
thymikee force-pushed the feat/react-devtools-profiler branch 2 times, most recently from a183e22 to 9fb9a4f Compare October 14, 2026 17:10
@thymikee
thymikee force-pushed the feat/react-devtools-profiler branch from 9fb9a4f to b7eb909 Compare October 14, 2026 17:12
@thymikee
thymikee force-pushed the feat/react-devtools-profiler branch from b7eb909 to f1f454b Compare October 14, 2026 17:18
@thymikee

Copy link
Copy Markdown
Member Author

At 7c60d2db8f, rebased onto main after #3612 merged:

  • Duplicate-name hint (r4273400840): the hint now lists each device's name when it is unique and its logical device id when names collide. --metro-device matches either value. A test covers two simulators with the same name and selects one by its key.
  • r4273400870, r4273400899, r4273400936: fixed (connect-URL assertion, "commits over the 16 ms frame budget" wording, per-group mount cost).
  • r4273400917: no longer applies, because f1f454b removed the stateDir fallback.
  • Defaulting to the session's device: not taken. An agent-device device name is often different from the deviceName that Metro reports (for example, a simulator UDID or an AVD name versus the model name), so a match would be by chance. Failing closed with values that select one device uniquely is safer.

check:affected --run, check:production-exports and check:di-seams pass locally.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/cli/react-profiler/react-profile-renders.ts Outdated
Comment thread website/docs/docs/commands.md Outdated
Comment thread src/cli/react-profiler/react-profile-renders.ts Outdated
Comment thread src/cli/react-profiler/react-profile-renders.ts Outdated
Comment thread src/cli/react-profiler/react-profile-renders.ts Outdated
Comment thread src/cli.ts
Comment thread src/cli/react-profiler/react-profile-analysis.ts
Comment thread src/cli/react-profiler/react-profile-analysis.ts Outdated
Comment thread docs/adr/0029-daemon-policy.md
Comment thread src/commands/perf/index.ts Outdated
… cost repeated eager mounts, and count changed props

- Elements the profile cannot name stay out of components and findings instead of folding into one Unknown.
- Animated(...) wrappers keep their name so they stay framework components; Memo(Row) and Row are separate components.
- A prop whose inspected instances disagree on its kind claims no new identity.
- memo-defeated evidence counts each changed prop; eager-mount costs every qualifying commit of a component.
- Devices without a logical id are keyed by target id before display name.
- Docs name --bundle-url; perf guidance and ADR 0029 no longer point at a daemon command react-devtools does not send.
@thymikee

Copy link
Copy Markdown
Member Author

At 6fb23c14fe, the 11 Cubic threads from the review at 54120679f8 are handled. 10 are fixed, with tests:

  • Renders the app could not name stay out of components and findings. The runtime no longer writes an Unknown entry.
  • Animated(...) wrappers keep their name, so they stay framework components.
  • Memo(Row) and Row are separate components.
  • A prop whose instances disagree on its value kind claims no new identity.
  • memo-defeated evidence counts each changed prop.
  • eager-mount is costed over every qualifying commit of a component.
  • Devices without a logical id are keyed by their target id, not their display name.
  • Docs name --bundle-url. The perf guidance and ADR 0029 are corrected.

The metroBearerToken thread does not apply: that token authorizes the companion proxy's control API, not the public dev-server URL that the profiler reads.

All 41 review threads have a reply and are resolved. check:affected --run, check:production-exports and check:di-seams pass locally.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 18 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/commands/schema/command-overrides.ts Outdated
Comment thread src/cli/react-profiler/react-profile-format.ts Outdated

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant