Skip to content

Commit

Permalink
feat[DevTools]: Use Chrome DevTools Performance extension API
Browse files Browse the repository at this point in the history
This change is a proof of concept of how the new Chrome DevTools
Performance extension API (https://bit.ly/rpp-e11y) can be used to
surface React runtime data directly in the Chrome DevTools Performance
panel.

To do this, the hooks in profilingHooks.js that mark beginning and end
of React measurements using [Performance marks](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMeasure)
                                            are modified to also use [Performance measure](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMeasure).
                with the `detail` field format specification of the
Performance extension API.

Because the marks in these hooks marks are used by React Profiler, they
are kept untouched and the calls to performance.measure are added on top
to surface them to the Chrome DevTools Performance panel, along with the
browser's native runtime data.

Because this is a proof of concept, not all the tasks and marks taken by
the React Profiler are added to the Chrome DevTools Performance panel
(f.e. update scheduling marks), but this could be done as a follow up of
this commit.

Note: to enable the user timings to be collected in the first place,
the React DevTools extension needs to be installed.
  • Loading branch information
and-oli committed Jul 19, 2024
1 parent 66df944 commit 9a4df9c
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ describe('Timeline profiler', () => {
markOptions.startTime++;
}
},
measure() {},
clearMeasures() {},
};
}

Expand Down Expand Up @@ -364,9 +366,10 @@ describe('Timeline profiler', () => {
"--render-start-128",
"--component-render-start-Foo",
"--component-render-stop",
"--render-yield",
"--render-yield-stop",
]
`);
await waitForPaint(["Bar"]);

Check failure on line 372 in packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js

View workflow job for this annotation

GitHub Actions / Run eslint

Strings must use singlequote
});

it('should mark concurrent render with suspense that resolves', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('Timeline profiler', () => {
let utils;
let assertLog;
let waitFor;

let waitForPaint;
describe('User Timing API', () => {
let currentlyNotClearedMarks;
let registeredMarks;
Expand Down Expand Up @@ -75,6 +75,8 @@ describe('Timeline profiler', () => {
markOptions.startTime++;
}
},
measure() {},
clearMeasures() {},
};
}

Expand All @@ -101,7 +103,7 @@ describe('Timeline profiler', () => {
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;

waitForPaint = InternalTestUtils.waitForPaint;
setPerformanceMock =
require('react-devtools-shared/src/backend/profilingHooks').setPerformanceMock_ONLY_FOR_TESTING;
setPerformanceMock(createUserTimingPolyfill());
Expand Down Expand Up @@ -1301,6 +1303,8 @@ describe('Timeline profiler', () => {
const data = await preprocessData(testMarks);
const event = data.nativeEvents.find(({type}) => type === 'click');
expect(event.warning).toBe(null);

await waitForPaint([]);
});

// @reactVersion >= 18.0
Expand Down
94 changes: 70 additions & 24 deletions packages/react-devtools-shared/src/backend/profilingHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export function createProfilingHooks({
let currentBatchUID: BatchUID = 0;
let currentReactComponentMeasure: ReactComponentMeasure | null = null;
let currentReactMeasuresStack: Array<ReactMeasure> = [];
const currentBeginMarksStack: Array<string> = [];
let currentTimelineData: TimelineData | null = null;
let currentFiberStacks: Map<SchedulingEvent, Array<Fiber>> = new Map();
let isProfiling: boolean = false;
Expand Down Expand Up @@ -214,6 +215,56 @@ export function createProfilingHooks({
((performanceTarget: any): Performance).clearMarks(markName);
}

function beginMark(taskName: string, ending: string | number) {
// This name format is used in preprocessData.js so it cannot just be changed.
const startMarkName = `--${taskName}-start-${ending}`;
currentBeginMarksStack.push(startMarkName);
// This method won't be called unless these functions are defined, so we can skip the extra typeof check.
((performanceTarget: any): Performance).mark(startMarkName);
}

function endMarkAndClear(taskName: string) {
const startMarkName = currentBeginMarksStack.pop();
if (!startMarkName) {
console.error(
'endMarkAndClear was unexpectedly called without a corresponding start mark',
);
return;
}
const markEnding = startMarkName.split('-').at(-1) || '';
const endMarkName = `--${taskName}-stop`;
const measureName = `${taskName} ${markEnding}`;

// Use different color for rendering tasks.
const color = taskName.includes('render') ? 'primary' : 'tertiary';
// If the ending is not a number, then it's a component name.
const properties = isNaN(parseInt(markEnding, 10))
? [['Component', markEnding]]
: [];
// This method won't be called unless these functions are defined, so we can skip the extra typeof check.
((performanceTarget: any): Performance).mark(endMarkName);
// Based on the format in https://bit.ly/rpp-e11y
const measureOptions = {
start: startMarkName,
end: endMarkName,
detail: {
devtools: {
dataType: 'track-entry',
color,
track: '⚛️ React',
properties,
},
},
};
((performanceTarget: any): Performance).measure(
measureName,
measureOptions,
);
((performanceTarget: any): Performance).clearMarks(startMarkName);
((performanceTarget: any): Performance).clearMarks(endMarkName);
((performanceTarget: any): Performance).clearMeasures(measureName);
}

function recordReactMeasureStarted(
type: ReactMeasureType,
lanes: Lanes,
Expand Down Expand Up @@ -301,7 +352,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear(`--commit-start-${lanes}`);
beginMark('commit', lanes);

// Some metadata only needs to be logged once per session,
// but if profiling information is being recorded via the Performance tab,
Expand All @@ -318,7 +369,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear('--commit-stop');
endMarkAndClear('commit');
}
}

Expand All @@ -340,7 +391,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear(`--component-render-start-${componentName}`);
beginMark('component-render', componentName);
}
}
}
Expand All @@ -361,9 +412,8 @@ export function createProfilingHooks({
currentReactComponentMeasure = null;
}
}

if (supportsUserTimingV3) {
markAndClear('--component-render-stop');
endMarkAndClear('component-render');
}
}

Expand All @@ -385,7 +435,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear(`--component-layout-effect-mount-start-${componentName}`);
beginMark('component-layout-effect-mount', componentName);
}
}
}
Expand All @@ -408,7 +458,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear('--component-layout-effect-mount-stop');
endMarkAndClear('component-layout-effect-mount');
}
}

Expand All @@ -430,9 +480,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear(
`--component-layout-effect-unmount-start-${componentName}`,
);
beginMark('component-layout-effect-unmount', componentName);
}
}
}
Expand All @@ -455,7 +503,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear('--component-layout-effect-unmount-stop');
endMarkAndClear('component-layout-effect-unmount');
}
}

Expand All @@ -477,7 +525,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear(`--component-passive-effect-mount-start-${componentName}`);
beginMark('component-passive-effect-mount', componentName);
}
}
}
Expand All @@ -500,7 +548,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear('--component-passive-effect-mount-stop');
endMarkAndClear('component-passive-effect-mount');
}
}

Expand All @@ -522,9 +570,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear(
`--component-passive-effect-unmount-start-${componentName}`,
);
beginMark('component-passive-effect-unmount', componentName);
}
}
}
Expand All @@ -547,7 +593,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear('--component-passive-effect-unmount-stop');
endMarkAndClear('component-passive-effect-unmount');
}
}

Expand Down Expand Up @@ -679,7 +725,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear(`--layout-effects-start-${lanes}`);
beginMark('layout-effects', lanes);
}
}

Expand All @@ -689,7 +735,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear('--layout-effects-stop');
endMarkAndClear('layout-effects');
}
}

Expand All @@ -699,7 +745,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear(`--passive-effects-start-${lanes}`);
beginMark('passive-effects', lanes);
}
}

Expand All @@ -709,7 +755,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear('--passive-effects-stop');
endMarkAndClear('passive-effects');
}
}

Expand All @@ -734,7 +780,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear(`--render-start-${lanes}`);
beginMark('render', lanes);
}
}

Expand All @@ -744,7 +790,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear('--render-yield');
endMarkAndClear('render-yield');
}
}

Expand All @@ -754,7 +800,7 @@ export function createProfilingHooks({
}

if (supportsUserTimingV3) {
markAndClear('--render-stop');
endMarkAndClear('render');
}
}

Expand Down

0 comments on commit 9a4df9c

Please sign in to comment.