There is an existing issue with serialisation logic for the traces from
Profiler panel.
I've discovered that `TREE_OPERATION_UPDATE_TREE_BASE_DURATION`
operation for some reason appears earlier in a sequence of operations,
before the `TREE_OPERATION_ADD` that registers the new node. It ends up
cloning non-existent node, which just creates an empty object and adds
it to the map of nodes.
This change only adds additional layer of validation to cloning logic,
so we don't swallow the error, if we attempt to clone non-existent node.
Fixes https://github.com/facebook/react/issues/31463,
https://github.com/facebook/react/issues/30114.
When switching between roots in the profiler flamegraph, the commit
index was preserved from the previous root. This caused an error
"Invalid commit X. There are only Y commits." when the new root had
fewer commits than the selected index.
This fix resets the commit index to 0 (or null if no commits) when the
commitData changes, which happens when switching roots.
## Summary
Add keyboard shortcuts (Cmd/Ctrl + Left/Right arrow keys) to navigate
between commits in the Profiler's snapshot view.
Moved `filteredCommitIndices` management and commit navigation logic
(`selectNextCommitIndex`, `selectPrevCommitIndex`) from
`SnapshotSelector` into `useCommitFilteringAndNavigation` used by
`ProfilerContext` to enable keyboard shortcuts from the top-level
Profiler component.
## How did you test this change?
- New tests in ProfilerContext-tests
- Built browser extension: `yarn build:<browser name>`
- tested in browser: `yarn run test:<browser name>`
- Manually verified Left/Right arrow navigation cycles through commits
- Verified navigation respects commit duration filter
- Verified reload-and-profile button unaffected
Chrome:
https://github.com/user-attachments/assets/01d2a749-13dc-4d08-8bcb-3d4d45a5f97c
Edge with duration filter:
https://github.com/user-attachments/assets/a7f76ff7-2a0b-4b9c-a0ce-d4449373308b
firefox mixing hotkey with clicking arrow buttons:
https://github.com/user-attachments/assets/48912d68-7c75-40f2-a203-5e6d7e6b2d99
When I moved the outline to above all other rects, I thought it was
clever to unify with the root so that the outline was also used for the
root selection. But the root outline is not drawn like the other rects.
It's outside the padding and doesn't have the 1px adjustment which leads
the overlay to be slightly inside the other rect instead of above it.
This goes back to just having the selected root be drawn by the root
element.
Before:
<img width="652" height="253" alt="Screenshot 2025-11-07 at 11 39 28 AM"
src="https://github.com/user-attachments/assets/334237d1-f190-4995-94cc-9690ec0f7ce1"
/>
After:
<img width="674" height="220" alt="Screenshot 2025-11-07 at 11 44 01 AM"
src="https://github.com/user-attachments/assets/afaa86d8-942a-44d8-a1a5-67c7fb642c0d"
/>
It's annoying to have to try to find where it lines up with no hints.
This way when you hover over something it should be on screen.
The strategy I went with is that it scrolls to a percentage along the
scrollable axis but the two might not be exactly the same. Partially
because they have different aspect ratios but also because suspended
boundaries can shrink the document while the suspense tab needs to still
be able to show the boundaries that are currently invisible.
Right now it's possible for things like server environments to appear
before other content in the timeline just because it's in a different
document order.
Ofc the order in production is not guaranteed but we can at least use
the timing information we have as a hint towards the actual order.
Unfortunately since the end time of the RSC stream itself is always
after the content that resolved to produce it, it becomes kind of
determined by the chunking. Similarly since for a clean refresh, the
scripts and styles will typically load after the server content they
appear later. Similarly SSR typically finishes after the RSC parts.
Therefore a hack here is that I artificially delay everything with a
non-null environment (RSC) so that RSC always comes after client-side
(Suspense). This is also consistent with how we color things that have
an environment even if children are just Suspense.
To ensure that we never show a child before a parent, in the timeline,
each child has a minimum time of its parent.
This shows the title in the top corner of the rect if there's enough
space.
The complex bit here is that it can be noisy if too many boundaries
occupy the same space to overlap or partially overlap.
This uses an R-tree to store all the rects to find overlapping
boundaries to cut the available space to draw inside the rect. We use
this to compute the rectangle within the rect which doesn't have any
overlapping boundaries.
The roots don't count as overlapping. Similarly, a parent rect is not
consider overlapping a child. However, if two sibling boundaries occupy
the same space, no title will be drawn.
<img width="734" height="813" alt="Screenshot 2025-10-19 at 5 34 49 PM"
src="https://github.com/user-attachments/assets/2b848b9c-3b78-48e5-9476-dd59a7baf6bf"
/>
We might also consider drawing the "Initial Paint" title at the root but
that's less interesting. It's interesting in the beginning before you
know about the special case at the root but after that it's just always
the same value so just adds noise.
Stacked on #34906.
Infer name from stack if it's the generic "lazy" name. It might be
wrapped in an abstraction. E.g. `next/dynamic`.
Also use the function name as a description of a resolved function
value.
<img width="310" height="166" alt="Screenshot 2025-10-18 at 10 42 05 AM"
src="https://github.com/user-attachments/assets/c63170b9-2b19-4f30-be7a-6429bb3ef3d9"
/>
Stacked on #34892.
In the timeline scrubber each timeline entry gets a label and color
assigned based on the environment computed for that step.
In the rects, we find the timeline step that this boundary is part of
and use that environment to assign a color. This is slightly different
than picking from the boundary itself since it takes into account parent
boundaries.
In the "suspended by" section we color each entry individually based on
the environment that spawned the I/O.
<img width="790" height="813" alt="Screenshot 2025-10-17 at 12 18 56 AM"
src="https://github.com/user-attachments/assets/c902b1fb-0992-4e24-8e94-a97ca8507551"
/>
Stacked on #34885.
This refactors the timeline to store not just an id but a complex object
for each step. This will later represent a group of boundaries.
Each timeline step is assigned an environment name. We pick the last
environment name (assumed to have resolved last) from the union of the
parent and child environment names. I.e. a child step is considered to
be blocked by the parent so if a child isn't blocked on any environment
name it still gets marked as the parent's environment name.
In a follow up, I'd like to reorder the document order timeline based on
environment names to favor loading everything in one environment before
the next.
Stacked on #34881.
We don't paint suspense boundaries if there are no suspenders. This does
the same with the root. The root is still selectable so you can confirm
but there's no affordance drawing attention to click the root.
This could happen if you don't use the built-ins of React to load things
like scripts and css. It would never happen in something like Next.js
where code and CSS is loaded through React-native like RSC.
However, it could also happen in the Activity scoped case when all
resources are always loaded early.
Stacked on #34880.
In #34861 I removed the highlight of the real view when hovering the
timeline since it was disruptive to stepping through the visuals.
This makes it so that when we hover the timeline we highlight the rect
with the subtle hover effect added in #34880.
We can now just use the one shared state for this and don't need the CSS
psuedo-selectors.
<img width="603" height="813" alt="Screenshot 2025-10-16 at 3 11 17 PM"
src="https://github.com/user-attachments/assets/a018b5ce-dd4d-4e77-ad47-b4ea068f1976"
/>
<img width="1011" height="811" alt="Screenshot 2025-10-16 at 2 20 46 PM"
src="https://github.com/user-attachments/assets/6dea3962-d369-4823-b44f-2c62b566c8f1"
/>
The selection is now clearer with a wider outline which spans the
bounding box if there are multi rects.
The color now gets darked changes on hover with a slight animation.
The colors are now mixed from constants defined which are consistently
used in the rects, the time span in the "suspended by" side bar and the
scrubber. I also have constants defined for "server" and "other" debug
environments which will be used in a follow up.
This ensures that the outline of a previous rectangle lines up on the
same pixel as the next rectangle so that they appear consecutive.
<img width="244" height="51" alt="Screenshot 2025-10-16 at 11 35 32 AM"
src="https://github.com/user-attachments/assets/75ffde6f-8cc6-49c1-8855-3953569546b4"
/>
I don't love this implementation. There's probably a smarter way. Was
trying to avoid adding another element.
Currently the sub-pixel precision is lost which can lead to things not
lining up properly and being slightly off or overlapping.
We need some sub-pixel precision.
Ideally we'd just keep the floating point as is. I'm not sure why the
operations is limited to integers. We don't send it as a typed array
anyway it seems which would ideally be more optimal. Even if we did, we
haven't defined a precision for the protocol. Is it 32bit integer?
64bit? If it's 64bit we can fit a float anyway. Ideally it would be more
variable precision like just pushing into a typed array directly with
the option to write whatever precision we want.
Add inspection button to Suspense tab which lets you select only among
Suspense nodes. It highlights all the DOM nodes in the root of the
Suspense node instead of just the DOM element you hover. The name is
inferred.
<img width="1172" height="841" alt="Screenshot 2025-10-15 at 8 03 34 PM"
src="https://github.com/user-attachments/assets/f04d965b-ef6e-4196-9ba0-51626148fa1a"
/>
I find it very frustrating that the highlight covers up the content that
I'm trying to review when stepping through the timeline. It also
triggered on keyboard navigation due to the focus which was annoying.
We could highlight something in the rects instead potentially.
This revealed that a lot of the event types were defined on the wrong
end of the bridge.
It was also a problem that events with the same name couldn't have
different arguments.
I get the wish to click the shadow but not all child boundaries are
within the bounds of the outer Suspense boundary's node.
Sometimes they overflow naturally and if we make it overflow hidden we
hide the boundaries. Maybe it would be ok if they're actually clipped by
the real DOM but right now it covers up boundaries that should be there.
Additionally, there's also a common case where the parent boundary
shrinks when suspending the children. That then causes the suspended
child boundaries to be clipped so that you can't restore them. Maybe the
virtual boundary shouldn't shrink in this case.