@samitouri / QOS-React / commits / 2e4663f616

Split performWork into renderRoot and commitRoot (#11264)

* Split performWork into renderRoot and commitRoot It turns out that the scheduler is too coupled to how the DOM renderer works. Specifically, the requestIdleCallback model, and how roots are committed immediately after completing. Other renderers have different constraints for when to yield and when to commit work. We're moving towards a model where the scheduler only works on a single root at a time, and the render phase and commit phase are split into distinct entry points. This gives the renderer more control over when roots are committed, coordinating multiple roots, deferring the commit phase, batching updates, when to yield execution, and so on. In this initial commit, I've left the renderers alone and only changed the scheduler. Mostly, this involved extracting logic related to multiple roots and moving it into its own section at the bottom of the file. The idea is that this section can be lifted pretty much as-is into the renderers. I'll do that next. * Remove FiberRoot scheduleAt Isn't actually used anywhere * Make the root schedule a linked list again Since this still lives inside the renderer, let's just use the FiberRoot type. The FiberRoot concept will likely be lifted out eventually, anyway. * commitRoot should accept a HostRoot This way it's less reliant on the alternate model * Unify branches * Remove dead branch onUncaughtError is only called while we're working on a root. * remainingWork -> remainingExpirationTime I was wary of leaking NoWork but mixing numbers and null is worse so let's just do it until we think of something better. * Rename stuff

Andrew Clark committed Oct 20, 2017 at 13:22 UTC 2e4663f61698e88e36d6fdfdf15e941ee29aa791
5 files changed +650 -633
packages/react-reconciler/src/ReactFiberExpirationTime.js
-22
@@ -48,25 +48,3 @@ function computeExpirationBucket(
48 );
49 }
50 exports.computeExpirationBucket = computeExpirationBucket;
51 -
52 -// Given the current clock time and an expiration time, returns the
53 -// relative expiration time. Possible values include NoWork, Sync, Task, and
54 -// Never. All other values represent an async expiration time.
55 -function relativeExpirationTime(
56 - currentTime: ExpirationTime,
57 - expirationTime: ExpirationTime,
58 -): ExpirationTime {
59 - switch (expirationTime) {
60 - case NoWork:
61 - case Sync:
62 - case Task:
63 - case Never:
64 - return expirationTime;
65 - }
66 - const delta = expirationTime - currentTime;
67 - if (delta <= 0) {
68 - return Task;
69 - }
70 - return msToExpirationTime(delta);
71 -}
72 -exports.relativeExpirationTime = relativeExpirationTime;
packages/react-reconciler/src/ReactFiberRoot.js
+17 -6
@@ -11,8 +11,10 @@
11 'use strict';
12
13 import type {Fiber} from 'ReactFiber';
14 +import type {ExpirationTime} from 'ReactFiberExpirationTime';
15
16 const {createHostRootFiber} = require('ReactFiber');
17 +const {NoWork} = require('ReactFiberExpirationTime');
18
19 export type FiberRoot = {
20 // Any additional information from the host associated with this root.
@@ -21,15 +23,22 @@ export type FiberRoot = {
23 pendingChildren: any,
24 // The currently active root fiber. This is the mutable root of the tree.
25 current: Fiber,
24 - // Determines if this root has already been added to the schedule for work.
25 - isScheduled: boolean,
26 - // The work schedule is a linked list.
27 - nextScheduledRoot: FiberRoot | null,
26 + // Remaining expiration time on this root.
27 + remainingExpirationTime: ExpirationTime,
28 + // Determines if this root can be committed.
29 + isReadyForCommit: boolean,
30 + // A finished work-in-progress HostRoot that's ready to be committed.
31 + // TODO: The reason this is separate from isReadyForCommit is because the
32 + // FiberRoot concept will likely be lifted out of the reconciler and into
33 + // the renderer.
34 + finishedWork: Fiber | null,
35 // Top context object, used by renderSubtreeIntoContainer
36 context: Object | null,
37 pendingContext: Object | null,
38 // Determines if we should attempt to hydrate on the initial mount
39 +hydrate: boolean,
40 + // Linked-list of roots
41 + nextScheduledRoot: FiberRoot | null,
42 };
43
44 exports.createFiberRoot = function(
@@ -43,11 +52,13 @@ exports.createFiberRoot = function(
52 current: uninitializedFiber,
53 containerInfo: containerInfo,
54 pendingChildren: null,
46 - isScheduled: false,
47 - nextScheduledRoot: null,
55 + remainingExpirationTime: NoWork,
56 + isReadyForCommit: false,
57 + finishedWork: null,
58 context: null,
59 pendingContext: null,
60 hydrate,
61 + nextScheduledRoot: null,
62 };
63 uninitializedFiber.stateNode = root;
64 return root;
packages/react-reconciler/src/ReactFiberScheduler.js
+424 -433
@@ -60,7 +60,6 @@ var {
60 Never,
61 msToExpirationTime,
62 computeExpirationBucket,
63 - relativeExpirationTime,
63 } = require('ReactFiberExpirationTime');
64
65 var {AsyncUpdates} = require('ReactTypeOfInternalContext');
@@ -151,8 +150,6 @@ if (__DEV__) {
150 };
151 }
152
154 -var timeHeuristicForUnitOfWork = 1;
155 -
153 module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
154 config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
155 ) {
@@ -199,36 +196,17 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
196 // updates in sync mode.)
197 let expirationContext: ExpirationTime = NoWork;
198
202 - // Keeps track of whether we're currently in a work loop.
203 - let isPerformingWork: boolean = false;
204 -
205 - // Keeps track of whether the current deadline has expired.
206 - let deadlineHasExpired: boolean = false;
207 -
208 - // Keeps track of whether we should should batch sync updates.
209 - let isBatchingUpdates: boolean = false;
210 -
211 - // This is needed for the weird case where the initial mount is synchronous
212 - // even inside batchedUpdates :(
213 - let isUnbatchingUpdates: boolean = false;
199 + let isWorking: boolean = false;
200
201 // The next work in progress fiber that we're currently working on.
202 let nextUnitOfWork: Fiber | null = null;
203 + let nextRoot: FiberRoot | null = null;
204 // The time at which we're currently rendering work.
205 let nextRenderExpirationTime: ExpirationTime = NoWork;
206
207 // The next fiber with an effect that we're currently committing.
208 let nextEffect: Fiber | null = null;
209
223 - let pendingCommit: Fiber | null = null;
224 -
225 - // Linked list of roots with scheduled work on them.
226 - let nextScheduledRoot: FiberRoot | null = null;
227 - let lastScheduledRoot: FiberRoot | null = null;
228 -
229 - // Keep track of which host environment callbacks are scheduled.
230 - let isCallbackScheduled: boolean = false;
231 -
210 // Keep track of which fibers have captured an error that need to be handled.
211 // Work is removed from this collection after componentDidCatch is called.
212 let capturedErrors: Map<Fiber, CapturedError> | null = null;
@@ -245,11 +223,6 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
223 let isCommitting: boolean = false;
224 let isUnmounting: boolean = false;
225
248 - // Use these to prevent an infinite loop of nested updates
249 - const NESTED_UPDATE_LIMIT = 1000;
250 - let nestedUpdateCount: number = 0;
251 - let nextRenderedTree: FiberRoot | null = null;
252 -
226 function resetContextStack() {
227 // Reset the stack
228 reset();
@@ -258,72 +231,6 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
231 resetHostContainer();
232 }
233
261 - function resetNextUnitOfWork() {
262 - // Clear out roots with no more work on them, or if they have uncaught errors
263 - while (
264 - nextScheduledRoot !== null &&
265 - nextScheduledRoot.current.expirationTime === NoWork
266 - ) {
267 - // Unschedule this root.
268 - nextScheduledRoot.isScheduled = false;
269 - // Read the next pointer now.
270 - // We need to clear it in case this root gets scheduled again later.
271 - const next = nextScheduledRoot.nextScheduledRoot;
272 - nextScheduledRoot.nextScheduledRoot = null;
273 - // Exit if we cleared all the roots and there's no work to do.
274 - if (nextScheduledRoot === lastScheduledRoot) {
275 - nextScheduledRoot = null;
276 - lastScheduledRoot = null;
277 - nextRenderExpirationTime = NoWork;
278 - return null;
279 - }
280 - // Continue with the next root.
281 - // If there's no work on it, it will get unscheduled too.
282 - nextScheduledRoot = next;
283 - }
284 -
285 - let root = nextScheduledRoot;
286 - let earliestExpirationRoot = null;
287 - let earliestExpirationTime = NoWork;
288 - while (root !== null) {
289 - if (
290 - root.current.expirationTime !== NoWork &&
291 - (earliestExpirationTime === NoWork ||
292 - earliestExpirationTime > root.current.expirationTime)
293 - ) {
294 - earliestExpirationTime = root.current.expirationTime;
295 - earliestExpirationRoot = root;
296 - }
297 - // We didn't find anything to do in this root, so let's try the next one.
298 - root = root.nextScheduledRoot;
299 - }
300 - if (earliestExpirationRoot !== null) {
301 - nextRenderExpirationTime = earliestExpirationTime;
302 - // Before we start any new work, let's make sure that we have a fresh
303 - // stack to work from.
304 - // TODO: This call is buried a bit too deep. It would be nice to have
305 - // a single point which happens right before any new work and
306 - // unfortunately this is it.
307 - resetContextStack();
308 -
309 - nextUnitOfWork = createWorkInProgress(
310 - earliestExpirationRoot.current,
311 - earliestExpirationTime,
312 - );
313 - if (earliestExpirationRoot !== nextRenderedTree) {
314 - // We've switched trees. Reset the nested update counter.
315 - nestedUpdateCount = 0;
316 - nextRenderedTree = earliestExpirationRoot;
317 - }
318 - return;
319 - }
320 -
321 - nextRenderExpirationTime = NoWork;
322 - nextUnitOfWork = null;
323 - nextRenderedTree = null;
324 - return;
325 - }
326 -
234 function commitAllHostEffects() {
235 while (nextEffect !== null) {
236 if (__DEV__) {
@@ -430,30 +337,25 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
337 }
338 }
339
433 - function commitAllWork(finishedWork: Fiber) {
340 + function commitRoot(finishedWork: Fiber): ExpirationTime {
341 // We keep track of this so that captureError can collect any boundaries
342 // that capture an error during the commit phase. The reason these aren't
343 // local to this function is because errors that occur during cWU are
344 // captured elsewhere, to prevent the unmount from being interrupted.
345 + isWorking = true;
346 isCommitting = true;
347 if (__DEV__) {
348 startCommitTimer();
349 }
350
443 - pendingCommit = null;
444 - const root: FiberRoot = (finishedWork.stateNode: any);
351 + const root: FiberRoot = finishedWork.stateNode;
352 invariant(
353 root.current !== finishedWork,
354 'Cannot commit the same tree as before. This is probably a bug ' +
355 'related to the return field. This error is likely caused by a bug ' +
356 'in React. Please file an issue.',
357 );
451 -
452 - if (nextRenderExpirationTime <= mostRecentCurrentTime) {
453 - // Keep track of the number of iterations to prevent an infinite
454 - // update loop.
455 - nestedUpdateCount++;
456 - }
358 + root.isReadyForCommit = false;
359
360 // Reset this to null before calling lifecycles
361 ReactCurrentOwner.current = null;
@@ -565,6 +467,7 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
467 }
468
469 isCommitting = false;
470 + isWorking = false;
471 if (__DEV__) {
472 stopCommitLifeCyclesTimer();
473 stopCommitTimer();
@@ -583,9 +486,20 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
486 commitPhaseBoundaries = null;
487 }
488
586 - // This tree is done. Reset the unit of work pointer to the root that
587 - // expires soonest. If there's no work left, the pointer is set to null.
588 - resetNextUnitOfWork();
489 + if (firstUncaughtError !== null) {
490 + const error = firstUncaughtError;
491 + firstUncaughtError = null;
492 + onUncaughtError(error);
493 + }
494 +
495 + const remainingTime = root.current.expirationTime;
496 +
497 + if (remainingTime === NoWork) {
498 + capturedErrors = null;
499 + failedBoundaries = null;
500 + }
501 +
502 + return remainingTime;
503 }
504
505 function resetExpirationTime(
@@ -702,10 +616,9 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
616 workInProgress = returnFiber;
617 continue;
618 } else {
705 - // We've reached the root. Mark the root as pending commit. Depending
706 - // on how much time we have left, we'll either commit it now or in
707 - // the next frame.
708 - pendingCommit = workInProgress;
619 + // We've reached the root.
620 + const root: FiberRoot = workInProgress.stateNode;
621 + root.isReadyForCommit = true;
622 return null;
623 }
624 }
@@ -780,26 +693,45 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
693 return next;
694 }
695
783 - function performDeferredWork(deadline: Deadline) {
784 - performWork(Never, deadline);
785 - }
696 + function workLoop(expirationTime: ExpirationTime) {
697 + if (capturedErrors !== null) {
698 + // If there are unhandled errors, switch to the slow work loop.
699 + // TODO: How to avoid this check in the fast path? Maybe the renderer
700 + // could keep track of which roots have unhandled errors and call a
701 + // forked version of renderRoot.
702 + slowWorkLoopThatChecksForFailedWork(expirationTime);
703 + return;
704 + }
705 + if (
706 + nextRenderExpirationTime === NoWork ||
707 + nextRenderExpirationTime > expirationTime
708 + ) {
709 + return;
710 + }
711
787 - function handleCommitPhaseErrors() {
788 - // This is a special work loop for handling commit phase errors. It's
789 - // similar to the syncrhonous work loop, but does an additional check on
790 - // each fiber to see if it's an error boundary with an unhandled error. If
791 - // so, it uses a forked version of performUnitOfWork that unmounts the
792 - // failed subtree.
793 - //
794 - // The loop stops once the children have unmounted and error lifecycles are
795 - // called. Then we return to the regular flow.
712 + if (nextRenderExpirationTime <= mostRecentCurrentTime) {
713 + // Flush all expired work.
714 + while (nextUnitOfWork !== null) {
715 + nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
716 + }
717 + } else {
718 + // Flush asynchronous work until the deadline runs out of time.
719 + while (nextUnitOfWork !== null && !shouldYield()) {
720 + nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
721 + }
722 + }
723 + }
724
725 + function slowWorkLoopThatChecksForFailedWork(expirationTime: ExpirationTime) {
726 if (
798 - capturedErrors !== null &&
799 - capturedErrors.size > 0 &&
800 - nextRenderExpirationTime !== NoWork &&
801 - nextRenderExpirationTime <= mostRecentCurrentTime
727 + nextRenderExpirationTime === NoWork ||
728 + nextRenderExpirationTime > expirationTime
729 ) {
730 + return;
731 + }
732 +
733 + if (nextRenderExpirationTime <= mostRecentCurrentTime) {
734 + // Flush all expired work.
735 while (nextUnitOfWork !== null) {
736 if (hasCapturedError(nextUnitOfWork)) {
737 // Use a forked version of performUnitOfWork
@@ -807,158 +739,25 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
739 } else {
740 nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
741 }
810 - if (nextUnitOfWork === null) {
811 - invariant(
812 - pendingCommit !== null,
813 - 'Should have a pending commit. This error is likely caused by ' +
814 - 'a bug in React. Please file an issue.',
815 - );
816 - // We just completed a root. Commit it now.
817 - commitAllWork(pendingCommit);
818 - if (
819 - capturedErrors === null ||
820 - capturedErrors.size === 0 ||
821 - nextRenderExpirationTime === NoWork ||
822 - nextRenderExpirationTime > mostRecentCurrentTime
823 - ) {
824 - // There are no more unhandled errors. We can exit this special
825 - // work loop. If there's still additional work, we'll perform it
826 - // using one of the normal work loops.
827 - break;
828 - }
829 - // The commit phase produced additional errors. Continue working.
830 - }
742 }
832 - }
833 - }
834 -
835 - function workLoop(
836 - minExpirationTime: ExpirationTime,
837 - deadline: Deadline | null,
838 - ) {
839 - loop: do {
840 - if (pendingCommit !== null) {
841 - commitAllWork(pendingCommit);
842 - handleCommitPhaseErrors();
843 - } else if (nextUnitOfWork === null) {
844 - resetNextUnitOfWork();
845 - }
846 -
847 - if (
848 - nextRenderExpirationTime === NoWork ||
849 - nextRenderExpirationTime > minExpirationTime
850 - ) {
851 - return;
852 - }
853 -
854 - if (nextRenderExpirationTime <= mostRecentCurrentTime) {
855 - // Flush all expired work.
856 - while (nextUnitOfWork !== null) {
743 + } else {
744 + // Flush asynchronous work until the deadline runs out of time.
745 + while (nextUnitOfWork !== null && !shouldYield()) {
746 + if (hasCapturedError(nextUnitOfWork)) {
747 + // Use a forked version of performUnitOfWork
748 + nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork);
749 + } else {
750 nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
858 - if (nextUnitOfWork === null) {
859 - invariant(
860 - pendingCommit !== null,
861 - 'Should have a pending commit. This error is likely caused by ' +
862 - 'a bug in React. Please file an issue.',
863 - );
864 - // We just completed a root. Commit it now.
865 - commitAllWork(pendingCommit);
866 - // Clear any errors that were scheduled during the commit phase.
867 - handleCommitPhaseErrors();
868 - // The render time may have changed. Check again.
869 - if (
870 - nextRenderExpirationTime === NoWork ||
871 - nextRenderExpirationTime > minExpirationTime ||
872 - nextRenderExpirationTime > mostRecentCurrentTime
873 - ) {
874 - // We've completed all the expired work.
875 - break;
876 - }
877 - }
878 - }
879 - } else if (deadline !== null) {
880 - // Flush asynchronous work until the deadline runs out of time.
881 - while (nextUnitOfWork !== null && !deadlineHasExpired) {
882 - if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
883 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
884 - // In a deferred work batch, iff nextUnitOfWork returns null, we just
885 - // completed a root and a pendingCommit exists. Logically, we could
886 - // omit either of the checks in the following condition, but we need
887 - // both to satisfy Flow.
888 - if (nextUnitOfWork === null) {
889 - invariant(
890 - pendingCommit !== null,
891 - 'Should have a pending commit. This error is likely caused by ' +
892 - 'a bug in React. Please file an issue.',
893 - );
894 - // We just completed a root. If we have time, commit it now.
895 - // Otherwise, we'll commit it in the next frame.
896 - if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
897 - commitAllWork(pendingCommit);
898 - // Clear any errors that were scheduled during the commit phase.
899 - handleCommitPhaseErrors();
900 - // The render time may have changed. Check again.
901 - if (
902 - nextRenderExpirationTime === NoWork ||
903 - nextRenderExpirationTime > minExpirationTime ||
904 - nextRenderExpirationTime <= mostRecentCurrentTime
905 - ) {
906 - // We've completed all the async work.
907 - break;
908 - }
909 - } else {
910 - deadlineHasExpired = true;
911 - }
912 - }
913 - } else {
914 - deadlineHasExpired = true;
915 - }
751 }
752 }
918 -
919 - // There might be work left. Depending on the priority, we should
920 - // either perform it now or schedule a callback to perform it later.
921 - const currentTime = recalculateCurrentTime();
922 - switch (relativeExpirationTime(currentTime, nextRenderExpirationTime)) {
923 - case NoWork:
924 - // No work left. We can exit.
925 - break loop;
926 - case Sync:
927 - case Task:
928 - // We have remaining synchronous or task work. Keep performing it,
929 - // regardless of whether we're inside a callback.
930 - if (nextRenderExpirationTime <= minExpirationTime) {
931 - // Sometimes minExpirationTime is Sync, which means we should skip
932 - // task work.
933 - continue loop;
934 - }
935 - break loop;
936 - default:
937 - // We have remaining async work.
938 - if (deadline === null) {
939 - // We're not inside a callback. Exit and perform the work during
940 - // the next callback.
941 - break loop;
942 - }
943 - // We are inside a callback.
944 - if (
945 - !deadlineHasExpired &&
946 - nextRenderExpirationTime <= minExpirationTime
947 - ) {
948 - // We still have time. Keep working.
949 - continue loop;
950 - }
951 - // We've run out of time. Exit.
952 - break loop;
953 - }
954 - } while (true);
753 + }
754 }
755
957 - function performWorkCatchBlock(
756 + function renderRootCatchBlock(
757 + root: FiberRoot,
758 failedWork: Fiber,
759 boundary: Fiber,
960 - minExpirationTime: ExpirationTime,
961 - deadline: Deadline | null,
760 + expirationTime: ExpirationTime,
761 ) {
762 // We're going to restart the error boundary that captured the error.
763 // Conceptually, we're unwinding the stack. We need to unwind the
@@ -973,37 +772,53 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
772 nextUnitOfWork = performFailedUnitOfWork(boundary);
773
774 // Continue working.
976 - workLoop(minExpirationTime, deadline);
775 + workLoop(expirationTime);
776 }
777
979 - function performWork(
980 - minExpirationTime: ExpirationTime,
981 - deadline: Deadline | null,
982 - ) {
778 + function renderRoot(
779 + root: FiberRoot,
780 + expirationTime: ExpirationTime,
781 + ): Fiber | null {
782 if (__DEV__) {
783 startWorkLoopTimer();
784 }
785
786 invariant(
988 - !isPerformingWork,
989 - 'performWork was called recursively. This error is likely caused ' +
787 + !isWorking,
788 + 'renderRoot was called recursively. This error is likely caused ' +
789 'by a bug in React. Please file an issue.',
790 );
992 - isPerformingWork = true;
791 + isWorking = true;
792
994 - nestedUpdateCount = 0;
793 + // We're about to mutate the work-in-progress tree. If the root was pending
794 + // commit, it no longer is: we'll need to complete it again.
795 + root.isReadyForCommit = false;
796 +
797 + // Check if we're starting from a fresh stack, or if we're resuming from
798 + // previously yielded work.
799 + if (
800 + root !== nextRoot ||
801 + expirationTime !== nextRenderExpirationTime ||
802 + nextUnitOfWork === null
803 + ) {
804 + // This is a restart. Reset the stack.
805 + resetContextStack();
806 + nextRoot = root;
807 + nextRenderExpirationTime = expirationTime;
808 + nextUnitOfWork = createWorkInProgress(nextRoot.current, expirationTime);
809 + }
810
811 let didError = false;
812 let error = null;
813 if (__DEV__) {
999 - invokeGuardedCallback(null, workLoop, null, minExpirationTime, deadline);
814 + invokeGuardedCallback(null, workLoop, null, expirationTime);
815 if (hasCaughtError()) {
816 didError = true;
817 error = clearCaughtError();
818 }
819 } else {
820 try {
1006 - workLoop(minExpirationTime, deadline);
821 + workLoop(expirationTime);
822 } catch (e) {
823 didError = true;
824 error = e;
@@ -1046,12 +861,12 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
861 if (__DEV__) {
862 invokeGuardedCallback(
863 null,
1049 - performWorkCatchBlock,
864 + renderRootCatchBlock,
865 null,
866 + root,
867 failedWork,
868 boundary,
1053 - minExpirationTime,
1054 - deadline,
869 + expirationTime,
870 );
871 if (hasCaughtError()) {
872 didError = true;
@@ -1060,12 +875,7 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
875 }
876 } else {
877 try {
1063 - performWorkCatchBlock(
1064 - failedWork,
1065 - boundary,
1066 - minExpirationTime,
1067 - deadline,
1068 - );
878 + renderRootCatchBlock(root, failedWork, boundary, expirationTime);
879 error = null;
880 } catch (e) {
881 didError = true;
@@ -1077,39 +887,22 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
887 break;
888 }
889
1080 - // If we're inside a callback, set this to false, since we just flushed it.
1081 - if (deadline !== null) {
1082 - isCallbackScheduled = false;
1083 - }
1084 - // If there's remaining async work, make sure we schedule another callback.
1085 - if (
1086 - nextRenderExpirationTime > mostRecentCurrentTime &&
1087 - !isCallbackScheduled
1088 - ) {
1089 - scheduleDeferredCallback(performDeferredWork);
1090 - isCallbackScheduled = true;
1091 - }
1092 -
1093 - const errorToThrow = firstUncaughtError;
890 + const uncaughtError = firstUncaughtError;
891
892 // We're done performing work. Time to clean up.
1096 - isPerformingWork = false;
1097 - deadlineHasExpired = false;
893 + isWorking = false;
894 didFatal = false;
895 firstUncaughtError = null;
1100 - capturedErrors = null;
1101 - failedBoundaries = null;
1102 - nextRenderedTree = null;
1103 - nestedUpdateCount = 0;
896
897 if (__DEV__) {
898 stopWorkLoopTimer();
899 }
900
1109 - // It's safe to throw any unhandled errors.
1110 - if (errorToThrow !== null) {
1111 - throw errorToThrow;
901 + if (uncaughtError !== null) {
902 + onUncaughtError(uncaughtError);
903 }
904 +
905 + return root.isReadyForCommit ? root.current.alternate : null;
906 }
907
908 // Returns the boundary that captured the error, or null if the error is ignored
@@ -1310,9 +1103,6 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
1103 return;
1104 case HostRoot:
1105 if (firstUncaughtError === null) {
1313 - // If this is the host container, we treat it as a no-op error
1314 - // boundary. We'll throw the first uncaught error once it's safe to
1315 - // do so, at the end of the batch.
1106 firstUncaughtError = capturedError.error;
1107 }
1108 return;
@@ -1354,25 +1144,6 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
1144 }
1145 }
1146
1357 - function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1358 - if (expirationTime === NoWork) {
1359 - return;
1360 - }
1361 -
1362 - if (!root.isScheduled) {
1363 - root.isScheduled = true;
1364 - if (lastScheduledRoot) {
1365 - // Schedule ourselves to the end.
1366 - lastScheduledRoot.nextScheduledRoot = root;
1367 - lastScheduledRoot = root;
1368 - } else {
1369 - // We're the only work scheduled.
1370 - nextScheduledRoot = root;
1371 - lastScheduledRoot = root;
1372 - }
1373 - }
1374 - }
1375 -
1147 function computeAsyncExpiration() {
1148 // Given the current clock time, returns an expiration time. We use rounding
1149 // to batch like updates together.
@@ -1388,7 +1159,7 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
1159 if (expirationContext !== NoWork) {
1160 // An explicit expiration context was set;
1161 expirationTime = expirationContext;
1391 - } else if (isPerformingWork) {
1162 + } else if (isWorking) {
1163 if (isCommitting) {
1164 // Updates that occur during the commit phase should have sync priority
1165 // by default.
@@ -1412,7 +1183,8 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
1183
1184 if (
1185 expirationTime === Sync &&
1415 - (isBatchingUpdates || (isUnbatchingUpdates && isCommitting))
1186 + isBatchingUpdates &&
1187 + (!isUnbatchingUpdates || isCommitting)
1188 ) {
1189 // If we're in a batch, downgrade sync to task.
1190 expirationTime = Task;
@@ -1433,24 +1205,6 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
1205 recordScheduleUpdate();
1206 }
1207
1436 - if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1437 - didFatal = true;
1438 - invariant(
1439 - false,
1440 - 'Maximum update depth exceeded. This can happen when a ' +
1441 - 'component repeatedly calls setState inside componentWillUpdate or ' +
1442 - 'componentDidUpdate. React limits the number of nested updates to ' +
1443 - 'prevent infinite loops.',
1444 - );
1445 - }
1446 -
1447 - if (!isPerformingWork && expirationTime <= nextRenderExpirationTime) {
1448 - // We must reset the current unit of work pointer so that we restart the
1449 - // search from the root during the next tick, in case there is now higher
1450 - // priority work somewhere earlier than before.
1451 - nextUnitOfWork = null;
1452 - }
1453 -
1208 if (__DEV__) {
1209 if (!isErrorRecovery && fiber.tag === ClassComponent) {
1210 const instance = fiber.stateNode;
@@ -1459,19 +1213,13 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
1213 }
1214
1215 let node = fiber;
1462 - let shouldContinue = true;
1463 - while (node !== null && shouldContinue) {
1464 - // Walk the parent path to the root and update each node's expiration
1465 - // time. Once we reach a node whose expiration matches (and whose
1466 - // alternate's expiration matches) we can exit safely knowing that the
1467 - // rest of the path is correct.
1468 - shouldContinue = false;
1216 + while (node !== null) {
1217 + // Walk the parent path to the root and update each node's
1218 + // expiration time.
1219 if (
1220 node.expirationTime === NoWork ||
1221 node.expirationTime > expirationTime
1222 ) {
1473 - // Expiration time did not match. Update and keep going.
1474 - shouldContinue = true;
1223 node.expirationTime = expirationTime;
1224 }
1225 if (node.alternate !== null) {
@@ -1479,44 +1227,23 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
1227 node.alternate.expirationTime === NoWork ||
1228 node.alternate.expirationTime > expirationTime
1229 ) {
1482 - // Expiration time did not match. Update and keep going.
1483 - shouldContinue = true;
1230 node.alternate.expirationTime = expirationTime;
1231 }
1232 }
1233 if (node.return === null) {
1234 if (node.tag === HostRoot) {
1235 const root: FiberRoot = (node.stateNode: any);
1490 - scheduleRoot(root, expirationTime);
1491 - if (!isPerformingWork) {
1492 - switch (expirationTime) {
1493 - case Sync:
1494 - if (isUnbatchingUpdates) {
1495 - // We're inside unbatchedUpdates, which is inside either
1496 - // batchedUpdates or a lifecycle. We should only flush
1497 - // synchronous work, not task work.
1498 - performWork(Sync, null);
1499 - } else {
1500 - // Flush both synchronous and task work.
1501 - performWork(Task, null);
1502 - }
1503 - break;
1504 - case Task:
1505 - invariant(
1506 - isBatchingUpdates,
1507 - 'Task updates can only be scheduled as a nested update or ' +
1508 - 'inside batchedUpdates. This error is likely caused by a ' +
1509 - 'bug in React. Please file an issue.',
1510 - );
1511 - break;
1512 - default:
1513 - // This update is async. Schedule a callback.
1514 - if (!isCallbackScheduled) {
1515 - scheduleDeferredCallback(performDeferredWork);
1516 - isCallbackScheduled = true;
1517 - }
1518 - }
1236 + if (
1237 + !isWorking &&
1238 + root === nextRoot &&
1239 + expirationTime <= nextRenderExpirationTime
1240 + ) {
1241 + // This is an interruption. Restart the root from the top.
1242 + nextRoot = null;
1243 + nextUnitOfWork = null;
1244 + nextRenderExpirationTime = NoWork;
1245 }
1246 + requestWork(root, expirationTime);
1247 } else {
1248 if (__DEV__) {
1249 if (!isErrorRecovery && fiber.tag === ClassComponent) {
@@ -1541,6 +1268,284 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
1268 return mostRecentCurrentTime;
1269 }
1270
1271 + function deferredUpdates<A>(fn: () => A): A {
1272 + const previousExpirationContext = expirationContext;
1273 + expirationContext = computeAsyncExpiration();
1274 + try {
1275 + return fn();
1276 + } finally {
1277 + expirationContext = previousExpirationContext;
1278 + }
1279 + }
1280 +
1281 + function syncUpdates<A>(fn: () => A): A {
1282 + const previousExpirationContext = expirationContext;
1283 + expirationContext = Sync;
1284 + try {
1285 + return fn();
1286 + } finally {
1287 + expirationContext = previousExpirationContext;
1288 + }
1289 + }
1290 +
1291 + // TODO: Everything below this is written as if it has been lifted to the
1292 + // renderers. I'll do this in a follow-up.
1293 +
1294 + // Linked-list of roots
1295 + let firstScheduledRoot: FiberRoot | null = null;
1296 + let lastScheduledRoot: FiberRoot | null = null;
1297 +
1298 + let isCallbackScheduled: boolean = false;
1299 + let isRendering: boolean = false;
1300 + let nextFlushedRoot: FiberRoot | null = null;
1301 + let nextFlushedExpirationTime: ExpirationTime = NoWork;
1302 + let deadlineDidExpire: boolean = false;
1303 + let hasUnhandledError: boolean = false;
1304 + let unhandledError: mixed | null = null;
1305 + let deadline: Deadline | null = null;
1306 +
1307 + let isBatchingUpdates: boolean = false;
1308 + let isUnbatchingUpdates: boolean = false;
1309 +
1310 + // Use these to prevent an infinite loop of nested updates
1311 + const NESTED_UPDATE_LIMIT = 1000;
1312 + let nestedUpdateCount: number = 0;
1313 +
1314 + const timeHeuristicForUnitOfWork = 1;
1315 +
1316 + // requestWork is called by the scheduler whenever a root receives an update.
1317 + // It's up to the renderer to call renderRoot at some point in the future.
1318 + function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
1319 + if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1320 + invariant(
1321 + false,
1322 + 'Maximum update depth exceeded. This can happen when a ' +
1323 + 'component repeatedly calls setState inside componentWillUpdate or ' +
1324 + 'componentDidUpdate. React limits the number of nested updates to ' +
1325 + 'prevent infinite loops.',
1326 + );
1327 + }
1328 +
1329 + // Check if this root is already part of the schedule.
1330 + if (root.remainingExpirationTime === NoWork) {
1331 + // This root is not already scheduled. Add it.
1332 + root.remainingExpirationTime = expirationTime;
1333 + if (lastScheduledRoot === null) {
1334 + firstScheduledRoot = lastScheduledRoot = root;
1335 + } else {
1336 + lastScheduledRoot.nextScheduledRoot = root;
1337 + lastScheduledRoot = root;
1338 + }
1339 + } else {
1340 + // This root is already scheduled, but its priority may have increased.
1341 + const remainingExpirationTime = root.remainingExpirationTime;
1342 + if (
1343 + remainingExpirationTime === NoWork ||
1344 + expirationTime < remainingExpirationTime
1345 + ) {
1346 + // Update the priority.
1347 + root.remainingExpirationTime = expirationTime;
1348 + }
1349 + }
1350 +
1351 + // If we're not already rendering, schedule work to flush now (if it's
1352 + // sync) or later (if it's async).
1353 + if (!isRendering) {
1354 + // TODO: Remove distinction between sync and task. Maybe we can remove
1355 + // these magic numbers entirely by always comparing to the current time?
1356 + if (expirationTime === Sync) {
1357 + if (isUnbatchingUpdates) {
1358 + performWork(Sync, null);
1359 + } else {
1360 + performWork(Task, null);
1361 + }
1362 + } else if (!isCallbackScheduled) {
1363 + isCallbackScheduled = true;
1364 + scheduleDeferredCallback(flushAsyncWork);
1365 + }
1366 + }
1367 + }
1368 +
1369 + function findHighestPriorityRoot() {
1370 + let highestPriorityWork = NoWork;
1371 + let highestPriorityRoot = null;
1372 +
1373 + let previousScheduledRoot = null;
1374 + let root = firstScheduledRoot;
1375 + while (root !== null) {
1376 + const remainingExpirationTime = root.remainingExpirationTime;
1377 + if (remainingExpirationTime === NoWork) {
1378 + // If this root no longer has work, remove it from the scheduler.
1379 + let next = root.nextScheduledRoot;
1380 + root.nextScheduledRoot = null;
1381 + if (previousScheduledRoot === null) {
1382 + firstScheduledRoot = next;
1383 + } else {
1384 + previousScheduledRoot.nextScheduledRoot = next;
1385 + }
1386 + if (next === null) {
1387 + lastScheduledRoot = null;
1388 + }
1389 + root = next;
1390 + continue;
1391 + } else if (
1392 + highestPriorityWork === NoWork ||
1393 + remainingExpirationTime < highestPriorityWork
1394 + ) {
1395 + // Update the priority, if it's higher
1396 + highestPriorityWork = remainingExpirationTime;
1397 + highestPriorityRoot = root;
1398 + }
1399 + previousScheduledRoot = root;
1400 + root = root.nextScheduledRoot;
1401 + }
1402 +
1403 + // If the next root is the same as the previous root, this is a nested
1404 + // update. To prevent an infinite loop, increment the nested update count.
1405 + const previousFlushedRoot = nextFlushedRoot;
1406 + if (
1407 + previousFlushedRoot !== null &&
1408 + previousFlushedRoot === highestPriorityRoot
1409 + ) {
1410 + nestedUpdateCount++;
1411 + } else {
1412 + // Reset whenever we switch roots.
1413 + nestedUpdateCount = 0;
1414 + }
1415 + nextFlushedRoot = highestPriorityRoot;
1416 + nextFlushedExpirationTime = highestPriorityWork;
1417 + }
1418 +
1419 + function flushAsyncWork(dl) {
1420 + performWork(NoWork, dl);
1421 + }
1422 +
1423 + function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
1424 + invariant(
1425 + !isRendering,
1426 + 'performWork was called recursively. This error is likely caused ' +
1427 + 'by a bug in React. Please file an issue.',
1428 + );
1429 +
1430 + isRendering = true;
1431 + deadline = dl;
1432 +
1433 + // Keep working on roots until there's no more work, or until the we reach
1434 + // the deadlne.
1435 + findHighestPriorityRoot();
1436 + while (
1437 + nextFlushedRoot !== null &&
1438 + nextFlushedExpirationTime !== NoWork &&
1439 + (minExpirationTime === NoWork ||
1440 + nextFlushedExpirationTime <= minExpirationTime) &&
1441 + !deadlineDidExpire
1442 + ) {
1443 + // Check if this is async work or sync/expired work.
1444 + // TODO: Pass current time as argument to renderRoot, commitRoot
1445 + if (nextFlushedExpirationTime <= recalculateCurrentTime()) {
1446 + // Flush sync work.
1447 + let finishedWork = nextFlushedRoot.finishedWork;
1448 + if (finishedWork !== null) {
1449 + // This root is already complete. We can commit it.
1450 + nextFlushedRoot.finishedWork = null;
1451 + nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork);
1452 + } else {
1453 + nextFlushedRoot.finishedWork = null;
1454 + finishedWork = renderRoot(nextFlushedRoot, nextFlushedExpirationTime);
1455 + if (finishedWork !== null) {
1456 + // We've completed the root. Commit it.
1457 + nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork);
1458 + }
1459 + }
1460 + } else {
1461 + // Flush async work.
1462 + let finishedWork = nextFlushedRoot.finishedWork;
1463 + if (finishedWork !== null) {
1464 + // This root is already complete. We can commit it.
1465 + nextFlushedRoot.finishedWork = null;
1466 + nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork);
1467 + } else {
1468 + nextFlushedRoot.finishedWork = null;
1469 + finishedWork = renderRoot(nextFlushedRoot, nextFlushedExpirationTime);
1470 + if (finishedWork !== null) {
1471 + // We've completed the root. Check the deadline one more time
1472 + // before committing.
1473 + if (!shouldYield()) {
1474 + // Still time left. Commit the root.
1475 + nextFlushedRoot.remainingExpirationTime = commitRoot(
1476 + finishedWork,
1477 + );
1478 + } else {
1479 + // There's no time left. Mark this root as complete. We'll come
1480 + // back and commit it later.
1481 + nextFlushedRoot.finishedWork = finishedWork;
1482 + }
1483 + }
1484 + }
1485 + }
1486 + // Find the next highest priority work.
1487 + findHighestPriorityRoot();
1488 + }
1489 +
1490 + // We're done flushing work. Either we ran out of time in this callback,
1491 + // or there's no more work left with sufficient priority.
1492 +
1493 + // If we're inside a callback, set this to false since we just completed it.
1494 + if (deadline !== null) {
1495 + isCallbackScheduled = false;
1496 + }
1497 + // If there's work left over, schedule a new callback.
1498 + if (nextFlushedRoot !== null && !isCallbackScheduled) {
1499 + isCallbackScheduled = true;
1500 + scheduleDeferredCallback(flushAsyncWork);
1501 + }
1502 +
1503 + // Clean-up.
1504 + deadline = null;
1505 + deadlineDidExpire = false;
1506 + isRendering = false;
1507 + nestedUpdateCount = 0;
1508 +
1509 + if (hasUnhandledError) {
1510 + const error = unhandledError;
1511 + unhandledError = null;
1512 + hasUnhandledError = false;
1513 + throw error;
1514 + }
1515 + }
1516 +
1517 + // When working on async work, the reconciler asks the renderer if it should
1518 + // yield execution. For DOM, we implement this with requestIdleCallback.
1519 + function shouldYield() {
1520 + if (deadline === null) {
1521 + return false;
1522 + }
1523 + if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
1524 + return false;
1525 + }
1526 + deadlineDidExpire = true;
1527 + return true;
1528 + }
1529 +
1530 + // TODO: Not happy about this hook. Conceptually, renderRoot should return a
1531 + // tuple of (isReadyForCommit, didError, error)
1532 + function onUncaughtError(error) {
1533 + invariant(
1534 + nextFlushedRoot !== null,
1535 + 'Should be working on a root. This error is likely caused by a bug in ' +
1536 + 'React. Please file an issue.',
1537 + );
1538 + // Unschedule this root so we don't work on it again until there's
1539 + // another update.
1540 + nextFlushedRoot.remainingExpirationTime = NoWork;
1541 + if (!hasUnhandledError) {
1542 + hasUnhandledError = true;
1543 + unhandledError = error;
1544 + }
1545 + }
1546 +
1547 + // TODO: Batching should be implemented at the renderer level, not inside
1548 + // the reconciler.
1549 function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
1550 const previousIsBatchingUpdates = isBatchingUpdates;
1551 isBatchingUpdates = true;
@@ -1548,41 +1553,37 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
1553 return fn(a);
1554 } finally {
1555 isBatchingUpdates = previousIsBatchingUpdates;
1551 - // If we're not already inside a batch, we need to flush any task work
1552 - // that was created by the user-provided function.
1553 - if (!isPerformingWork && !isBatchingUpdates) {
1556 + if (!isBatchingUpdates && !isRendering) {
1557 performWork(Task, null);
1558 }
1559 }
1560 }
1561
1562 + // TODO: Batching should be implemented at the renderer level, not inside
1563 + // the reconciler.
1564 function unbatchedUpdates<A>(fn: () => A): A {
1560 - const previousIsUnbatchingUpdates = isUnbatchingUpdates;
1561 - const previousIsBatchingUpdates = isBatchingUpdates;
1562 - // This is only true if we're nested inside batchedUpdates.
1563 - isUnbatchingUpdates = isBatchingUpdates;
1564 - isBatchingUpdates = false;
1565 - try {
1566 - return fn();
1567 - } finally {
1568 - isBatchingUpdates = previousIsBatchingUpdates;
1569 - isUnbatchingUpdates = previousIsUnbatchingUpdates;
1565 + if (isBatchingUpdates && !isUnbatchingUpdates) {
1566 + isUnbatchingUpdates = true;
1567 + try {
1568 + return fn();
1569 + } finally {
1570 + isUnbatchingUpdates = false;
1571 + }
1572 }
1573 + return fn();
1574 }
1575
1573 - function flushSync<A>(batch: () => A): A {
1576 + // TODO: Batching should be implemented at the renderer level, not within
1577 + // the reconciler.
1578 + function flushSync<A>(fn: () => A): A {
1579 const previousIsBatchingUpdates = isBatchingUpdates;
1575 - const previousExpirationContext = expirationContext;
1580 isBatchingUpdates = true;
1577 - expirationContext = Sync;
1581 try {
1579 - return batch();
1582 + return syncUpdates(fn);
1583 } finally {
1584 isBatchingUpdates = previousIsBatchingUpdates;
1582 - expirationContext = previousExpirationContext;
1583 -
1585 invariant(
1585 - !isPerformingWork,
1586 + !isRendering,
1587 'flushSync was called from inside a lifecycle method. It cannot be ' +
1588 'called when React is already rendering.',
1589 );
@@ -1590,23 +1591,13 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
1591 }
1592 }
1593
1593 - function deferredUpdates<A>(fn: () => A): A {
1594 - const previousExpirationContext = expirationContext;
1595 - expirationContext = computeAsyncExpiration();
1596 - try {
1597 - return fn();
1598 - } finally {
1599 - expirationContext = previousExpirationContext;
1600 - }
1601 - }
1602 -
1594 return {
1604 - computeAsyncExpiration: computeAsyncExpiration,
1605 - computeExpirationForFiber: computeExpirationForFiber,
1606 - scheduleWork: scheduleWork,
1607 - batchedUpdates: batchedUpdates,
1608 - unbatchedUpdates: unbatchedUpdates,
1609 - flushSync: flushSync,
1610 - deferredUpdates: deferredUpdates,
1595 + computeAsyncExpiration,
1596 + computeExpirationForFiber,
1597 + scheduleWork,
1598 + batchedUpdates,
1599 + unbatchedUpdates,
1600 + flushSync,
1601 + deferredUpdates,
1602 };
1603 };
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap
+135 -98
@@ -6,10 +6,11 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
6 ⚛ AllLifecycles [mount]
7 ⚛ AllLifecycles.componentWillMount
8 ⚛ AllLifecycles.getChildContext
9 - ⚛ (Committing Changes)
10 - ⚛ (Committing Host Effects: 1 Total)
11 - ⚛ (Calling Lifecycle Methods: 1 Total)
12 - ⚛ AllLifecycles.componentDidMount
9 +
10 +⚛ (Committing Changes)
11 + ⚛ (Committing Host Effects: 1 Total)
12 + ⚛ (Calling Lifecycle Methods: 1 Total)
13 + ⚛ AllLifecycles.componentDidMount
14
15 // Update
16 ⚛ (React Tree Reconciliation)
@@ -18,17 +19,19 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
19 ⚛ AllLifecycles.shouldComponentUpdate
20 ⚛ AllLifecycles.componentWillUpdate
21 ⚛ AllLifecycles.getChildContext
21 - ⚛ (Committing Changes)
22 - ⚛ (Committing Host Effects: 2 Total)
23 - ⚛ (Calling Lifecycle Methods: 2 Total)
24 - ⚛ AllLifecycles.componentDidUpdate
22 +
23 +⚛ (Committing Changes)
24 + ⚛ (Committing Host Effects: 2 Total)
25 + ⚛ (Calling Lifecycle Methods: 2 Total)
26 + ⚛ AllLifecycles.componentDidUpdate
27
28 // Unmount
29 ⚛ (React Tree Reconciliation)
28 - ⚛ (Committing Changes)
29 - ⚛ (Committing Host Effects: 1 Total)
30 - ⚛ AllLifecycles.componentWillUnmount
31 - ⚛ (Calling Lifecycle Methods: 0 Total)
30 +
31 +⚛ (Committing Changes)
32 + ⚛ (Committing Host Effects: 1 Total)
33 + ⚛ AllLifecycles.componentWillUnmount
34 + ⚛ (Calling Lifecycle Methods: 0 Total)
35 "
36 `;
37
@@ -40,44 +43,53 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc
43 ⚛ B [update]
44 ⚛ A [update]
45 ⚛ B [update]
43 - ⚛ (Committing Changes)
44 - ⚛ (Committing Host Effects: 9 Total)
45 - ⚛ (Calling Lifecycle Methods: 9 Total)
46 - ⚛ A.componentDidUpdate
47 - ⚛ B.componentDidUpdate
46 +
47 +⚛ (Committing Changes)
48 + ⚛ (Committing Host Effects: 9 Total)
49 + ⚛ (Calling Lifecycle Methods: 9 Total)
50 + ⚛ A.componentDidUpdate
51 + ⚛ B.componentDidUpdate
52
53 // Because of deduplication, we don't know B was cascading,
54 // but we should still see the warning for the commit phase.
51 -⛔ (React Tree Reconciliation) Warning: There were cascading updates
55 +⚛ (React Tree Reconciliation)
56 ⚛ Parent [update]
57 ⚛ A [update]
58 ⚛ B [update]
59 ⚛ A [update]
60 ⚛ B [update]
57 - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
58 - ⚛ (Committing Host Effects: 9 Total)
59 - ⚛ (Calling Lifecycle Methods: 9 Total)
60 - ⚛ A.componentDidUpdate
61 - ⚛ B.componentDidUpdate
61 +
62 +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
63 + ⚛ (Committing Host Effects: 9 Total)
64 + ⚛ (Calling Lifecycle Methods: 9 Total)
65 + ⚛ A.componentDidUpdate
66 + ⚛ B.componentDidUpdate
67 +
68 +⚛ (React Tree Reconciliation)
69 ⚛ B [update]
63 - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit
64 - ⚛ (Committing Host Effects: 3 Total)
65 - ⚛ (Calling Lifecycle Methods: 3 Total)
66 - ⚛ B.componentDidUpdate
70 +
71 +⚛ (Committing Changes)
72 + ⚛ (Committing Host Effects: 3 Total)
73 + ⚛ (Calling Lifecycle Methods: 3 Total)
74 + ⚛ B.componentDidUpdate
75 "
76 `;
77
78 exports[`ReactDebugFiberPerf does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = `
71 -"⛔ (React Tree Reconciliation) Warning: There were cascading updates
79 +"⚛ (React Tree Reconciliation)
80 ⚛ Component [mount]
73 - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
74 - ⚛ (Committing Host Effects: 1 Total)
75 - ⚛ (Calling Lifecycle Methods: 1 Total)
76 - ⛔ Component.componentDidMount Warning: Scheduled a cascading update
81 +
82 +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
83 + ⚛ (Committing Host Effects: 1 Total)
84 + ⚛ (Calling Lifecycle Methods: 1 Total)
85 + ⛔ Component.componentDidMount Warning: Scheduled a cascading update
86 +
87 +⚛ (React Tree Reconciliation)
88 ⚛ Component [update]
78 - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit
79 - ⚛ (Committing Host Effects: 1 Total)
80 - ⚛ (Calling Lifecycle Methods: 1 Total)
89 +
90 +⚛ (Committing Changes)
91 + ⚛ (Committing Host Effects: 1 Total)
92 + ⚛ (Calling Lifecycle Methods: 1 Total)
93 "
94 `;
95
@@ -87,18 +99,20 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi
99 ⚛ Parent [mount]
100 ⚛ NotCascading [mount]
101 ⚛ NotCascading.componentWillMount
90 - ⚛ (Committing Changes)
91 - ⚛ (Committing Host Effects: 1 Total)
92 - ⚛ (Calling Lifecycle Methods: 0 Total)
102 +
103 +⚛ (Committing Changes)
104 + ⚛ (Committing Host Effects: 1 Total)
105 + ⚛ (Calling Lifecycle Methods: 0 Total)
106
107 // Should not print a warning
108 ⚛ (React Tree Reconciliation)
109 ⚛ Parent [update]
110 ⚛ NotCascading [update]
111 ⚛ NotCascading.componentWillReceiveProps
99 - ⚛ (Committing Changes)
100 - ⚛ (Committing Host Effects: 2 Total)
101 - ⚛ (Calling Lifecycle Methods: 2 Total)
112 +
113 +⚛ (Committing Changes)
114 + ⚛ (Committing Host Effects: 2 Total)
115 + ⚛ (Calling Lifecycle Methods: 2 Total)
116 "
117 `;
118
@@ -107,23 +121,26 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
121 ⚛ (React Tree Reconciliation)
122 ⚛ Parent [mount]
123 ⚛ Child [mount]
110 - ⚛ (Committing Changes)
111 - ⚛ (Committing Host Effects: 1 Total)
112 - ⚛ (Calling Lifecycle Methods: 0 Total)
124 +
125 +⚛ (Committing Changes)
126 + ⚛ (Committing Host Effects: 1 Total)
127 + ⚛ (Calling Lifecycle Methods: 0 Total)
128
129 // Update
130 ⚛ (React Tree Reconciliation)
131 ⚛ Parent [update]
132 ⚛ Child [update]
118 - ⚛ (Committing Changes)
119 - ⚛ (Committing Host Effects: 2 Total)
120 - ⚛ (Calling Lifecycle Methods: 2 Total)
133 +
134 +⚛ (Committing Changes)
135 + ⚛ (Committing Host Effects: 2 Total)
136 + ⚛ (Calling Lifecycle Methods: 2 Total)
137
138 // Unmount
139 ⚛ (React Tree Reconciliation)
124 - ⚛ (Committing Changes)
125 - ⚛ (Committing Host Effects: 1 Total)
126 - ⚛ (Calling Lifecycle Methods: 0 Total)
140 +
141 +⚛ (Committing Changes)
142 + ⚛ (Committing Host Effects: 1 Total)
143 + ⚛ (Calling Lifecycle Methods: 0 Total)
144 "
145 `;
146
@@ -144,9 +161,10 @@ exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = `
161 ⚛ Parent [mount]
162 ⚛ B [mount]
163 ⚛ Child [mount]
147 - ⚛ (Committing Changes)
148 - ⚛ (Committing Host Effects: 1 Total)
149 - ⚛ (Calling Lifecycle Methods: 0 Total)
164 +
165 +⚛ (Committing Changes)
166 + ⚛ (Committing Host Effects: 1 Total)
167 + ⚛ (Calling Lifecycle Methods: 0 Total)
168 "
169 `;
170
@@ -154,35 +172,41 @@ exports[`ReactDebugFiberPerf measures deprioritized work 1`] = `
172 "// Flush the parent
173 ⚛ (React Tree Reconciliation)
174 ⚛ Parent [mount]
157 - ⚛ (Committing Changes)
158 - ⚛ (Committing Host Effects: 1 Total)
159 - ⚛ (Calling Lifecycle Methods: 0 Total)
175 +
176 +⚛ (Committing Changes)
177 + ⚛ (Committing Host Effects: 1 Total)
178 + ⚛ (Calling Lifecycle Methods: 0 Total)
179
180 // Flush the child
181 ⚛ (React Tree Reconciliation)
182 ⚛ Child [mount]
164 - ⚛ (Committing Changes)
165 - ⚛ (Committing Host Effects: 3 Total)
166 - ⚛ (Calling Lifecycle Methods: 2 Total)
183 +
184 +⚛ (Committing Changes)
185 + ⚛ (Committing Host Effects: 3 Total)
186 + ⚛ (Calling Lifecycle Methods: 2 Total)
187 "
188 `;
189
190 exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
191 "// Stop on Baddie and restart from Boundary
172 -⛔ (React Tree Reconciliation) Warning: There were cascading updates
192 +⚛ (React Tree Reconciliation)
193 ⚛ Parent [mount]
194 ⛔ Boundary [mount] Warning: An error was thrown inside this error boundary
195 ⚛ Parent [mount]
196 ⚛ Baddie [mount]
197 ⚛ Boundary [mount]
178 - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
179 - ⚛ (Committing Host Effects: 2 Total)
180 - ⚛ (Calling Lifecycle Methods: 1 Total)
198 +
199 +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
200 + ⚛ (Committing Host Effects: 2 Total)
201 + ⚛ (Calling Lifecycle Methods: 1 Total)
202 +
203 +⚛ (React Tree Reconciliation)
204 ⚛ Boundary [update]
205 ⚛ ErrorReport [mount]
183 - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit
184 - ⚛ (Committing Host Effects: 2 Total)
185 - ⚛ (Calling Lifecycle Methods: 1 Total)
206 +
207 +⚛ (Committing Changes)
208 + ⚛ (Committing Host Effects: 2 Total)
209 + ⚛ (Calling Lifecycle Methods: 1 Total)
210 "
211 `;
212
@@ -191,17 +215,19 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
215 ⚛ (React Tree Reconciliation)
216 ⚛ Parent [mount]
217 ⚛ Baddie [mount]
194 - ⚛ (Committing Changes)
195 - ⚛ (Committing Host Effects: 1 Total)
196 - ⚛ (Calling Lifecycle Methods: 1 Total)
218 +
219 +⚛ (Committing Changes)
220 + ⚛ (Committing Host Effects: 1 Total)
221 + ⚛ (Calling Lifecycle Methods: 1 Total)
222
223 // Will reconcile from a clean state
224 ⚛ (React Tree Reconciliation)
225 ⚛ Parent [mount]
226 ⚛ Child [mount]
202 - ⚛ (Committing Changes)
203 - ⚛ (Committing Host Effects: 1 Total)
204 - ⚛ (Calling Lifecycle Methods: 0 Total)
227 +
228 +⚛ (Committing Changes)
229 + ⚛ (Committing Host Effects: 1 Total)
230 + ⚛ (Calling Lifecycle Methods: 0 Total)
231 "
232 `;
233
@@ -210,9 +236,10 @@ exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
236 ⚛ (React Tree Reconciliation)
237 ⚛ A [update]
238 ⚛ B [update]
213 - ⚛ (Committing Changes)
214 - ⚛ (Committing Host Effects: 6 Total)
215 - ⚛ (Calling Lifecycle Methods: 6 Total)
239 +
240 +⚛ (Committing Changes)
241 + ⚛ (Committing Host Effects: 6 Total)
242 + ⚛ (Calling Lifecycle Methods: 6 Total)
243 "
244 `;
245
@@ -226,9 +253,10 @@ exports[`ReactDebugFiberPerf supports coroutines 1`] = `
253 ⚛ CoChild [mount]
254 ⚛ Continuation [mount]
255 ⚛ Continuation [mount]
229 - ⚛ (Committing Changes)
230 - ⚛ (Committing Host Effects: 3 Total)
231 - ⚛ (Calling Lifecycle Methods: 0 Total)
256 +
257 +⚛ (Committing Changes)
258 + ⚛ (Committing Host Effects: 3 Total)
259 + ⚛ (Calling Lifecycle Methods: 0 Total)
260 "
261 `;
262
@@ -236,40 +264,49 @@ exports[`ReactDebugFiberPerf supports portals 1`] = `
264 "⚛ (React Tree Reconciliation)
265 ⚛ Parent [mount]
266 ⚛ Child [mount]
239 - ⚛ (Committing Changes)
240 - ⚛ (Committing Host Effects: 2 Total)
241 - ⚛ (Calling Lifecycle Methods: 0 Total)
267 +
268 +⚛ (Committing Changes)
269 + ⚛ (Committing Host Effects: 2 Total)
270 + ⚛ (Calling Lifecycle Methods: 0 Total)
271 "
272 `;
273
274 exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = `
275 "// Should print a warning
247 -⛔ (React Tree Reconciliation) Warning: There were cascading updates
276 +⚛ (React Tree Reconciliation)
277 ⚛ Parent [mount]
278 ⚛ Cascading [mount]
250 - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
251 - ⚛ (Committing Host Effects: 2 Total)
252 - ⚛ (Calling Lifecycle Methods: 1 Total)
253 - ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update
279 +
280 +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
281 + ⚛ (Committing Host Effects: 2 Total)
282 + ⚛ (Calling Lifecycle Methods: 1 Total)
283 + ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update
284 +
285 +⚛ (React Tree Reconciliation)
286 ⚛ Cascading [update]
255 - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit
256 - ⚛ (Committing Host Effects: 2 Total)
257 - ⚛ (Calling Lifecycle Methods: 2 Total)
287 +
288 +⚛ (Committing Changes)
289 + ⚛ (Committing Host Effects: 2 Total)
290 + ⚛ (Calling Lifecycle Methods: 2 Total)
291 "
292 `;
293
294 exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = `
295 "// Rendering the first root
263 -⛔ (React Tree Reconciliation) Warning: There were cascading updates
296 +⚛ (React Tree Reconciliation)
297 ⚛ Cascading [mount]
265 - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
266 - ⚛ (Committing Host Effects: 1 Total)
267 - ⚛ (Calling Lifecycle Methods: 1 Total)
268 - ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update
269 - // Scheduling another root from componentDidMount
298 +
299 +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
300 + ⚛ (Committing Host Effects: 1 Total)
301 + ⚛ (Calling Lifecycle Methods: 1 Total)
302 + ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update
303 + // Scheduling another root from componentDidMount
304 +
305 +⚛ (React Tree Reconciliation)
306 ⚛ Child [mount]
271 - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit
272 - ⚛ (Committing Host Effects: 1 Total)
273 - ⚛ (Calling Lifecycle Methods: 0 Total)
307 +
308 +⚛ (Committing Changes)
309 + ⚛ (Committing Host Effects: 1 Total)
310 + ⚛ (Calling Lifecycle Methods: 0 Total)
311 "
312 `;
scripts/rollup/results.json
+74 -74
@@ -17,36 +17,36 @@
17 "gzip": 2364
18 },
19 "React-dev.js (FB_DEV)": {
20 - "size": 43016,
21 - "gzip": 11325
20 + "size": 43033,
21 + "gzip": 11331
22 },
23 "React-prod.js (FB_PROD)": {
24 - "size": 24831,
25 - "gzip": 6707
24 + "size": 24848,
25 + "gzip": 6713
26 },
27 "react-dom.development.js (UMD_DEV)": {
28 - "size": 631104,
29 - "gzip": 144685
28 + "size": 629078,
29 + "gzip": 144076
30 },
31 "react-dom.production.min.js (UMD_PROD)": {
32 - "size": 100038,
33 - "gzip": 31532
32 + "size": 101020,
33 + "gzip": 31886
34 },
35 "react-dom.development.js (NODE_DEV)": {
36 - "size": 593383,
37 - "gzip": 135883
36 + "size": 591361,
37 + "gzip": 135305
38 },
39 "react-dom.production.min.js (NODE_PROD)": {
40 - "size": 106647,
41 - "gzip": 33435
40 + "size": 107254,
41 + "gzip": 33591
42 },
43 "ReactDOMFiber-dev.js (FB_DEV)": {
44 - "size": 590572,
45 - "gzip": 135237
44 + "size": 588633,
45 + "gzip": 134646
46 },
47 "ReactDOMFiber-prod.js (FB_PROD)": {
48 - "size": 419911,
49 - "gzip": 93354
48 + "size": 418791,
49 + "gzip": 93017
50 },
51 "react-dom-test-utils.development.js (NODE_DEV)": {
52 "size": 41743,
@@ -73,96 +73,96 @@
73 "gzip": 4510
74 },
75 "ReactDOMUnstableNativeDependencies-dev.js (FB_DEV)": {
76 - "size": 80190,
77 - "gzip": 19882
76 + "size": 80207,
77 + "gzip": 19890
78 },
79 "ReactDOMUnstableNativeDependencies-prod.js (FB_PROD)": {
80 - "size": 65164,
81 - "gzip": 15538
80 + "size": 65181,
81 + "gzip": 15545
82 },
83 "react-dom-server.browser.development.js (UMD_DEV)": {
84 - "size": 124910,
85 - "gzip": 32260
84 + "size": 124902,
85 + "gzip": 32255
86 },
87 "react-dom-server.browser.production.min.js (UMD_PROD)": {
88 - "size": 15345,
89 - "gzip": 5968
88 + "size": 15337,
89 + "gzip": 5967
90 },
91 "react-dom-server.browser.development.js (NODE_DEV)": {
92 - "size": 94830,
93 - "gzip": 25139
92 + "size": 94822,
93 + "gzip": 25134
94 },
95 "react-dom-server.browser.production.min.js (NODE_PROD)": {
96 - "size": 15071,
97 - "gzip": 5902
96 + "size": 15063,
97 + "gzip": 5899
98 },
99 "ReactDOMServer-dev.js (FB_DEV)": {
100 - "size": 94490,
101 - "gzip": 25063
100 + "size": 94499,
101 + "gzip": 25066
102 },
103 "ReactDOMServer-prod.js (FB_PROD)": {
104 - "size": 42454,
105 - "gzip": 11844
104 + "size": 42463,
105 + "gzip": 11846
106 },
107 "react-dom-server.node.development.js (NODE_DEV)": {
108 - "size": 97092,
109 - "gzip": 25688
108 + "size": 97084,
109 + "gzip": 25683
110 },
111 "react-dom-server.node.production.min.js (NODE_PROD)": {
112 - "size": 15996,
113 - "gzip": 6238
112 + "size": 15988,
113 + "gzip": 6235
114 },
115 "react-art.development.js (UMD_DEV)": {
116 - "size": 378408,
117 - "gzip": 83017
116 + "size": 376390,
117 + "gzip": 82374
118 },
119 "react-art.production.min.js (UMD_PROD)": {
120 - "size": 82432,
121 - "gzip": 25592
120 + "size": 83413,
121 + "gzip": 25876
122 },
123 "react-art.development.js (NODE_DEV)": {
124 - "size": 302763,
125 - "gzip": 63846
124 + "size": 300749,
125 + "gzip": 63250
126 },
127 "react-art.production.min.js (NODE_PROD)": {
128 - "size": 53897,
129 - "gzip": 16869
128 + "size": 54504,
129 + "gzip": 17048
130 },
131 "ReactARTFiber-dev.js (FB_DEV)": {
132 - "size": 301605,
133 - "gzip": 63774
132 + "size": 299674,
133 + "gzip": 63165
134 },
135 "ReactARTFiber-prod.js (FB_PROD)": {
136 - "size": 225472,
137 - "gzip": 46455
136 + "size": 224360,
137 + "gzip": 46067
138 },
139 "ReactNativeFiber-dev.js (RN_DEV)": {
140 - "size": 285948,
141 - "gzip": 49225
140 + "size": 286193,
141 + "gzip": 49282
142 },
143 "ReactNativeFiber-prod.js (RN_PROD)": {
144 - "size": 223676,
145 - "gzip": 38463
144 + "size": 223893,
145 + "gzip": 38520
146 },
147 "react-test-renderer.development.js (NODE_DEV)": {
148 - "size": 306451,
149 - "gzip": 64239
148 + "size": 304437,
149 + "gzip": 63667
150 },
151 "ReactTestRendererFiber-dev.js (FB_DEV)": {
152 - "size": 305254,
153 - "gzip": 64170
152 + "size": 303323,
153 + "gzip": 63582
154 },
155 "react-test-renderer-shallow.development.js (NODE_DEV)": {
156 "size": 9364,
157 "gzip": 2335
158 },
159 "ReactShallowRenderer-dev.js (FB_DEV)": {
160 - "size": 9020,
161 - "gzip": 2254
160 + "size": 9037,
161 + "gzip": 2262
162 },
163 "react-noop-renderer.development.js (NODE_DEV)": {
164 - "size": 295660,
165 - "gzip": 61436
164 + "size": 293646,
165 + "gzip": 60851
166 },
167 "react-dom-server.development.js (UMD_DEV)": {
168 "size": 120897,
@@ -189,16 +189,16 @@
189 "gzip": 7520
190 },
191 "ReactNativeRTFiber-dev.js (RN_DEV)": {
192 - "size": 217811,
193 - "gzip": 36735
192 + "size": 218056,
193 + "gzip": 36811
194 },
195 "ReactNativeRTFiber-prod.js (RN_PROD)": {
196 - "size": 165325,
197 - "gzip": 27464
196 + "size": 165542,
197 + "gzip": 27540
198 },
199 "react-test-renderer.production.min.js (NODE_PROD)": {
200 - "size": 55449,
201 - "gzip": 17169
200 + "size": 56061,
201 + "gzip": 17256
202 },
203 "react-test-renderer-shallow.production.min.js (NODE_PROD)": {
204 "size": 4630,
@@ -209,20 +209,20 @@
209 "gzip": 4241
210 },
211 "react-reconciler.development.js (NODE_DEV)": {
212 - "size": 281332,
213 - "gzip": 58301
212 + "size": 279318,
213 + "gzip": 57691
214 },
215 "react-reconciler.production.min.js (NODE_PROD)": {
216 - "size": 37658,
217 - "gzip": 11762
216 + "size": 38320,
217 + "gzip": 11959
218 },
219 "ReactNativeCSFiber-dev.js (RN_DEV)": {
220 - "size": 210232,
221 - "gzip": 34986
220 + "size": 210477,
221 + "gzip": 35055
222 },
223 "ReactNativeCSFiber-prod.js (RN_PROD)": {
224 - "size": 160320,
225 - "gzip": 26275
224 + "size": 160537,
225 + "gzip": 26346
226 }
227 }
228 }
\ No newline at end of file