react-render-detective 0.3.0 → 0.5.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.
package/dist/index.cjs CHANGED
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkJD4IJ4MN_cjs = require('./chunk-JD4IJ4MN.cjs');
4
- var chunkDZ3BZ654_cjs = require('./chunk-DZ3BZ654.cjs');
4
+ var chunkOSPTERGK_cjs = require('./chunk-OSPTERGK.cjs');
5
+ var chunkH5RG2EPP_cjs = require('./chunk-H5RG2EPP.cjs');
5
6
  var React = require('react');
6
7
  var jsxRuntime = require('react/jsx-runtime');
7
8
 
@@ -25,6 +26,40 @@ function _interopNamespace(e) {
25
26
 
26
27
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
27
28
 
29
+ // src/console/style.ts
30
+ var PALETTE = {
31
+ plain: "",
32
+ dim: "color:#8a94a6",
33
+ strong: "font-weight:600",
34
+ good: "color:#1f9d5b",
35
+ warn: "color:#c2820a",
36
+ bad: "color:#d1493f;font-weight:600"
37
+ };
38
+ var supportsStyling = () => {
39
+ try {
40
+ return typeof window !== "undefined" && typeof document !== "undefined";
41
+ } catch {
42
+ return false;
43
+ }
44
+ };
45
+ function styled(segments) {
46
+ if (!supportsStyling()) {
47
+ return [segments.map((s) => typeof s === "string" ? s : s[0]).join("")];
48
+ }
49
+ let format = "";
50
+ const styles = [];
51
+ for (const segment of segments) {
52
+ if (typeof segment === "string") {
53
+ format += segment.replace(/%/g, "%%");
54
+ continue;
55
+ }
56
+ const [text, tone] = segment;
57
+ format += `%c${text.replace(/%/g, "%%")}%c`;
58
+ styles.push(PALETTE[tone], "");
59
+ }
60
+ return [format, ...styles];
61
+ }
62
+
28
63
  // src/console/reporter.ts
29
64
  var ICON = {
30
65
  normal: "\xB7",
@@ -33,30 +68,102 @@ var ICON = {
33
68
  "very-slow": "\u25B2\u25B2",
34
69
  critical: "\u25A0"
35
70
  };
71
+ var BATCH_WINDOW_MS = 400;
36
72
  function attachConsoleReporter(detective) {
37
- return detective.subscribe((event) => {
38
- const mode = detective.config.mode;
39
- if (mode === "silent") return;
73
+ let pending = [];
74
+ let timer;
75
+ const flush = () => {
76
+ timer = void 0;
77
+ const batch = pending;
78
+ pending = [];
79
+ if (batch.length === 0) return;
40
80
  try {
41
- if (mode === "verbose") printVerbose(event);
42
- else printConcise(event, detective.config.slowRenderThreshold);
81
+ printBatch(batch, detective.config.slowRenderThreshold);
43
82
  } catch {
44
83
  }
84
+ };
85
+ const unsubscribe = detective.subscribe((event) => {
86
+ const mode = detective.config.mode;
87
+ if (mode === "silent") return;
88
+ if (mode === "verbose") {
89
+ try {
90
+ printVerbose(event);
91
+ } catch {
92
+ }
93
+ return;
94
+ }
95
+ pending.push(event);
96
+ if (timer === void 0) {
97
+ timer = setTimeout(flush, BATCH_WINDOW_MS);
98
+ timer.unref?.();
99
+ }
45
100
  });
101
+ return () => {
102
+ unsubscribe();
103
+ if (timer !== void 0) clearTimeout(timer);
104
+ };
46
105
  }
47
- function printConcise(event, slowThreshold) {
48
- const { diagnosis, timings, component } = event;
49
- const changed = event.changedProps.map((c) => c.key).join(", ");
50
- const parts = [
51
- `[RRD] ${component.name} #${event.renderNumber}`,
52
- `Reason: ${label(event)}`,
53
- changed ? `Changed: ${changed}` : void 0,
54
- timings.subtreeDuration > 0 ? `Duration: ${timings.selfDuration.toFixed(1)}ms` : void 0
55
- ].filter(Boolean);
56
- const line = `${ICON[diagnosis.severity] ?? "\xB7"} ${parts.join(" ")}`;
57
- const slow = timings.selfDuration >= slowThreshold;
58
- if (slow) console.warn(line);
59
- else console.log(line);
106
+ function printBatch(batch, slowThreshold) {
107
+ const byComponent = /* @__PURE__ */ new Map();
108
+ let totalMs = 0;
109
+ let avoidable = 0;
110
+ for (const event of batch) {
111
+ const key = event.component.name;
112
+ const entry = byComponent.get(key) ?? {
113
+ name: key,
114
+ source: event.component.source,
115
+ count: 0,
116
+ totalMs: 0,
117
+ reasons: /* @__PURE__ */ new Map(),
118
+ notableReasons: /* @__PURE__ */ new Map(),
119
+ avoidable: 0,
120
+ remount: false,
121
+ slow: false
122
+ };
123
+ entry.count++;
124
+ entry.totalMs += event.timings.selfDuration;
125
+ entry.reasons.set(event.diagnosis.reason, (entry.reasons.get(event.diagnosis.reason) ?? 0) + 1);
126
+ if (event.diagnosis.potentiallyAvoidable || event.timings.selfDuration >= slowThreshold) {
127
+ entry.notableReasons.set(event.diagnosis.reason, (entry.notableReasons.get(event.diagnosis.reason) ?? 0) + 1);
128
+ }
129
+ if (event.diagnosis.potentiallyAvoidable) entry.avoidable++;
130
+ if (event.diagnosis.summary.includes("rebuilt")) entry.remount = true;
131
+ if (event.timings.selfDuration >= slowThreshold) entry.slow = true;
132
+ if (!entry.worst || event.timings.selfDuration > entry.worst.timings.selfDuration) entry.worst = event;
133
+ byComponent.set(key, entry);
134
+ totalMs += event.timings.selfDuration;
135
+ if (event.diagnosis.potentiallyAvoidable) avoidable++;
136
+ }
137
+ const notable = [...byComponent.values()].filter((a) => a.avoidable > 0 || a.slow || a.remount);
138
+ if (notable.length === 0) return;
139
+ const ranked = [...notable].sort((a, b) => b.totalMs - a.totalMs);
140
+ const anySlow = ranked.some((a) => a.slow);
141
+ const segments = [
142
+ ["[RRD] ", "dim"],
143
+ [`${batch.length} render${batch.length === 1 ? "" : "s"}`, "strong"],
144
+ [` \xB7 ${totalMs.toFixed(1)}ms`, "dim"]
145
+ ];
146
+ if (avoidable > 0) segments.push([` \xB7 ${avoidable} potentially avoidable`, "warn"]);
147
+ for (const a of ranked.slice(0, 8)) {
148
+ const reasonSource = a.notableReasons.size > 0 ? a.notableReasons : a.reasons;
149
+ const reason = [...reasonSource.entries()].sort((x, y) => y[1] - x[1])[0]?.[0] ?? "unknown";
150
+ const tone = a.remount ? "bad" : a.slow ? "warn" : a.avoidable > 0 ? "warn" : "good";
151
+ const label2 = ` ${ICON[a.slow ? "slow" : "normal"]} ${a.name}${a.count > 1 ? ` \xD7${a.count}` : ""}`;
152
+ segments.push("\n", [label2.padEnd(30), tone], [`${a.totalMs.toFixed(1)}ms`, "plain"], [` ${reason}`, "dim"]);
153
+ if (a.remount) segments.push([" rebuilt", "bad"]);
154
+ if (a.avoidable > 0) segments.push([` ${a.avoidable} avoidable`, "warn"]);
155
+ if (a.source) segments.push(["\n " + a.source, "dim"]);
156
+ }
157
+ const worst = ranked[0]?.worst;
158
+ segments.push(
159
+ "\n",
160
+ [
161
+ worst?.diagnosis.suggestion ? ` \u2192 ${worst.diagnosis.suggestion}` : " \u2192 rrd.printOpportunities() to rank everything by recoverable time",
162
+ "dim"
163
+ ]
164
+ );
165
+ const log = anySlow ? console.warn : console.log;
166
+ log(...styled(segments));
60
167
  }
61
168
  function printVerbose(event) {
62
169
  const { diagnosis, timings } = event;
@@ -79,12 +186,10 @@ function printVerbose(event) {
79
186
  for (const c of event.changedProps) console.log(propLine(c));
80
187
  console.groupEnd();
81
188
  }
82
- if (event.unchangedProps.length > 0) {
83
- console.log(`Props same: ${event.unchangedProps.join(", ")}`);
84
- }
189
+ if (event.unchangedProps.length > 0) console.log(`Props same: ${event.unchangedProps.join(", ")}`);
85
190
  if (event.trackedState.length > 0) {
86
191
  console.log(
87
- `State: ${event.trackedState.map((s) => `${s.name}: ${chunkDZ3BZ654_cjs.formatInspected(s.previous)} \u2192 ${chunkDZ3BZ654_cjs.formatInspected(s.current)}`).join(", ")}`
192
+ `State: ${event.trackedState.map((s) => `${s.name}: ${chunkOSPTERGK_cjs.formatInspected(s.previous)} \u2192 ${chunkOSPTERGK_cjs.formatInspected(s.current)}`).join(", ")}`
88
193
  );
89
194
  }
90
195
  if (event.contextChanges.length > 0) {
@@ -101,11 +206,11 @@ function printVerbose(event) {
101
206
  }
102
207
  function propLine(c) {
103
208
  const head = `${c.key} (${c.valueType}) \u2014 ${describeKind(c)}`;
104
- if (c.kind === "added") return `${head}: ${chunkDZ3BZ654_cjs.formatInspected(c.current)}`;
105
- if (c.kind === "removed") return `${head}: was ${chunkDZ3BZ654_cjs.formatInspected(c.previous)}`;
209
+ if (c.kind === "added") return `${head}: ${chunkOSPTERGK_cjs.formatInspected(c.current)}`;
210
+ if (c.kind === "removed") return `${head}: was ${chunkOSPTERGK_cjs.formatInspected(c.previous)}`;
106
211
  return `${head}
107
- previous: ${chunkDZ3BZ654_cjs.formatInspected(c.previous)}
108
- current: ${chunkDZ3BZ654_cjs.formatInspected(c.current)}`;
212
+ previous: ${chunkOSPTERGK_cjs.formatInspected(c.previous)}
213
+ current: ${chunkOSPTERGK_cjs.formatInspected(c.current)}`;
109
214
  }
110
215
  function describeKind(c) {
111
216
  switch (c.kind) {
@@ -138,10 +243,249 @@ function label(event) {
138
243
  return "undetermined";
139
244
  }
140
245
  }
246
+
247
+ // src/core/interactions.ts
248
+ var COMMIT_SLACK_MS = 100;
249
+ var FALLBACK_CLOSE_MS = 50;
250
+ var InteractionTracker = class {
251
+ constructor(capacity = 50) {
252
+ this.capacity = capacity;
253
+ this.records = [];
254
+ this.nextId = 0;
255
+ }
256
+ /** Returns false when the browser cannot report event timing. */
257
+ start() {
258
+ if (this.observer) return true;
259
+ const PO = globalThis.PerformanceObserver;
260
+ const supported = PO?.supportedEntryTypes?.includes("event");
261
+ if (!PO || !supported) return false;
262
+ try {
263
+ const observer = new PO((list) => {
264
+ for (const entry of list.getEntries()) {
265
+ const timing = entry;
266
+ this.record({
267
+ name: timing.name,
268
+ startTime: timing.startTime,
269
+ duration: timing.duration,
270
+ target: describeTarget(timing.target)
271
+ });
272
+ }
273
+ });
274
+ observer.observe({ type: "event", buffered: true, durationThreshold: 16 });
275
+ this.observer = observer;
276
+ return true;
277
+ } catch {
278
+ return false;
279
+ }
280
+ }
281
+ stop() {
282
+ this.observer?.disconnect();
283
+ this.observer = void 0;
284
+ }
285
+ /** Is the automatic path available in this browser? */
286
+ get automatic() {
287
+ return this.observer !== void 0;
288
+ }
289
+ /**
290
+ * Time an interaction by hand.
291
+ *
292
+ * The automatic path depends on the Event Timing API, which Safari only
293
+ * gained in 16.4 and which does not fire for synthetic input at all — so
294
+ * anything driven by a test harness records nothing. This measures a specific
295
+ * action instead, up to the paint that follows it, and needs no browser
296
+ * support beyond `performance.now`.
297
+ */
298
+ measure(label2, action) {
299
+ const startTime = now();
300
+ let handlerMs = 0;
301
+ let finished = false;
302
+ const finish = () => {
303
+ if (finished) return;
304
+ finished = true;
305
+ this.record({ name: label2, startTime, duration: now() - startTime, handlerMs });
306
+ };
307
+ let result;
308
+ try {
309
+ result = action();
310
+ handlerMs = now() - startTime;
311
+ } catch (error) {
312
+ handlerMs = now() - startTime;
313
+ finish();
314
+ throw error;
315
+ }
316
+ const raf = globalThis.requestAnimationFrame;
317
+ if (raf) raf(() => raf(finish));
318
+ setTimeout(finish, FALLBACK_CLOSE_MS);
319
+ return result;
320
+ }
321
+ /** Exposed for tests and for `measure`. */
322
+ record(timing) {
323
+ const record = {
324
+ id: `interaction_${++this.nextId}`,
325
+ type: timing.name,
326
+ target: timing.target,
327
+ startTime: timing.startTime,
328
+ durationMs: timing.duration,
329
+ handlerMs: timing.handlerMs,
330
+ renders: [],
331
+ renderTimeMs: 0,
332
+ avoidableRenderTimeMs: 0
333
+ };
334
+ this.records.push(record);
335
+ if (this.records.length > this.capacity) this.records.shift();
336
+ return record;
337
+ }
338
+ clear() {
339
+ this.records.length = 0;
340
+ }
341
+ /**
342
+ * Joins render events to interactions by commit time. A render belongs to an
343
+ * interaction when it committed between the event starting and shortly after
344
+ * it finished — React commits just after the event handler returns.
345
+ */
346
+ attribute(events) {
347
+ for (const record of this.records) {
348
+ const from = record.startTime;
349
+ const to = record.startTime + record.durationMs + COMMIT_SLACK_MS;
350
+ record.renders = events.filter((e) => e.timings.commitTime >= from && e.timings.commitTime <= to);
351
+ record.renderTimeMs = record.renders.reduce((a, e) => a + e.timings.selfDuration, 0);
352
+ record.avoidableRenderTimeMs = record.renders.filter((e) => e.diagnosis.potentiallyAvoidable).reduce((a, e) => a + e.timings.selfDuration, 0);
353
+ }
354
+ return [...this.records].sort((a, b) => b.durationMs - a.durationMs);
355
+ }
356
+ };
357
+ function summarise(record) {
358
+ const byComponent = /* @__PURE__ */ new Map();
359
+ for (const event of record.renders) {
360
+ const entry = byComponent.get(event.component.name) ?? {
361
+ renders: 0,
362
+ totalMs: 0,
363
+ source: event.component.source,
364
+ cause: event.diagnosis.reason
365
+ };
366
+ entry.renders++;
367
+ entry.totalMs += event.timings.selfDuration;
368
+ byComponent.set(event.component.name, entry);
369
+ }
370
+ const contributors = [...byComponent.entries()].map(([component, v]) => ({ component, source: v.source, renders: v.renders, totalMs: v.totalMs, cause: v.cause })).sort((a, b) => b.totalMs - a.totalMs);
371
+ const top = contributors[0];
372
+ const accounted = (record.handlerMs ?? record.durationMs) + record.renderTimeMs;
373
+ const idleWindow = record.handlerMs !== void 0 && record.durationMs > accounted * 3 && record.durationMs - accounted > 100;
374
+ const effectiveMs = idleWindow ? accounted : record.durationMs;
375
+ const share = effectiveMs > 0 ? record.renderTimeMs / effectiveMs : 0;
376
+ let headline;
377
+ let nextStep;
378
+ let confidence = "medium";
379
+ if (idleWindow) {
380
+ headline = `${record.type}: ${fmt(record.handlerMs ?? 0)} in the handler and ${fmt(record.renderTimeMs)} rendering. The measured window was ${fmt(record.durationMs)}, but most of that was the page waiting for a frame \u2014 ignore it.`;
381
+ nextStep = record.renderTimeMs > (record.handlerMs ?? 0) ? `Rendering dominates the real work${top ? `; start with ${top.component}` : ""}.` : "The handler itself costs more than rendering. Profile the handler, not React.";
382
+ return { interaction: record, contributors, headline, nextStep, confidence: "medium" };
383
+ }
384
+ if (record.renders.length === 0) {
385
+ headline = `${record.type} took ${fmt(record.durationMs)}, and no instrumented component rendered inside it.`;
386
+ nextStep = "The cost is somewhere other than React rendering \u2014 an event handler, a layout, or an uninstrumented component. Instrument more of the tree to narrow it down.";
387
+ confidence = "low";
388
+ } else if (share >= 0.4 && record.avoidableRenderTimeMs > 0) {
389
+ headline = `${record.type} took ${fmt(record.durationMs)}; ${fmt(record.renderTimeMs)} of it was rendering, and ${fmt(record.avoidableRenderTimeMs)} of that had no input change to explain it.`;
390
+ nextStep = top ? `Start with ${top.component}${top.source ? ` (${top.source})` : ""} \u2014 ${fmt(top.totalMs)} across ${top.renders} render${top.renders === 1 ? "" : "s"}.` : "Look at the top contributor below.";
391
+ confidence = "high";
392
+ } else if (share >= 0.4) {
393
+ headline = `${record.type} took ${fmt(record.durationMs)}; ${fmt(record.renderTimeMs)} of it was rendering, all of it explained by real input changes.`;
394
+ nextStep = "This is genuine work. Make the renders cheaper rather than fewer \u2014 or do less of it per interaction.";
395
+ confidence = "high";
396
+ } else {
397
+ headline = `${record.type} took ${fmt(record.durationMs)}, but only ${fmt(record.renderTimeMs)} was React rendering.`;
398
+ nextStep = "Most of the cost is outside rendering \u2014 event handlers, layout or paint. A browser profile will show more than this tool can.";
399
+ confidence = "medium";
400
+ }
401
+ return { interaction: record, contributors, headline, nextStep, confidence };
402
+ }
403
+ function formatInteraction(summary) {
404
+ const { interaction: i } = summary;
405
+ const lines = [
406
+ `${i.type}${i.target ? ` on ${i.target}` : ""} ${fmt(i.durationMs)}`,
407
+ "",
408
+ summary.headline
409
+ ];
410
+ if (summary.contributors.length > 0) {
411
+ lines.push("", "Rendering inside this interaction");
412
+ for (const c of summary.contributors.slice(0, 8)) {
413
+ lines.push(
414
+ ` ${c.component.padEnd(22)} ${String(c.renders).padStart(4)} render(s) ${fmt(c.totalMs).padStart(8)} ${c.cause}`
415
+ );
416
+ }
417
+ }
418
+ lines.push("", `Next step`, ` ${summary.nextStep}`, "", `Confidence: ${summary.confidence}`);
419
+ return lines.join("\n");
420
+ }
421
+ function describeTarget(target) {
422
+ if (!target || typeof target !== "object") return void 0;
423
+ const el = target;
424
+ if (!el.tagName) return void 0;
425
+ const tag = el.tagName.toLowerCase();
426
+ if (el.id) return `${tag}#${el.id}`;
427
+ const className = typeof el.className === "string" ? el.className.trim().split(/\s+/)[0] : void 0;
428
+ if (className) return `${tag}.${className}`;
429
+ const text = el.textContent?.trim().slice(0, 20);
430
+ return text ? `${tag} "${text}"` : tag;
431
+ }
432
+ var fmt = (ms) => `${ms.toFixed(1)}ms`;
433
+ var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
434
+
435
+ // src/core/opportunities.ts
436
+ var DEFAULT_MIN_SAVING_MS = 1;
437
+ function rankOpportunities({ events, lifecycles, minSavingMs = DEFAULT_MIN_SAVING_MS }) {
438
+ const names = new Set(events.map((e) => e.component.name));
439
+ const out = [];
440
+ for (const name of names) {
441
+ const lifecycle = lifecycles.get(name);
442
+ const explanation = chunkJD4IJ4MN_cjs.explainEvents(name, events, lifecycle);
443
+ if (!explanation) continue;
444
+ const mounts = events.filter((e) => e.component.name === name && e.phase === "mount");
445
+ const averageMountCost = mounts.length ? mounts.reduce((a, e) => a + e.timings.selfDuration, 0) / mounts.length : 0;
446
+ const remountSaving = explanation.remounts * averageMountCost;
447
+ const estimatedSavingMs = explanation.estimatedAvoidableTime + remountSaving;
448
+ if (estimatedSavingMs < minSavingMs) continue;
449
+ out.push({
450
+ component: name,
451
+ source: explanation.source,
452
+ estimatedSavingMs,
453
+ avoidableRenders: explanation.potentiallyAvoidableRenders,
454
+ remounts: explanation.remounts,
455
+ averageSelfDuration: explanation.averageSelfDuration,
456
+ summary: explanation.headline,
457
+ nextStep: explanation.nextStep,
458
+ confidence: explanation.confidence
459
+ });
460
+ }
461
+ return out.sort((a, b) => b.estimatedSavingMs - a.estimatedSavingMs);
462
+ }
463
+ function formatOpportunities(opportunities) {
464
+ if (opportunities.length === 0) {
465
+ return "React Render Detective\n\nNo measurable render waste found yet. Interact with the app and try again.";
466
+ }
467
+ const lines = [
468
+ "React Render Detective \u2014 where to spend your next hour",
469
+ "",
470
+ "Ranked by estimated recoverable time. These are estimates, not promises:",
471
+ "measure each fix.",
472
+ ""
473
+ ];
474
+ for (const [index, o] of opportunities.entries()) {
475
+ lines.push(
476
+ `${String(index + 1).padStart(2)}. ${o.component}${o.source ? ` ${o.source}` : ""}`,
477
+ ` ~${o.estimatedSavingMs.toFixed(0)}ms recoverable ${o.avoidableRenders} avoidable render${o.avoidableRenders === 1 ? "" : "s"}${o.remounts > 0 ? `, ${o.remounts}\xD7 rebuilt` : ""} (confidence: ${o.confidence})`,
478
+ ` ${o.summary}`,
479
+ ` \u2192 ${o.nextStep}`,
480
+ ""
481
+ );
482
+ }
483
+ return lines.join("\n");
484
+ }
141
485
  var AncestryContext = React.createContext(void 0);
142
486
  AncestryContext.displayName = "RenderDetectiveAncestry";
143
487
  function useInstrumentedNode(name, props, source) {
144
- const detective = chunkDZ3BZ654_cjs.getDetective();
488
+ const detective = chunkOSPTERGK_cjs.getDetective();
145
489
  const parent = React.useContext(AncestryContext);
146
490
  const [node] = React.useState(
147
491
  () => detective.enabled ? detective.createNode(name, parent, source) : void 0
@@ -191,7 +535,7 @@ var IS_WRAPPER = /* @__PURE__ */ Symbol.for("react-render-detective.wrapper");
191
535
  function withRenderDetective(Component, options = {}) {
192
536
  if (Component[IS_WRAPPER]) return Component;
193
537
  const name = options.name ?? componentName(Component);
194
- chunkDZ3BZ654_cjs.getDetective().noteDefinition(name, options.source, options.declaredInRender === true);
538
+ chunkOSPTERGK_cjs.getDetective().noteDefinition(name, options.source, options.declaredInRender === true);
195
539
  function RenderDetected(props) {
196
540
  const { node, onRender } = useInstrumentedNode(name, props, options.source);
197
541
  return renderInstrumented(node, onRender, /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props }));
@@ -207,7 +551,7 @@ function RenderDetective({ name, children }) {
207
551
  return renderInstrumented(node, onRender, children);
208
552
  }
209
553
  function useRenderDiagnostics(name, props) {
210
- const detective = chunkDZ3BZ654_cjs.getDetective();
554
+ const detective = chunkOSPTERGK_cjs.getDetective();
211
555
  const parent = React.useContext(AncestryContext);
212
556
  const [node] = React.useState(() => {
213
557
  if (!detective.enabled) return void 0;
@@ -242,7 +586,7 @@ function useRenderDiagnostics(name, props) {
242
586
  }
243
587
  var useOwnerId = typeof React__namespace.useId === "function" ? React__namespace.useId : () => "";
244
588
  function useTrackedState(name, initial) {
245
- const detective = chunkDZ3BZ654_cjs.getDetective();
589
+ const detective = chunkOSPTERGK_cjs.getDetective();
246
590
  const node = React.useContext(AncestryContext);
247
591
  const ownerId = useOwnerId();
248
592
  const [state, setState] = React.useState(initial);
@@ -252,8 +596,8 @@ function useTrackedState(name, initial) {
252
596
  if (node && owns && !Object.is(previous.current, state)) {
253
597
  detective.recordStateChange(node, {
254
598
  name,
255
- previous: chunkDZ3BZ654_cjs.inspect(previous.current, detective.config.inspection),
256
- current: chunkDZ3BZ654_cjs.inspect(state, detective.config.inspection)
599
+ previous: chunkOSPTERGK_cjs.inspect(previous.current, detective.config.inspection),
600
+ current: chunkOSPTERGK_cjs.inspect(state, detective.config.inspection)
257
601
  });
258
602
  previous.current = state;
259
603
  } else if (!owns) {
@@ -262,7 +606,7 @@ function useTrackedState(name, initial) {
262
606
  return [state, setState];
263
607
  }
264
608
  function useTrackedEffect(name, effect, deps) {
265
- const detective = chunkDZ3BZ654_cjs.getDetective();
609
+ const detective = chunkOSPTERGK_cjs.getDetective();
266
610
  const previous = React.useRef(void 0);
267
611
  const changed = React.useRef([]);
268
612
  if (detective.enabled) {
@@ -278,7 +622,7 @@ function useTrackedEffect(name, effect, deps) {
278
622
  }, deps);
279
623
  }
280
624
  function useTrackedContextValue(contextName, value) {
281
- const detective = chunkDZ3BZ654_cjs.getDetective();
625
+ const detective = chunkOSPTERGK_cjs.getDetective();
282
626
  const previous = React.useRef(void 0);
283
627
  const first = React.useRef(true);
284
628
  if (detective.enabled) {
@@ -286,8 +630,8 @@ function useTrackedContextValue(contextName, value) {
286
630
  first.current = false;
287
631
  } else if (!Object.is(previous.current, value)) {
288
632
  const prev = previous.current;
289
- const equal = chunkDZ3BZ654_cjs.shallowEqual(prev, value, detective.config);
290
- const changedKeys = isRecord(prev) && isRecord(value) ? chunkDZ3BZ654_cjs.diffProps(prev, value, detective.config).changed.map((c) => c.key) : [];
633
+ const equal = chunkOSPTERGK_cjs.shallowEqual(prev, value, detective.config);
634
+ const changedKeys = isRecord(prev) && isRecord(value) ? chunkOSPTERGK_cjs.diffProps(prev, value, detective.config).changed.map((c) => c.key) : [];
291
635
  detective.recordContextChange({
292
636
  contextName,
293
637
  changedKeys,
@@ -305,12 +649,21 @@ function isRecord(v) {
305
649
 
306
650
  // src/index.ts
307
651
  var REPORTER = /* @__PURE__ */ Symbol.for("react-render-detective.reporter");
652
+ var INTERACTIONS = /* @__PURE__ */ Symbol.for("react-render-detective.interactions");
653
+ function tracker() {
654
+ const g = globalThis;
655
+ if (!g[INTERACTIONS]) g[INTERACTIONS] = new InteractionTracker();
656
+ return g[INTERACTIONS];
657
+ }
308
658
  function init(options = {}) {
309
- const detective = chunkDZ3BZ654_cjs.getDetective();
659
+ const detective = chunkOSPTERGK_cjs.getDetective();
310
660
  detective.init(options);
311
661
  const g = globalThis;
312
662
  g[REPORTER]?.();
313
663
  g[REPORTER] = void 0;
664
+ if (detective.enabled) {
665
+ tracker().start();
666
+ }
314
667
  if (detective.enabled && detective.config.mode !== "silent") {
315
668
  g[REPORTER] = attachConsoleReporter(detective);
316
669
  } else if (!detective.enabled) {
@@ -320,41 +673,85 @@ function init(options = {}) {
320
673
  }
321
674
  }
322
675
  function configure(options) {
323
- chunkDZ3BZ654_cjs.getDetective().configure(options);
676
+ chunkOSPTERGK_cjs.getDetective().configure(options);
324
677
  }
325
678
  function getConfig() {
326
- return chunkDZ3BZ654_cjs.getDetective().config;
679
+ return chunkOSPTERGK_cjs.getDetective().config;
327
680
  }
328
681
  function isEnabled() {
329
- return chunkDZ3BZ654_cjs.getDetective().enabled;
682
+ return chunkOSPTERGK_cjs.getDetective().enabled;
330
683
  }
331
684
  function getEvents() {
332
- return chunkDZ3BZ654_cjs.getDetective().getEvents();
685
+ return chunkOSPTERGK_cjs.getDetective().getEvents();
333
686
  }
334
687
  function getStats() {
335
- return chunkDZ3BZ654_cjs.getDetective().getStats();
688
+ return chunkOSPTERGK_cjs.getDetective().getStats();
336
689
  }
337
690
  function getComponentStats(name) {
338
- return chunkDZ3BZ654_cjs.getDetective().getComponentStats(name);
691
+ return chunkOSPTERGK_cjs.getDetective().getComponentStats(name);
339
692
  }
340
693
  function subscribe(listener) {
341
- return chunkDZ3BZ654_cjs.getDetective().subscribe(listener);
694
+ return chunkOSPTERGK_cjs.getDetective().subscribe(listener);
342
695
  }
343
696
  function clear() {
344
- chunkDZ3BZ654_cjs.getDetective().clear();
697
+ chunkOSPTERGK_cjs.getDetective().clear();
698
+ tracker().clear();
345
699
  }
346
700
  function reset() {
347
701
  const g = globalThis;
348
702
  g[REPORTER]?.();
349
703
  g[REPORTER] = void 0;
350
- chunkDZ3BZ654_cjs.getDetective().reset();
704
+ g[INTERACTIONS]?.stop();
705
+ g[INTERACTIONS] = void 0;
706
+ chunkOSPTERGK_cjs.getDetective().reset();
351
707
  }
352
708
  function explain(componentName2) {
353
709
  const explanation = explainStructured(componentName2);
354
710
  return explanation ? chunkJD4IJ4MN_cjs.formatExplanation(explanation) : void 0;
355
711
  }
356
712
  function explainStructured(componentName2) {
357
- return chunkJD4IJ4MN_cjs.explainEvents(componentName2, getEvents(), chunkDZ3BZ654_cjs.getDetective().lifecycleOf(componentName2));
713
+ return chunkJD4IJ4MN_cjs.explainEvents(componentName2, getEvents(), chunkOSPTERGK_cjs.getDetective().lifecycleOf(componentName2));
714
+ }
715
+ function getRenderProfile(scenario) {
716
+ const remounts = {};
717
+ for (const stats of chunkOSPTERGK_cjs.getDetective().getComponentStats()) remounts[stats.name] = stats.remountCount;
718
+ return chunkH5RG2EPP_cjs.profileFromEvents(scenario, getEvents(), remounts);
719
+ }
720
+ function getInteractions() {
721
+ return tracker().attribute(getEvents());
722
+ }
723
+ function explainInteractionStructured(id) {
724
+ const records = getInteractions();
725
+ const record = id ? records.find((r) => r.id === id) : records[0];
726
+ return record ? summarise(record) : void 0;
727
+ }
728
+ function explainInteraction(id) {
729
+ const summary = explainInteractionStructured(id);
730
+ return summary ? formatInteraction(summary) : void 0;
731
+ }
732
+ function printInteractions(limit = 5) {
733
+ const records = getInteractions().slice(0, limit);
734
+ if (records.length === 0) {
735
+ console.log(
736
+ tracker().automatic ? "No interactions recorded yet. Event timing is working \u2014 nothing has taken longer than 16ms.\nSynthetic clicks from a test harness never produce these entries; use measureInteraction() there." : "This browser does not report event timing (Safari before 16.4, jsdom).\nUse measureInteraction(label, fn) to time interactions by hand."
737
+ );
738
+ return;
739
+ }
740
+ console.log(records.map((r) => formatInteraction(summarise(r))).join("\n\n"));
741
+ }
742
+ function measureInteraction(label2, action) {
743
+ return tracker().measure(label2, action);
744
+ }
745
+ function getOpportunities(limit = 10) {
746
+ const detective = chunkOSPTERGK_cjs.getDetective();
747
+ const lifecycles = /* @__PURE__ */ new Map();
748
+ for (const stats of detective.getComponentStats()) {
749
+ lifecycles.set(stats.name, { remounts: stats.remountCount });
750
+ }
751
+ return rankOpportunities({ events: detective.getEvents(), lifecycles }).slice(0, limit);
752
+ }
753
+ function printOpportunities(limit = 10) {
754
+ console.log(formatOpportunities(getOpportunities(limit)));
358
755
  }
359
756
  function printStats() {
360
757
  const s = getStats();
@@ -401,6 +798,14 @@ var ReactRenderDetective = {
401
798
  reset,
402
799
  explain,
403
800
  explainStructured,
801
+ getOpportunities,
802
+ printOpportunities,
803
+ getInteractions,
804
+ explainInteraction,
805
+ explainInteractionStructured,
806
+ printInteractions,
807
+ measureInteraction,
808
+ getRenderProfile,
404
809
  printStats
405
810
  };
406
811
 
@@ -412,21 +817,34 @@ Object.defineProperty(exports, "formatExplanation", {
412
817
  enumerable: true,
413
818
  get: function () { return chunkJD4IJ4MN_cjs.formatExplanation; }
414
819
  });
820
+ exports.InteractionTracker = InteractionTracker;
415
821
  exports.ReactRenderDetective = ReactRenderDetective;
416
822
  exports.RenderDetective = RenderDetective;
417
823
  exports.clear = clear;
418
824
  exports.configure = configure;
419
825
  exports.explain = explain;
826
+ exports.explainInteraction = explainInteraction;
827
+ exports.explainInteractionStructured = explainInteractionStructured;
420
828
  exports.explainStructured = explainStructured;
829
+ exports.formatInteraction = formatInteraction;
830
+ exports.formatOpportunities = formatOpportunities;
421
831
  exports.getComponentStats = getComponentStats;
422
832
  exports.getConfig = getConfig;
423
833
  exports.getEvents = getEvents;
834
+ exports.getInteractions = getInteractions;
835
+ exports.getOpportunities = getOpportunities;
836
+ exports.getRenderProfile = getRenderProfile;
424
837
  exports.getStats = getStats;
425
838
  exports.init = init;
426
839
  exports.isEnabled = isEnabled;
840
+ exports.measureInteraction = measureInteraction;
841
+ exports.printInteractions = printInteractions;
842
+ exports.printOpportunities = printOpportunities;
427
843
  exports.printStats = printStats;
844
+ exports.rankOpportunities = rankOpportunities;
428
845
  exports.reset = reset;
429
846
  exports.subscribe = subscribe;
847
+ exports.summariseInteraction = summarise;
430
848
  exports.useRenderDiagnostics = useRenderDiagnostics;
431
849
  exports.useTrackedContextValue = useTrackedContextValue;
432
850
  exports.useTrackedEffect = useTrackedEffect;