Skip to content

Commit

Permalink
feat(scan): replay collection runtime
Browse files Browse the repository at this point in the history
collect detailed browser timing on interaction

implement react scan rrweb replayer plugin

refactor monitoring interaction and component collection
  • Loading branch information
RobPruzan committed Jan 22, 2025
1 parent f982809 commit c6456e9
Show file tree
Hide file tree
Showing 27 changed files with 4,690 additions and 387 deletions.
46 changes: 37 additions & 9 deletions packages/scan/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
{
"name": "react-scan",
"version": "0.0.54",
"version": "0.0.1083",
"description": "Scan your React app for renders",
"keywords": ["react", "react-scan", "react scan", "render", "performance"],
"keywords": [
"react",
"react-scan",
"react scan",
"render",
"performance"
],
"homepage": "https://react-scan.million.dev",
"bugs": {
"url": "https://github.com/aidenybai/react-scan/issues"
Expand Down Expand Up @@ -161,17 +167,27 @@
"types": "dist/index.d.ts",
"typesVersions": {
"*": {
"monitoring": ["./dist/core/monitor/index.d.ts"],
"monitoring/next": ["./dist/core/monitor/params/next.d.ts"],
"monitoring": [
"./dist/core/monitor/index.d.ts"
],
"monitoring/next": [
"./dist/core/monitor/params/next.d.ts"
],
"monitoring/react-router-legacy": [
"./dist/core/monitor/params/react-router-v5.d.ts"
],
"monitoring/react-router": [
"./dist/core/monitor/params/react-router-v6.d.ts"
],
"monitoring/remix": ["./dist/core/monitor/params/remix.d.ts"],
"monitoring/astro": ["./dist/core/monitor/params/astro/index.ts"],
"react-component-name/vite": ["./dist/react-component-name/vite.d.ts"],
"monitoring/remix": [
"./dist/core/monitor/params/remix.d.ts"
],
"monitoring/astro": [
"./dist/core/monitor/params/astro/index.ts"
],
"react-component-name/vite": [
"./dist/react-component-name/vite.d.ts"
],
"react-component-name/webpack": [
"./dist/react-component-name/webpack.d.ts"
],
Expand All @@ -187,11 +203,20 @@
"react-component-name/rollup": [
"./dist/react-component-name/rollup.d.ts"
],
"react-component-name/astro": ["./dist/react-component-name/astro.d.ts"]
"react-component-name/astro": [
"./dist/react-component-name/astro.d.ts"
]
}
},
"bin": "bin/cli.js",
"files": ["dist", "bin", "package.json", "README.md", "LICENSE", "auto.d.ts"],
"files": [
"dist",
"bin",
"package.json",
"README.md",
"LICENSE",
"auto.d.ts"
],
"scripts": {
"build": "npm run build:css && NODE_ENV=production tsup",
"postbuild": "pnpm copy-astro && node ../../scripts/version-warning.mjs",
Expand All @@ -217,6 +242,7 @@
"@clack/prompts": "^0.8.2",
"@preact/signals": "^1.3.1",
"@rollup/pluginutils": "^5.1.3",
"@rrweb/types": "2.0.0-alpha.18",
"@types/node": "^20.17.9",
"bippy": "^0.0.25",
"esbuild": "^0.24.0",
Expand All @@ -225,6 +251,8 @@
"mri": "^1.2.0",
"playwright": "^1.49.0",
"preact": "^10.25.1",
"rrweb": "2.0.0-alpha.4",
"rrweb-snapshot": "2.0.0-alpha.4",
"tsx": "^4.0.0"
},
"devDependencies": {
Expand Down
11 changes: 6 additions & 5 deletions packages/scan/src/auto-monitor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'bippy'; // implicit init RDT hook
import { Store } from 'src';
import { scanMonitoring } from 'src/core/monitor';
import { initPerformanceMonitoring } from 'src/core/monitor/performance';
// import { initPerformanceMonitoring } from 'src/core/monitor/performance';
import { Device } from 'src/core/monitor/types';

if (typeof window !== 'undefined') {
Expand All @@ -28,9 +28,10 @@ if (typeof window !== 'undefined') {
route: '<mock-route>',
commit: '<mock-commit>',
branch: '<mock-branch>',
interactionListeningForRenders: null,
};
scanMonitoring({
enabled: true,
});
initPerformanceMonitoring();
// scanMonitoring({
// enabled: true,
// });
// initPerformanceMonitoring();
}
4 changes: 3 additions & 1 deletion packages/scan/src/auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import 'bippy'; // implicit init RDT hook
import { scan } from './index';

if (typeof window !== 'undefined') {
scan();
scan({
dangerouslyForceRunInProduction: true,
});
window.reactScan = scan;
}

Expand Down
33 changes: 28 additions & 5 deletions packages/scan/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ export type MonitoringOptions = Pick<
interface Monitor {
pendingRequests: number;
interactions: Array<InternalInteraction>;
interactionListeningForRenders:
| ((fiber: Fiber, renders: Array<Render>) => void)
| null;
session: ReturnType<typeof getSession>;
url: string | null;
route: string | null;
Expand Down Expand Up @@ -381,12 +384,14 @@ export const reportRender = (fiber: Fiber, renders: Array<Render>) => {

// Get data from both current and alternate fibers
const currentData = Store.reportData.get(reportFiber);
const alternateData = fiber.alternate ? Store.reportData.get(fiber.alternate) : null;
const alternateData = fiber.alternate
? Store.reportData.get(fiber.alternate)
: null;

// More efficient null checks and Math.max
const existingCount = Math.max(
(currentData && currentData.count) || 0,
(alternateData && alternateData.count) || 0
(alternateData && alternateData.count) || 0,
);

// Create single shared object for both fibers
Expand All @@ -395,7 +400,7 @@ export const reportRender = (fiber: Fiber, renders: Array<Render>) => {
time: selfTime || 0,
renders,
displayName,
type: getType(fiber.type) || null
type: getType(fiber.type) || null,
};

// Store in both fibers
Expand Down Expand Up @@ -461,7 +466,12 @@ 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 || !(domFiber.stateNode instanceof Element)) continue;
if (
!domFiber ||
!domFiber.stateNode ||
!(domFiber.stateNode instanceof Element)
)
continue;

if (ReactScanInternals.scheduledOutlines.has(fiber)) {
const existingOutline = ReactScanInternals.scheduledOutlines.get(fiber)!;
Expand Down Expand Up @@ -512,6 +522,10 @@ export const getIsProduction = () => {
return isProduction;
};

export const attachReplayCanvas = () => {
startFlushOutlineInterval();
};

export const start = () => {
if (typeof window === 'undefined') return;

Expand Down Expand Up @@ -540,6 +554,13 @@ export const start = () => {

const instrumentation = createInstrumentation('devtools', {
onActive() {
const rdtHook = getRDTHook();
for (const renderer of rdtHook.renderers.values()) {
const buildType = detectReactBuildType(renderer);
if (buildType === 'production') {
isProduction = true;
}
}
const existingRoot = document.querySelector('react-scan-root');
if (existingRoot) {
return;
Expand All @@ -556,7 +577,9 @@ export const start = () => {
void audioContext.resume();
};

window.addEventListener('pointerdown', createAudioContextOnInteraction, { once: true });
window.addEventListener('pointerdown', createAudioContextOnInteraction, {
once: true,
});

const container = document.createElement('div');
container.id = 'react-scan-root';
Expand Down
47 changes: 37 additions & 10 deletions packages/scan/src/core/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,42 @@ let lastTime = performance.now();
let frameCount = 0;
let initedFps = false;

const updateFPS = () => {
let fpsListeners: Array<(fps: number) => void> = [];

export const listenToFps = (listener: (fps: number) => void) => {
// console.log('oushed', listener);

fpsListeners.push(listener);

return () => {
// console.log('unsub listener');

fpsListeners = fpsListeners.filter(
(currListener) => currListener !== listener,
);
};
};

const updateFPS = (onChange?: (fps: number) => void) => {
frameCount++;
const now = performance.now();
if (now - lastTime >= 1000) {
fps = frameCount;
const timeSinceLastUpdate = now - lastTime;

if (timeSinceLastUpdate >= 500) {
const calculatedFPS = Math.round((frameCount / timeSinceLastUpdate) * 1000);

if (calculatedFPS !== fps) {
for (const listener of fpsListeners) {
listener(calculatedFPS);
}
}

fps = calculatedFPS;
frameCount = 0;
lastTime = now;
}
requestAnimationFrame(updateFPS);

requestAnimationFrame(() => updateFPS(onChange));
};

export const getFPS = () => {
Expand Down Expand Up @@ -361,30 +388,30 @@ export const createInstrumentation = (

const changes: Array<RenderChange> = [];

const propsChanges = getChangedPropsDetailed(fiber).map(change => ({
const propsChanges = getChangedPropsDetailed(fiber).map((change) => ({
type: 'props' as const,
name: change.name,
value: change.value,
prevValue: change.prevValue,
unstable: false
unstable: false,
}));

const stateChanges = getStateChanges(fiber).map(change => ({
const stateChanges = getStateChanges(fiber).map((change) => ({
type: 'state' as const,
name: change.name,
value: change.value,
prevValue: change.prevValue,
count: change.count,
unstable: false
unstable: false,
}));

const contextChanges = getContextChanges(fiber).map(change => ({
const contextChanges = getContextChanges(fiber).map((change) => ({
type: 'context' as const,
name: change.name,
value: change.value,
prevValue: change.prevValue,
count: change.count,
unstable: false
unstable: false,
}));

changes.push(...propsChanges, ...stateChanges, ...contextChanges);
Expand Down
42 changes: 24 additions & 18 deletions packages/scan/src/core/monitor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import {
} from '..';
import { createInstrumentation, type Render } from '../instrumentation';
import { updateFiberRenderData } from '../utils';
import { initPerformanceMonitoring } from './performance';
import { getSession } from './utils';
import { flush } from './network';
import { computeRoute } from './params/utils';
import { scanWithRecord } from 'src/core/monitor/session-replay/record';

// max retries before the set of components do not get reported (avoid memory leaks of the set of fibers stored on the component aggregation)
const MAX_RETRIES_BEFORE_COMPONENT_GC = 7;
Expand Down Expand Up @@ -50,30 +49,23 @@ export const Monitoring = ({
}: MonitoringProps) => {
if (!apiKey)
throw new Error('Please provide a valid API key for React Scan monitoring');
url ??= 'https://monitoring.react-scan.com/api/v1/ingest';

// url ??= "https://monitoring.react-scan.com/api/v1/ingest";
Store.monitor.value ??= {
pendingRequests: 0,
url: 'http://localhost:4200/api/ingest',
apiKey,
interactions: [],
session: getSession({ commit, branch }).catch(() => null),
url,
apiKey,
route,
commit,
branch,
};
branch: 'main',
commit: '0x00000',

// When using Monitoring without framework, we need to compute the route from the path and params
if (!route && path && params) {
Store.monitor.value.route = computeRoute(path, params);
} else if (typeof window !== 'undefined') {
Store.monitor.value.route =
route ?? path ?? new URL(window.location.toString()).pathname; // this is inaccurate on vanilla react if the path is not provided but used for session route
}
interactionListeningForRenders: null,
};

useEffect(() => {
scanWithRecord();
scanMonitoring({ enabled: true });
return initPerformanceMonitoring();
}, []);

return null;
Expand Down Expand Up @@ -101,6 +93,7 @@ export const startMonitoring = () => {

flushInterval = setInterval(() => {
try {

void flush();
} catch {
/* */
Expand All @@ -126,6 +119,7 @@ export const startMonitoring = () => {
if (isCompositeFiber(fiber)) {
aggregateComponentRenderToInteraction(fiber, renders);
}
publishToListeningInteraction(fiber, renders);
ReactScanInternals.options.value.onRender?.(fiber, renders);
},
onCommitFinish() {
Expand All @@ -151,7 +145,7 @@ const aggregateComponentRenderToInteraction = (
const displayName = getDisplayName(fiber.type);
if (!displayName) return; // TODO(nisarg): it may be useful to somehow report the first ancestor with a display name instead of completely ignoring

let component = lastInteraction.components.get(displayName); // TODO(nisarg): Same names are grouped together which is wrong.
let component = lastInteraction.components.get(displayName); // TODO(rob): we can be more precise with fiber types, but display name is fine for now

if (!component) {
component = {
Expand Down Expand Up @@ -181,3 +175,15 @@ const aggregateComponentRenderToInteraction = (
component.selfTime += selfTime;
}
};

const publishToListeningInteraction = (
fiber: Fiber,
renders: Array<Render>,
) => {
const monitor = Store.monitor.value;
if (!monitor || !monitor.interactionListeningForRenders) {
return;
}

monitor.interactionListeningForRenders(fiber, renders);
};
Loading

0 comments on commit c6456e9

Please sign in to comment.