@buoy-gg/highlight-updates 5.0.0 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -178
- package/lib/commonjs/highlight-updates/HighlightUpdatesOverlay.js +11 -15
- package/lib/commonjs/highlight-updates/utils/CommitProfiler.js +279 -0
- package/lib/commonjs/highlight-updates/utils/HighlightUpdatesController.js +200 -174
- package/lib/commonjs/highlight-updates/utils/devtoolsComponentNames.js +127 -0
- package/lib/commonjs/index.js +67 -0
- package/lib/module/highlight-updates/HighlightUpdatesOverlay.js +12 -16
- package/lib/module/highlight-updates/utils/CommitProfiler.js +276 -0
- package/lib/module/highlight-updates/utils/HighlightUpdatesController.js +196 -170
- package/lib/module/highlight-updates/utils/devtoolsComponentNames.js +119 -0
- package/lib/module/index.js +50 -0
- package/lib/typescript/highlight-updates/HighlightUpdatesOverlay.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/utils/CommitProfiler.d.ts +32 -0
- package/lib/typescript/highlight-updates/utils/CommitProfiler.d.ts.map +1 -0
- package/lib/typescript/highlight-updates/utils/HighlightUpdatesController.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/utils/devtoolsComponentNames.d.ts +19 -0
- package/lib/typescript/highlight-updates/utils/devtoolsComponentNames.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,210 +1,61 @@
|
|
|
1
|
-
# @buoy/highlight-updates
|
|
1
|
+
# @buoy-gg/highlight-updates
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@buoy-gg/highlight-updates) [](https://www.npmjs.com/package/@buoy-gg/highlight-updates)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**See exactly WHY your React Native components re-render — every render gets an overlay tagged with its cause: mount, state, props, or parent.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Part of [Buoy](https://github.com/Buoy-gg/buoy) — devtools that live inside your React Native app. Install it and it auto-appears in the floating menu from [`@buoy-gg/core`](https://www.npmjs.com/package/@buoy-gg/core).
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Install
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install @buoy/highlight-updates
|
|
13
|
-
# or
|
|
14
|
-
yarn add @buoy/highlight-updates
|
|
15
|
-
# or
|
|
16
|
-
pnpm add @buoy/highlight-updates
|
|
12
|
+
npm install @buoy-gg/core @buoy-gg/highlight-updates
|
|
17
13
|
```
|
|
18
14
|
|
|
19
|
-
##
|
|
20
|
-
|
|
21
|
-
### With FloatingDevTools (Recommended)
|
|
22
|
-
|
|
23
|
-
The easiest way to use this tool is with the FloatingDevTools menu:
|
|
24
|
-
|
|
25
|
-
```tsx
|
|
26
|
-
import { FloatingDevTools, autoDiscoverPresets } from '@buoy/core';
|
|
27
|
-
|
|
28
|
-
// The highlight updates tool is automatically discovered if installed
|
|
29
|
-
const apps = autoDiscoverPresets();
|
|
30
|
-
|
|
31
|
-
function App() {
|
|
32
|
-
return (
|
|
33
|
-
<FloatingDevTools apps={apps}>
|
|
34
|
-
<YourApp />
|
|
35
|
-
</FloatingDevTools>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Or explicitly add the preset:
|
|
15
|
+
## Quick start
|
|
41
16
|
|
|
42
17
|
```tsx
|
|
43
|
-
import { FloatingDevTools } from
|
|
44
|
-
import { highlightUpdatesPreset } from '@buoy/highlight-updates';
|
|
18
|
+
import { FloatingDevTools } from "@buoy-gg/core";
|
|
45
19
|
|
|
46
|
-
function App() {
|
|
20
|
+
export default function App() {
|
|
47
21
|
return (
|
|
48
|
-
|
|
22
|
+
<>
|
|
49
23
|
<YourApp />
|
|
50
|
-
|
|
24
|
+
<FloatingDevTools />
|
|
25
|
+
</>
|
|
51
26
|
);
|
|
52
27
|
}
|
|
53
28
|
```
|
|
54
29
|
|
|
55
|
-
|
|
30
|
+
That's it — the Render Highlighter appears in the floating menu. It's a standalone implementation: no React DevTools connection, no Chrome tab, no Flipper required.
|
|
56
31
|
|
|
57
|
-
|
|
32
|
+
Prefer to drive it yourself?
|
|
58
33
|
|
|
59
34
|
```tsx
|
|
60
|
-
import { HighlightUpdatesController } from
|
|
61
|
-
|
|
62
|
-
// Initialize when DevTools is connected
|
|
63
|
-
HighlightUpdatesController.initialize();
|
|
35
|
+
import { HighlightUpdatesController } from "@buoy-gg/highlight-updates";
|
|
64
36
|
|
|
65
|
-
// Toggle highlights
|
|
66
37
|
HighlightUpdatesController.toggle();
|
|
67
|
-
|
|
68
|
-
// Enable/disable directly
|
|
69
|
-
HighlightUpdatesController.enable();
|
|
70
|
-
HighlightUpdatesController.disable();
|
|
71
|
-
|
|
72
|
-
// Check state
|
|
73
|
-
const isEnabled = HighlightUpdatesController.isEnabled();
|
|
74
|
-
|
|
75
|
-
// Subscribe to state changes
|
|
76
|
-
const unsubscribe = HighlightUpdatesController.subscribe((enabled) => {
|
|
77
|
-
console.log('Highlights enabled:', enabled);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Cleanup when done
|
|
81
|
-
HighlightUpdatesController.destroy();
|
|
82
38
|
```
|
|
83
39
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Create a customized version of the tool:
|
|
87
|
-
|
|
88
|
-
```tsx
|
|
89
|
-
import { createHighlightUpdatesTool } from '@buoy/highlight-updates';
|
|
90
|
-
|
|
91
|
-
const myHighlightTool = createHighlightUpdatesTool({
|
|
92
|
-
name: 'RENDERS',
|
|
93
|
-
enabledColor: '#ec4899', // Pink when enabled
|
|
94
|
-
disabledColor: '#9ca3af', // Gray when disabled
|
|
95
|
-
autoInitialize: true, // Initialize automatically
|
|
96
|
-
});
|
|
97
|
-
```
|
|
40
|
+
## What you get
|
|
98
41
|
|
|
99
|
-
|
|
42
|
+
- **Render overlays with the cause** — every render flashes a bounding box tagged mount / state change / prop change / parent re-render. No more guessing why a component updated.
|
|
43
|
+
- **Hook value tracking** — when state caused the render, see the before → after values of the exact `useState` or `useReducer` that changed.
|
|
44
|
+
- **Overlay mode** — lightweight real-time highlights while you interact with the app; perfect for spotting unnecessary re-renders.
|
|
45
|
+
- **Modal mode** — full inspector with render history, filtering, and detailed per-render cause breakdowns for deep debugging sessions.
|
|
46
|
+
- **Powers Bench's render capture** — with [`@buoy-gg/perf-monitor`](https://www.npmjs.com/package/@buoy-gg/perf-monitor) installed, benchmark recordings capture per-component render counts and durations.
|
|
47
|
+
- **Filters out the framework noise** — you see your components, not `View`/`Text` internals.
|
|
100
48
|
|
|
101
|
-
|
|
49
|
+
## Desktop & AI
|
|
102
50
|
|
|
103
|
-
|
|
104
|
-
2. **"Highlight updates when components render" must be enabled** in DevTools Profiler settings
|
|
105
|
-
|
|
106
|
-
This tool then allows you to temporarily disable the highlighting without going back to DevTools settings.
|
|
107
|
-
|
|
108
|
-
## How It Works
|
|
109
|
-
|
|
110
|
-
The tool intercepts the DevTools agent's `drawTraceUpdates` event listener. When disabled, it simply doesn't forward the event to the original handler, effectively blocking the highlight rendering.
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
React DevTools Frontend (Chrome/Flipper)
|
|
114
|
-
| (sends drawTraceUpdates event)
|
|
115
|
-
v
|
|
116
|
-
React DevTools Agent
|
|
117
|
-
| (event intercepted by HighlightUpdatesController)
|
|
118
|
-
v
|
|
119
|
-
HighlightUpdatesController.controlledListener
|
|
120
|
-
| (if enabled, forwards to original)
|
|
121
|
-
v
|
|
122
|
-
DebuggingOverlayRegistry (original listener)
|
|
123
|
-
| (processes component bounds)
|
|
124
|
-
v
|
|
125
|
-
DebuggingOverlay (native component)
|
|
126
|
-
| (renders highlights on screen)
|
|
127
|
-
```
|
|
51
|
+
The same live session streams to [Buoy Desktop](https://github.com/Buoy-gg/Buoy-Desktop) (free, macOS/Windows/Linux) and to Claude Code or Cursor via the [Buoy MCP server](https://buoy.gg/buoy/latest/docs/mcp).
|
|
128
52
|
|
|
129
|
-
##
|
|
53
|
+
## Free vs Pro
|
|
130
54
|
|
|
131
|
-
|
|
55
|
+
Every tool is free. [Pro](https://buoy.gg/pricing) unlocks production builds, the MCP server, and unlimited capture. Every weekend, Pro features unlock free for everyone.
|
|
132
56
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
### `createHighlightUpdatesTool(options?)`
|
|
136
|
-
|
|
137
|
-
Factory function to create a customized tool.
|
|
138
|
-
|
|
139
|
-
| Option | Type | Default | Description |
|
|
140
|
-
|--------|------|---------|-------------|
|
|
141
|
-
| `name` | `string` | `"UPDATES"` | Display name in the menu |
|
|
142
|
-
| `description` | `string` | `"Toggle component render highlights"` | Tool description |
|
|
143
|
-
| `enabledColor` | `string` | `"#10b981"` | Icon color when enabled (green) |
|
|
144
|
-
| `disabledColor` | `string` | `"#6b7280"` | Icon color when disabled (gray) |
|
|
145
|
-
| `id` | `string` | `"highlight-updates"` | Unique tool identifier |
|
|
146
|
-
| `autoInitialize` | `boolean` | `false` | Initialize automatically on load |
|
|
147
|
-
|
|
148
|
-
### `HighlightUpdatesController`
|
|
149
|
-
|
|
150
|
-
Standalone controller for programmatic use.
|
|
151
|
-
|
|
152
|
-
| Method | Description |
|
|
153
|
-
|--------|-------------|
|
|
154
|
-
| `initialize()` | Initialize the controller. Returns `true` on success. |
|
|
155
|
-
| `enable()` | Enable highlight rendering |
|
|
156
|
-
| `disable()` | Disable highlight rendering |
|
|
157
|
-
| `toggle()` | Toggle the enabled state |
|
|
158
|
-
| `isEnabled()` | Get current enabled state |
|
|
159
|
-
| `setEnabled(enabled)` | Set enabled state |
|
|
160
|
-
| `isInitialized()` | Check if controller is initialized |
|
|
161
|
-
| `subscribe(callback)` | Subscribe to state changes. Returns unsubscribe function. |
|
|
162
|
-
| `destroy()` | Cleanup and restore original listener |
|
|
163
|
-
|
|
164
|
-
## Console Access
|
|
165
|
-
|
|
166
|
-
When in development mode, the controller is exposed globally:
|
|
167
|
-
|
|
168
|
-
```javascript
|
|
169
|
-
// In Chrome DevTools console
|
|
170
|
-
window.__HIGHLIGHT_UPDATES_CONTROLLER__.toggle();
|
|
171
|
-
window.__HIGHLIGHT_UPDATES_CONTROLLER__.isEnabled();
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
## Troubleshooting
|
|
175
|
-
|
|
176
|
-
### Controller Not Initializing
|
|
177
|
-
|
|
178
|
-
**Problem:** `initialize()` returns false
|
|
179
|
-
|
|
180
|
-
**Solutions:**
|
|
181
|
-
1. Check DevTools is connected: `window.__REACT_DEVTOOLS_GLOBAL_HOOK__.reactDevtoolsAgent`
|
|
182
|
-
2. Add initialization delay: Wait 1-2 seconds after app launch
|
|
183
|
-
3. Ensure "Highlight updates" is enabled in DevTools first
|
|
184
|
-
|
|
185
|
-
### Highlights Still Showing After Disable
|
|
186
|
-
|
|
187
|
-
**Problem:** Highlights appear even when disabled
|
|
188
|
-
|
|
189
|
-
**Solutions:**
|
|
190
|
-
1. Verify controller is initialized: `HighlightUpdatesController.isInitialized()`
|
|
191
|
-
2. Check enabled state: `HighlightUpdatesController.isEnabled()`
|
|
192
|
-
3. Ensure listener was properly intercepted (check console logs)
|
|
193
|
-
|
|
194
|
-
## Development
|
|
195
|
-
|
|
196
|
-
### Building
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
pnpm build
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Type Checking
|
|
203
|
-
|
|
204
|
-
```bash
|
|
205
|
-
pnpm typecheck
|
|
206
|
-
```
|
|
57
|
+
---
|
|
207
58
|
|
|
208
|
-
|
|
59
|
+
📚 [Full docs](https://buoy.gg/buoy/latest/docs/tools/highlight-updates) · [All Buoy tools](https://github.com/Buoy-gg/buoy)
|
|
209
60
|
|
|
210
|
-
|
|
61
|
+
Proprietary software. © Buoy LLC. [Terms](https://buoy.gg/terms)
|
|
@@ -194,35 +194,31 @@ function HighlightUpdatesOverlay({
|
|
|
194
194
|
height: rect.height,
|
|
195
195
|
borderColor: rect.color
|
|
196
196
|
}],
|
|
197
|
-
children: rect.count > 0 &&
|
|
198
|
-
onPress: () => handleBadgePress(rect.id),
|
|
197
|
+
children: rect.count > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
199
198
|
style: [styles.badge, {
|
|
200
199
|
backgroundColor: rect.color
|
|
201
200
|
}],
|
|
202
|
-
|
|
201
|
+
nativeID: `__highlight_badge_${rect.id}__`,
|
|
202
|
+
pointerEvents: hasBadgePressHandler ? "auto" : "none",
|
|
203
203
|
hitSlop: {
|
|
204
204
|
top: 8,
|
|
205
205
|
bottom: 8,
|
|
206
206
|
left: 8,
|
|
207
207
|
right: 8
|
|
208
208
|
},
|
|
209
|
+
onStartShouldSetResponder: () => !!hasBadgePressHandler
|
|
210
|
+
// Fire on touch-DOWN: on churning screens the badge views are
|
|
211
|
+
// torn down and recreated between finger-down and finger-up
|
|
212
|
+
// (every highlight event rebuilds the boxes), which kills the
|
|
213
|
+
// responder before a release can fire. Grant fires immediately.
|
|
214
|
+
,
|
|
215
|
+
onResponderGrant: () => handleBadgePress(rect.id),
|
|
209
216
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
210
217
|
style: styles.badgeText,
|
|
211
218
|
nativeID: `__highlight_text_${rect.id}__`,
|
|
212
219
|
children: rect.count
|
|
213
220
|
})
|
|
214
|
-
})
|
|
215
|
-
style: [styles.badge, {
|
|
216
|
-
backgroundColor: rect.color
|
|
217
|
-
}],
|
|
218
|
-
nativeID: `__highlight_badge_${rect.id}__`,
|
|
219
|
-
pointerEvents: "none",
|
|
220
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
221
|
-
style: styles.badgeText,
|
|
222
|
-
nativeID: `__highlight_text_${rect.id}__`,
|
|
223
|
-
children: rect.count
|
|
224
|
-
})
|
|
225
|
-
}))
|
|
221
|
+
})
|
|
226
222
|
}, `highlight-${rect.id}`))]
|
|
227
223
|
});
|
|
228
224
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.CommitProfiler = void 0;
|
|
7
|
+
var _devtoolsComponentNames = require("./devtoolsComponentNames");
|
|
8
|
+
/**
|
|
9
|
+
* CommitProfiler — per-component render counts + durations across a capture
|
|
10
|
+
* window, React-DevTools-profiler style, without React DevTools attached.
|
|
11
|
+
*
|
|
12
|
+
* Wraps `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` (the callback React
|
|
13
|
+
* invokes after every commit) and, while a capture is active, walks the
|
|
14
|
+
* committed fiber tree collecting composite fibers that performed work this
|
|
15
|
+
* commit. Self duration follows React's convention: `actualDuration` minus the
|
|
16
|
+
* sum of direct children's `actualDuration` (profiling timers are enabled in
|
|
17
|
+
* dev builds, where Buoy runs).
|
|
18
|
+
*
|
|
19
|
+
* This is intentionally separate from the traceUpdates flow the highlight
|
|
20
|
+
* overlay uses — traceUpdates emits host views with no timing data.
|
|
21
|
+
*
|
|
22
|
+
* Consumed by perf-monitor benchmarks via the shared-ui render-capture
|
|
23
|
+
* registry (see the registration in this package's index.tsx).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// React work tags for components that render user code.
|
|
27
|
+
const FunctionComponent = 0;
|
|
28
|
+
const ClassComponent = 1;
|
|
29
|
+
const ForwardRef = 11;
|
|
30
|
+
const MemoComponent = 14;
|
|
31
|
+
const SimpleMemoComponent = 15;
|
|
32
|
+
|
|
33
|
+
// React fiber flag: this fiber rendered (performed work) in the last commit.
|
|
34
|
+
// Stable value 0b1 across React 16-19 (effectTag pre-18, flags after).
|
|
35
|
+
const PerformedWork = 0b1;
|
|
36
|
+
|
|
37
|
+
/** Overhead guardrail: stop walking a commit past this many visited fibers. */
|
|
38
|
+
const MAX_FIBERS_PER_COMMIT = 10_000;
|
|
39
|
+
|
|
40
|
+
/** Bound on distinct component names tracked; overflow still counts in totals. */
|
|
41
|
+
const MAX_TRACKED_NAMES = 300;
|
|
42
|
+
|
|
43
|
+
/** Components returned in the summary. */
|
|
44
|
+
const TOP_COMPONENTS_CAP = 20;
|
|
45
|
+
let hookInstalled = false;
|
|
46
|
+
let active = false;
|
|
47
|
+
let sawDurations = false;
|
|
48
|
+
let totalCommits = 0;
|
|
49
|
+
let totalRenders = 0;
|
|
50
|
+
let totalRenderMs = 0;
|
|
51
|
+
let buckets = new Map();
|
|
52
|
+
function getHook() {
|
|
53
|
+
if (typeof global === "undefined") return null;
|
|
54
|
+
return global.__REACT_DEVTOOLS_GLOBAL_HOOK__ ?? null;
|
|
55
|
+
}
|
|
56
|
+
function getFlags(fiber) {
|
|
57
|
+
const f = fiber.flags ?? fiber.effectTag;
|
|
58
|
+
return typeof f === "number" ? f : 0;
|
|
59
|
+
}
|
|
60
|
+
function isCompositeTag(tag) {
|
|
61
|
+
return tag === FunctionComponent || tag === ClassComponent || tag === ForwardRef || tag === MemoComponent || tag === SimpleMemoComponent;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Resolve a composite fiber's display name (DevTools conventions). */
|
|
65
|
+
function getComponentName(fiber) {
|
|
66
|
+
const type = fiber.type;
|
|
67
|
+
try {
|
|
68
|
+
switch (fiber.tag) {
|
|
69
|
+
case FunctionComponent:
|
|
70
|
+
case ClassComponent:
|
|
71
|
+
return type?.displayName || type?.name || "Anonymous";
|
|
72
|
+
case ForwardRef:
|
|
73
|
+
return type?.displayName || type?.render?.displayName || type?.render?.name || "ForwardRef";
|
|
74
|
+
case MemoComponent:
|
|
75
|
+
case SimpleMemoComponent:
|
|
76
|
+
{
|
|
77
|
+
const inner = type?.type ?? fiber.elementType?.type;
|
|
78
|
+
return type?.displayName || inner?.displayName || inner?.name || "Memo";
|
|
79
|
+
}
|
|
80
|
+
default:
|
|
81
|
+
return "Anonymous";
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
return "Anonymous";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Self duration = own actualDuration minus direct children's. React's
|
|
90
|
+
* bubbleProperties adds every direct child's actualDuration into the parent,
|
|
91
|
+
* so subtracting them all back out is symmetric and exact.
|
|
92
|
+
*/
|
|
93
|
+
function computeSelfMs(fiber) {
|
|
94
|
+
const actual = fiber.actualDuration;
|
|
95
|
+
if (typeof actual !== "number") return null;
|
|
96
|
+
let self = actual;
|
|
97
|
+
for (let child = fiber.child; child != null; child = child.sibling) {
|
|
98
|
+
if (typeof child.actualDuration === "number") self -= child.actualDuration;
|
|
99
|
+
}
|
|
100
|
+
return self > 0 ? self : 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// React Native framework wrapper components. Their renders are real work
|
|
104
|
+
// but the actionable signal lives on the OWNING component (a Touchable
|
|
105
|
+
// re-rendering is already visible as its parent re-rendering), so they're
|
|
106
|
+
// folded into the capture totals and kept OUT of the per-component list.
|
|
107
|
+
// This is a report-level filter only — unlike isDevToolsComponentName it
|
|
108
|
+
// must NOT prune subtrees (their children are user components).
|
|
109
|
+
const FRAMEWORK_WRAPPER_NAMES = new Set(["View", "Text", "Image", "ImageBackground", "ScrollView", "FlatList", "SectionList", "SafeAreaView", "KeyboardAvoidingView", "StatusBar", "ActivityIndicator", "RefreshControl", "Modal", "Switch", "Pressable", "PlatformPressable", "TouchableOpacity", "TouchableHighlight", "TouchableWithoutFeedback", "TouchableNativeFeedback", "PressabilityDebugView", "AnimatedComponent", "Wrapper"]);
|
|
110
|
+
const FRAMEWORK_WRAPPER_PREFIXES = ["Animated(",
|
|
111
|
+
// Animated(View), Animated(Text), Animated(FlatList), …
|
|
112
|
+
"Pressability", "Virtualized",
|
|
113
|
+
// VirtualizedList + its context providers
|
|
114
|
+
"CellRenderer"];
|
|
115
|
+
function isFrameworkWrapperName(name) {
|
|
116
|
+
if (FRAMEWORK_WRAPPER_NAMES.has(name)) return true;
|
|
117
|
+
for (let i = 0; i < FRAMEWORK_WRAPPER_PREFIXES.length; i++) {
|
|
118
|
+
if (name.startsWith(FRAMEWORK_WRAPPER_PREFIXES[i])) return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
function recordRender(fiber) {
|
|
123
|
+
const name = getComponentName(fiber);
|
|
124
|
+
const selfMs = computeSelfMs(fiber);
|
|
125
|
+
if (selfMs !== null) sawDurations = true;
|
|
126
|
+
const ms = selfMs ?? 0;
|
|
127
|
+
totalRenders += 1;
|
|
128
|
+
totalRenderMs += ms;
|
|
129
|
+
|
|
130
|
+
// Framework wrappers count toward totals but never surface as a row.
|
|
131
|
+
if (isFrameworkWrapperName(name)) return;
|
|
132
|
+
let bucket = buckets.get(name);
|
|
133
|
+
if (!bucket) {
|
|
134
|
+
// Bounded map: overflow renders still count toward the totals above.
|
|
135
|
+
if (buckets.size >= MAX_TRACKED_NAMES) return;
|
|
136
|
+
bucket = {
|
|
137
|
+
renders: 0,
|
|
138
|
+
totalSelfMs: 0,
|
|
139
|
+
maxSelfMs: 0
|
|
140
|
+
};
|
|
141
|
+
buckets.set(name, bucket);
|
|
142
|
+
}
|
|
143
|
+
bucket.renders += 1;
|
|
144
|
+
bucket.totalSelfMs += ms;
|
|
145
|
+
if (ms > bucket.maxSelfMs) bucket.maxSelfMs = ms;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Iterative walk (child → sibling → return) of the committed tree. Never
|
|
150
|
+
* throws upward — a broken walk must not break React's commit.
|
|
151
|
+
*/
|
|
152
|
+
function handleCommit(root) {
|
|
153
|
+
const start = root?.current;
|
|
154
|
+
if (!start) return;
|
|
155
|
+
const rendersBefore = totalRenders;
|
|
156
|
+
let visited = 0;
|
|
157
|
+
let fiber = start;
|
|
158
|
+
while (fiber != null) {
|
|
159
|
+
visited += 1;
|
|
160
|
+
if (visited > MAX_FIBERS_PER_COMMIT) break;
|
|
161
|
+
let skipSubtree = false;
|
|
162
|
+
const composite = isCompositeTag(fiber.tag);
|
|
163
|
+
if (composite) {
|
|
164
|
+
// Prune Buoy's own devtools UI wholesale.
|
|
165
|
+
if ((0, _devtoolsComponentNames.isDevToolsComponentName)(getComponentName(fiber))) {
|
|
166
|
+
skipSubtree = true;
|
|
167
|
+
} else if ((getFlags(fiber) & PerformedWork) !== 0) {
|
|
168
|
+
// PerformedWork is the only trustworthy "rendered this commit"
|
|
169
|
+
// signal — actualDuration is stale on bailed-out fibers.
|
|
170
|
+
recordRender(fiber);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!skipSubtree) {
|
|
174
|
+
// Prune subtrees with no work. subtreeFlags only exists on React 18+;
|
|
175
|
+
// when absent, always descend.
|
|
176
|
+
const subtreeFlags = fiber.subtreeFlags;
|
|
177
|
+
const noSubtreeWork = typeof subtreeFlags === "number" && (subtreeFlags & PerformedWork) === 0 && (getFlags(fiber) & PerformedWork) === 0;
|
|
178
|
+
if (!noSubtreeWork && fiber.child != null) {
|
|
179
|
+
fiber = fiber.child;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// No descend: advance to sibling, climbing up as needed.
|
|
185
|
+
while (fiber != null) {
|
|
186
|
+
if (fiber === start) return finalizeCommit(rendersBefore);
|
|
187
|
+
if (fiber.sibling != null) {
|
|
188
|
+
fiber = fiber.sibling;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
fiber = fiber.return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
finalizeCommit(rendersBefore);
|
|
195
|
+
}
|
|
196
|
+
function finalizeCommit(rendersBefore) {
|
|
197
|
+
if (totalRenders > rendersBefore) totalCommits += 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Install the onCommitFiberRoot wrapper once and never restore it — the
|
|
202
|
+
* `active` flag gates all work, and never restoring avoids clobber races if
|
|
203
|
+
* something else (e.g. React DevTools attaching mid-session) wraps the hook
|
|
204
|
+
* after us. The wrapper dies with the JS realm on reload.
|
|
205
|
+
*/
|
|
206
|
+
function ensureHookInstalled() {
|
|
207
|
+
if (hookInstalled) return true;
|
|
208
|
+
const hook = getHook();
|
|
209
|
+
if (!hook) return false;
|
|
210
|
+
const original = hook.onCommitFiberRoot;
|
|
211
|
+
hook.onCommitFiberRoot = function (rendererID, root, ...rest) {
|
|
212
|
+
if (active) {
|
|
213
|
+
try {
|
|
214
|
+
handleCommit(root);
|
|
215
|
+
} catch {
|
|
216
|
+
// Capture must never break React's commit path.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return typeof original === "function" ? original.call(hook, rendererID, root, ...rest) : undefined;
|
|
220
|
+
};
|
|
221
|
+
hookInstalled = true;
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
function isSupported() {
|
|
225
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return false;
|
|
226
|
+
const hook = getHook();
|
|
227
|
+
return Boolean(hook && hook.renderers && hook.renderers.size > 0);
|
|
228
|
+
}
|
|
229
|
+
function isCapturing() {
|
|
230
|
+
return active;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Begin a capture window, discarding any previous unclaimed data. */
|
|
234
|
+
function startCapture() {
|
|
235
|
+
if (!ensureHookInstalled()) return;
|
|
236
|
+
buckets = new Map();
|
|
237
|
+
sawDurations = false;
|
|
238
|
+
totalCommits = 0;
|
|
239
|
+
totalRenders = 0;
|
|
240
|
+
totalRenderMs = 0;
|
|
241
|
+
active = true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** End the window and return the aggregate (null if never started). */
|
|
245
|
+
function stopCapture() {
|
|
246
|
+
if (!active && totalRenders === 0 && buckets.size === 0) return null;
|
|
247
|
+
active = false;
|
|
248
|
+
const topComponents = Array.from(buckets.entries()).sort((a, b) => {
|
|
249
|
+
const byMs = b[1].totalSelfMs - a[1].totalSelfMs;
|
|
250
|
+
return byMs !== 0 ? byMs : b[1].renders - a[1].renders;
|
|
251
|
+
}).slice(0, TOP_COMPONENTS_CAP).map(([name, b]) => ({
|
|
252
|
+
name,
|
|
253
|
+
renders: b.renders,
|
|
254
|
+
totalMs: round2(b.totalSelfMs),
|
|
255
|
+
maxMs: round2(b.maxSelfMs),
|
|
256
|
+
avgMs: b.renders > 0 ? round2(b.totalSelfMs / b.renders) : 0
|
|
257
|
+
}));
|
|
258
|
+
const summary = {
|
|
259
|
+
totalCommits,
|
|
260
|
+
totalRenders,
|
|
261
|
+
capturedDurations: sawDurations,
|
|
262
|
+
totalRenderMs: round2(totalRenderMs),
|
|
263
|
+
topComponents
|
|
264
|
+
};
|
|
265
|
+
buckets = new Map();
|
|
266
|
+
totalCommits = 0;
|
|
267
|
+
totalRenders = 0;
|
|
268
|
+
totalRenderMs = 0;
|
|
269
|
+
return summary;
|
|
270
|
+
}
|
|
271
|
+
function round2(n) {
|
|
272
|
+
return Math.round(n * 100) / 100;
|
|
273
|
+
}
|
|
274
|
+
const CommitProfiler = exports.CommitProfiler = {
|
|
275
|
+
isSupported,
|
|
276
|
+
isCapturing,
|
|
277
|
+
startCapture,
|
|
278
|
+
stopCapture
|
|
279
|
+
};
|