Skip to content

fix(profiler): accept the component names the analyze report actually prints - #663

Open
filip131311 wants to merge 2 commits into
mainfrom
filip/profiler-component-name-resolution
Open

fix(profiler): accept the component names the analyze report actually prints#663
filip131311 wants to merge 2 commits into
mainfrom
filip/profiler-component-name-resolution

Conversation

@filip131311

Copy link
Copy Markdown
Collaborator

Fixes #632.

The problem

react-profiler-analyze strips Forget() / Memo() / ForwardRef() wrappers for readability, then the drill-down tools matched the raw DevTools name with ===. The report names a component and the very next call refuses it. React Compiler is on by default in Expo SDK 54, so this hits essentially every new Expo app.

Reproduced live

Real session on an Expo SDK 54 app (experiments.reactCompiler: true, RN 0.81.5) — 821 fiber renders, 16 commits. The report printed `ParallaxScrollView` [React Compiler]:

profiler-cpu-query component_cpu ParallaxScrollView         → "_Component `ParallaxScrollView` not found in commit data._"
profiler-cpu-query component_cpu Forget(ParallaxScrollView) → "_No CPU samples found during `Forget(...)` commits._"

The two messages differ, and that is the proof: "not found in commit data" means the name never matched; the other is emitted only after a match.

Two things beyond the report:

  1. profiler-commit-query mode=by_component has the same bug — same refusal, full table under the raw name.
  2. It is not React-Compiler-specific. Memo() and ForwardRef() are stripped too; the same report printed `InnerScreen` [forwardRef].

The workflow was a deadlock

The AST index is keyed on bare source identifiers, so react-profiler-component-source accepts only the name the query tools reject:

name query tools component-source
IconSymbol ✗ not found ✓ file + line + source
Forget(IconSymbol) ✓ resolves ✗ not found

There was no single name an agent could carry from analyze → drill-down → read source. Fixing only the query half would have left the issue's stated workflow broken.

Approach

Match a query against each recorded name's display form, rather than stripping both sides.

Stripping both sides looks equivalent and is not:

  • it would quietly answer a query for Forget(Foo) with Memo(Foo) when only the latter exists — the agent asked about the compiler-wrapped fiber and silently got the memo one;
  • it isn't even reliable, because the display stripper stops after 4 wrappers, so a deeply wrapped name still displays with a wrapper and would never match.

Comparing against the display form compares against exactly the string that was printed.

Exact matches always win and are never widened. StaticContainer and Memo(StaticContainer) both appear in one ordinary 18-second session — I checked all 70 distinct names in the dump, and that is the one real collision. Asking for the bare name returns the bare fiber, and the output now names the sibling instead of silently showing one of two.

Ambiguity is refused, not merged. Where a display name maps to several distinct fibers the tools list the exact strings to retry with. Memo(X) and X are often one component's two fibers whose durations nest, so a merged total describes no real component; and picking the largest silently picks the memo wrapper's inclusive time, which is the wrong fiber for self-cost analysis.

The AST lookups get the reverse fallback: raw → stripped → expo-router file suffix removed (the report shows TabTwoScreen(./(tabs)/explore.tsx); the source declares TabTwoScreen). Loosening is safe there in a way it is not for commit data — a wrong guess against the index simply misses and returns found: false.

Verified against the live session

Case Result
component_cpu ParallaxScrollView resolves to Forget(ParallaxScrollView) — was refused
by_component ParallaxScrollView full table + > Resolved … (React Compiler). Either name works here.
by_component Forget(ParallaxScrollView) byte-identical to before, no note
by_component ParallaxScroll (typo) miss + Forget(ParallaxScrollView) as a re-queryable suggestion
component-source Forget(IconSymbol) found: true, icon-symbol.tsx:28 — was found: false
component-source Forget(TabTwoScreen(./(tabs)/explore.tsx)) found: true, app/(tabs)/explore.tsx:12

Visible report change (intentional)

Fixing the analyze-side lookup means wrapped components stop losing their source location. In the live session exactly one row changes — every other row is a node_modules component with no user source:

before:  | `IconSymbol` [React Compiler] | 6 | 0.1ms | … | — |
after:   | `IconSymbol` [React Compiler] | 6 | 0.1ms | … | `ui/icon-symbol.tsx:28` |

Deliberately out of scope

  • profiler-combined-report.ts:448 correlates leaks to components by substring-matching raw names into native symbols, so wrapped components are structurally excluded. Same bug class — affected, deferred, not unaffected.
  • A deeper namespace bug, filed separately: react-profiler-stop joins fiber metadata by name across two different namespaces, so wrapped fibers lose parentName, hookTypes and isCompilerOptimized entirely. Measured on this session: 41 wrapped fibers, 0 with any of the three, versus 181/272 bare fibers carrying parentName. That is why the report's header says React Compiler: ✗ while the same document tags six components [React Compiler].

Checks

  • 3104 tests pass (298 files); 18 new, built on names taken verbatim from the real session
  • the annotateComponentName move is a pure refactor — verified output-identical before anything else changed
  • 03-tag.ts's ANIMATED_PATTERN deliberately left alone (it matches unanchored and covers Motion/Transition, which the stripper must not)
  • tool description: fields unchanged, so the SpiderShield average is untouched; skills gate 10.0; prettier, eslint, both typechecks clean

react-profiler-analyze strips Forget()/Memo()/ForwardRef() wrappers for
readability, then the drill-down tools matched on the raw DevTools name with
string equality — so the report named a component and the next call refused it.
React Compiler is on by default in Expo SDK 54, so this hit essentially every
new Expo app.

Resolution now matches a query against each recorded name's DISPLAY form rather
than stripping both sides. Stripping both sides looks equivalent but is not: it
would quietly answer a query for Forget(Foo) with Memo(Foo) when only the latter
exists, and it is not even reliable, because the display stripper stops after
four wrappers so a deeply wrapped name still displays with one. Comparing
against the display form compares against exactly the string that was printed.

Exact matches always win and are never widened. Where a bare name and a wrapped
one coexist — StaticContainer and Memo(StaticContainer) both appear in a single
ordinary session — the exact name resolves and the output names the sibling
rather than silently showing one of two. When a stripped name maps to several
distinct fibers the tools refuse and list the exact strings to retry with; those
fibers are not merged, because a combined total would not describe any real
component.

The same mismatch ran in the opposite direction and made the workflow a
deadlock: the AST index is keyed on bare source identifiers, so
react-profiler-component-source accepted only the name the query tools rejected.
There was no single name an agent could carry from analyze through to reading
source. Both AST lookups now fall back through the stripped name, plus the
expo-router file suffix (the report shows TabTwoScreen(./(tabs)/explore.tsx);
the source declares TabTwoScreen). Loosening is safe there in a way it is not
for commit data — a wrong guess against the index simply misses.

That also fixes react-profiler-analyze silently dropping sourceLocation for
every wrapped component, which is why the File column read '—' for project
files that were sitting right there.
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.

profiler-cpu-query component_cpu can't find components under their displayed names in React Compiler projects (Forget(...) wrappers)

1 participant