Skip to content

Commit 7526d13

Browse files
committed
feat(scan): interpolation to rects
fix time accumulation fix label aggregation join fix label merging fix color merging add back is render unncessary monitoring optimizations fix label jitter
1 parent 428a614 commit 7526d13

File tree

5 files changed

+291
-182
lines changed

5 files changed

+291
-182
lines changed

packages/scan/src/auto.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import 'bippy'; // implicit init RDT hook
22
import { scan } from './index';
33

44
if (typeof window !== 'undefined') {
5-
scan();
5+
scan({
6+
dangerouslyForceRunInProduction: true,
7+
});
68
window.reactScan = scan;
79
}
810

packages/scan/src/core/index.ts

Lines changed: 74 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@ import {
2828
} from 'src/core/utils';
2929
import { readLocalStorage, saveLocalStorage } from '@web-utils/helpers';
3030
import { initReactScanOverlay } from './web/overlay';
31-
import {
32-
createInstrumentation,
33-
type Render,
34-
} from './instrumentation';
31+
import { createInstrumentation, type Render } from './instrumentation';
3532
import { createToolbar } from './web/toolbar';
3633
import type { InternalInteraction } from './monitor/types';
3734
import { type getSession } from './monitor/utils';
@@ -133,6 +130,24 @@ export interface Options {
133130
*/
134131
animationSpeed?: 'slow' | 'fast' | 'off';
135132

133+
/**
134+
* Smoothly animate the re-render outline when the element moves
135+
*
136+
* @default true
137+
*/
138+
smoothlyAnimateOutlines?: boolean;
139+
140+
/**
141+
* Track unnecessary renders, and mark their outlines gray when detected
142+
*
143+
* An unnecessary render is defined as the component re-rendering with no change to the component's
144+
* corresponding dom subtree
145+
*
146+
* @default false
147+
* @warning tracking unnecessary renders can add meaningful overhead to react-scan
148+
*/
149+
trackUnnecessaryRenders?: boolean;
150+
136151
onCommitStart?: () => void;
137152
onRender?: (fiber: Fiber, renders: Array<Render>) => void;
138153
onCommitFinish?: () => void;
@@ -217,6 +232,8 @@ export const ReactScanInternals: Internals = {
217232
alwaysShowLabels: false,
218233
animationSpeed: 'fast',
219234
dangerouslyForceRunInProduction: false,
235+
smoothlyAnimateOutlines: true,
236+
trackUnnecessaryRenders: true,
220237
}),
221238
onRender: null,
222239
scheduledOutlines: new Map(),
@@ -281,6 +298,17 @@ const validateOptions = (options: Partial<Options>): Partial<Options> => {
281298
(validOptions as any)[key] = value;
282299
}
283300
break;
301+
case 'trackUnnecessaryRenders': {
302+
validOptions['trackUnnecessaryRenders'] =
303+
typeof value === 'boolean' ? value : false;
304+
break;
305+
}
306+
307+
case 'smoothlyAnimateOutlines': {
308+
validOptions['smoothlyAnimateOutlines'] =
309+
typeof value === 'boolean' ? value : false;
310+
break;
311+
}
284312
default:
285313
errors.push(`- Unknown option "${key}"`);
286314
}
@@ -417,6 +445,46 @@ const startFlushOutlineInterval = (ctx: CanvasRenderingContext2D) => {
417445
});
418446
}, 30);
419447
};
448+
449+
const updateScheduledOutlines = (fiber: Fiber, renders: Array<Render>) => {
450+
for (let i = 0, len = renders.length; i < len; i++) {
451+
const render = renders[i];
452+
const domFiber = getNearestHostFiber(fiber);
453+
if (!domFiber || !domFiber.stateNode) continue;
454+
455+
if (ReactScanInternals.scheduledOutlines.has(fiber)) {
456+
const existingOutline = ReactScanInternals.scheduledOutlines.get(fiber)!;
457+
aggregateRender(render, existingOutline.aggregatedRender);
458+
} else {
459+
ReactScanInternals.scheduledOutlines.set(fiber, {
460+
domNode: domFiber.stateNode,
461+
aggregatedRender: {
462+
computedCurrent: null,
463+
name:
464+
renders.find((render) => render.componentName)?.componentName ??
465+
'Unknown',
466+
aggregatedCount: 1,
467+
changes: aggregateChanges(render.changes),
468+
didCommit: render.didCommit,
469+
forget: render.forget,
470+
fps: render.fps,
471+
phase: new Set([render.phase]),
472+
time: render.time,
473+
unnecessary: render.unnecessary,
474+
frame: 0,
475+
computedKey: null,
476+
},
477+
alpha: null,
478+
groupedAggregatedRender: null,
479+
target: null,
480+
current: null,
481+
totalFrames: null,
482+
estimatedTextWidth: null,
483+
});
484+
}
485+
}
486+
};
487+
export let isProduction = false;
420488
export const start = () => {
421489
if (typeof window === 'undefined') return;
422490

@@ -448,7 +516,6 @@ export const start = () => {
448516
onActive() {
449517
if (!Store.monitor.value) {
450518
const rdtHook = getRDTHook();
451-
let isProduction = false;
452519
for (const renderer of rdtHook.renderers.values()) {
453520
const buildType = detectReactBuildType(renderer);
454521
if (buildType === 'production') {
@@ -548,6 +615,7 @@ export const start = () => {
548615
},
549616
isValidFiber,
550617
onRender(fiber, renders) {
618+
// todo: don't track renders at all if paused, reduce overhead
551619
if (
552620
Boolean(ReactScanInternals.instrumentation?.isPaused.value) ||
553621
!ctx ||
@@ -578,43 +646,9 @@ export const start = () => {
578646

579647
ReactScanInternals.options.value.onRender?.(fiber, renders);
580648

649+
updateScheduledOutlines(fiber, renders);
581650
for (let i = 0, len = renders.length; i < len; i++) {
582651
const render = renders[i];
583-
const domFiber = getNearestHostFiber(fiber);
584-
if (!domFiber || !domFiber.stateNode) continue;
585-
586-
if (ReactScanInternals.scheduledOutlines.has(fiber)) {
587-
const existingOutline =
588-
ReactScanInternals.scheduledOutlines.get(fiber)!;
589-
aggregateRender(render, existingOutline.aggregatedRender);
590-
} else {
591-
ReactScanInternals.scheduledOutlines.set(fiber, {
592-
domNode: domFiber.stateNode,
593-
aggregatedRender: {
594-
name:
595-
renders.find((render) => render.componentName)?.componentName ??
596-
'Unknown',
597-
aggregatedCount: 1,
598-
changes: aggregateChanges(render.changes),
599-
didCommit: render.didCommit,
600-
forget: render.forget,
601-
fps: render.fps,
602-
phase: new Set([render.phase]),
603-
time: render.time,
604-
// todo: add back a when clear use case in the UI is needed for isRenderUnnecessary, or performance is optimized
605-
// unnecessary: isRenderUnnecessary(fiber),
606-
unnecessary: false,
607-
frame: 0,
608-
609-
computedKey: null,
610-
},
611-
alpha: null,
612-
groupedAggregatedRender: null,
613-
rect: null,
614-
totalFrames: null,
615-
estimatedTextWidth: null,
616-
});
617-
}
618652

619653
// - audio context can take up an insane amount of cpu, todo: figure out why
620654
// - we may want to take this out of hot path

packages/scan/src/core/instrumentation.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
traverseProps,
1515
} from 'bippy';
1616
import { type Signal, signal } from '@preact/signals';
17-
import { ReactScanInternals, Store } from './index';
17+
import { isProduction, ReactScanInternals, Store } from './index';
1818

1919
let fps = 0;
2020
let lastTime = performance.now();
@@ -304,6 +304,29 @@ export const isRenderUnnecessary = (fiber: Fiber) => {
304304
return true;
305305
};
306306

307+
const shouldRunUnnecessaryRenderCheck = () => {
308+
// yes, this can be condensed into one conditional, but ifs are easier to reason/build on than long boolean expressions
309+
if (!ReactScanInternals.options.value.trackUnnecessaryRenders) {
310+
return false;
311+
}
312+
313+
// only run unnecessaryRenderCheck when monitoring is active in production if the user set dangerouslyForceRunInProduction
314+
if (
315+
isProduction &&
316+
Store.monitor.value &&
317+
ReactScanInternals.options.value.dangerouslyForceRunInProduction &&
318+
ReactScanInternals.options.value.trackUnnecessaryRenders
319+
) {
320+
return true;
321+
}
322+
323+
if (isProduction && Store.monitor.value) {
324+
return false;
325+
}
326+
327+
return ReactScanInternals.options.value.trackUnnecessaryRenders;
328+
};
329+
307330
export const createInstrumentation = (
308331
instanceKey: string,
309332
config: InstrumentationConfig,
@@ -364,7 +387,13 @@ export const createInstrumentation = (
364387
changes,
365388
time: selfTime,
366389
forget: hasMemoCache(fiber),
367-
unnecessary: Store.monitor ? null : isRenderUnnecessary(fiber),
390+
// todo: optimize isRenderUnnecessary so it can be turned on by default
391+
// todo: allow this to be toggle-able through toolbar
392+
// todo: performance optimization: if the last fiber measure was very off screen, do not run isRenderUnnecessary
393+
unnecessary: shouldRunUnnecessaryRenderCheck()
394+
? isRenderUnnecessary(fiber)
395+
: null,
396+
368397
didCommit: didFiberCommit(fiber),
369398
fps,
370399
};

0 commit comments

Comments
 (0)