-
Notifications
You must be signed in to change notification settings - Fork 46.8k
/
ReactDebugFiberPerf.js
472 lines (430 loc) · 13.3 KB
/
ReactDebugFiberPerf.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Fiber} from './ReactFiber';
import {enableUserTimingAPI} from 'shared/ReactFeatureFlags';
import getComponentName from 'shared/getComponentName';
import {
HostRoot,
HostComponent,
HostText,
HostPortal,
CallComponent,
ReturnComponent,
Fragment,
} from 'shared/ReactTypeOfWork';
type MeasurementPhase =
| 'componentWillMount'
| 'componentWillUnmount'
| 'componentWillReceiveProps'
| 'shouldComponentUpdate'
| 'componentWillUpdate'
| 'componentDidUpdate'
| 'componentDidMount'
| 'getChildContext';
// Prefix measurements so that it's possible to filter them.
// Longer prefixes are hard to read in DevTools.
const reactEmoji = '\u269B';
const warningEmoji = '\u26D4';
const supportsUserTiming =
typeof performance !== 'undefined' &&
typeof performance.mark === 'function' &&
typeof performance.clearMarks === 'function' &&
typeof performance.measure === 'function' &&
typeof performance.clearMeasures === 'function';
// Keep track of current fiber so that we know the path to unwind on pause.
// TODO: this looks the same as nextUnitOfWork in scheduler. Can we unify them?
let currentFiber: Fiber | null = null;
// If we're in the middle of user code, which fiber and method is it?
// Reusing `currentFiber` would be confusing for this because user code fiber
// can change during commit phase too, but we don't need to unwind it (since
// lifecycles in the commit phase don't resemble a tree).
let currentPhase: MeasurementPhase | null = null;
let currentPhaseFiber: Fiber | null = null;
// Did lifecycle hook schedule an update? This is often a performance problem,
// so we will keep track of it, and include it in the report.
// Track commits caused by cascading updates.
let isCommitting: boolean = false;
let hasScheduledUpdateInCurrentCommit: boolean = false;
let hasScheduledUpdateInCurrentPhase: boolean = false;
let commitCountInCurrentWorkLoop: number = 0;
let effectCountInCurrentCommit: number = 0;
let isWaitingForCallback: boolean = false;
// During commits, we only show a measurement once per method name
// to avoid stretch the commit phase with measurement overhead.
const labelsInCurrentCommit: Set<string> = new Set();
const formatMarkName = (markName: string) => {
return `${reactEmoji} ${markName}`;
};
const formatLabel = (label: string, warning: string | null) => {
const prefix = warning ? `${warningEmoji} ` : `${reactEmoji} `;
const suffix = warning ? ` Warning: ${warning}` : '';
return `${prefix}${label}${suffix}`;
};
const beginMark = (markName: string) => {
performance.mark(formatMarkName(markName));
};
const clearMark = (markName: string) => {
performance.clearMarks(formatMarkName(markName));
};
const endMark = (label: string, markName: string, warning: string | null) => {
const formattedMarkName = formatMarkName(markName);
const formattedLabel = formatLabel(label, warning);
try {
performance.measure(formattedLabel, formattedMarkName);
} catch (err) {
// If previous mark was missing for some reason, this will throw.
// This could only happen if React crashed in an unexpected place earlier.
// Don't pile on with more errors.
}
// Clear marks immediately to avoid growing buffer.
performance.clearMarks(formattedMarkName);
performance.clearMeasures(formattedLabel);
};
const getFiberMarkName = (label: string, debugID: number) => {
return `${label} (#${debugID})`;
};
const getFiberLabel = (
componentName: string,
isMounted: boolean,
phase: MeasurementPhase | null,
) => {
if (phase === null) {
// These are composite component total time measurements.
return `${componentName} [${isMounted ? 'update' : 'mount'}]`;
} else {
// Composite component methods.
return `${componentName}.${phase}`;
}
};
const beginFiberMark = (
fiber: Fiber,
phase: MeasurementPhase | null,
): boolean => {
const componentName = getComponentName(fiber) || 'Unknown';
const debugID = ((fiber._debugID: any): number);
const isMounted = fiber.alternate !== null;
const label = getFiberLabel(componentName, isMounted, phase);
if (isCommitting && labelsInCurrentCommit.has(label)) {
// During the commit phase, we don't show duplicate labels because
// there is a fixed overhead for every measurement, and we don't
// want to stretch the commit phase beyond necessary.
return false;
}
labelsInCurrentCommit.add(label);
const markName = getFiberMarkName(label, debugID);
beginMark(markName);
return true;
};
const clearFiberMark = (fiber: Fiber, phase: MeasurementPhase | null) => {
const componentName = getComponentName(fiber) || 'Unknown';
const debugID = ((fiber._debugID: any): number);
const isMounted = fiber.alternate !== null;
const label = getFiberLabel(componentName, isMounted, phase);
const markName = getFiberMarkName(label, debugID);
clearMark(markName);
};
const endFiberMark = (
fiber: Fiber,
phase: MeasurementPhase | null,
warning: string | null,
) => {
const componentName = getComponentName(fiber) || 'Unknown';
const debugID = ((fiber._debugID: any): number);
const isMounted = fiber.alternate !== null;
const label = getFiberLabel(componentName, isMounted, phase);
const markName = getFiberMarkName(label, debugID);
endMark(label, markName, warning);
};
const shouldIgnoreFiber = (fiber: Fiber): boolean => {
// Host components should be skipped in the timeline.
// We could check typeof fiber.type, but does this work with RN?
switch (fiber.tag) {
case HostRoot:
case HostComponent:
case HostText:
case HostPortal:
case CallComponent:
case ReturnComponent:
case Fragment:
return true;
default:
return false;
}
};
const clearPendingPhaseMeasurement = () => {
if (currentPhase !== null && currentPhaseFiber !== null) {
clearFiberMark(currentPhaseFiber, currentPhase);
}
currentPhaseFiber = null;
currentPhase = null;
hasScheduledUpdateInCurrentPhase = false;
};
const pauseTimers = () => {
// Stops all currently active measurements so that they can be resumed
// if we continue in a later deferred loop from the same unit of work.
let fiber = currentFiber;
while (fiber) {
if (fiber._debugIsCurrentlyTiming) {
endFiberMark(fiber, null, null);
}
fiber = fiber.return;
}
};
const resumeTimersRecursively = (fiber: Fiber) => {
if (fiber.return !== null) {
resumeTimersRecursively(fiber.return);
}
if (fiber._debugIsCurrentlyTiming) {
beginFiberMark(fiber, null);
}
};
const resumeTimers = () => {
// Resumes all measurements that were active during the last deferred loop.
if (currentFiber !== null) {
resumeTimersRecursively(currentFiber);
}
};
export function recordEffect(): void {
if (enableUserTimingAPI) {
effectCountInCurrentCommit++;
}
}
export function recordScheduleUpdate(): void {
if (enableUserTimingAPI) {
if (isCommitting) {
hasScheduledUpdateInCurrentCommit = true;
}
if (
currentPhase !== null &&
currentPhase !== 'componentWillMount' &&
currentPhase !== 'componentWillReceiveProps'
) {
hasScheduledUpdateInCurrentPhase = true;
}
}
}
export function startRequestCallbackTimer(): void {
if (enableUserTimingAPI) {
if (supportsUserTiming && !isWaitingForCallback) {
isWaitingForCallback = true;
beginMark('(Waiting for async callback...)');
}
}
}
export function stopRequestCallbackTimer(didExpire: boolean): void {
if (enableUserTimingAPI) {
if (supportsUserTiming) {
isWaitingForCallback = false;
const warning = didExpire ? 'React was blocked by main thread' : null;
endMark(
'(Waiting for async callback...)',
'(Waiting for async callback...)',
warning,
);
}
}
}
export function startWorkTimer(fiber: Fiber): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
return;
}
// If we pause, this is the fiber to unwind from.
currentFiber = fiber;
if (!beginFiberMark(fiber, null)) {
return;
}
fiber._debugIsCurrentlyTiming = true;
}
}
export function cancelWorkTimer(fiber: Fiber): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
return;
}
// Remember we shouldn't complete measurement for this fiber.
// Otherwise flamechart will be deep even for small updates.
fiber._debugIsCurrentlyTiming = false;
clearFiberMark(fiber, null);
}
}
export function stopWorkTimer(fiber: Fiber): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
return;
}
// If we pause, its parent is the fiber to unwind from.
currentFiber = fiber.return;
if (!fiber._debugIsCurrentlyTiming) {
return;
}
fiber._debugIsCurrentlyTiming = false;
endFiberMark(fiber, null, null);
}
}
export function stopFailedWorkTimer(fiber: Fiber): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
return;
}
// If we pause, its parent is the fiber to unwind from.
currentFiber = fiber.return;
if (!fiber._debugIsCurrentlyTiming) {
return;
}
fiber._debugIsCurrentlyTiming = false;
const warning = 'An error was thrown inside this error boundary';
endFiberMark(fiber, null, warning);
}
}
export function startPhaseTimer(fiber: Fiber, phase: MeasurementPhase): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming) {
return;
}
clearPendingPhaseMeasurement();
if (!beginFiberMark(fiber, phase)) {
return;
}
currentPhaseFiber = fiber;
currentPhase = phase;
}
}
export function stopPhaseTimer(): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming) {
return;
}
if (currentPhase !== null && currentPhaseFiber !== null) {
const warning = hasScheduledUpdateInCurrentPhase
? 'Scheduled a cascading update'
: null;
endFiberMark(currentPhaseFiber, currentPhase, warning);
}
currentPhase = null;
currentPhaseFiber = null;
}
}
export function startWorkLoopTimer(nextUnitOfWork: Fiber | null): void {
if (enableUserTimingAPI) {
currentFiber = nextUnitOfWork;
if (!supportsUserTiming) {
return;
}
commitCountInCurrentWorkLoop = 0;
// This is top level call.
// Any other measurements are performed within.
beginMark('(React Tree Reconciliation)');
// Resume any measurements that were in progress during the last loop.
resumeTimers();
}
}
export function stopWorkLoopTimer(interruptedBy: Fiber | null): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming) {
return;
}
let warning = null;
if (interruptedBy !== null) {
if (interruptedBy.tag === HostRoot) {
warning = 'A top-level update interrupted the previous render';
} else {
const componentName = getComponentName(interruptedBy) || 'Unknown';
warning = `An update to ${
componentName
} interrupted the previous render`;
}
} else if (commitCountInCurrentWorkLoop > 1) {
warning = 'There were cascading updates';
}
commitCountInCurrentWorkLoop = 0;
// Pause any measurements until the next loop.
pauseTimers();
endMark(
'(React Tree Reconciliation)',
'(React Tree Reconciliation)',
warning,
);
}
}
export function startCommitTimer(): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming) {
return;
}
isCommitting = true;
hasScheduledUpdateInCurrentCommit = false;
labelsInCurrentCommit.clear();
beginMark('(Committing Changes)');
}
}
export function stopCommitTimer(): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming) {
return;
}
let warning = null;
if (hasScheduledUpdateInCurrentCommit) {
warning = 'Lifecycle hook scheduled a cascading update';
} else if (commitCountInCurrentWorkLoop > 0) {
warning = 'Caused by a cascading update in earlier commit';
}
hasScheduledUpdateInCurrentCommit = false;
commitCountInCurrentWorkLoop++;
isCommitting = false;
labelsInCurrentCommit.clear();
endMark('(Committing Changes)', '(Committing Changes)', warning);
}
}
export function startCommitHostEffectsTimer(): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming) {
return;
}
effectCountInCurrentCommit = 0;
beginMark('(Committing Host Effects)');
}
}
export function stopCommitHostEffectsTimer(): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming) {
return;
}
const count = effectCountInCurrentCommit;
effectCountInCurrentCommit = 0;
endMark(
`(Committing Host Effects: ${count} Total)`,
'(Committing Host Effects)',
null,
);
}
}
export function startCommitLifeCyclesTimer(): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming) {
return;
}
effectCountInCurrentCommit = 0;
beginMark('(Calling Lifecycle Methods)');
}
}
export function stopCommitLifeCyclesTimer(): void {
if (enableUserTimingAPI) {
if (!supportsUserTiming) {
return;
}
const count = effectCountInCurrentCommit;
effectCountInCurrentCommit = 0;
endMark(
`(Calling Lifecycle Methods: ${count} Total)`,
'(Calling Lifecycle Methods)',
null,
);
}
}