Skip to content

Commit 51947a1

Browse files
author
Brian Vaughn
authoredJan 13, 2022
Refactored how React/DevTools log Timeline performance data (#23102)
Until now, DEV and PROFILING builds of React recorded Timeline profiling data using the User Timing API. This commit changes things so that React records this data by calling methods on the DevTools hook. (For now, DevTools still records that data using the User Timing API, to match previous behavior.) This commit is large but most of it is just moving things around: * New methods have been added to the DevTools hook (in "backend/profilingHooks") for recording the Timeline performance events. * Reconciler's "ReactFiberDevToolsHook" has been updated to call these new methods (when they're present). * User Timing method calls in "SchedulingProfiler" have been moved to DevTools "backend/profilingHooks" (to match previous behavior, for now). * The old reconciler tests, "SchedulingProfiler-test" and "SchedulingProfilerLabels-test", have been moved into DevTools "TimelineProfiler-test" to ensure behavior didn't change unexpectedly. * Two new methods have been added to the injected renderer interface: injectProfilingHooks() and getLaneLabelMap(). Relates to #22529.
1 parent c09596c commit 51947a1

28 files changed

+4208
-3593
lines changed
 

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

+1,124
Large diffs are not rendered by default.

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

+1,929
Large diffs are not rendered by default.

‎packages/react-devtools-shared/src/__tests__/setupTests.js

+4
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,7 @@ env.afterEach(() => {
162162
// so that we don't disconnect the ReactCurrentDispatcher ref.
163163
jest.resetModules();
164164
});
165+
166+
expect.extend({
167+
...require('../../../../scripts/jest/matchers/schedulerTestMatchers'),
168+
});

‎packages/react-devtools-shared/src/__tests__/utils.js

+8
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,21 @@ export function getRendererID(): number {
163163
}
164164

165165
export function legacyRender(elements, container) {
166+
if (container == null) {
167+
container = document.createElement('div');
168+
}
169+
166170
const ReactDOM = require('react-dom');
167171
withErrorsOrWarningsIgnored(
168172
['ReactDOM.render is no longer supported in React 18'],
169173
() => {
170174
ReactDOM.render(elements, container);
171175
},
172176
);
177+
178+
return () => {
179+
ReactDOM.unmountComponentAtNode(container);
180+
};
173181
}
174182

175183
export function requireTestRenderer(): ReactTestRenderer {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {
11+
Lane,
12+
Lanes,
13+
DevToolsProfilingHooks,
14+
} from 'react-devtools-shared/src/backend/types';
15+
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
16+
import type {Wakeable} from 'shared/ReactTypes';
17+
18+
import isArray from 'shared/isArray';
19+
import {SCHEDULING_PROFILER_VERSION} from 'react-devtools-timeline/src/constants';
20+
21+
let performanceTarget: Performance | null = null;
22+
23+
// If performance exists and supports the subset of the User Timing API that we require.
24+
let supportsUserTiming =
25+
typeof performance !== 'undefined' &&
26+
typeof performance.mark === 'function' &&
27+
typeof performance.clearMarks === 'function';
28+
29+
let supportsUserTimingV3 = false;
30+
if (supportsUserTiming) {
31+
const CHECK_V3_MARK = '__v3';
32+
const markOptions = {};
33+
// $FlowFixMe: Ignore Flow complaining about needing a value
34+
Object.defineProperty(markOptions, 'startTime', {
35+
get: function() {
36+
supportsUserTimingV3 = true;
37+
return 0;
38+
},
39+
set: function() {},
40+
});
41+
42+
try {
43+
// $FlowFixMe: Flow expects the User Timing level 2 API.
44+
performance.mark(CHECK_V3_MARK, markOptions);
45+
} catch (error) {
46+
// Ignore
47+
} finally {
48+
performance.clearMarks(CHECK_V3_MARK);
49+
}
50+
}
51+
52+
if (supportsUserTimingV3) {
53+
performanceTarget = performance;
54+
}
55+
56+
// Mocking the Performance Object (and User Timing APIs) for testing is fragile.
57+
// This API allows tests to directly override the User Timing APIs.
58+
export function setPerformanceMock_ONLY_FOR_TESTING(
59+
performanceMock: Performance | null,
60+
) {
61+
performanceTarget = performanceMock;
62+
supportsUserTiming = performanceMock !== null;
63+
supportsUserTimingV3 = performanceMock !== null;
64+
}
65+
66+
function markAndClear(markName) {
67+
// This method won't be called unless these functions are defined, so we can skip the extra typeof check.
68+
((performanceTarget: any): Performance).mark(markName);
69+
((performanceTarget: any): Performance).clearMarks(markName);
70+
}
71+
72+
export function createProfilingHooks({
73+
getDisplayNameForFiber,
74+
getLaneLabelMap,
75+
reactVersion,
76+
}: {|
77+
getDisplayNameForFiber: (fiber: Fiber) => string | null,
78+
getLaneLabelMap?: () => Map<Lane, string>,
79+
reactVersion: string,
80+
|}): DevToolsProfilingHooks {
81+
function markMetadata() {
82+
markAndClear(`--react-version-${reactVersion}`);
83+
markAndClear(`--profiler-version-${SCHEDULING_PROFILER_VERSION}`);
84+
85+
/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
86+
if (
87+
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
88+
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.getInternalModuleRanges ===
89+
'function'
90+
) {
91+
// Ask the DevTools hook for module ranges that may have been reported by the current renderer(s).
92+
const ranges = __REACT_DEVTOOLS_GLOBAL_HOOK__.getInternalModuleRanges();
93+
94+
// This check would not be required,
95+
// except that it's possible for things to override __REACT_DEVTOOLS_GLOBAL_HOOK__.
96+
if (isArray(ranges)) {
97+
for (let i = 0; i < ranges.length; i++) {
98+
const range = ranges[i];
99+
if (isArray(range) && range.length === 2) {
100+
const [startStackFrame, stopStackFrame] = ranges[i];
101+
102+
markAndClear(`--react-internal-module-start-${startStackFrame}`);
103+
markAndClear(`--react-internal-module-stop-${stopStackFrame}`);
104+
}
105+
}
106+
}
107+
}
108+
109+
if (typeof getLaneLabelMap === 'function') {
110+
const map = getLaneLabelMap();
111+
const labels = Array.from(map.values()).join(',');
112+
markAndClear(`--react-lane-labels-${labels}`);
113+
}
114+
}
115+
116+
function markCommitStarted(lanes: Lanes): void {
117+
if (supportsUserTimingV3) {
118+
markAndClear(`--commit-start-${lanes}`);
119+
120+
// Certain types of metadata should be logged infrequently.
121+
// Normally we would log this during module init,
122+
// but there's no guarantee a user is profiling at that time.
123+
// Commits happen infrequently (less than renders or state updates)
124+
// so we log this extra information along with a commit.
125+
// It will likely be logged more than once but that's okay.
126+
//
127+
// TODO (timeline) Only log this once, when profiling starts.
128+
// For the first phase– refactoring– we'll match the previous behavior.
129+
markMetadata();
130+
}
131+
}
132+
133+
function markCommitStopped(): void {
134+
if (supportsUserTimingV3) {
135+
markAndClear('--commit-stop');
136+
}
137+
}
138+
139+
function markComponentRenderStarted(fiber: Fiber): void {
140+
if (supportsUserTimingV3) {
141+
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
142+
// TODO (timeline) Record and cache component stack
143+
markAndClear(`--component-render-start-${componentName}`);
144+
}
145+
}
146+
147+
function markComponentRenderStopped(): void {
148+
if (supportsUserTimingV3) {
149+
markAndClear('--component-render-stop');
150+
}
151+
}
152+
153+
function markComponentPassiveEffectMountStarted(fiber: Fiber): void {
154+
if (supportsUserTimingV3) {
155+
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
156+
// TODO (timeline) Record and cache component stack
157+
markAndClear(`--component-passive-effect-mount-start-${componentName}`);
158+
}
159+
}
160+
161+
function markComponentPassiveEffectMountStopped(): void {
162+
if (supportsUserTimingV3) {
163+
markAndClear('--component-passive-effect-mount-stop');
164+
}
165+
}
166+
167+
function markComponentPassiveEffectUnmountStarted(fiber: Fiber): void {
168+
if (supportsUserTimingV3) {
169+
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
170+
// TODO (timeline) Record and cache component stack
171+
markAndClear(`--component-passive-effect-unmount-start-${componentName}`);
172+
}
173+
}
174+
175+
function markComponentPassiveEffectUnmountStopped(): void {
176+
if (supportsUserTimingV3) {
177+
markAndClear('--component-passive-effect-unmount-stop');
178+
}
179+
}
180+
181+
function markComponentLayoutEffectMountStarted(fiber: Fiber): void {
182+
if (supportsUserTimingV3) {
183+
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
184+
// TODO (timeline) Record and cache component stack
185+
markAndClear(`--component-layout-effect-mount-start-${componentName}`);
186+
}
187+
}
188+
189+
function markComponentLayoutEffectMountStopped(): void {
190+
if (supportsUserTimingV3) {
191+
markAndClear('--component-layout-effect-mount-stop');
192+
}
193+
}
194+
195+
function markComponentLayoutEffectUnmountStarted(fiber: Fiber): void {
196+
if (supportsUserTimingV3) {
197+
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
198+
// TODO (timeline) Record and cache component stack
199+
markAndClear(`--component-layout-effect-unmount-start-${componentName}`);
200+
}
201+
}
202+
203+
function markComponentLayoutEffectUnmountStopped(): void {
204+
if (supportsUserTimingV3) {
205+
markAndClear('--component-layout-effect-unmount-stop');
206+
}
207+
}
208+
209+
function markComponentErrored(
210+
fiber: Fiber,
211+
thrownValue: mixed,
212+
lanes: Lanes,
213+
): void {
214+
if (supportsUserTimingV3) {
215+
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
216+
const phase = fiber.alternate === null ? 'mount' : 'update';
217+
218+
let message = '';
219+
if (
220+
thrownValue !== null &&
221+
typeof thrownValue === 'object' &&
222+
typeof thrownValue.message === 'string'
223+
) {
224+
message = thrownValue.message;
225+
} else if (typeof thrownValue === 'string') {
226+
message = thrownValue;
227+
}
228+
229+
// TODO (timeline) Record and cache component stack
230+
markAndClear(`--error-${componentName}-${phase}-${message}`);
231+
}
232+
}
233+
234+
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
235+
236+
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
237+
const wakeableIDs: WeakMap<Wakeable, number> = new PossiblyWeakMap();
238+
let wakeableID: number = 0;
239+
function getWakeableID(wakeable: Wakeable): number {
240+
if (!wakeableIDs.has(wakeable)) {
241+
wakeableIDs.set(wakeable, wakeableID++);
242+
}
243+
return ((wakeableIDs.get(wakeable): any): number);
244+
}
245+
246+
function markComponentSuspended(
247+
fiber: Fiber,
248+
wakeable: Wakeable,
249+
lanes: Lanes,
250+
): void {
251+
if (supportsUserTimingV3) {
252+
const eventType = wakeableIDs.has(wakeable) ? 'resuspend' : 'suspend';
253+
const id = getWakeableID(wakeable);
254+
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
255+
const phase = fiber.alternate === null ? 'mount' : 'update';
256+
257+
// Following the non-standard fn.displayName convention,
258+
// frameworks like Relay may also annotate Promises with a displayName,
259+
// describing what operation/data the thrown Promise is related to.
260+
// When this is available we should pass it along to the Timeline.
261+
const displayName = (wakeable: any).displayName || '';
262+
263+
// TODO (timeline) Record and cache component stack
264+
markAndClear(
265+
`--suspense-${eventType}-${id}-${componentName}-${phase}-${lanes}-${displayName}`,
266+
);
267+
wakeable.then(
268+
() => markAndClear(`--suspense-resolved-${id}-${componentName}`),
269+
() => markAndClear(`--suspense-rejected-${id}-${componentName}`),
270+
);
271+
}
272+
}
273+
274+
function markLayoutEffectsStarted(lanes: Lanes): void {
275+
if (supportsUserTimingV3) {
276+
markAndClear(`--layout-effects-start-${lanes}`);
277+
}
278+
}
279+
280+
function markLayoutEffectsStopped(): void {
281+
if (supportsUserTimingV3) {
282+
markAndClear('--layout-effects-stop');
283+
}
284+
}
285+
286+
function markPassiveEffectsStarted(lanes: Lanes): void {
287+
if (supportsUserTimingV3) {
288+
markAndClear(`--passive-effects-start-${lanes}`);
289+
}
290+
}
291+
292+
function markPassiveEffectsStopped(): void {
293+
if (supportsUserTimingV3) {
294+
markAndClear('--passive-effects-stop');
295+
}
296+
}
297+
298+
function markRenderStarted(lanes: Lanes): void {
299+
if (supportsUserTimingV3) {
300+
markAndClear(`--render-start-${lanes}`);
301+
}
302+
}
303+
304+
function markRenderYielded(): void {
305+
if (supportsUserTimingV3) {
306+
markAndClear('--render-yield');
307+
}
308+
}
309+
310+
function markRenderStopped(): void {
311+
if (supportsUserTimingV3) {
312+
markAndClear('--render-stop');
313+
}
314+
}
315+
316+
function markRenderScheduled(lane: Lane): void {
317+
if (supportsUserTimingV3) {
318+
markAndClear(`--schedule-render-${lane}`);
319+
}
320+
}
321+
322+
function markForceUpdateScheduled(fiber: Fiber, lane: Lane): void {
323+
if (supportsUserTimingV3) {
324+
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
325+
// TODO (timeline) Record and cache component stack
326+
markAndClear(`--schedule-forced-update-${lane}-${componentName}`);
327+
}
328+
}
329+
330+
function markStateUpdateScheduled(fiber: Fiber, lane: Lane): void {
331+
if (supportsUserTimingV3) {
332+
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
333+
// TODO (timeline) Record and cache component stack
334+
markAndClear(`--schedule-state-update-${lane}-${componentName}`);
335+
}
336+
}
337+
338+
return {
339+
markCommitStarted,
340+
markCommitStopped,
341+
markComponentRenderStarted,
342+
markComponentRenderStopped,
343+
markComponentPassiveEffectMountStarted,
344+
markComponentPassiveEffectMountStopped,
345+
markComponentPassiveEffectUnmountStarted,
346+
markComponentPassiveEffectUnmountStopped,
347+
markComponentLayoutEffectMountStarted,
348+
markComponentLayoutEffectMountStopped,
349+
markComponentLayoutEffectUnmountStarted,
350+
markComponentLayoutEffectUnmountStopped,
351+
markComponentErrored,
352+
markComponentSuspended,
353+
markLayoutEffectsStarted,
354+
markLayoutEffectsStopped,
355+
markPassiveEffectsStarted,
356+
markPassiveEffectsStopped,
357+
markRenderStarted,
358+
markRenderYielded,
359+
markRenderStopped,
360+
markRenderScheduled,
361+
markForceUpdateScheduled,
362+
markStateUpdateScheduled,
363+
};
364+
}

0 commit comments

Comments
 (0)