Performance tool: Warn when interrupting an in-progress tree (#11480)
* Performance tool: Warn when interrupting an in-progress tree * Include the name of the component that caused the interruption
Andrew Clark committed
Nov 7, 2017 at 16:52 UTC
05f3ecc3eae7a6c10109f48548c001c28f4cb480
4 files changed
+64
-15
packages/react-reconciler/src/ReactDebugFiberPerf.js
+14
-5
@@ -318,8 +318,9 @@ export function stopPhaseTimer(): void {
318
}
319
}
320
321
-export function startWorkLoopTimer(): void {
321
+export function startWorkLoopTimer(nextUnitOfWork: Fiber | null): void {
322
if (enableUserTimingAPI) {
323
+ currentFiber = nextUnitOfWork;
324
if (!supportsUserTiming) {
325
return;
326
}
@@ -332,14 +333,22 @@ export function startWorkLoopTimer(): void {
333
}
334
}
335
335
-export function stopWorkLoopTimer(): void {
336
+export function stopWorkLoopTimer(interruptedBy: Fiber | null): void {
337
if (enableUserTimingAPI) {
338
if (!supportsUserTiming) {
339
return;
340
}
340
- const warning = commitCountInCurrentWorkLoop > 1
341
- ? 'There were cascading updates'
342
- : null;
341
+ let warning = null;
342
+ if (interruptedBy !== null) {
343
+ if (interruptedBy.tag === HostRoot) {
344
+ warning = 'A top-level update interrupted the previous render';
345
+ } else {
346
+ const componentName = getComponentName(interruptedBy) || 'Unknown';
347
+ warning = `An update to ${componentName} interrupted the previous render`;
348
+ }
349
+ } else if (commitCountInCurrentWorkLoop > 1) {
350
+ warning = 'There were cascading updates';
351
+ }
352
commitCountInCurrentWorkLoop = 0;
353
// Pause any measurements until the next loop.
354
pauseTimers();
packages/react-reconciler/src/ReactFiberScheduler.js
+13
-6
@@ -216,6 +216,9 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
216
let isCommitting: boolean = false;
217
let isUnmounting: boolean = false;
218
219
+ // Used for performance tracking.
220
+ let interruptedBy: Fiber | null = null;
221
+
222
function resetContextStack() {
223
// Reset the stack
224
reset();
@@ -752,8 +755,6 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
755
root: FiberRoot,
756
expirationTime: ExpirationTime,
757
): Fiber | null {
755
- startWorkLoopTimer();
756
-
758
invariant(
759
!isWorking,
760
'renderRoot was called recursively. This error is likely caused ' +
@@ -772,7 +773,7 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
773
expirationTime !== nextRenderExpirationTime ||
774
nextUnitOfWork === null
775
) {
775
- // This is a restart. Reset the stack.
776
+ // Reset the stack and start working from the root.
777
resetContextStack();
778
nextRoot = root;
779
nextRenderExpirationTime = expirationTime;
@@ -783,6 +784,8 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
784
);
785
}
786
787
+ startWorkLoopTimer(nextUnitOfWork);
788
+
789
let didError = false;
790
let error = null;
791
if (__DEV__) {
@@ -865,12 +868,12 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
868
const uncaughtError = firstUncaughtError;
869
870
// We're done performing work. Time to clean up.
871
+ stopWorkLoopTimer(interruptedBy);
872
+ interruptedBy = null;
873
isWorking = false;
874
didFatal = false;
875
firstUncaughtError = null;
876
872
- stopWorkLoopTimer();
873
-
877
if (uncaughtError !== null) {
878
onUncaughtError(uncaughtError);
879
}
@@ -1198,7 +1201,11 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
1201
root === nextRoot &&
1202
expirationTime <= nextRenderExpirationTime
1203
) {
1201
- // This is an interruption. Restart the root from the top.
1204
+ // Restart the root from the top.
1205
+ if (nextUnitOfWork !== null) {
1206
+ // This is an interruption. (Used for performance tracking.)
1207
+ interruptedBy = fiber;
1208
+ }
1209
nextRoot = null;
1210
nextUnitOfWork = null;
1211
nextRenderExpirationTime = NoWork;
packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.js
+14
@@ -498,4 +498,18 @@ describe('ReactDebugFiberPerf', () => {
498
});
499
expect(getFlameChart()).toMatchSnapshot();
500
});
501
+
502
+ it('warns if an in-progress update is interrupted', () => {
503
+ function Foo() {
504
+ return <span />;
505
+ }
506
+
507
+ ReactNoop.render(<Foo />);
508
+ ReactNoop.flushUnitsOfWork(2);
509
+ ReactNoop.flushSync(() => {
510
+ ReactNoop.render(<Foo />);
511
+ });
512
+ ReactNoop.flush();
513
+ expect(getFlameChart()).toMatchSnapshot();
514
+ });
515
});
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap
+23
-4
@@ -243,6 +243,17 @@ exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
243
"
244
`;
245
246
+exports[`ReactDebugFiberPerf supports portals 1`] = `
247
+"⚛ (React Tree Reconciliation)
248
+ ⚛ Parent [mount]
249
+ ⚛ Child [mount]
250
+
251
+⚛ (Committing Changes)
252
+ ⚛ (Committing Host Effects: 2 Total)
253
+ ⚛ (Calling Lifecycle Methods: 0 Total)
254
+"
255
+`;
256
+
257
exports[`ReactDebugFiberPerf supports returns 1`] = `
258
"⚛ (React Tree Reconciliation)
259
⚛ App [mount]
@@ -260,14 +271,22 @@ exports[`ReactDebugFiberPerf supports returns 1`] = `
271
"
272
`;
273
263
-exports[`ReactDebugFiberPerf supports portals 1`] = `
274
+exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = `
275
"⚛ (React Tree Reconciliation)
265
- ⚛ Parent [mount]
266
- ⚛ Child [mount]
276
+ ⚛ Foo [mount]
277
+
278
+⛔ (React Tree Reconciliation) Warning: A top-level update interrupted the previous render
279
+ ⚛ Foo [mount]
280
281
⚛ (Committing Changes)
269
- ⚛ (Committing Host Effects: 2 Total)
282
+ ⚛ (Committing Host Effects: 1 Total)
283
⚛ (Calling Lifecycle Methods: 0 Total)
284
+
285
+⚛ (React Tree Reconciliation)
286
+
287
+⚛ (Committing Changes)
288
+ ⚛ (Committing Host Effects: 1 Total)
289
+ ⚛ (Calling Lifecycle Methods: 1 Total)
290
"
291
`;
292