Skip to content

Commit

Permalink
feat(scan): interpolation to rects
Browse files Browse the repository at this point in the history
fix time accumulation

fix label aggregation join

fix label merging

fix color merging

add back is render unncessary

monitoring optimizations

fix label jitter
  • Loading branch information
RobPruzan committed Dec 21, 2024
1 parent 428a614 commit 47c46ca
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 181 deletions.
114 changes: 74 additions & 40 deletions packages/scan/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ import {
} from 'src/core/utils';
import { readLocalStorage, saveLocalStorage } from '@web-utils/helpers';
import { initReactScanOverlay } from './web/overlay';
import {
createInstrumentation,
type Render,
} from './instrumentation';
import { createInstrumentation, type Render } from './instrumentation';
import { createToolbar } from './web/toolbar';
import type { InternalInteraction } from './monitor/types';
import { type getSession } from './monitor/utils';
Expand Down Expand Up @@ -133,6 +130,24 @@ export interface Options {
*/
animationSpeed?: 'slow' | 'fast' | 'off';

/**
* Smoothly animate the re-render outline when the element moves
*
* @default true
*/
smoothlyAnimateOutlines?: boolean;

/**
* Track unnecessary renders, and mark their outlines gray when detected
*
* An unnecessary render is defined as the component re-rendering with no change to the component's
* corresponding dom subtree
*
* @default false
* @warning tracking unnecessary renders can add meaningful overhead to react-scan
*/
trackUnnecessaryRenders?: boolean;

onCommitStart?: () => void;
onRender?: (fiber: Fiber, renders: Array<Render>) => void;
onCommitFinish?: () => void;
Expand Down Expand Up @@ -217,6 +232,8 @@ export const ReactScanInternals: Internals = {
alwaysShowLabels: false,
animationSpeed: 'fast',
dangerouslyForceRunInProduction: false,
smoothlyAnimateOutlines: true,
trackUnnecessaryRenders: true,
}),
onRender: null,
scheduledOutlines: new Map(),
Expand Down Expand Up @@ -281,6 +298,17 @@ const validateOptions = (options: Partial<Options>): Partial<Options> => {
(validOptions as any)[key] = value;
}
break;
case 'trackUnnecessaryRenders': {
validOptions['trackUnnecessaryRenders'] =
typeof value === 'boolean' ? value : false;
break;
}

case 'smoothlyAnimateOutlines': {
validOptions['smoothlyAnimateOutlines'] =
typeof value === 'boolean' ? value : false;
break;
}
default:
errors.push(`- Unknown option "${key}"`);
}
Expand Down Expand Up @@ -417,6 +445,46 @@ const startFlushOutlineInterval = (ctx: CanvasRenderingContext2D) => {
});
}, 30);
};

const updateScheduledOutlines = (fiber: Fiber, renders: Array<Render>) => {
for (let i = 0, len = renders.length; i < len; i++) {
const render = renders[i];
const domFiber = getNearestHostFiber(fiber);
if (!domFiber || !domFiber.stateNode) continue;

if (ReactScanInternals.scheduledOutlines.has(fiber)) {
const existingOutline = ReactScanInternals.scheduledOutlines.get(fiber)!;
aggregateRender(render, existingOutline.aggregatedRender);
} else {
ReactScanInternals.scheduledOutlines.set(fiber, {
domNode: domFiber.stateNode,
aggregatedRender: {
computedCurrent: null,
name:
renders.find((render) => render.componentName)?.componentName ??
'Unknown',
aggregatedCount: 1,
changes: aggregateChanges(render.changes),
didCommit: render.didCommit,
forget: render.forget,
fps: render.fps,
phase: new Set([render.phase]),
time: render.time,
unnecessary: render.unnecessary,
frame: 0,
computedKey: null,
},
alpha: null,
groupedAggregatedRender: null,
target: null,
current: null,
totalFrames: null,
estimatedTextWidth: null,
});
}
}
};
export let isProduction = false;
export const start = () => {
if (typeof window === 'undefined') return;

Expand Down Expand Up @@ -448,7 +516,6 @@ export const start = () => {
onActive() {
if (!Store.monitor.value) {
const rdtHook = getRDTHook();
let isProduction = false;
for (const renderer of rdtHook.renderers.values()) {
const buildType = detectReactBuildType(renderer);
if (buildType === 'production') {
Expand Down Expand Up @@ -548,6 +615,7 @@ export const start = () => {
},
isValidFiber,
onRender(fiber, renders) {
// todo: don't track renders at all if paused, reduce overhead
if (
Boolean(ReactScanInternals.instrumentation?.isPaused.value) ||
!ctx ||
Expand Down Expand Up @@ -578,43 +646,9 @@ export const start = () => {

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

updateScheduledOutlines(fiber, renders);
for (let i = 0, len = renders.length; i < len; i++) {
const render = renders[i];
const domFiber = getNearestHostFiber(fiber);
if (!domFiber || !domFiber.stateNode) continue;

if (ReactScanInternals.scheduledOutlines.has(fiber)) {
const existingOutline =
ReactScanInternals.scheduledOutlines.get(fiber)!;
aggregateRender(render, existingOutline.aggregatedRender);
} else {
ReactScanInternals.scheduledOutlines.set(fiber, {
domNode: domFiber.stateNode,
aggregatedRender: {
name:
renders.find((render) => render.componentName)?.componentName ??
'Unknown',
aggregatedCount: 1,
changes: aggregateChanges(render.changes),
didCommit: render.didCommit,
forget: render.forget,
fps: render.fps,
phase: new Set([render.phase]),
time: render.time,
// todo: add back a when clear use case in the UI is needed for isRenderUnnecessary, or performance is optimized
// unnecessary: isRenderUnnecessary(fiber),
unnecessary: false,
frame: 0,

computedKey: null,
},
alpha: null,
groupedAggregatedRender: null,
rect: null,
totalFrames: null,
estimatedTextWidth: null,
});
}

// - audio context can take up an insane amount of cpu, todo: figure out why
// - we may want to take this out of hot path
Expand Down
33 changes: 31 additions & 2 deletions packages/scan/src/core/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
traverseProps,
} from 'bippy';
import { type Signal, signal } from '@preact/signals';
import { ReactScanInternals, Store } from './index';
import { isProduction, ReactScanInternals, Store } from './index';

let fps = 0;
let lastTime = performance.now();
Expand Down Expand Up @@ -304,6 +304,29 @@ export const isRenderUnnecessary = (fiber: Fiber) => {
return true;
};

const shouldRunUnnecessaryRenderCheck = () => {
// yes, this can be condensed into one conditional, but ifs are easier to reason/build on than long boolean expressions
if (!ReactScanInternals.options.value.trackUnnecessaryRenders) {
return false;
}

// only run unnecessaryRenderCheck when monitoring is active in production if the user set dangerouslyForceRunInProduction
if (
isProduction &&
Store.monitor.value &&
ReactScanInternals.options.value.dangerouslyForceRunInProduction &&
ReactScanInternals.options.value.trackUnnecessaryRenders
) {
return true;
}

if (isProduction && Store.monitor.value) {
return false;
}

return ReactScanInternals.options.value.trackUnnecessaryRenders;
};

export const createInstrumentation = (
instanceKey: string,
config: InstrumentationConfig,
Expand Down Expand Up @@ -364,7 +387,13 @@ export const createInstrumentation = (
changes,
time: selfTime,
forget: hasMemoCache(fiber),
unnecessary: Store.monitor ? null : isRenderUnnecessary(fiber),
// todo: optimize isRenderUnnecessary so it can be turned on by default
// todo: allow this to be toggle-able through toolbar
// todo: performance optimization: if the last fiber measure was very off screen, do not run isRenderUnnecessary
unnecessary: shouldRunUnnecessaryRenderCheck()
? isRenderUnnecessary(fiber)
: null,

didCommit: didFiberCommit(fiber),
fps,
};
Expand Down
Loading

0 comments on commit 47c46ca

Please sign in to comment.