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:
# --sourcemap-path takes the .map FILE, not a directory or a zip.
# The output lands in the CURRENT DIRECTORY — the CLI has no output flag.
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 performance skill's
anti-pattern catalogue and per-pattern guides
(mm-selector-memoization.md, mm-unstable-hook-return.md,
mm-redux-antipatterns.md, mm-hook-dependency-arrays.md,
mm-context-performance.md, mm-eager-work-on-mount.md, etc.) for the fix
recipe rather than inventing one from scratch.
For each hot non-swaps frame, work out what pulled it in. Do not stop
at the relation label — establish the concrete link before reporting it.
For Called by swaps, find the swaps call site (grep the hook/component
name under app/components/UI/Bridge) and say which swaps surface drives
it, since the fix is frequently on the swaps side. For Hosts swaps screen, say what the screen makes the host do (mount cost, prop churn,
provider re-render). For Concurrent (off swaps path), name the mechanism
(a poll, a subscription, an animation) — that row is context for the user,
not a swaps task.
Always report the timing tables; keep prose minimal. Every report leads
with the timing data — the analyzer's Metric | Value block, then the
swaps-owned area table, then the two non-swaps tables — regardless of
whether an issue was found. Follow it with exactly one short line stating
the outcome: where the capture's JS work actually went, and whether the
expensive part is swaps-owned. Do not add speculation, hedging, or
suggestions about what to check/run next beyond what's in Step 7 — the
report is data plus outcome, nothing else.
If an issue was found, add a probable-cause/fix table — nothing more.
Each row carries an Owned by swaps cell so the reader sees at a glance
what the swaps team can act on. Order rows by self time, swaps-owned first.
Per row: the screen or area, the probable cause in plain language (what's
happening and why — not "unmemoized selector" but e.g. "re-sorts the whole
quote list every time you interact with the screen", with the relation,
file:line, self-ms and calls folded into that same cell), and the fix.
Depth differs by ownership: for Owned by swaps = Yes, give the concrete
fix citing the relevant performance skill guide; for No, keep the fix
cell to a short pointer plus who should take it (the owning area/team —
.github/CODEOWNERS maps the file path to a team if the user needs to
route it), and do not write a detailed patch plan for code another team
maintains.
Be ruthless about what earns a row — the table exists to be acted on, and
every row that can't be is noise that buries the ones that can:
- Every swaps-owned area with real self time, or with a large inclusive
span, gets its own row. This detail is the point of the audit.
- Non-swaps areas: only the few that genuinely matter (roughly ≥5% of JS
work each, and rarely more than three rows). A dependency at 0.6% is not
a finding.
- Never a row for
Runtime / idle, [root], or GC. If GC volume looks
symptomatic, it belongs in the cause cell of the row that caused it, not
in a row of its own.
- Never a row you cannot explain in plain language. If you couldn't read
the code behind it (unsymbolicated, or a minified dependency internal),
say so in the caveat line instead of inventing a row.
Do not append anything after this table — no next-steps, no suggestion to
re-run tests or capture a new profile, no broader speculation. The only
exception is a single factual caveat line when something materially
undermines the finding: no sourcemaps were available, the source map did
not match the build that produced the capture (wrong function names,
everything resolving to [root]), most of the capture was idle, or the
capture looks too short to cover the full journey (quote fetch, screen
transitions, animations). Skip that caveat whenever the capture and
findings look adequate.
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>
1---2name: swaps-cpu-profile-audit3description: 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 wheth4---56# Swaps CPU Profile Audit78Turns a captured Hermes CPU profile into a performance audit of the9swaps/bridge experience: which frames actually burned time in the capture,10which surface they belong to (main screen, quote select, post-trade, batch11sell, asset picker, ...), **whether the swaps team owns them**, and what to12change. This skill owns the profile-parsing protocol, the swaps-tree area13map, and the ownership/relation classification; the fix recipes themselves14live in the `performance` skill, and the live on-device measurement15counterpart is `swaps-perf-audit`.1617Auditing only swaps-owned files misses a whole class of real regressions: a18capture taken on the swaps screen can be dominated by a slow navigator19transition, a re-rendering provider, an unmemoized shared selector, or a20polling controller — none of it under `app/components/UI/Bridge/**`, all of21it felt by the user as "swaps is slow". So the analyzer classifies **every**22frame in the capture, and the report names the non-swaps contributors instead23of dropping them.2425## When To Use2627Use when the user:2829- Hands you a `.cpuprofile` (or its converted JSON) and asks what's slow in30 swaps/bridge, or to audit/analyze/explain it.31- Asks "why is swaps slow" and already has a recorded trace, RC build32 profiling session, or Release Profiler capture in hand.33- Wants swaps performance fixes justified by trace evidence instead of a34 static code sweep.35- Needs to know whether the cost on a swaps screen is actually the swaps36 team's to fix, or belongs to another team's code.3738Do not use for:3940- Recording a *new* profile live on a simulator/device, or driving the app —41 that is manual, human-only work described in42 `docs/readme/release-build-profiler.md` (shake the device, Start, reproduce,43 Stop). This skill starts only once a `.cpuprofile` file already exists on44 disk.45- Live render-count / re-render measurement on a running simulator — use46 `swaps-perf-audit`.47- A capture that was not recorded on the swaps/bridge screens, or a general48 app-wide performance sweep — the whole model here assumes the user was49 sitting on a swaps surface while recording, which is what makes non-swaps50 frames in the trace attributable at all. Point them at the general51 `performance` skill instead.52- MetaMask Extension — Hermes/Release Profiler `.cpuprofile` capture doesn't53 exist there; this skill installs only for `metamask-mobile`.5455## How ownership and relation are decided5657Two independent axes, both computed by the analyzer:5859**Owned by swaps** — the frame's resolved source path matches one of the60swaps-owned roots (`app/components/UI/Bridge/**` plus the swaps redux slice,61selectors, bridge controller/messenger wiring, bridge utils, and the62swaps/bridge confirmation rows). This is a path-list decision, so it is63stable and does not depend on the trace.6465**Relation to swaps** — where the frame sat relative to swaps code in the66recorded call stacks:6768| Relation | Meaning | Typical example |69|---|---|---|70| `Swaps-owned` | The frame is swaps code | `BridgeView`, `useQuotes` |71| `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 |72| `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` |73| `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 |74| `Runtime / idle` | Engine bookkeeping owned by nobody, excluded from every percentage | `[root]` (wall time with no JS running), `[GC young gen]` |7576The distinction drives what you do with a finding: `Called by swaps` is often77still a swaps bug (swaps calls it too often, or with unstable arguments),78`Hosts swaps screen` usually means the screen makes the host do too much79work, and `Concurrent` is someone else's timer stealing frames while the user80is mid-swap.8182### Self time vs inclusive time — read both8384Self time answers "which frame was executing", so it lands on the *leaf* of85the stack. A React component that renders a heavy tree therefore often shows86**0 ms of self time** while the cost piles up in the reconciler, a selector,87or a dependency it called. `Swaps-owned code: 0.00 ms` never means "swaps did88nothing" on its own — check the inclusive column and the89`Called by swaps` rows before concluding anything, because "swaps triggers90expensive work" is a swaps finding even when no swaps frame is hot.9192Two corollaries for reading the analyzer output honestly:9394- **Idle is not work.** `[root]` owns all the wall time when no JS is running,95 which on a capture where the user paused can be most of it. The analyzer96 splits it (and GC) into `Runtime / idle` and excludes it from every97 percentage, so "% of JS work" means what it says. Never put an idle or GC98 row in the fix table.99- **A zero-self area with a trivial inclusive span means nothing.** Usually100 it is just a module getting evaluated (a screen the user never opened).101 The analyzer only keeps a zero-self swaps row when the work it triggered102 clears `--trigger-min-pct` (default 5% of JS work); do not reintroduce103 those rows by hand.104105## Workflow1061071. **Locate the inputs.** You need the `.cpuprofile` path. Source maps are108 optional but strongly preferred — without them, frames stay at the109 minified-bundle level, so nothing resolves to a real path and both the110 ownership and area columns collapse to "unknown". If the user has111 sourcemaps (from a local `--sourcemap-path` or a `*-sourcemaps-<build>` CI112 artifact) but hasn't converted yet, do that next.1132. **Symbolicate.** Convert the raw profile with the project's own tool,114 which resolves bundle positions back to original `app/...` source paths:115 ```bash116 # --sourcemap-path takes the .map FILE, not a directory or a zip.117 # The output lands in the CURRENT DIRECTORY — the CLI has no output flag.118 yarn react-native-release-profiler --local <profile.cpuprofile> \119 --sourcemap-path <sourcemaps>/index.js.map120 ```121 This writes `<profile-basename>-converted.json` into the directory you ran122 it from. Never omit `--sourcemap-path` just to "see what happens": without123 it the CLI silently substitutes a map of its own (an Android *debug* build124 map, or `/index.map` downloaded from a running Metro server), which cannot125 symbolicate a release Hermes capture — it either crashes the transformer or126 produces confidently wrong file attributions. See the repo overlay for the127 exact commands and traps. Skip this step only if the user already has a128 converted JSON, or has no sourcemaps at all (state clearly in the report129 that findings are then best-effort and file/line attribution is130 unreliable).1313. **Aggregate.** Run the bundled analyzer against the converted JSON (or the132 raw `.cpuprofile` as a fallback — see the repo overlay for exact usage).133 It reconstructs self time and total (inclusive) time per frame from the134 sampling data, then emits four separate tables: swaps-owned areas135 (`app/components/UI/Bridge/**` subfolder → BridgeView / QuoteSelectorView /136 PostTradeBottomSheet / BatchSell\* / BridgeTokenSelector / ... — see the137 repo overlay's area table), non-swaps areas on the swaps path, non-swaps138 areas running concurrently, and runtime/idle. Areas that did no work and139 triggered none are dropped, so every row you see earned its place.1404. **Read the code at the hot frames.** A `file:line` with high self time is141 a *symptom*, not a diagnosis — open the file, read the actual code at that142 location and its call sites, and identify the concrete anti-pattern (dead143 re-render, unmemoized sort/filter, unstable hook return, JSON.stringify in144 a dependency array, etc.). Cross-reference the `performance` skill's145 anti-pattern catalogue and per-pattern guides146 (`mm-selector-memoization.md`, `mm-unstable-hook-return.md`,147 `mm-redux-antipatterns.md`, `mm-hook-dependency-arrays.md`,148 `mm-context-performance.md`, `mm-eager-work-on-mount.md`, etc.) for the fix149 recipe rather than inventing one from scratch.1505. **For each hot non-swaps frame, work out what pulled it in.** Do not stop151 at the relation label — establish the concrete link before reporting it.152 For `Called by swaps`, find the swaps call site (`grep` the hook/component153 name under `app/components/UI/Bridge`) and say which swaps surface drives154 it, since the fix is frequently on the swaps side. For `Hosts swaps155 screen`, say what the screen makes the host do (mount cost, prop churn,156 provider re-render). For `Concurrent (off swaps path)`, name the mechanism157 (a poll, a subscription, an animation) — that row is context for the user,158 not a swaps task.1596. **Always report the timing tables; keep prose minimal.** Every report leads160 with the timing data — the analyzer's `Metric | Value` block, then the161 swaps-owned area table, then the two non-swaps tables — regardless of162 whether an issue was found. Follow it with exactly one short line stating163 the outcome: where the capture's JS work actually went, and whether the164 expensive part is swaps-owned. Do not add speculation, hedging, or165 suggestions about what to check/run next beyond what's in Step 7 — the166 report is data plus outcome, nothing else.1677. **If an issue was found, add a probable-cause/fix table — nothing more.**168 Each row carries an `Owned by swaps` cell so the reader sees at a glance169 what the swaps team can act on. Order rows by self time, swaps-owned first.170 Per row: the screen or area, the probable cause in plain language (what's171 happening and why — not "unmemoized selector" but e.g. "re-sorts the whole172 quote list every time you interact with the screen", with the relation,173 `file:line`, self-ms and calls folded into that same cell), and the fix.174 Depth differs by ownership: for `Owned by swaps = Yes`, give the concrete175 fix citing the relevant `performance` skill guide; for `No`, keep the fix176 cell to a short pointer plus who should take it (the owning area/team —177 `.github/CODEOWNERS` maps the file path to a team if the user needs to178 route it), and do not write a detailed patch plan for code another team179 maintains.180181 Be ruthless about what earns a row — the table exists to be acted on, and182 every row that can't be is noise that buries the ones that can:183 - Every swaps-owned area with real self time, or with a large inclusive184 span, gets its own row. This detail is the point of the audit.185 - Non-swaps areas: only the few that genuinely matter (roughly ≥5% of JS186 work each, and rarely more than three rows). A dependency at 0.6% is not187 a finding.188 - Never a row for `Runtime / idle`, `[root]`, or GC. If GC volume looks189 symptomatic, it belongs in the cause cell of the row that caused it, not190 in a row of its own.191 - Never a row you cannot explain in plain language. If you couldn't read192 the code behind it (unsymbolicated, or a minified dependency internal),193 say so in the caveat line instead of inventing a row.194195 Do not append anything after this table — no next-steps, no suggestion to196 re-run tests or capture a new profile, no broader speculation. The only197 exception is a single factual caveat line when something materially198 undermines the finding: no sourcemaps were available, the source map did199 not match the build that produced the capture (wrong function names,200 everything resolving to `[root]`), most of the capture was idle, or the201 capture looks too short to cover the full journey (quote fetch, screen202 transitions, animations). Skip that caveat whenever the capture and203 findings look adequate.204205The exact commands, the swaps-tree area map, the context area map, and the206bundled analyzer's usage are in the repo overlay207(`repos/metamask-mobile.md`).208209## Output format210211Lead with the metrics table, then the swaps-owned area table, then the two212non-swaps tables (omit either if it has no rows), then exactly one short213outcome line. Keep the tables separate — swaps-owned detail is the subject of214the audit, and the non-swaps tables are context around it. Do not add anything215beyond what's specified below — no speculation, no assumptions, no suggestions216about what to check or run next.217218**No issue found:**219220```221# CPU Profile Audit — swaps/bridge222223| Metric | Value |224|---|---|225| Capture length | ~<Xs> |226| JS work sampled | <Y>ms |227| Idle / GC (excluded from the splits) | <Y>ms (<Z>% of capture) |228| Swaps-owned code | <Y>ms (<Z>% of JS work) |229| Non-swaps code on the swaps path | <Y>ms (<Z>%) |230| Non-swaps code running concurrently | <Y>ms (<Z>%) |231232**Swaps-owned areas**233234| Area | Self time (ms) | % of swaps time | Inclusive (ms) |235|---|---|---|---|236| <Area> | <ms> | <%> | <ms> |237238No meaningful performance issue in this capture — <one-line factual summary, e.g. "only <Y>ms of JS work in <Xs>, most of it <what>">.239```240241**Issue(s) found** — same tables, plus a probable-cause/fix table. Keep it242skimmable (a non-engineer should get the gist without decoding jargon); fold243relation/`file:line`/self-ms/calls detail into the "Probable cause" cell244rather than adding separate columns:245246```247# CPU Profile Audit — swaps/bridge248249| Metric | Value |250|---|---|251| Capture length | ~<Xs> |252| JS work sampled | <Y>ms |253| Idle / GC (excluded from the splits) | <Y>ms (<Z>% of capture) |254| Swaps-owned code | <Y>ms (<Z>% of JS work) |255| Non-swaps code on the swaps path | <Y>ms (<Z>%) |256| Non-swaps code running concurrently | <Y>ms (<Z>%) |257258**Swaps-owned areas**259260| Area | Self time (ms) | % of swaps time | Inclusive (ms) |261|---|---|---|---|262| <Area> | <ms> | <%> | <ms> |263264**Non-swaps areas on the swaps path**265266| Area | Relation to swaps | Self time (ms) | % of JS work |267|---|---|---|---|268| <Area> | Called by swaps / Hosts swaps screen | <ms> | <%> |269270**Non-swaps areas running concurrently**271272| Area | Self time (ms) | % of JS work |273|---|---|---|274| <Area> | <ms> | <%> |275276Found <N> issue(s) costing ~<Y>ms, of which <M> are swaps-owned.277278| Screen / area | Owned by swaps | Probable cause | Fix |279|---|---|---|---|280| <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`) |281| <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 |282283<optional single factual caveat line — only if source maps were missing or mismatched, the capture was mostly idle, or it looked too short>284```