Skip to content

Commit 54db944

Browse files
committed
feat[DevTools]: Use Chrome DevTools Performance extension API
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.
1 parent 66df944 commit 54db944

File tree

3 files changed

+80
-27
lines changed

3 files changed

+80
-27
lines changed

packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ describe('Timeline profiler', () => {
8585
markOptions.startTime++;
8686
}
8787
},
88+
measure() {},
89+
clearMeasures() {},
8890
};
8991
}
9092

@@ -364,9 +366,10 @@ describe('Timeline profiler', () => {
364366
"--render-start-128",
365367
"--component-render-start-Foo",
366368
"--component-render-stop",
367-
"--render-yield",
369+
"--render-yield-stop",
368370
]
369371
`);
372+
await waitForPaint(['Bar']);
370373
});
371374

372375
it('should mark concurrent render with suspense that resolves', async () => {

packages/react-devtools-shared/src/__tests__/preprocessData-test.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ describe('Timeline profiler', () => {
2424
let utils;
2525
let assertLog;
2626
let waitFor;
27-
27+
let waitForPaint;
2828
describe('User Timing API', () => {
2929
let currentlyNotClearedMarks;
3030
let registeredMarks;
@@ -75,6 +75,8 @@ describe('Timeline profiler', () => {
7575
markOptions.startTime++;
7676
}
7777
},
78+
measure() {},
79+
clearMeasures() {},
7880
};
7981
}
8082

@@ -101,7 +103,7 @@ describe('Timeline profiler', () => {
101103
const InternalTestUtils = require('internal-test-utils');
102104
assertLog = InternalTestUtils.assertLog;
103105
waitFor = InternalTestUtils.waitFor;
104-
106+
waitForPaint = InternalTestUtils.waitForPaint;
105107
setPerformanceMock =
106108
require('react-devtools-shared/src/backend/profilingHooks').setPerformanceMock_ONLY_FOR_TESTING;
107109
setPerformanceMock(createUserTimingPolyfill());
@@ -1301,6 +1303,8 @@ describe('Timeline profiler', () => {
13011303
const data = await preprocessData(testMarks);
13021304
const event = data.nativeEvents.find(({type}) => type === 'click');
13031305
expect(event.warning).toBe(null);
1306+
1307+
await waitForPaint([]);
13041308
});
13051309

13061310
// @reactVersion >= 18.0

packages/react-devtools-shared/src/backend/profilingHooks.js

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export function createProfilingHooks({
123123
let currentBatchUID: BatchUID = 0;
124124
let currentReactComponentMeasure: ReactComponentMeasure | null = null;
125125
let currentReactMeasuresStack: Array<ReactMeasure> = [];
126+
const currentBeginMarksStack: Array<string> = [];
126127
let currentTimelineData: TimelineData | null = null;
127128
let currentFiberStacks: Map<SchedulingEvent, Array<Fiber>> = new Map();
128129
let isProfiling: boolean = false;
@@ -214,6 +215,56 @@ export function createProfilingHooks({
214215
((performanceTarget: any): Performance).clearMarks(markName);
215216
}
216217

218+
function beginMark(taskName: string, ending: string | number) {
219+
// This name format is used in preprocessData.js so it cannot just be changed.
220+
const startMarkName = `--${taskName}-start-${ending}`;
221+
currentBeginMarksStack.push(startMarkName);
222+
// This method won't be called unless these functions are defined, so we can skip the extra typeof check.
223+
((performanceTarget: any): Performance).mark(startMarkName);
224+
}
225+
226+
function endMarkAndClear(taskName: string) {
227+
const startMarkName = currentBeginMarksStack.pop();
228+
if (!startMarkName) {
229+
console.error(
230+
'endMarkAndClear was unexpectedly called without a corresponding start mark',
231+
);
232+
return;
233+
}
234+
const markEnding = startMarkName.split('-').at(-1) || '';
235+
const endMarkName = `--${taskName}-stop`;
236+
const measureName = `${taskName} ${markEnding}`;
237+
238+
// Use different color for rendering tasks.
239+
const color = taskName.includes('render') ? 'primary' : 'tertiary';
240+
// If the ending is not a number, then it's a component name.
241+
const properties = isNaN(parseInt(markEnding, 10))
242+
? [['Component', markEnding]]
243+
: [];
244+
// This method won't be called unless these functions are defined, so we can skip the extra typeof check.
245+
((performanceTarget: any): Performance).mark(endMarkName);
246+
// Based on the format in https://bit.ly/rpp-e11y
247+
const measureOptions = {
248+
start: startMarkName,
249+
end: endMarkName,
250+
detail: {
251+
devtools: {
252+
dataType: 'track-entry',
253+
color,
254+
track: '⚛️ React',
255+
properties,
256+
},
257+
},
258+
};
259+
((performanceTarget: any): Performance).measure(
260+
measureName,
261+
measureOptions,
262+
);
263+
((performanceTarget: any): Performance).clearMarks(startMarkName);
264+
((performanceTarget: any): Performance).clearMarks(endMarkName);
265+
((performanceTarget: any): Performance).clearMeasures(measureName);
266+
}
267+
217268
function recordReactMeasureStarted(
218269
type: ReactMeasureType,
219270
lanes: Lanes,
@@ -301,7 +352,7 @@ export function createProfilingHooks({
301352
}
302353

303354
if (supportsUserTimingV3) {
304-
markAndClear(`--commit-start-${lanes}`);
355+
beginMark('commit', lanes);
305356

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

320371
if (supportsUserTimingV3) {
321-
markAndClear('--commit-stop');
372+
endMarkAndClear('commit');
322373
}
323374
}
324375

@@ -340,7 +391,7 @@ export function createProfilingHooks({
340391
}
341392

342393
if (supportsUserTimingV3) {
343-
markAndClear(`--component-render-start-${componentName}`);
394+
beginMark('component-render', componentName);
344395
}
345396
}
346397
}
@@ -361,9 +412,8 @@ export function createProfilingHooks({
361412
currentReactComponentMeasure = null;
362413
}
363414
}
364-
365415
if (supportsUserTimingV3) {
366-
markAndClear('--component-render-stop');
416+
endMarkAndClear('component-render');
367417
}
368418
}
369419

@@ -385,7 +435,7 @@ export function createProfilingHooks({
385435
}
386436

387437
if (supportsUserTimingV3) {
388-
markAndClear(`--component-layout-effect-mount-start-${componentName}`);
438+
beginMark('component-layout-effect-mount', componentName);
389439
}
390440
}
391441
}
@@ -408,7 +458,7 @@ export function createProfilingHooks({
408458
}
409459

410460
if (supportsUserTimingV3) {
411-
markAndClear('--component-layout-effect-mount-stop');
461+
endMarkAndClear('component-layout-effect-mount');
412462
}
413463
}
414464

@@ -430,9 +480,7 @@ export function createProfilingHooks({
430480
}
431481

432482
if (supportsUserTimingV3) {
433-
markAndClear(
434-
`--component-layout-effect-unmount-start-${componentName}`,
435-
);
483+
beginMark('component-layout-effect-unmount', componentName);
436484
}
437485
}
438486
}
@@ -455,7 +503,7 @@ export function createProfilingHooks({
455503
}
456504

457505
if (supportsUserTimingV3) {
458-
markAndClear('--component-layout-effect-unmount-stop');
506+
endMarkAndClear('component-layout-effect-unmount');
459507
}
460508
}
461509

@@ -477,7 +525,7 @@ export function createProfilingHooks({
477525
}
478526

479527
if (supportsUserTimingV3) {
480-
markAndClear(`--component-passive-effect-mount-start-${componentName}`);
528+
beginMark('component-passive-effect-mount', componentName);
481529
}
482530
}
483531
}
@@ -500,7 +548,7 @@ export function createProfilingHooks({
500548
}
501549

502550
if (supportsUserTimingV3) {
503-
markAndClear('--component-passive-effect-mount-stop');
551+
endMarkAndClear('component-passive-effect-mount');
504552
}
505553
}
506554

@@ -522,9 +570,7 @@ export function createProfilingHooks({
522570
}
523571

524572
if (supportsUserTimingV3) {
525-
markAndClear(
526-
`--component-passive-effect-unmount-start-${componentName}`,
527-
);
573+
beginMark('component-passive-effect-unmount', componentName);
528574
}
529575
}
530576
}
@@ -547,7 +593,7 @@ export function createProfilingHooks({
547593
}
548594

549595
if (supportsUserTimingV3) {
550-
markAndClear('--component-passive-effect-unmount-stop');
596+
endMarkAndClear('component-passive-effect-unmount');
551597
}
552598
}
553599

@@ -679,7 +725,7 @@ export function createProfilingHooks({
679725
}
680726

681727
if (supportsUserTimingV3) {
682-
markAndClear(`--layout-effects-start-${lanes}`);
728+
beginMark('layout-effects', lanes);
683729
}
684730
}
685731

@@ -689,7 +735,7 @@ export function createProfilingHooks({
689735
}
690736

691737
if (supportsUserTimingV3) {
692-
markAndClear('--layout-effects-stop');
738+
endMarkAndClear('layout-effects');
693739
}
694740
}
695741

@@ -699,7 +745,7 @@ export function createProfilingHooks({
699745
}
700746

701747
if (supportsUserTimingV3) {
702-
markAndClear(`--passive-effects-start-${lanes}`);
748+
beginMark('passive-effects', lanes);
703749
}
704750
}
705751

@@ -709,7 +755,7 @@ export function createProfilingHooks({
709755
}
710756

711757
if (supportsUserTimingV3) {
712-
markAndClear('--passive-effects-stop');
758+
endMarkAndClear('passive-effects');
713759
}
714760
}
715761

@@ -734,7 +780,7 @@ export function createProfilingHooks({
734780
}
735781

736782
if (supportsUserTimingV3) {
737-
markAndClear(`--render-start-${lanes}`);
783+
beginMark('render', lanes);
738784
}
739785
}
740786

@@ -744,7 +790,7 @@ export function createProfilingHooks({
744790
}
745791

746792
if (supportsUserTimingV3) {
747-
markAndClear('--render-yield');
793+
endMarkAndClear('render-yield');
748794
}
749795
}
750796

@@ -754,7 +800,7 @@ export function createProfilingHooks({
754800
}
755801

756802
if (supportsUserTimingV3) {
757-
markAndClear('--render-stop');
803+
endMarkAndClear('render');
758804
}
759805
}
760806

0 commit comments

Comments
 (0)