From 2e4663f61698e88e36d6fdfdf15e941ee29aa791 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 20 Oct 2017 13:22:29 -0700 Subject: [PATCH] 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 --- .../src/ReactFiberExpirationTime.js | 22 - .../react-reconciler/src/ReactFiberRoot.js | 23 +- .../src/ReactFiberScheduler.js | 897 +++++++++--------- .../ReactIncrementalPerf-test.js.snap | 233 +++-- scripts/rollup/results.json | 148 +-- 5 files changed, 670 insertions(+), 653 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberExpirationTime.js b/packages/react-reconciler/src/ReactFiberExpirationTime.js index 8eeb26e781..d79c795b1a 100644 --- a/packages/react-reconciler/src/ReactFiberExpirationTime.js +++ b/packages/react-reconciler/src/ReactFiberExpirationTime.js @@ -48,25 +48,3 @@ function computeExpirationBucket( ); } exports.computeExpirationBucket = computeExpirationBucket; - -// Given the current clock time and an expiration time, returns the -// relative expiration time. Possible values include NoWork, Sync, Task, and -// Never. All other values represent an async expiration time. -function relativeExpirationTime( - currentTime: ExpirationTime, - expirationTime: ExpirationTime, -): ExpirationTime { - switch (expirationTime) { - case NoWork: - case Sync: - case Task: - case Never: - return expirationTime; - } - const delta = expirationTime - currentTime; - if (delta <= 0) { - return Task; - } - return msToExpirationTime(delta); -} -exports.relativeExpirationTime = relativeExpirationTime; diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 1288fee9bb..6c6b894a4c 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -11,8 +11,10 @@ 'use strict'; import type {Fiber} from 'ReactFiber'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {createHostRootFiber} = require('ReactFiber'); +const {NoWork} = require('ReactFiberExpirationTime'); export type FiberRoot = { // Any additional information from the host associated with this root. @@ -21,15 +23,22 @@ export type FiberRoot = { pendingChildren: any, // The currently active root fiber. This is the mutable root of the tree. current: Fiber, - // Determines if this root has already been added to the schedule for work. - isScheduled: boolean, - // The work schedule is a linked list. - nextScheduledRoot: FiberRoot | null, + // Remaining expiration time on this root. + remainingExpirationTime: ExpirationTime, + // Determines if this root can be committed. + isReadyForCommit: boolean, + // A finished work-in-progress HostRoot that's ready to be committed. + // TODO: The reason this is separate from isReadyForCommit is because the + // FiberRoot concept will likely be lifted out of the reconciler and into + // the renderer. + finishedWork: Fiber | null, // Top context object, used by renderSubtreeIntoContainer context: Object | null, pendingContext: Object | null, // Determines if we should attempt to hydrate on the initial mount +hydrate: boolean, + // Linked-list of roots + nextScheduledRoot: FiberRoot | null, }; exports.createFiberRoot = function( @@ -43,11 +52,13 @@ exports.createFiberRoot = function( current: uninitializedFiber, containerInfo: containerInfo, pendingChildren: null, - isScheduled: false, - nextScheduledRoot: null, + remainingExpirationTime: NoWork, + isReadyForCommit: false, + finishedWork: null, context: null, pendingContext: null, hydrate, + nextScheduledRoot: null, }; uninitializedFiber.stateNode = root; return root; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index c163c1028b..da726ef51e 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -60,7 +60,6 @@ var { Never, msToExpirationTime, computeExpirationBucket, - relativeExpirationTime, } = require('ReactFiberExpirationTime'); var {AsyncUpdates} = require('ReactTypeOfInternalContext'); @@ -151,8 +150,6 @@ if (__DEV__) { }; } -var timeHeuristicForUnitOfWork = 1; - module.exports = function( config: HostConfig, ) { @@ -199,36 +196,17 @@ module.exports = function( // updates in sync mode.) let expirationContext: ExpirationTime = NoWork; - // Keeps track of whether we're currently in a work loop. - let isPerformingWork: boolean = false; - - // Keeps track of whether the current deadline has expired. - let deadlineHasExpired: boolean = false; - - // Keeps track of whether we should should batch sync updates. - let isBatchingUpdates: boolean = false; - - // This is needed for the weird case where the initial mount is synchronous - // even inside batchedUpdates :( - let isUnbatchingUpdates: boolean = false; + let isWorking: boolean = false; // The next work in progress fiber that we're currently working on. let nextUnitOfWork: Fiber | null = null; + let nextRoot: FiberRoot | null = null; // The time at which we're currently rendering work. let nextRenderExpirationTime: ExpirationTime = NoWork; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; - let pendingCommit: Fiber | null = null; - - // Linked list of roots with scheduled work on them. - let nextScheduledRoot: FiberRoot | null = null; - let lastScheduledRoot: FiberRoot | null = null; - - // Keep track of which host environment callbacks are scheduled. - let isCallbackScheduled: boolean = false; - // Keep track of which fibers have captured an error that need to be handled. // Work is removed from this collection after componentDidCatch is called. let capturedErrors: Map | null = null; @@ -245,11 +223,6 @@ module.exports = function( let isCommitting: boolean = false; let isUnmounting: boolean = false; - // Use these to prevent an infinite loop of nested updates - const NESTED_UPDATE_LIMIT = 1000; - let nestedUpdateCount: number = 0; - let nextRenderedTree: FiberRoot | null = null; - function resetContextStack() { // Reset the stack reset(); @@ -258,72 +231,6 @@ module.exports = function( resetHostContainer(); } - function resetNextUnitOfWork() { - // Clear out roots with no more work on them, or if they have uncaught errors - while ( - nextScheduledRoot !== null && - nextScheduledRoot.current.expirationTime === NoWork - ) { - // Unschedule this root. - nextScheduledRoot.isScheduled = false; - // Read the next pointer now. - // We need to clear it in case this root gets scheduled again later. - const next = nextScheduledRoot.nextScheduledRoot; - nextScheduledRoot.nextScheduledRoot = null; - // Exit if we cleared all the roots and there's no work to do. - if (nextScheduledRoot === lastScheduledRoot) { - nextScheduledRoot = null; - lastScheduledRoot = null; - nextRenderExpirationTime = NoWork; - return null; - } - // Continue with the next root. - // If there's no work on it, it will get unscheduled too. - nextScheduledRoot = next; - } - - let root = nextScheduledRoot; - let earliestExpirationRoot = null; - let earliestExpirationTime = NoWork; - while (root !== null) { - if ( - root.current.expirationTime !== NoWork && - (earliestExpirationTime === NoWork || - earliestExpirationTime > root.current.expirationTime) - ) { - earliestExpirationTime = root.current.expirationTime; - earliestExpirationRoot = root; - } - // We didn't find anything to do in this root, so let's try the next one. - root = root.nextScheduledRoot; - } - if (earliestExpirationRoot !== null) { - nextRenderExpirationTime = earliestExpirationTime; - // Before we start any new work, let's make sure that we have a fresh - // stack to work from. - // TODO: This call is buried a bit too deep. It would be nice to have - // a single point which happens right before any new work and - // unfortunately this is it. - resetContextStack(); - - nextUnitOfWork = createWorkInProgress( - earliestExpirationRoot.current, - earliestExpirationTime, - ); - if (earliestExpirationRoot !== nextRenderedTree) { - // We've switched trees. Reset the nested update counter. - nestedUpdateCount = 0; - nextRenderedTree = earliestExpirationRoot; - } - return; - } - - nextRenderExpirationTime = NoWork; - nextUnitOfWork = null; - nextRenderedTree = null; - return; - } - function commitAllHostEffects() { while (nextEffect !== null) { if (__DEV__) { @@ -430,30 +337,25 @@ module.exports = function( } } - function commitAllWork(finishedWork: Fiber) { + function commitRoot(finishedWork: Fiber): ExpirationTime { // We keep track of this so that captureError can collect any boundaries // that capture an error during the commit phase. The reason these aren't // local to this function is because errors that occur during cWU are // captured elsewhere, to prevent the unmount from being interrupted. + isWorking = true; isCommitting = true; if (__DEV__) { startCommitTimer(); } - pendingCommit = null; - const root: FiberRoot = (finishedWork.stateNode: any); + const root: FiberRoot = finishedWork.stateNode; invariant( root.current !== finishedWork, 'Cannot commit the same tree as before. This is probably a bug ' + 'related to the return field. This error is likely caused by a bug ' + 'in React. Please file an issue.', ); - - if (nextRenderExpirationTime <= mostRecentCurrentTime) { - // Keep track of the number of iterations to prevent an infinite - // update loop. - nestedUpdateCount++; - } + root.isReadyForCommit = false; // Reset this to null before calling lifecycles ReactCurrentOwner.current = null; @@ -565,6 +467,7 @@ module.exports = function( } isCommitting = false; + isWorking = false; if (__DEV__) { stopCommitLifeCyclesTimer(); stopCommitTimer(); @@ -583,9 +486,20 @@ module.exports = function( commitPhaseBoundaries = null; } - // This tree is done. Reset the unit of work pointer to the root that - // expires soonest. If there's no work left, the pointer is set to null. - resetNextUnitOfWork(); + if (firstUncaughtError !== null) { + const error = firstUncaughtError; + firstUncaughtError = null; + onUncaughtError(error); + } + + const remainingTime = root.current.expirationTime; + + if (remainingTime === NoWork) { + capturedErrors = null; + failedBoundaries = null; + } + + return remainingTime; } function resetExpirationTime( @@ -702,10 +616,9 @@ module.exports = function( workInProgress = returnFiber; continue; } else { - // We've reached the root. Mark the root as pending commit. Depending - // on how much time we have left, we'll either commit it now or in - // the next frame. - pendingCommit = workInProgress; + // We've reached the root. + const root: FiberRoot = workInProgress.stateNode; + root.isReadyForCommit = true; return null; } } @@ -780,26 +693,45 @@ module.exports = function( return next; } - function performDeferredWork(deadline: Deadline) { - performWork(Never, deadline); + function workLoop(expirationTime: ExpirationTime) { + if (capturedErrors !== null) { + // If there are unhandled errors, switch to the slow work loop. + // TODO: How to avoid this check in the fast path? Maybe the renderer + // could keep track of which roots have unhandled errors and call a + // forked version of renderRoot. + slowWorkLoopThatChecksForFailedWork(expirationTime); + return; + } + if ( + nextRenderExpirationTime === NoWork || + nextRenderExpirationTime > expirationTime + ) { + return; + } + + if (nextRenderExpirationTime <= mostRecentCurrentTime) { + // Flush all expired work. + while (nextUnitOfWork !== null) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + } + } else { + // Flush asynchronous work until the deadline runs out of time. + while (nextUnitOfWork !== null && !shouldYield()) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + } + } } - function handleCommitPhaseErrors() { - // This is a special work loop for handling commit phase errors. It's - // similar to the syncrhonous work loop, but does an additional check on - // each fiber to see if it's an error boundary with an unhandled error. If - // so, it uses a forked version of performUnitOfWork that unmounts the - // failed subtree. - // - // The loop stops once the children have unmounted and error lifecycles are - // called. Then we return to the regular flow. - + function slowWorkLoopThatChecksForFailedWork(expirationTime: ExpirationTime) { if ( - capturedErrors !== null && - capturedErrors.size > 0 && - nextRenderExpirationTime !== NoWork && - nextRenderExpirationTime <= mostRecentCurrentTime + nextRenderExpirationTime === NoWork || + nextRenderExpirationTime > expirationTime ) { + return; + } + + if (nextRenderExpirationTime <= mostRecentCurrentTime) { + // Flush all expired work. while (nextUnitOfWork !== null) { if (hasCapturedError(nextUnitOfWork)) { // Use a forked version of performUnitOfWork @@ -807,158 +739,25 @@ module.exports = function( } else { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } - if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. Commit it now. - commitAllWork(pendingCommit); - if ( - capturedErrors === null || - capturedErrors.size === 0 || - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > mostRecentCurrentTime - ) { - // There are no more unhandled errors. We can exit this special - // work loop. If there's still additional work, we'll perform it - // using one of the normal work loops. - break; - } - // The commit phase produced additional errors. Continue working. + } + } else { + // Flush asynchronous work until the deadline runs out of time. + while (nextUnitOfWork !== null && !shouldYield()) { + if (hasCapturedError(nextUnitOfWork)) { + // Use a forked version of performUnitOfWork + nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork); + } else { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } } } } - function workLoop( - minExpirationTime: ExpirationTime, - deadline: Deadline | null, - ) { - loop: do { - if (pendingCommit !== null) { - commitAllWork(pendingCommit); - handleCommitPhaseErrors(); - } else if (nextUnitOfWork === null) { - resetNextUnitOfWork(); - } - - if ( - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > minExpirationTime - ) { - return; - } - - if (nextRenderExpirationTime <= mostRecentCurrentTime) { - // Flush all expired work. - while (nextUnitOfWork !== null) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); - if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. Commit it now. - commitAllWork(pendingCommit); - // Clear any errors that were scheduled during the commit phase. - handleCommitPhaseErrors(); - // The render time may have changed. Check again. - if ( - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > minExpirationTime || - nextRenderExpirationTime > mostRecentCurrentTime - ) { - // We've completed all the expired work. - break; - } - } - } - } else if (deadline !== null) { - // Flush asynchronous work until the deadline runs out of time. - while (nextUnitOfWork !== null && !deadlineHasExpired) { - if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); - // In a deferred work batch, iff nextUnitOfWork returns null, we just - // completed a root and a pendingCommit exists. Logically, we could - // omit either of the checks in the following condition, but we need - // both to satisfy Flow. - if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. If we have time, commit it now. - // Otherwise, we'll commit it in the next frame. - if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - commitAllWork(pendingCommit); - // Clear any errors that were scheduled during the commit phase. - handleCommitPhaseErrors(); - // The render time may have changed. Check again. - if ( - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > minExpirationTime || - nextRenderExpirationTime <= mostRecentCurrentTime - ) { - // We've completed all the async work. - break; - } - } else { - deadlineHasExpired = true; - } - } - } else { - deadlineHasExpired = true; - } - } - } - - // There might be work left. Depending on the priority, we should - // either perform it now or schedule a callback to perform it later. - const currentTime = recalculateCurrentTime(); - switch (relativeExpirationTime(currentTime, nextRenderExpirationTime)) { - case NoWork: - // No work left. We can exit. - break loop; - case Sync: - case Task: - // We have remaining synchronous or task work. Keep performing it, - // regardless of whether we're inside a callback. - if (nextRenderExpirationTime <= minExpirationTime) { - // Sometimes minExpirationTime is Sync, which means we should skip - // task work. - continue loop; - } - break loop; - default: - // We have remaining async work. - if (deadline === null) { - // We're not inside a callback. Exit and perform the work during - // the next callback. - break loop; - } - // We are inside a callback. - if ( - !deadlineHasExpired && - nextRenderExpirationTime <= minExpirationTime - ) { - // We still have time. Keep working. - continue loop; - } - // We've run out of time. Exit. - break loop; - } - } while (true); - } - - function performWorkCatchBlock( + function renderRootCatchBlock( + root: FiberRoot, failedWork: Fiber, boundary: Fiber, - minExpirationTime: ExpirationTime, - deadline: Deadline | null, + expirationTime: ExpirationTime, ) { // We're going to restart the error boundary that captured the error. // Conceptually, we're unwinding the stack. We need to unwind the @@ -973,37 +772,53 @@ module.exports = function( nextUnitOfWork = performFailedUnitOfWork(boundary); // Continue working. - workLoop(minExpirationTime, deadline); + workLoop(expirationTime); } - function performWork( - minExpirationTime: ExpirationTime, - deadline: Deadline | null, - ) { + function renderRoot( + root: FiberRoot, + expirationTime: ExpirationTime, + ): Fiber | null { if (__DEV__) { startWorkLoopTimer(); } invariant( - !isPerformingWork, - 'performWork was called recursively. This error is likely caused ' + + !isWorking, + 'renderRoot was called recursively. This error is likely caused ' + 'by a bug in React. Please file an issue.', ); - isPerformingWork = true; + isWorking = true; - nestedUpdateCount = 0; + // We're about to mutate the work-in-progress tree. If the root was pending + // commit, it no longer is: we'll need to complete it again. + root.isReadyForCommit = false; + + // Check if we're starting from a fresh stack, or if we're resuming from + // previously yielded work. + if ( + root !== nextRoot || + expirationTime !== nextRenderExpirationTime || + nextUnitOfWork === null + ) { + // This is a restart. Reset the stack. + resetContextStack(); + nextRoot = root; + nextRenderExpirationTime = expirationTime; + nextUnitOfWork = createWorkInProgress(nextRoot.current, expirationTime); + } let didError = false; let error = null; if (__DEV__) { - invokeGuardedCallback(null, workLoop, null, minExpirationTime, deadline); + invokeGuardedCallback(null, workLoop, null, expirationTime); if (hasCaughtError()) { didError = true; error = clearCaughtError(); } } else { try { - workLoop(minExpirationTime, deadline); + workLoop(expirationTime); } catch (e) { didError = true; error = e; @@ -1046,12 +861,12 @@ module.exports = function( if (__DEV__) { invokeGuardedCallback( null, - performWorkCatchBlock, + renderRootCatchBlock, null, + root, failedWork, boundary, - minExpirationTime, - deadline, + expirationTime, ); if (hasCaughtError()) { didError = true; @@ -1060,12 +875,7 @@ module.exports = function( } } else { try { - performWorkCatchBlock( - failedWork, - boundary, - minExpirationTime, - deadline, - ); + renderRootCatchBlock(root, failedWork, boundary, expirationTime); error = null; } catch (e) { didError = true; @@ -1077,39 +887,22 @@ module.exports = function( break; } - // If we're inside a callback, set this to false, since we just flushed it. - if (deadline !== null) { - isCallbackScheduled = false; - } - // If there's remaining async work, make sure we schedule another callback. - if ( - nextRenderExpirationTime > mostRecentCurrentTime && - !isCallbackScheduled - ) { - scheduleDeferredCallback(performDeferredWork); - isCallbackScheduled = true; - } - - const errorToThrow = firstUncaughtError; + const uncaughtError = firstUncaughtError; // We're done performing work. Time to clean up. - isPerformingWork = false; - deadlineHasExpired = false; + isWorking = false; didFatal = false; firstUncaughtError = null; - capturedErrors = null; - failedBoundaries = null; - nextRenderedTree = null; - nestedUpdateCount = 0; if (__DEV__) { stopWorkLoopTimer(); } - // It's safe to throw any unhandled errors. - if (errorToThrow !== null) { - throw errorToThrow; + if (uncaughtError !== null) { + onUncaughtError(uncaughtError); } + + return root.isReadyForCommit ? root.current.alternate : null; } // Returns the boundary that captured the error, or null if the error is ignored @@ -1310,9 +1103,6 @@ module.exports = function( return; case HostRoot: if (firstUncaughtError === null) { - // If this is the host container, we treat it as a no-op error - // boundary. We'll throw the first uncaught error once it's safe to - // do so, at the end of the batch. firstUncaughtError = capturedError.error; } return; @@ -1354,25 +1144,6 @@ module.exports = function( } } - function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) { - if (expirationTime === NoWork) { - return; - } - - if (!root.isScheduled) { - root.isScheduled = true; - if (lastScheduledRoot) { - // Schedule ourselves to the end. - lastScheduledRoot.nextScheduledRoot = root; - lastScheduledRoot = root; - } else { - // We're the only work scheduled. - nextScheduledRoot = root; - lastScheduledRoot = root; - } - } - } - function computeAsyncExpiration() { // Given the current clock time, returns an expiration time. We use rounding // to batch like updates together. @@ -1388,7 +1159,7 @@ module.exports = function( if (expirationContext !== NoWork) { // An explicit expiration context was set; expirationTime = expirationContext; - } else if (isPerformingWork) { + } else if (isWorking) { if (isCommitting) { // Updates that occur during the commit phase should have sync priority // by default. @@ -1412,7 +1183,8 @@ module.exports = function( if ( expirationTime === Sync && - (isBatchingUpdates || (isUnbatchingUpdates && isCommitting)) + isBatchingUpdates && + (!isUnbatchingUpdates || isCommitting) ) { // If we're in a batch, downgrade sync to task. expirationTime = Task; @@ -1433,24 +1205,6 @@ module.exports = function( recordScheduleUpdate(); } - if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { - didFatal = true; - invariant( - false, - 'Maximum update depth exceeded. This can happen when a ' + - 'component repeatedly calls setState inside componentWillUpdate or ' + - 'componentDidUpdate. React limits the number of nested updates to ' + - 'prevent infinite loops.', - ); - } - - if (!isPerformingWork && expirationTime <= nextRenderExpirationTime) { - // We must reset the current unit of work pointer so that we restart the - // search from the root during the next tick, in case there is now higher - // priority work somewhere earlier than before. - nextUnitOfWork = null; - } - if (__DEV__) { if (!isErrorRecovery && fiber.tag === ClassComponent) { const instance = fiber.stateNode; @@ -1459,19 +1213,13 @@ module.exports = function( } let node = fiber; - let shouldContinue = true; - while (node !== null && shouldContinue) { - // Walk the parent path to the root and update each node's expiration - // time. Once we reach a node whose expiration matches (and whose - // alternate's expiration matches) we can exit safely knowing that the - // rest of the path is correct. - shouldContinue = false; + while (node !== null) { + // Walk the parent path to the root and update each node's + // expiration time. if ( node.expirationTime === NoWork || node.expirationTime > expirationTime ) { - // Expiration time did not match. Update and keep going. - shouldContinue = true; node.expirationTime = expirationTime; } if (node.alternate !== null) { @@ -1479,44 +1227,23 @@ module.exports = function( node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime ) { - // Expiration time did not match. Update and keep going. - shouldContinue = true; node.alternate.expirationTime = expirationTime; } } if (node.return === null) { if (node.tag === HostRoot) { const root: FiberRoot = (node.stateNode: any); - scheduleRoot(root, expirationTime); - if (!isPerformingWork) { - switch (expirationTime) { - case Sync: - if (isUnbatchingUpdates) { - // We're inside unbatchedUpdates, which is inside either - // batchedUpdates or a lifecycle. We should only flush - // synchronous work, not task work. - performWork(Sync, null); - } else { - // Flush both synchronous and task work. - performWork(Task, null); - } - break; - case Task: - invariant( - isBatchingUpdates, - 'Task updates can only be scheduled as a nested update or ' + - 'inside batchedUpdates. This error is likely caused by a ' + - 'bug in React. Please file an issue.', - ); - break; - default: - // This update is async. Schedule a callback. - if (!isCallbackScheduled) { - scheduleDeferredCallback(performDeferredWork); - isCallbackScheduled = true; - } - } + if ( + !isWorking && + root === nextRoot && + expirationTime <= nextRenderExpirationTime + ) { + // This is an interruption. Restart the root from the top. + nextRoot = null; + nextUnitOfWork = null; + nextRenderExpirationTime = NoWork; } + requestWork(root, expirationTime); } else { if (__DEV__) { if (!isErrorRecovery && fiber.tag === ClassComponent) { @@ -1541,55 +1268,6 @@ module.exports = function( return mostRecentCurrentTime; } - function batchedUpdates(fn: (a: A) => R, a: A): R { - const previousIsBatchingUpdates = isBatchingUpdates; - isBatchingUpdates = true; - try { - return fn(a); - } finally { - isBatchingUpdates = previousIsBatchingUpdates; - // If we're not already inside a batch, we need to flush any task work - // that was created by the user-provided function. - if (!isPerformingWork && !isBatchingUpdates) { - performWork(Task, null); - } - } - } - - function unbatchedUpdates(fn: () => A): A { - const previousIsUnbatchingUpdates = isUnbatchingUpdates; - const previousIsBatchingUpdates = isBatchingUpdates; - // This is only true if we're nested inside batchedUpdates. - isUnbatchingUpdates = isBatchingUpdates; - isBatchingUpdates = false; - try { - return fn(); - } finally { - isBatchingUpdates = previousIsBatchingUpdates; - isUnbatchingUpdates = previousIsUnbatchingUpdates; - } - } - - function flushSync(batch: () => A): A { - const previousIsBatchingUpdates = isBatchingUpdates; - const previousExpirationContext = expirationContext; - isBatchingUpdates = true; - expirationContext = Sync; - try { - return batch(); - } finally { - isBatchingUpdates = previousIsBatchingUpdates; - expirationContext = previousExpirationContext; - - invariant( - !isPerformingWork, - 'flushSync was called from inside a lifecycle method. It cannot be ' + - 'called when React is already rendering.', - ); - performWork(Task, null); - } - } - function deferredUpdates(fn: () => A): A { const previousExpirationContext = expirationContext; expirationContext = computeAsyncExpiration(); @@ -1600,13 +1278,326 @@ module.exports = function( } } + function syncUpdates(fn: () => A): A { + const previousExpirationContext = expirationContext; + expirationContext = Sync; + try { + return fn(); + } finally { + expirationContext = previousExpirationContext; + } + } + + // TODO: Everything below this is written as if it has been lifted to the + // renderers. I'll do this in a follow-up. + + // Linked-list of roots + let firstScheduledRoot: FiberRoot | null = null; + let lastScheduledRoot: FiberRoot | null = null; + + let isCallbackScheduled: boolean = false; + let isRendering: boolean = false; + let nextFlushedRoot: FiberRoot | null = null; + let nextFlushedExpirationTime: ExpirationTime = NoWork; + let deadlineDidExpire: boolean = false; + let hasUnhandledError: boolean = false; + let unhandledError: mixed | null = null; + let deadline: Deadline | null = null; + + let isBatchingUpdates: boolean = false; + let isUnbatchingUpdates: boolean = false; + + // Use these to prevent an infinite loop of nested updates + const NESTED_UPDATE_LIMIT = 1000; + let nestedUpdateCount: number = 0; + + const timeHeuristicForUnitOfWork = 1; + + // requestWork is called by the scheduler whenever a root receives an update. + // It's up to the renderer to call renderRoot at some point in the future. + function requestWork(root: FiberRoot, expirationTime: ExpirationTime) { + if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { + invariant( + false, + 'Maximum update depth exceeded. This can happen when a ' + + 'component repeatedly calls setState inside componentWillUpdate or ' + + 'componentDidUpdate. React limits the number of nested updates to ' + + 'prevent infinite loops.', + ); + } + + // Check if this root is already part of the schedule. + if (root.remainingExpirationTime === NoWork) { + // This root is not already scheduled. Add it. + root.remainingExpirationTime = expirationTime; + if (lastScheduledRoot === null) { + firstScheduledRoot = lastScheduledRoot = root; + } else { + lastScheduledRoot.nextScheduledRoot = root; + lastScheduledRoot = root; + } + } else { + // This root is already scheduled, but its priority may have increased. + const remainingExpirationTime = root.remainingExpirationTime; + if ( + remainingExpirationTime === NoWork || + expirationTime < remainingExpirationTime + ) { + // Update the priority. + root.remainingExpirationTime = expirationTime; + } + } + + // If we're not already rendering, schedule work to flush now (if it's + // sync) or later (if it's async). + if (!isRendering) { + // TODO: Remove distinction between sync and task. Maybe we can remove + // these magic numbers entirely by always comparing to the current time? + if (expirationTime === Sync) { + if (isUnbatchingUpdates) { + performWork(Sync, null); + } else { + performWork(Task, null); + } + } else if (!isCallbackScheduled) { + isCallbackScheduled = true; + scheduleDeferredCallback(flushAsyncWork); + } + } + } + + function findHighestPriorityRoot() { + let highestPriorityWork = NoWork; + let highestPriorityRoot = null; + + let previousScheduledRoot = null; + let root = firstScheduledRoot; + while (root !== null) { + const remainingExpirationTime = root.remainingExpirationTime; + if (remainingExpirationTime === NoWork) { + // If this root no longer has work, remove it from the scheduler. + let next = root.nextScheduledRoot; + root.nextScheduledRoot = null; + if (previousScheduledRoot === null) { + firstScheduledRoot = next; + } else { + previousScheduledRoot.nextScheduledRoot = next; + } + if (next === null) { + lastScheduledRoot = null; + } + root = next; + continue; + } else if ( + highestPriorityWork === NoWork || + remainingExpirationTime < highestPriorityWork + ) { + // Update the priority, if it's higher + highestPriorityWork = remainingExpirationTime; + highestPriorityRoot = root; + } + previousScheduledRoot = root; + root = root.nextScheduledRoot; + } + + // If the next root is the same as the previous root, this is a nested + // update. To prevent an infinite loop, increment the nested update count. + const previousFlushedRoot = nextFlushedRoot; + if ( + previousFlushedRoot !== null && + previousFlushedRoot === highestPriorityRoot + ) { + nestedUpdateCount++; + } else { + // Reset whenever we switch roots. + nestedUpdateCount = 0; + } + nextFlushedRoot = highestPriorityRoot; + nextFlushedExpirationTime = highestPriorityWork; + } + + function flushAsyncWork(dl) { + performWork(NoWork, dl); + } + + function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) { + invariant( + !isRendering, + 'performWork was called recursively. This error is likely caused ' + + 'by a bug in React. Please file an issue.', + ); + + isRendering = true; + deadline = dl; + + // Keep working on roots until there's no more work, or until the we reach + // the deadlne. + findHighestPriorityRoot(); + while ( + nextFlushedRoot !== null && + nextFlushedExpirationTime !== NoWork && + (minExpirationTime === NoWork || + nextFlushedExpirationTime <= minExpirationTime) && + !deadlineDidExpire + ) { + // Check if this is async work or sync/expired work. + // TODO: Pass current time as argument to renderRoot, commitRoot + if (nextFlushedExpirationTime <= recalculateCurrentTime()) { + // Flush sync work. + let finishedWork = nextFlushedRoot.finishedWork; + if (finishedWork !== null) { + // This root is already complete. We can commit it. + nextFlushedRoot.finishedWork = null; + nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork); + } else { + nextFlushedRoot.finishedWork = null; + finishedWork = renderRoot(nextFlushedRoot, nextFlushedExpirationTime); + if (finishedWork !== null) { + // We've completed the root. Commit it. + nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork); + } + } + } else { + // Flush async work. + let finishedWork = nextFlushedRoot.finishedWork; + if (finishedWork !== null) { + // This root is already complete. We can commit it. + nextFlushedRoot.finishedWork = null; + nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork); + } else { + nextFlushedRoot.finishedWork = null; + finishedWork = renderRoot(nextFlushedRoot, nextFlushedExpirationTime); + if (finishedWork !== null) { + // We've completed the root. Check the deadline one more time + // before committing. + if (!shouldYield()) { + // Still time left. Commit the root. + nextFlushedRoot.remainingExpirationTime = commitRoot( + finishedWork, + ); + } else { + // There's no time left. Mark this root as complete. We'll come + // back and commit it later. + nextFlushedRoot.finishedWork = finishedWork; + } + } + } + } + // Find the next highest priority work. + findHighestPriorityRoot(); + } + + // We're done flushing work. Either we ran out of time in this callback, + // or there's no more work left with sufficient priority. + + // If we're inside a callback, set this to false since we just completed it. + if (deadline !== null) { + isCallbackScheduled = false; + } + // If there's work left over, schedule a new callback. + if (nextFlushedRoot !== null && !isCallbackScheduled) { + isCallbackScheduled = true; + scheduleDeferredCallback(flushAsyncWork); + } + + // Clean-up. + deadline = null; + deadlineDidExpire = false; + isRendering = false; + nestedUpdateCount = 0; + + if (hasUnhandledError) { + const error = unhandledError; + unhandledError = null; + hasUnhandledError = false; + throw error; + } + } + + // When working on async work, the reconciler asks the renderer if it should + // yield execution. For DOM, we implement this with requestIdleCallback. + function shouldYield() { + if (deadline === null) { + return false; + } + if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { + return false; + } + deadlineDidExpire = true; + return true; + } + + // TODO: Not happy about this hook. Conceptually, renderRoot should return a + // tuple of (isReadyForCommit, didError, error) + function onUncaughtError(error) { + invariant( + nextFlushedRoot !== null, + 'Should be working on a root. This error is likely caused by a bug in ' + + 'React. Please file an issue.', + ); + // Unschedule this root so we don't work on it again until there's + // another update. + nextFlushedRoot.remainingExpirationTime = NoWork; + if (!hasUnhandledError) { + hasUnhandledError = true; + unhandledError = error; + } + } + + // TODO: Batching should be implemented at the renderer level, not inside + // the reconciler. + function batchedUpdates(fn: (a: A) => R, a: A): R { + const previousIsBatchingUpdates = isBatchingUpdates; + isBatchingUpdates = true; + try { + return fn(a); + } finally { + isBatchingUpdates = previousIsBatchingUpdates; + if (!isBatchingUpdates && !isRendering) { + performWork(Task, null); + } + } + } + + // TODO: Batching should be implemented at the renderer level, not inside + // the reconciler. + function unbatchedUpdates(fn: () => A): A { + if (isBatchingUpdates && !isUnbatchingUpdates) { + isUnbatchingUpdates = true; + try { + return fn(); + } finally { + isUnbatchingUpdates = false; + } + } + return fn(); + } + + // TODO: Batching should be implemented at the renderer level, not within + // the reconciler. + function flushSync(fn: () => A): A { + const previousIsBatchingUpdates = isBatchingUpdates; + isBatchingUpdates = true; + try { + return syncUpdates(fn); + } finally { + isBatchingUpdates = previousIsBatchingUpdates; + invariant( + !isRendering, + 'flushSync was called from inside a lifecycle method. It cannot be ' + + 'called when React is already rendering.', + ); + performWork(Task, null); + } + } + return { - computeAsyncExpiration: computeAsyncExpiration, - computeExpirationForFiber: computeExpirationForFiber, - scheduleWork: scheduleWork, - batchedUpdates: batchedUpdates, - unbatchedUpdates: unbatchedUpdates, - flushSync: flushSync, - deferredUpdates: deferredUpdates, + computeAsyncExpiration, + computeExpirationForFiber, + scheduleWork, + batchedUpdates, + unbatchedUpdates, + flushSync, + deferredUpdates, }; }; diff --git a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap index 2e7dcb5371..9b83f7fd4b 100644 --- a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap +++ b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap @@ -6,10 +6,11 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = ` ⚛ AllLifecycles [mount] ⚛ AllLifecycles.componentWillMount ⚛ AllLifecycles.getChildContext - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⚛ AllLifecycles.componentDidMount + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⚛ AllLifecycles.componentDidMount // Update ⚛ (React Tree Reconciliation) @@ -18,17 +19,19 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = ` ⚛ AllLifecycles.shouldComponentUpdate ⚛ AllLifecycles.componentWillUpdate ⚛ AllLifecycles.getChildContext - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) - ⚛ AllLifecycles.componentDidUpdate + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) + ⚛ AllLifecycles.componentDidUpdate // Unmount ⚛ (React Tree Reconciliation) - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ AllLifecycles.componentWillUnmount - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ AllLifecycles.componentWillUnmount + ⚛ (Calling Lifecycle Methods: 0 Total) " `; @@ -40,44 +43,53 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc ⚛ B [update] ⚛ A [update] ⚛ B [update] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 9 Total) - ⚛ (Calling Lifecycle Methods: 9 Total) - ⚛ A.componentDidUpdate - ⚛ B.componentDidUpdate + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 9 Total) + ⚛ (Calling Lifecycle Methods: 9 Total) + ⚛ A.componentDidUpdate + ⚛ B.componentDidUpdate // Because of deduplication, we don't know B was cascading, // but we should still see the warning for the commit phase. -⛔ (React Tree Reconciliation) Warning: There were cascading updates +⚛ (React Tree Reconciliation) ⚛ Parent [update] ⚛ A [update] ⚛ B [update] ⚛ A [update] ⚛ B [update] - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Host Effects: 9 Total) - ⚛ (Calling Lifecycle Methods: 9 Total) - ⚛ A.componentDidUpdate - ⚛ B.componentDidUpdate + +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 9 Total) + ⚛ (Calling Lifecycle Methods: 9 Total) + ⚛ A.componentDidUpdate + ⚛ B.componentDidUpdate + +⚛ (React Tree Reconciliation) ⚛ B [update] - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit - ⚛ (Committing Host Effects: 3 Total) - ⚛ (Calling Lifecycle Methods: 3 Total) - ⚛ B.componentDidUpdate + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 3 Total) + ⚛ (Calling Lifecycle Methods: 3 Total) + ⚛ B.componentDidUpdate " `; exports[`ReactDebugFiberPerf does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = ` -"⛔ (React Tree Reconciliation) Warning: There were cascading updates +"⚛ (React Tree Reconciliation) ⚛ Component [mount] - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⛔ Component.componentDidMount Warning: Scheduled a cascading update + +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⛔ Component.componentDidMount Warning: Scheduled a cascading update + +⚛ (React Tree Reconciliation) ⚛ Component [update] - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) " `; @@ -87,18 +99,20 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi ⚛ Parent [mount] ⚛ NotCascading [mount] ⚛ NotCascading.componentWillMount - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) // Should not print a warning ⚛ (React Tree Reconciliation) ⚛ Parent [update] ⚛ NotCascading [update] ⚛ NotCascading.componentWillReceiveProps - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) " `; @@ -107,23 +121,26 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = ` ⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⚛ Child [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) // Update ⚛ (React Tree Reconciliation) ⚛ Parent [update] ⚛ Child [update] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) // Unmount ⚛ (React Tree Reconciliation) - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; @@ -144,9 +161,10 @@ exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = ` ⚛ Parent [mount] ⚛ B [mount] ⚛ Child [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; @@ -154,35 +172,41 @@ exports[`ReactDebugFiberPerf measures deprioritized work 1`] = ` "// Flush the parent ⚛ (React Tree Reconciliation) ⚛ Parent [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) // Flush the child ⚛ (React Tree Reconciliation) ⚛ Child [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 3 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 3 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) " `; exports[`ReactDebugFiberPerf recovers from caught errors 1`] = ` "// Stop on Baddie and restart from Boundary -⛔ (React Tree Reconciliation) Warning: There were cascading updates +⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⛔ Boundary [mount] Warning: An error was thrown inside this error boundary ⚛ Parent [mount] ⚛ Baddie [mount] ⚛ Boundary [mount] - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) + +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + +⚛ (React Tree Reconciliation) ⚛ Boundary [update] ⚛ ErrorReport [mount] - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) " `; @@ -191,17 +215,19 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = ` ⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⚛ Baddie [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) // Will reconcile from a clean state ⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⚛ Child [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; @@ -210,9 +236,10 @@ exports[`ReactDebugFiberPerf skips parents during setState 1`] = ` ⚛ (React Tree Reconciliation) ⚛ A [update] ⚛ B [update] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 6 Total) - ⚛ (Calling Lifecycle Methods: 6 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 6 Total) + ⚛ (Calling Lifecycle Methods: 6 Total) " `; @@ -226,9 +253,10 @@ exports[`ReactDebugFiberPerf supports coroutines 1`] = ` ⚛ CoChild [mount] ⚛ Continuation [mount] ⚛ Continuation [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 3 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 3 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; @@ -236,40 +264,49 @@ exports[`ReactDebugFiberPerf supports portals 1`] = ` "⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⚛ Child [mount] - ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = ` "// Should print a warning -⛔ (React Tree Reconciliation) Warning: There were cascading updates +⚛ (React Tree Reconciliation) ⚛ Parent [mount] ⚛ Cascading [mount] - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update + +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update + +⚛ (React Tree Reconciliation) ⚛ Cascading [update] - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) " `; exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = ` "// Rendering the first root -⛔ (React Tree Reconciliation) Warning: There were cascading updates +⚛ (React Tree Reconciliation) ⚛ Cascading [mount] - ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update - // Scheduling another root from componentDidMount + +⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update + // Scheduling another root from componentDidMount + +⚛ (React Tree Reconciliation) ⚛ Child [mount] - ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + +⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index b60e29c156..513aadf84e 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -17,36 +17,36 @@ "gzip": 2364 }, "React-dev.js (FB_DEV)": { - "size": 43016, - "gzip": 11325 + "size": 43033, + "gzip": 11331 }, "React-prod.js (FB_PROD)": { - "size": 24831, - "gzip": 6707 + "size": 24848, + "gzip": 6713 }, "react-dom.development.js (UMD_DEV)": { - "size": 631104, - "gzip": 144685 + "size": 629078, + "gzip": 144076 }, "react-dom.production.min.js (UMD_PROD)": { - "size": 100038, - "gzip": 31532 + "size": 101020, + "gzip": 31886 }, "react-dom.development.js (NODE_DEV)": { - "size": 593383, - "gzip": 135883 + "size": 591361, + "gzip": 135305 }, "react-dom.production.min.js (NODE_PROD)": { - "size": 106647, - "gzip": 33435 + "size": 107254, + "gzip": 33591 }, "ReactDOMFiber-dev.js (FB_DEV)": { - "size": 590572, - "gzip": 135237 + "size": 588633, + "gzip": 134646 }, "ReactDOMFiber-prod.js (FB_PROD)": { - "size": 419911, - "gzip": 93354 + "size": 418791, + "gzip": 93017 }, "react-dom-test-utils.development.js (NODE_DEV)": { "size": 41743, @@ -73,96 +73,96 @@ "gzip": 4510 }, "ReactDOMUnstableNativeDependencies-dev.js (FB_DEV)": { - "size": 80190, - "gzip": 19882 + "size": 80207, + "gzip": 19890 }, "ReactDOMUnstableNativeDependencies-prod.js (FB_PROD)": { - "size": 65164, - "gzip": 15538 + "size": 65181, + "gzip": 15545 }, "react-dom-server.browser.development.js (UMD_DEV)": { - "size": 124910, - "gzip": 32260 + "size": 124902, + "gzip": 32255 }, "react-dom-server.browser.production.min.js (UMD_PROD)": { - "size": 15345, - "gzip": 5968 + "size": 15337, + "gzip": 5967 }, "react-dom-server.browser.development.js (NODE_DEV)": { - "size": 94830, - "gzip": 25139 + "size": 94822, + "gzip": 25134 }, "react-dom-server.browser.production.min.js (NODE_PROD)": { - "size": 15071, - "gzip": 5902 + "size": 15063, + "gzip": 5899 }, "ReactDOMServer-dev.js (FB_DEV)": { - "size": 94490, - "gzip": 25063 + "size": 94499, + "gzip": 25066 }, "ReactDOMServer-prod.js (FB_PROD)": { - "size": 42454, - "gzip": 11844 + "size": 42463, + "gzip": 11846 }, "react-dom-server.node.development.js (NODE_DEV)": { - "size": 97092, - "gzip": 25688 + "size": 97084, + "gzip": 25683 }, "react-dom-server.node.production.min.js (NODE_PROD)": { - "size": 15996, - "gzip": 6238 + "size": 15988, + "gzip": 6235 }, "react-art.development.js (UMD_DEV)": { - "size": 378408, - "gzip": 83017 + "size": 376390, + "gzip": 82374 }, "react-art.production.min.js (UMD_PROD)": { - "size": 82432, - "gzip": 25592 + "size": 83413, + "gzip": 25876 }, "react-art.development.js (NODE_DEV)": { - "size": 302763, - "gzip": 63846 + "size": 300749, + "gzip": 63250 }, "react-art.production.min.js (NODE_PROD)": { - "size": 53897, - "gzip": 16869 + "size": 54504, + "gzip": 17048 }, "ReactARTFiber-dev.js (FB_DEV)": { - "size": 301605, - "gzip": 63774 + "size": 299674, + "gzip": 63165 }, "ReactARTFiber-prod.js (FB_PROD)": { - "size": 225472, - "gzip": 46455 + "size": 224360, + "gzip": 46067 }, "ReactNativeFiber-dev.js (RN_DEV)": { - "size": 285948, - "gzip": 49225 + "size": 286193, + "gzip": 49282 }, "ReactNativeFiber-prod.js (RN_PROD)": { - "size": 223676, - "gzip": 38463 + "size": 223893, + "gzip": 38520 }, "react-test-renderer.development.js (NODE_DEV)": { - "size": 306451, - "gzip": 64239 + "size": 304437, + "gzip": 63667 }, "ReactTestRendererFiber-dev.js (FB_DEV)": { - "size": 305254, - "gzip": 64170 + "size": 303323, + "gzip": 63582 }, "react-test-renderer-shallow.development.js (NODE_DEV)": { "size": 9364, "gzip": 2335 }, "ReactShallowRenderer-dev.js (FB_DEV)": { - "size": 9020, - "gzip": 2254 + "size": 9037, + "gzip": 2262 }, "react-noop-renderer.development.js (NODE_DEV)": { - "size": 295660, - "gzip": 61436 + "size": 293646, + "gzip": 60851 }, "react-dom-server.development.js (UMD_DEV)": { "size": 120897, @@ -189,16 +189,16 @@ "gzip": 7520 }, "ReactNativeRTFiber-dev.js (RN_DEV)": { - "size": 217811, - "gzip": 36735 + "size": 218056, + "gzip": 36811 }, "ReactNativeRTFiber-prod.js (RN_PROD)": { - "size": 165325, - "gzip": 27464 + "size": 165542, + "gzip": 27540 }, "react-test-renderer.production.min.js (NODE_PROD)": { - "size": 55449, - "gzip": 17169 + "size": 56061, + "gzip": 17256 }, "react-test-renderer-shallow.production.min.js (NODE_PROD)": { "size": 4630, @@ -209,20 +209,20 @@ "gzip": 4241 }, "react-reconciler.development.js (NODE_DEV)": { - "size": 281332, - "gzip": 58301 + "size": 279318, + "gzip": 57691 }, "react-reconciler.production.min.js (NODE_PROD)": { - "size": 37658, - "gzip": 11762 + "size": 38320, + "gzip": 11959 }, "ReactNativeCSFiber-dev.js (RN_DEV)": { - "size": 210232, - "gzip": 34986 + "size": 210477, + "gzip": 35055 }, "ReactNativeCSFiber-prod.js (RN_PROD)": { - "size": 160320, - "gzip": 26275 + "size": 160537, + "gzip": 26346 } } } \ No newline at end of file