@buoy-gg/highlight-updates 5.0.0 → 6.0.0

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.
@@ -194,35 +194,31 @@ function HighlightUpdatesOverlay({
194
194
  height: rect.height,
195
195
  borderColor: rect.color
196
196
  }],
197
- children: rect.count > 0 && (hasBadgePressHandler ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
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
- activeOpacity: 0.7,
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
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
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
+ };