@buoy-gg/highlight-updates 7.0.21 → 7.0.23
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/lib/commonjs/highlight-updates/components/IsolatedRenderList.js +10 -7
- package/lib/commonjs/highlight-updates/components/RenderDetailView.js +9 -9
- package/lib/commonjs/highlight-updates/components/RenderHistoryViewer.js +15 -18
- package/lib/commonjs/highlight-updates/utils/CommitProfiler.js +170 -8
- package/lib/commonjs/highlight-updates/utils/HighlightUpdatesController.js +6 -1
- package/lib/commonjs/highlight-updates/utils/devtoolsComponentNames.js +46 -5
- package/lib/commonjs/sync/highlightUpdatesSyncAdapter.js +43 -1
- package/lib/module/highlight-updates/components/IsolatedRenderList.js +10 -8
- package/lib/module/highlight-updates/components/RenderDetailView.js +10 -10
- package/lib/module/highlight-updates/components/RenderHistoryViewer.js +16 -20
- package/lib/module/highlight-updates/utils/CommitProfiler.js +171 -9
- package/lib/module/highlight-updates/utils/HighlightUpdatesController.js +7 -2
- package/lib/module/highlight-updates/utils/devtoolsComponentNames.js +44 -4
- package/lib/module/sync/highlightUpdatesSyncAdapter.js +43 -1
- package/lib/typescript/highlight-updates/components/IsolatedRenderList.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/components/RenderDetailView.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/components/RenderHistoryViewer.d.ts +1 -1
- package/lib/typescript/highlight-updates/components/RenderHistoryViewer.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/utils/CommitProfiler.d.ts +12 -2
- package/lib/typescript/highlight-updates/utils/CommitProfiler.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/utils/HighlightUpdatesController.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/utils/devtoolsComponentNames.d.ts +34 -0
- package/lib/typescript/highlight-updates/utils/devtoolsComponentNames.d.ts.map +1 -1
- package/lib/typescript/sync/highlightUpdatesSyncAdapter.d.ts +8 -0
- package/lib/typescript/sync/highlightUpdatesSyncAdapter.d.ts.map +1 -1
- package/package.json +8 -8
|
@@ -23,7 +23,7 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
// Free tier limit for tracked components
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
/**
|
|
28
28
|
* Isolated list component that owns the render subscription.
|
|
29
29
|
* Parent component does NOT re-render when renders change.
|
|
@@ -62,16 +62,19 @@ function IsolatedRenderListInner({
|
|
|
62
62
|
}, [searchText, filters, onRendersChange]);
|
|
63
63
|
|
|
64
64
|
// Limit visible components for free tier
|
|
65
|
+
// `isPro` stays a prop so callers can force the unlimited view; the tier cap
|
|
66
|
+
// applies otherwise. useLimit() already returns Infinity for Pro, so the two
|
|
67
|
+
// agree — this only preserves the prop contract for cross-package callers.
|
|
68
|
+
const tierLimit = (0, _sharedUi.useLimit)("highlightUpdates.components");
|
|
69
|
+
const componentLimit = isPro ? Infinity : tierLimit;
|
|
65
70
|
const visibleRenders = (0, _react.useMemo)(() => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}, [renders, isPro]);
|
|
71
|
+
return renders.slice(0, componentLimit);
|
|
72
|
+
}, [renders, componentLimit]);
|
|
69
73
|
|
|
70
74
|
// Calculate locked component count
|
|
71
75
|
const lockedComponentCount = (0, _react.useMemo)(() => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}, [renders.length, isPro]);
|
|
76
|
+
return Math.max(0, renders.length - componentLimit);
|
|
77
|
+
}, [renders.length, componentLimit]);
|
|
75
78
|
|
|
76
79
|
// Stable keyExtractor - moved outside to avoid recreation
|
|
77
80
|
const keyExtractor = (0, _react.useCallback)(item => item.id, []);
|
|
@@ -26,7 +26,6 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
// Free tier limit for render history events
|
|
29
|
-
const FREE_TIER_EVENT_LIMIT = 5;
|
|
30
29
|
function RenderDetailView({
|
|
31
30
|
render,
|
|
32
31
|
disableInternalFooter = false,
|
|
@@ -52,11 +51,12 @@ function RenderDetailView({
|
|
|
52
51
|
const totalEvents = events.length;
|
|
53
52
|
const currentEvent = events[selectedEventIndex];
|
|
54
53
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
// `isPro` stays a prop so callers can force the unlimited view; the tier cap
|
|
55
|
+
// applies otherwise. useLimit() already returns Infinity for Pro, so the two
|
|
56
|
+
// agree — this only preserves the prop contract for cross-package callers.
|
|
57
|
+
const tierLimit = (0, _sharedUi.useLimit)("highlightUpdates.detailEvents");
|
|
58
|
+
const eventLimit = isPro ? Infinity : tierLimit;
|
|
59
|
+
const hiddenEventCount = (0, _react.useMemo)(() => Math.max(0, totalEvents - eventLimit), [eventLimit, totalEvents]);
|
|
60
60
|
|
|
61
61
|
// Use current event's cause if available, otherwise fall back to lastRenderCause
|
|
62
62
|
const displayCause = currentEvent?.cause || render.lastRenderCause;
|
|
@@ -68,12 +68,12 @@ function RenderDetailView({
|
|
|
68
68
|
}
|
|
69
69
|
}, [selectedEventIndex, onEventIndexChange]);
|
|
70
70
|
const goToNext = (0, _react.useCallback)(() => {
|
|
71
|
-
//
|
|
72
|
-
const maxIndex =
|
|
71
|
+
// Infinity - 1 is Infinity, so an unlimited tier just gets totalEvents - 1.
|
|
72
|
+
const maxIndex = Math.min(eventLimit - 1, totalEvents - 1);
|
|
73
73
|
if (selectedEventIndex < maxIndex) {
|
|
74
74
|
onEventIndexChange(selectedEventIndex + 1);
|
|
75
75
|
}
|
|
76
|
-
}, [selectedEventIndex, totalEvents, onEventIndexChange,
|
|
76
|
+
}, [selectedEventIndex, totalEvents, onEventIndexChange, eventLimit]);
|
|
77
77
|
|
|
78
78
|
// Memoize copy data
|
|
79
79
|
const copyData = (0, _react.useMemo)(() => (0, _rendersExportFormatter.generateSingleComponentExport)(render), [render]);
|
|
@@ -21,8 +21,6 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
21
21
|
* Shows current state view or diff view between renders.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
// Free tier limit for render history events
|
|
25
|
-
const FREE_TIER_EVENT_LIMIT = 3;
|
|
26
24
|
/**
|
|
27
25
|
* Format timestamp with milliseconds
|
|
28
26
|
*/
|
|
@@ -63,18 +61,16 @@ function RenderHistoryViewer({
|
|
|
63
61
|
return [...render.renderHistory].sort((a, b) => a.timestamp - b.timestamp);
|
|
64
62
|
}, [render.renderHistory]);
|
|
65
63
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}, [allEvents, isPro]);
|
|
64
|
+
// `isPro` stays a prop so callers can force the unlimited view; the tier cap
|
|
65
|
+
// applies otherwise. useLimit() already returns Infinity for Pro, so the two
|
|
66
|
+
// agree — this only preserves the prop contract for cross-package callers.
|
|
67
|
+
const tierLimit = (0, _sharedUi.useLimit)("highlightUpdates.historyEvents");
|
|
68
|
+
const eventLimit = isPro ? Infinity : tierLimit;
|
|
72
69
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}, [allEvents.length, isPro]);
|
|
70
|
+
// Show the most recent N events (negative index clamps to 0, so an
|
|
71
|
+
// unlimited tier returns the whole list).
|
|
72
|
+
const events = (0, _react.useMemo)(() => allEvents.slice(-eventLimit), [allEvents, eventLimit]);
|
|
73
|
+
const lockedEventCount = (0, _react.useMemo)(() => Math.max(0, allEvents.length - eventLimit), [allEvents.length, eventLimit]);
|
|
78
74
|
const totalEvents = events.length;
|
|
79
75
|
const currentEvent = events[selectedEventIndex];
|
|
80
76
|
const previousEvent = selectedEventIndex > 0 ? events[selectedEventIndex - 1] : null;
|
|
@@ -470,11 +466,12 @@ function RenderHistoryFooter({
|
|
|
470
466
|
return [...render.renderHistory].sort((a, b) => a.timestamp - b.timestamp);
|
|
471
467
|
}, [render.renderHistory]);
|
|
472
468
|
|
|
473
|
-
//
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
469
|
+
// Same cap as RenderHistoryViewer above; `isPro` remains a caller override.
|
|
470
|
+
const tierLimit = (0, _sharedUi.useLimit)("highlightUpdates.historyEvents");
|
|
471
|
+
const eventLimit = isPro ? Infinity : tierLimit;
|
|
472
|
+
|
|
473
|
+
// Most recent N (negative index clamps to 0, so unlimited returns all).
|
|
474
|
+
const events = (0, _react.useMemo)(() => allEvents.slice(-eventLimit), [allEvents, eventLimit]);
|
|
478
475
|
const totalEvents = events.length;
|
|
479
476
|
const currentEvent = events[selectedEventIndex];
|
|
480
477
|
const goToPrevious = (0, _react.useCallback)(() => {
|
|
@@ -45,9 +45,11 @@ const TOP_COMPONENTS_CAP = 20;
|
|
|
45
45
|
let hookInstalled = false;
|
|
46
46
|
let active = false;
|
|
47
47
|
let sawDurations = false;
|
|
48
|
+
let captureDetail = false;
|
|
48
49
|
let totalCommits = 0;
|
|
49
50
|
let totalRenders = 0;
|
|
50
51
|
let totalRenderMs = 0;
|
|
52
|
+
let totalCauses = {};
|
|
51
53
|
let buckets = new Map();
|
|
52
54
|
function getHook() {
|
|
53
55
|
if (typeof global === "undefined") return null;
|
|
@@ -119,13 +121,129 @@ function isFrameworkWrapperName(name) {
|
|
|
119
121
|
}
|
|
120
122
|
return false;
|
|
121
123
|
}
|
|
122
|
-
|
|
124
|
+
|
|
125
|
+
/** Bound on prop keys compared per render, so a fat props object can't stall a commit. */
|
|
126
|
+
const MAX_PROPS_COMPARED = 40;
|
|
127
|
+
/** Bound on distinct changed-prop key names remembered per component (detail tier). */
|
|
128
|
+
const MAX_CHANGED_KEYS = 5;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Are two values for the same prop key equivalent despite differing identity?
|
|
132
|
+
*
|
|
133
|
+
* Functions always count as equivalent: a fresh arrow per render is THE
|
|
134
|
+
* unstable-prop bug, and there's no way to compare behaviour anyway. Plain
|
|
135
|
+
* objects and arrays compare one level deep, which catches `style={{...}}`
|
|
136
|
+
* and `data={[...]}`. Anything else falls through to a real value change.
|
|
137
|
+
*/
|
|
138
|
+
function isEquivalentValue(a, b) {
|
|
139
|
+
if (Object.is(a, b)) return true;
|
|
140
|
+
const ta = typeof a;
|
|
141
|
+
if (ta !== typeof b) return false;
|
|
142
|
+
if (ta === "function") return true;
|
|
143
|
+
if (a === null || b === null || ta !== "object") return false;
|
|
144
|
+
const aIsArray = Array.isArray(a);
|
|
145
|
+
if (aIsArray !== Array.isArray(b)) return false;
|
|
146
|
+
if (aIsArray) {
|
|
147
|
+
if (a.length !== b.length) return false;
|
|
148
|
+
for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
const keysA = Object.keys(a);
|
|
152
|
+
const keysB = Object.keys(b);
|
|
153
|
+
if (keysA.length !== keysB.length) return false;
|
|
154
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
155
|
+
const k = keysA[i];
|
|
156
|
+
if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
|
|
157
|
+
if (!Object.is(a[k], b[k])) return false;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Why did this fiber render?
|
|
163
|
+
*
|
|
164
|
+
* Simpler than the overlay's RenderCauseDetector, and deliberately so: that
|
|
165
|
+
* one carries WeakMap fallbacks and fiber-swap detection because traceUpdates
|
|
166
|
+
* hands it fibers at an ambiguous moment. Here we're inside onCommitFiberRoot
|
|
167
|
+
* walking the committed tree, so `fiber` is current and `fiber.alternate` is
|
|
168
|
+
* previous by definition. Porting the swap logic would be cargo-cult.
|
|
169
|
+
*/
|
|
170
|
+
function detectCause(fiber, wantKeys) {
|
|
171
|
+
const prev = fiber.alternate;
|
|
172
|
+
if (!prev) return {
|
|
173
|
+
cause: "mount"
|
|
174
|
+
};
|
|
175
|
+
const nextProps = fiber.memoizedProps;
|
|
176
|
+
const prevProps = prev.memoizedProps;
|
|
177
|
+
if (nextProps !== prevProps && nextProps && prevProps) {
|
|
178
|
+
let sawChange = false;
|
|
179
|
+
let sawValueChange = false;
|
|
180
|
+
let changedKeys;
|
|
181
|
+
const keys = Object.keys(nextProps);
|
|
182
|
+
const limit = keys.length < MAX_PROPS_COMPARED ? keys.length : MAX_PROPS_COMPARED;
|
|
183
|
+
for (let i = 0; i < limit; i++) {
|
|
184
|
+
const k = keys[i];
|
|
185
|
+
const a = prevProps[k];
|
|
186
|
+
const b = nextProps[k];
|
|
187
|
+
if (Object.is(a, b)) continue;
|
|
188
|
+
sawChange = true;
|
|
189
|
+
if (wantKeys) {
|
|
190
|
+
if (!changedKeys) changedKeys = [];
|
|
191
|
+
if (changedKeys.length < MAX_CHANGED_KEYS) changedKeys.push(k);
|
|
192
|
+
}
|
|
193
|
+
if (!isEquivalentValue(a, b)) {
|
|
194
|
+
sawValueChange = true;
|
|
195
|
+
if (!wantKeys) break; // cause is settled; only keys need the full pass
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// A removed key counts as a real change; cheap to detect via key count.
|
|
199
|
+
if (!sawValueChange && Object.keys(prevProps).length !== keys.length) {
|
|
200
|
+
sawChange = true;
|
|
201
|
+
sawValueChange = true;
|
|
202
|
+
}
|
|
203
|
+
if (sawChange) {
|
|
204
|
+
return {
|
|
205
|
+
cause: sawValueChange ? "props" : "propsUnstable",
|
|
206
|
+
changedKeys
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// No prop change of substance. Own state/hooks next.
|
|
212
|
+
if (prev.memoizedState !== fiber.memoizedState) return {
|
|
213
|
+
cause: "hooks"
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
cause: "parent"
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Nearest ancestor that ALSO rendered this commit and is worth naming — i.e.
|
|
222
|
+
* skipping framework wrappers, which are never where a fix goes. Answers "who
|
|
223
|
+
* dragged this along", which is the actionable half of a cascade.
|
|
224
|
+
*/
|
|
225
|
+
function nearestRenderedAncestorName(fiber) {
|
|
226
|
+
for (let p = fiber.return; p != null; p = p.return) {
|
|
227
|
+
if (!isCompositeTag(p.tag)) continue;
|
|
228
|
+
if ((getFlags(p) & PerformedWork) === 0) continue;
|
|
229
|
+
const name = getComponentName(p);
|
|
230
|
+
if (isFrameworkWrapperName(name)) continue;
|
|
231
|
+
return name;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
function recordRender(fiber, parentName) {
|
|
123
236
|
const name = getComponentName(fiber);
|
|
124
237
|
const selfMs = computeSelfMs(fiber);
|
|
125
238
|
if (selfMs !== null) sawDurations = true;
|
|
126
239
|
const ms = selfMs ?? 0;
|
|
127
240
|
totalRenders += 1;
|
|
128
241
|
totalRenderMs += ms;
|
|
242
|
+
const {
|
|
243
|
+
cause,
|
|
244
|
+
changedKeys
|
|
245
|
+
} = detectCause(fiber, captureDetail);
|
|
246
|
+
totalCauses[cause] = (totalCauses[cause] ?? 0) + 1;
|
|
129
247
|
|
|
130
248
|
// Framework wrappers count toward totals but never surface as a row.
|
|
131
249
|
if (isFrameworkWrapperName(name)) return;
|
|
@@ -136,13 +254,27 @@ function recordRender(fiber) {
|
|
|
136
254
|
bucket = {
|
|
137
255
|
renders: 0,
|
|
138
256
|
totalSelfMs: 0,
|
|
139
|
-
maxSelfMs: 0
|
|
257
|
+
maxSelfMs: 0,
|
|
258
|
+
causes: {}
|
|
140
259
|
};
|
|
141
260
|
buckets.set(name, bucket);
|
|
142
261
|
}
|
|
143
262
|
bucket.renders += 1;
|
|
144
263
|
bucket.totalSelfMs += ms;
|
|
145
264
|
if (ms > bucket.maxSelfMs) bucket.maxSelfMs = ms;
|
|
265
|
+
bucket.causes[cause] = (bucket.causes[cause] ?? 0) + 1;
|
|
266
|
+
if (captureDetail) {
|
|
267
|
+
if (changedKeys) {
|
|
268
|
+
if (!bucket.changedProps) bucket.changedProps = new Set();
|
|
269
|
+
for (let i = 0; i < changedKeys.length; i++) {
|
|
270
|
+
if (bucket.changedProps.size >= MAX_CHANGED_KEYS) break;
|
|
271
|
+
bucket.changedProps.add(changedKeys[i]);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// First parent wins — a component rendered under several parents is rare,
|
|
275
|
+
// and the first is the one that showed up in the heaviest commit.
|
|
276
|
+
if (parentName && !bucket.parentName) bucket.parentName = parentName;
|
|
277
|
+
}
|
|
146
278
|
}
|
|
147
279
|
|
|
148
280
|
/**
|
|
@@ -161,13 +293,21 @@ function handleCommit(root) {
|
|
|
161
293
|
let skipSubtree = false;
|
|
162
294
|
const composite = isCompositeTag(fiber.tag);
|
|
163
295
|
if (composite) {
|
|
164
|
-
|
|
165
|
-
|
|
296
|
+
const compositeName = getComponentName(fiber);
|
|
297
|
+
// Only Buoy's render-tracking UI gets its SUBTREE pruned — walking it
|
|
298
|
+
// feeds its own renders back in and loops. Everything else that's merely
|
|
299
|
+
// devtools noise is skipped as a NODE and we keep descending: app
|
|
300
|
+
// components live underneath Buoy chrome, and pruning a subtree on a name
|
|
301
|
+
// match is what made a stray "Virtualized" prefix hide every FlatList in
|
|
302
|
+
// every app.
|
|
303
|
+
if ((0, _devtoolsComponentNames.isRenderLoopUnsafeName)(compositeName)) {
|
|
166
304
|
skipSubtree = true;
|
|
305
|
+
} else if ((0, _devtoolsComponentNames.isDevToolsComponentName)(compositeName)) {
|
|
306
|
+
// skip this node, keep walking its children
|
|
167
307
|
} else if ((getFlags(fiber) & PerformedWork) !== 0) {
|
|
168
308
|
// PerformedWork is the only trustworthy "rendered this commit"
|
|
169
309
|
// signal — actualDuration is stale on bailed-out fibers.
|
|
170
|
-
recordRender(fiber);
|
|
310
|
+
recordRender(fiber, captureDetail ? nearestRenderedAncestorName(fiber) : undefined);
|
|
171
311
|
}
|
|
172
312
|
}
|
|
173
313
|
if (!skipSubtree) {
|
|
@@ -230,14 +370,24 @@ function isCapturing() {
|
|
|
230
370
|
return active;
|
|
231
371
|
}
|
|
232
372
|
|
|
233
|
-
/**
|
|
234
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Begin a capture window, discarding any previous unclaimed data.
|
|
375
|
+
*
|
|
376
|
+
* `detail` turns on the allocating tier — changed prop key names and parent
|
|
377
|
+
* component names. Causes themselves are always classified: it costs a shallow
|
|
378
|
+
* prop compare, against a `computeSelfMs` that already walks every direct
|
|
379
|
+
* child on every render, and the wasted-render total is the number most worth
|
|
380
|
+
* seeing without having to know to ask for it.
|
|
381
|
+
*/
|
|
382
|
+
function startCapture(opts) {
|
|
235
383
|
if (!ensureHookInstalled()) return;
|
|
236
384
|
buckets = new Map();
|
|
237
385
|
sawDurations = false;
|
|
386
|
+
captureDetail = opts?.detail === true;
|
|
238
387
|
totalCommits = 0;
|
|
239
388
|
totalRenders = 0;
|
|
240
389
|
totalRenderMs = 0;
|
|
390
|
+
totalCauses = {};
|
|
241
391
|
active = true;
|
|
242
392
|
}
|
|
243
393
|
|
|
@@ -253,19 +403,31 @@ function stopCapture() {
|
|
|
253
403
|
renders: b.renders,
|
|
254
404
|
totalMs: round2(b.totalSelfMs),
|
|
255
405
|
maxMs: round2(b.maxSelfMs),
|
|
256
|
-
avgMs: b.renders > 0 ? round2(b.totalSelfMs / b.renders) : 0
|
|
406
|
+
avgMs: b.renders > 0 ? round2(b.totalSelfMs / b.renders) : 0,
|
|
407
|
+
causes: b.causes,
|
|
408
|
+
...(b.changedProps && b.changedProps.size > 0 ? {
|
|
409
|
+
changedProps: Array.from(b.changedProps)
|
|
410
|
+
} : null),
|
|
411
|
+
...(b.parentName ? {
|
|
412
|
+
parentName: b.parentName
|
|
413
|
+
} : null)
|
|
257
414
|
}));
|
|
258
415
|
const summary = {
|
|
259
416
|
totalCommits,
|
|
260
417
|
totalRenders,
|
|
261
418
|
capturedDurations: sawDurations,
|
|
262
419
|
totalRenderMs: round2(totalRenderMs),
|
|
420
|
+
// Parent cascades and identity-only prop churn are both removable without
|
|
421
|
+
// changing behaviour — together they're the recoverable share.
|
|
422
|
+
wastedRenders: (totalCauses.parent ?? 0) + (totalCauses.propsUnstable ?? 0),
|
|
423
|
+
causes: totalCauses,
|
|
263
424
|
topComponents
|
|
264
425
|
};
|
|
265
426
|
buckets = new Map();
|
|
266
427
|
totalCommits = 0;
|
|
267
428
|
totalRenders = 0;
|
|
268
429
|
totalRenderMs = 0;
|
|
430
|
+
totalCauses = {};
|
|
269
431
|
return summary;
|
|
270
432
|
}
|
|
271
433
|
function round2(n) {
|
|
@@ -555,7 +555,12 @@ function isOurOverlayNode(stateNode) {
|
|
|
555
555
|
|
|
556
556
|
// Only check other dev tools if excludeDevTools is enabled
|
|
557
557
|
if (excludeDevTools) {
|
|
558
|
-
|
|
558
|
+
// Two sets, same meaning: COMPONENTS is the original list, EXACT
|
|
559
|
+
// holds the names that used to be matched by over-broad prefixes
|
|
560
|
+
// (the "Virtualized" that hid every FlatList, the "Dial" that hid
|
|
561
|
+
// every Dialog). Both paths must consult both, or Buoy's own chrome
|
|
562
|
+
// starts flashing highlight boxes in one of them and not the other.
|
|
563
|
+
if (_devtoolsComponentNames.OTHER_DEV_TOOLS_COMPONENTS.has(name) || _devtoolsComponentNames.OTHER_DEV_TOOLS_EXACT.has(name)) {
|
|
559
564
|
result = true;
|
|
560
565
|
break;
|
|
561
566
|
}
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.OTHER_DEV_TOOLS_PREFIXES = exports.OTHER_DEV_TOOLS_COMPONENTS = exports.HIGHLIGHT_OVERLAY_PREFIXES = exports.HIGHLIGHT_OVERLAY_COMPONENTS = void 0;
|
|
6
|
+
exports.OTHER_DEV_TOOLS_PREFIXES = exports.OTHER_DEV_TOOLS_EXACT = exports.OTHER_DEV_TOOLS_COMPONENTS = exports.HIGHLIGHT_OVERLAY_PREFIXES = exports.HIGHLIGHT_OVERLAY_COMPONENTS = void 0;
|
|
7
7
|
exports.isCapturingBuoyToolRenders = isCapturingBuoyToolRenders;
|
|
8
8
|
exports.isDevToolsComponentName = isDevToolsComponentName;
|
|
9
9
|
exports.isJsTopComponentName = isJsTopComponentName;
|
|
10
10
|
exports.isPerfMonitorComponentName = isPerfMonitorComponentName;
|
|
11
|
+
exports.isRenderLoopUnsafeName = isRenderLoopUnsafeName;
|
|
11
12
|
exports.setCaptureBuoyToolRenders = setCaptureBuoyToolRenders;
|
|
12
13
|
// ============================================================================
|
|
13
14
|
// Buoy devtools component-name sets, shared by HighlightUpdatesController's
|
|
@@ -66,16 +67,37 @@ const OTHER_DEV_TOOLS_COMPONENTS = exports.OTHER_DEV_TOOLS_COMPONENTS = new Set(
|
|
|
66
67
|
// separately). The modal/table re-render on every 250ms snapshot tick.
|
|
67
68
|
"TaskTable", "TaskRow", "BusySparkline"]);
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Other dev-tools prefixes — noise reduction, toggleable via excludeDevTools.
|
|
72
|
+
*
|
|
73
|
+
* EVERY ENTRY HERE MUST BE BUOY-NAMESPACED. These are prefix matches, and a
|
|
74
|
+
* match hides the component from render reports. Generic English words do not
|
|
75
|
+
* belong in this list, because app components share them.
|
|
76
|
+
*
|
|
77
|
+
* This list used to contain `Virtualized`, meant for Buoy's own
|
|
78
|
+
* VirtualizedDataExplorer. React Native's FlatList renders a `VirtualizedList`,
|
|
79
|
+
* so the prefix matched — and because CommitProfiler pruned the whole subtree
|
|
80
|
+
* on a match, Buoy could not see inside ANY FlatList or SectionList in ANY
|
|
81
|
+
* app. Lists are the number-one source of render problems in React Native, and
|
|
82
|
+
* the render tools were blind to all of them. `Dial` was the same trap:
|
|
83
|
+
* "Dialog".startsWith("Dial") is true, so every Dialog vanished too.
|
|
84
|
+
*
|
|
85
|
+
* Names that are generic on their own now live in OTHER_DEV_TOOLS_COMPONENTS
|
|
86
|
+
* as exact matches instead.
|
|
87
|
+
*/
|
|
88
|
+
const OTHER_DEV_TOOLS_PREFIXES = exports.OTHER_DEV_TOOLS_PREFIXES = ["JsModal", "ImageOverlay",
|
|
73
89
|
// Perf-monitor prefixes (safe: user code is Perf*, buoy code is
|
|
74
90
|
// PerfMonitor* / PerfHud*)
|
|
75
91
|
"PerfMonitor", "PerfHud",
|
|
76
92
|
// JS TOP (js-top package)
|
|
77
93
|
"JsTop"];
|
|
78
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Buoy component names that were previously matched by over-broad prefixes.
|
|
97
|
+
* Exact matches only — see the warning above.
|
|
98
|
+
*/
|
|
99
|
+
const OTHER_DEV_TOOLS_EXACT = exports.OTHER_DEV_TOOLS_EXACT = new Set(["VirtualizedDataExplorer", "TreeDiffView", "TreeDiffRow", "DataViewer", "DiffView", "DiffSummary", "FloatingDevTools", "FloatingMenu", "FloatingBubble", "MinimizedDock", "DialMenu", "DialItem", "ExpandableSection", "ChevronIcon", "GlitchText", "SettingsModal", "SettingsRow", "OnboardingFlow", "DevToolsMenu"]);
|
|
100
|
+
|
|
79
101
|
// ── Bench "profile our own tools" mode ──────────────────────────────────
|
|
80
102
|
// When enabled (by a dedicated benchmark screen, e.g. the example app's
|
|
81
103
|
// /tool-bench page), render capture INCLUDES Buoy's tool UI so we can
|
|
@@ -140,8 +162,27 @@ function isDevToolsComponentName(name) {
|
|
|
140
162
|
return isPerfMonitorComponentName(name) || isJsTopComponentName(name);
|
|
141
163
|
}
|
|
142
164
|
if (OTHER_DEV_TOOLS_COMPONENTS.has(name)) return true;
|
|
165
|
+
if (OTHER_DEV_TOOLS_EXACT.has(name)) return true;
|
|
143
166
|
for (let i = 0; i < OTHER_DEV_TOOLS_PREFIXES.length; i++) {
|
|
144
167
|
if (name.startsWith(OTHER_DEV_TOOLS_PREFIXES[i])) return true;
|
|
145
168
|
}
|
|
146
169
|
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Is this name one whose SUBTREE must be skipped, not merely the node?
|
|
174
|
+
*
|
|
175
|
+
* Only the always-excluded group qualifies: walking Buoy's own render-tracking
|
|
176
|
+
* UI feeds its own renders back in and loops. Everything else in
|
|
177
|
+
* `isDevToolsComponentName` is noise reduction — hide the row, but keep
|
|
178
|
+
* walking, because app components live underneath Buoy chrome (a tool modal
|
|
179
|
+
* renders the host app behind it) and pruning on a NAME is how one bad prefix
|
|
180
|
+
* cost us every FlatList in every app.
|
|
181
|
+
*/
|
|
182
|
+
function isRenderLoopUnsafeName(name) {
|
|
183
|
+
if (HIGHLIGHT_OVERLAY_COMPONENTS.has(name)) return true;
|
|
184
|
+
for (let i = 0; i < HIGHLIGHT_OVERLAY_PREFIXES.length; i++) {
|
|
185
|
+
if (name.startsWith(HIGHLIGHT_OVERLAY_PREFIXES[i])) return true;
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
147
188
|
}
|
|
@@ -7,6 +7,7 @@ exports.highlightUpdatesSyncAdapter = void 0;
|
|
|
7
7
|
var _HighlightUpdatesController = _interopRequireDefault(require("../highlight-updates/utils/HighlightUpdatesController"));
|
|
8
8
|
var _RenderTracker = require("../highlight-updates/utils/RenderTracker");
|
|
9
9
|
var _screenElements = require("../highlight-updates/utils/screenElements");
|
|
10
|
+
var _CommitProfiler = require("../highlight-updates/utils/CommitProfiler");
|
|
10
11
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
12
|
function ensureInitialized() {
|
|
12
13
|
if (!_HighlightUpdatesController.default.isInitialized()) {
|
|
@@ -65,6 +66,41 @@ function jsonSafeRender(render) {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Measurement window for automated callers (MCP `measure_renders`).
|
|
71
|
+
*
|
|
72
|
+
* Backed by CommitProfiler, NOT the RenderTracker that feeds the overlay, and
|
|
73
|
+
* the difference is the whole point. RenderTracker tracks HOST VIEWS: ask it
|
|
74
|
+
* why `DealRow` re-rendered and it answers about the native `View` inside it —
|
|
75
|
+
* "props changed: accessibilityState, accessibilityValue" — which is true and
|
|
76
|
+
* useless. CommitProfiler walks COMPOSITE fibers, so it answers about the
|
|
77
|
+
* component you named: which of ITS props changed, whether they changed by
|
|
78
|
+
* value or only by identity, and which parent dragged it along.
|
|
79
|
+
*
|
|
80
|
+
* It also touches none of the user's overlay state — no enabling, no clearing
|
|
81
|
+
* counters someone was collecting, nothing drawn on screen. A measurement is
|
|
82
|
+
* invisible.
|
|
83
|
+
*/
|
|
84
|
+
function beginMeasurement() {
|
|
85
|
+
if (!_CommitProfiler.CommitProfiler.isSupported()) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
reason: "render capture needs a dev build with the React DevTools hook available"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
_CommitProfiler.CommitProfiler.startCapture({
|
|
92
|
+
detail: true
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
ok: true
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function endMeasurement() {
|
|
99
|
+
return {
|
|
100
|
+
summary: _CommitProfiler.CommitProfiler.stopCapture()
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
68
104
|
/**
|
|
69
105
|
* Sync adapter for the highlight-updates tool, consumed by
|
|
70
106
|
* @buoy-gg/external-sync's `useExternalSync` (structurally matches its
|
|
@@ -81,7 +117,9 @@ function jsonSafeRender(render) {
|
|
|
81
117
|
* stay on the device. Dashboards fetch them via `getRenderDetail`.
|
|
82
118
|
*/
|
|
83
119
|
const highlightUpdatesSyncAdapter = exports.highlightUpdatesSyncAdapter = {
|
|
84
|
-
|
|
120
|
+
// v3: beginMeasurement / endMeasurement — a CommitProfiler capture window
|
|
121
|
+
// for automated callers, with per-component causes. Invisible to the user.
|
|
122
|
+
version: 3,
|
|
85
123
|
getSnapshot: () => ({
|
|
86
124
|
enabled: _HighlightUpdatesController.default.isEnabled(),
|
|
87
125
|
frozen: _HighlightUpdatesController.default.getFrozen(),
|
|
@@ -103,6 +141,10 @@ const highlightUpdatesSyncAdapter = exports.highlightUpdatesSyncAdapter = {
|
|
|
103
141
|
};
|
|
104
142
|
},
|
|
105
143
|
actions: {
|
|
144
|
+
// Capture window for automated callers (MCP `measure_renders`). Pairs:
|
|
145
|
+
// begin -> drive the UI -> end. `end` always restores.
|
|
146
|
+
beginMeasurement: () => beginMeasurement(),
|
|
147
|
+
endMeasurement: () => endMeasurement(),
|
|
106
148
|
/**
|
|
107
149
|
* On-demand detail: one component's real renderHistory + lastRenderCause
|
|
108
150
|
* (including hookChanges). Wire-form snapshots omit those.
|
|
@@ -15,11 +15,10 @@ import React, { useState, useEffect, useRef, useCallback, memo, useMemo } from "
|
|
|
15
15
|
import { View, Text, FlatList, StyleSheet } from "react-native";
|
|
16
16
|
import { RenderTracker } from "../utils/RenderTracker";
|
|
17
17
|
import { RenderListItem } from "./RenderListItem";
|
|
18
|
-
import { Activity, Lock, buoyColors } from "@buoy-gg/shared-ui";
|
|
18
|
+
import { Activity, Lock, buoyColors, useLimit } from "@buoy-gg/shared-ui";
|
|
19
19
|
|
|
20
20
|
// Free tier limit for tracked components
|
|
21
21
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
22
|
-
const FREE_TIER_COMPONENT_LIMIT = 25;
|
|
23
22
|
/**
|
|
24
23
|
* Isolated list component that owns the render subscription.
|
|
25
24
|
* Parent component does NOT re-render when renders change.
|
|
@@ -58,16 +57,19 @@ function IsolatedRenderListInner({
|
|
|
58
57
|
}, [searchText, filters, onRendersChange]);
|
|
59
58
|
|
|
60
59
|
// Limit visible components for free tier
|
|
60
|
+
// `isPro` stays a prop so callers can force the unlimited view; the tier cap
|
|
61
|
+
// applies otherwise. useLimit() already returns Infinity for Pro, so the two
|
|
62
|
+
// agree — this only preserves the prop contract for cross-package callers.
|
|
63
|
+
const tierLimit = useLimit("highlightUpdates.components");
|
|
64
|
+
const componentLimit = isPro ? Infinity : tierLimit;
|
|
61
65
|
const visibleRenders = useMemo(() => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}, [renders, isPro]);
|
|
66
|
+
return renders.slice(0, componentLimit);
|
|
67
|
+
}, [renders, componentLimit]);
|
|
65
68
|
|
|
66
69
|
// Calculate locked component count
|
|
67
70
|
const lockedComponentCount = useMemo(() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}, [renders.length, isPro]);
|
|
71
|
+
return Math.max(0, renders.length - componentLimit);
|
|
72
|
+
}, [renders.length, componentLimit]);
|
|
71
73
|
|
|
72
74
|
// Stable keyExtractor - moved outside to avoid recreation
|
|
73
75
|
const keyExtractor = useCallback(item => item.id, []);
|
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
|
|
15
15
|
import React, { useMemo, useState, useCallback } from "react";
|
|
16
16
|
import { View, Text, TouchableOpacity, ScrollView, StyleSheet } from "react-native";
|
|
17
|
-
import { CopyButton, formatRelativeTime, PlusIcon, MinusIcon, EventStepperFooter, Lock, buoyColors } from "@buoy-gg/shared-ui";
|
|
17
|
+
import { CopyButton, formatRelativeTime, PlusIcon, MinusIcon, EventStepperFooter, Lock, buoyColors, useLimit } from "@buoy-gg/shared-ui";
|
|
18
18
|
import { TreeDiffViewer } from "@buoy-gg/shared-ui/dataViewer";
|
|
19
19
|
import { CAUSE_CONFIG, COMPONENT_CAUSE_CONFIG } from "./RenderCauseBadge";
|
|
20
20
|
import { generateSingleComponentExport } from "../utils/rendersExportFormatter";
|
|
21
21
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
22
22
|
// Free tier limit for render history events
|
|
23
|
-
const FREE_TIER_EVENT_LIMIT = 5;
|
|
24
23
|
export function RenderDetailView({
|
|
25
24
|
render,
|
|
26
25
|
disableInternalFooter = false,
|
|
@@ -46,11 +45,12 @@ export function RenderDetailView({
|
|
|
46
45
|
const totalEvents = events.length;
|
|
47
46
|
const currentEvent = events[selectedEventIndex];
|
|
48
47
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
// `isPro` stays a prop so callers can force the unlimited view; the tier cap
|
|
49
|
+
// applies otherwise. useLimit() already returns Infinity for Pro, so the two
|
|
50
|
+
// agree — this only preserves the prop contract for cross-package callers.
|
|
51
|
+
const tierLimit = useLimit("highlightUpdates.detailEvents");
|
|
52
|
+
const eventLimit = isPro ? Infinity : tierLimit;
|
|
53
|
+
const hiddenEventCount = useMemo(() => Math.max(0, totalEvents - eventLimit), [eventLimit, totalEvents]);
|
|
54
54
|
|
|
55
55
|
// Use current event's cause if available, otherwise fall back to lastRenderCause
|
|
56
56
|
const displayCause = currentEvent?.cause || render.lastRenderCause;
|
|
@@ -62,12 +62,12 @@ export function RenderDetailView({
|
|
|
62
62
|
}
|
|
63
63
|
}, [selectedEventIndex, onEventIndexChange]);
|
|
64
64
|
const goToNext = useCallback(() => {
|
|
65
|
-
//
|
|
66
|
-
const maxIndex =
|
|
65
|
+
// Infinity - 1 is Infinity, so an unlimited tier just gets totalEvents - 1.
|
|
66
|
+
const maxIndex = Math.min(eventLimit - 1, totalEvents - 1);
|
|
67
67
|
if (selectedEventIndex < maxIndex) {
|
|
68
68
|
onEventIndexChange(selectedEventIndex + 1);
|
|
69
69
|
}
|
|
70
|
-
}, [selectedEventIndex, totalEvents, onEventIndexChange,
|
|
70
|
+
}, [selectedEventIndex, totalEvents, onEventIndexChange, eventLimit]);
|
|
71
71
|
|
|
72
72
|
// Memoize copy data
|
|
73
73
|
const copyData = useMemo(() => generateSingleComponentExport(render), [render]);
|