| name | swaps-cpu-profile-audit |
| description | Parse an already-recorded Hermes / React Native Release Profiler `.cpuprofile` (ideally symbolicated with source maps) and audit it for slow frames on the swaps/bridge screen and the modals or subpages it opens — quote select screen, post-trade modal, batch sell, asset picker/token selector. Use when a user hands you a `.cpuprofile` file (e.g. a `sampling-profiler-trace*.cpuprofile`, or its already-converted `*-converted.json`) recorded per `docs/readme/release-build-profiler.md` and asks to audit, analyze, explain, or find why the swaps/bridge flow is slow based on that trace. This is an offline, file-based analysis — no simulator, device, Metro, or `mm` session is required, unlike `swaps-perf-audit` (which measures live render counts on a running simulator). The audit accounts for ALL time in the capture, not just swaps-owned code: non-swaps frames that ran while the user sat on a swaps screen (navigation, redux, design system, polling controllers, React internals) are reported too, each labelled with whether the swaps team owns it and how it relates to the swaps call stacks. The report always leads with a timing table (capture metrics + by-area self time with an ownership column) and a short outcome line, and only adds a probable-cause/fix table when there is an actual issue — deep fixes are proposed for swaps-owned rows, while non-owned rows are named and routed. MetaMask Mobile only. |
| maturity | stable |
Swaps CPU Profile Audit
Turns a captured Hermes CPU profile into a performance audit of the
swaps/bridge experience: which frames actually burned time in the capture,
which surface they belong to (main screen, quote select, post-trade, batch
sell, asset picker, ...), whether the swaps team owns them, and what to
change. This skill owns the profile-parsing protocol, the swaps-tree area
map, and the ownership/relation classification; the fix recipes themselves
live in the performance skill, and the live on-device measurement
counterpart is swaps-perf-audit.
Auditing only swaps-owned files misses a whole class of real regressions: a
capture taken on the swaps screen can be dominated by a slow navigator
transition, a re-rendering provider, an unmemoized shared selector, or a
polling controller — none of it under app/components/UI/Bridge/**, all of
it felt by the user as "swaps is slow". So the analyzer classifies every
frame in the capture, and the report names the non-swaps contributors instead
of dropping them.
When To Use
Use when the user:
- Hands you a
.cpuprofile (or its converted JSON) and asks what's slow in
swaps/bridge, or to audit/analyze/explain it.
- Asks "why is swaps slow" and already has a recorded trace, RC build
profiling session, or Release Profiler capture in hand.
- Wants swaps performance fixes justified by trace evidence instead of a
static code sweep.
- Needs to know whether the cost on a swaps screen is actually the swaps
team's to fix, or belongs to another team's code.
Do not use for:
- Recording a new profile live on a simulator/device, or driving the app —
that is manual, human-only work described in
docs/readme/release-build-profiler.md (shake the device, Start, reproduce,
Stop). This skill starts only once a .cpuprofile file already exists on
disk.
- Live render-count / re-render measurement on a running simulator — use
swaps-perf-audit.
- A capture that was not recorded on the swaps/bridge screens, or a general
app-wide performance sweep — the whole model here assumes the user was
sitting on a swaps surface while recording, which is what makes non-swaps
frames in the trace attributable at all. Point them at the general
performance skill instead.
- MetaMask Extension — Hermes/Release Profiler
.cpuprofile capture doesn't
exist there; this skill installs only for metamask-mobile.
How ownership and relation are decided
Two independent axes, both computed by the analyzer:
Owned by swaps — the frame's resolved source path matches one of the
swaps-owned roots (app/components/UI/Bridge/** plus the swaps redux slice,
selectors, bridge controller/messenger wiring, bridge utils, and the
swaps/bridge confirmation rows). This is a path-list decision, so it is
stable and does not depend on the trace.
Relation to swaps — where the frame sat relative to swaps code in the
recorded call stacks:
| Relation | Meaning | Typical example |
|---|
Swaps-owned | The frame is swaps code | BridgeView, useQuotes |
Called by swaps | Non-swaps frame that ran with a swaps frame below it on the stack — swaps code invoked it | useSelector, a shared hook, a design-system component rendered by swaps |
Hosts swaps screen | Non-swaps frame that was on the stack when swaps code was entered — it renders/hosts the screen | React reconciler, navigator, redux Provider |
Concurrent (off swaps path) | Non-swaps frame that never shares a stack with swaps code — it competes for the JS thread anyway | Token detection / balance polling, analytics flush |
Runtime / idle | Engine bookkeeping owned by nobody, excluded from every percentage | [root] (wall time with no JS running), [GC young gen] |
The distinction drives what you do with a finding: Called by swaps is often
still a swaps bug (swaps calls it too often, or with unstable arguments),
Hosts swaps screen usually means the screen makes the host do too much
work, and Concurrent is someone else's timer stealing frames while the user
is mid-swap.
Self time vs inclusive time — read both
Self time answers "which frame was executing", so it lands on the leaf of
the stack. A React component that renders a heavy tree therefore often shows
0 ms of self time while the cost piles up in the reconciler, a selector,
or a dependency it called. Swaps-owned code: 0.00 ms never means "swaps did
nothing" on its own — check the inclusive column and the
Called by swaps rows before concluding anything, because "swaps triggers
expensive work" is a swaps finding even when no swaps frame is hot.
Two corollaries for reading the analyzer output honestly:
- Idle is not work.
[root] owns all the wall time when no JS is running,
which on a capture where the user paused can be most of it. The analyzer
splits it (and GC) into Runtime / idle and excludes it from every
percentage, so "% of JS work" means what it says. Never put an idle or GC
row in the fix table.
- A zero-self area with a trivial inclusive span means nothing. Usually
it is just a module getting evaluated (a screen the user never opened).
The analyzer only keeps a zero-self swaps row when the work it triggered
clears
--trigger-min-pct (default 5% of JS work); do not reintroduce
those rows by hand.
Workflow
-
Locate the inputs. You need the .cpuprofile path. Source maps are
optional but strongly preferred — without them, frames stay at the
minified-bundle level, so nothing resolves to a real path and both the
ownership and area columns collapse to "unknown". If the user has
sourcemaps (from a local --sourcemap-path or a *-sourcemaps-<build> CI
artifact) but hasn't converted yet, do that next.
-
Symbolicate. Convert the raw profile with the project's own tool,
which resolves bundle positions back to original app/... source paths:
yarn react-native-release-profiler --local <profile.cpuprofile> \
--sourcemap-path <sourcemaps>/index.js.map
This writes <profile-basename>-converted.json into the directory you ran
it from. Never omit --sourcemap-path just to "see what happens": without
it the CLI silently substitutes a map of its own (an Android debug build
map, or /index.map downloaded from a running Metro server), which cannot
symbolicate a release Hermes capture — it either crashes the transformer or
produces confidently wrong file attributions. See the repo overlay for the
exact commands and traps. Skip this step only if the user already has a
converted JSON, or has no sourcemaps at all (state clearly in the report
that findings are then best-effort and file/line attribution is
unreliable).
-
Aggregate. Run the bundled analyzer against the converted JSON (or the
raw .cpuprofile as a fallback — see the repo overlay for exact usage).
It reconstructs self time and total (inclusive) time per frame from the
sampling data, then emits four separate tables: swaps-owned areas
(app/components/UI/Bridge/** subfolder → BridgeView / QuoteSelectorView /
PostTradeBottomSheet / BatchSell* / BridgeTokenSelector / ... — see the
repo overlay's area table), non-swaps areas on the swaps path, non-swaps
areas running concurrently, and runtime/idle. Areas that did no work and
triggered none are dropped, so every row you see earned its place.
-
Read the code at the hot frames. A file:line with high self time is
a symptom, not a diagnosis — open the file, read the actual code at that
location and its call sites, and identify the concrete anti-pattern (dead
re-render, unmemoized sort/filter, unstable hook return, JSON.stringify in
a dependency array, etc.). Cross-reference the skill's
anti-pattern catalogue and per-pattern guides
(, ,
, ,
, , etc.) for the fix
recipe rather than inventing one from scratch.
The exact commands, the swaps-tree area map, the context area map, and the
bundled analyzer's usage are in the repo overlay
(repos/metamask-mobile.md).
Output format
Lead with the metrics table, then the swaps-owned area table, then the two
non-swaps tables (omit either if it has no rows), then exactly one short
outcome line. Keep the tables separate — swaps-owned detail is the subject of
the audit, and the non-swaps tables are context around it. Do not add anything
beyond what's specified below — no speculation, no assumptions, no suggestions
about what to check or run next.
No issue found:
# CPU Profile Audit — swaps/bridge
| Metric | Value |
|---|---|
| Capture length | ~<Xs> |
| JS work sampled | <Y>ms |
| Idle / GC (excluded from the splits) | <Y>ms (<Z>% of capture) |
| Swaps-owned code | <Y>ms (<Z>% of JS work) |
| Non-swaps code on the swaps path | <Y>ms (<Z>%) |
| Non-swaps code running concurrently | <Y>ms (<Z>%) |
**Swaps-owned areas**
| Area | Self time (ms) | % of swaps time | Inclusive (ms) |
|---|---|---|---|
| <Area> | <ms> | <%> | <ms> |
No meaningful performance issue in this capture — <one-line factual summary, e.g. "only <Y>ms of JS work in <Xs>, most of it <what>">.
Issue(s) found — same tables, plus a probable-cause/fix table. Keep it
skimmable (a non-engineer should get the gist without decoding jargon); fold
relation/file:line/self-ms/calls detail into the "Probable cause" cell
rather than adding separate columns:
# CPU Profile Audit — swaps/bridge
| Metric | Value |
|---|---|
| Capture length | ~<Xs> |
| JS work sampled | <Y>ms |
| Idle / GC (excluded from the splits) | <Y>ms (<Z>% of capture) |
| Swaps-owned code | <Y>ms (<Z>% of JS work) |
| Non-swaps code on the swaps path | <Y>ms (<Z>%) |
| Non-swaps code running concurrently | <Y>ms (<Z>%) |
**Swaps-owned areas**
| Area | Self time (ms) | % of swaps time | Inclusive (ms) |
|---|---|---|---|
| <Area> | <ms> | <%> | <ms> |
**Non-swaps areas on the swaps path**
| Area | Relation to swaps | Self time (ms) | % of JS work |
|---|---|---|---|
| <Area> | Called by swaps / Hosts swaps screen | <ms> | <%> |
**Non-swaps areas running concurrently**
| Area | Self time (ms) | % of JS work |
|---|---|---|
| <Area> | <ms> | <%> |
Found <N> issue(s) costing ~<Y>ms, of which <M> are swaps-owned.
| Screen / area | Owned by swaps | Probable cause | Fix |
|---|---|---|---|
| <Area, e.g. "Quote select screen"> | Yes | <plain-language root cause, e.g. "re-sorts the whole quote list every time you interact with the screen"> — <self>ms, <calls>x, `<file>:<line>` | <concrete fix> (see `<guide>.md`) |
| <Area, e.g. "Navigation (app nav stack)"> | No | <plain-language root cause + how it relates to swaps, e.g. "the navigator re-renders the whole tab stack while the swaps screen mounts"> — <self>ms, <calls>x, `<file>:<line>` | <short pointer>; owned by <area/team>, raise it with them |
<optional single factual caveat line — only if source maps were missing or mismatched, the capture was mostly idle, or it looked too short>