From 2ca796b4ffe2816a02fa2ee9dc60f3afa3c0e9ac Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 10 Sep 2019 17:14:15 -0700 Subject: [PATCH] Remove recursive calls to `renderRoot`. There are a few leftover cases where `renderRoot` is called recursively. All of them are related to synchronously flushing work before its expiration time. We can remove these calls by tracking the last expired level on the root, similar to what we do for other types of pending work, like pings. --- .../react-reconciler/src/ReactFiberRoot.js | 21 ++++++++ .../src/ReactFiberWorkLoop.js | 49 ++++++++++++------- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 156e6f365cef0..72dba87355d72 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -84,6 +84,7 @@ type BaseFiberRootProperties = {| // The latest time at which a suspended component pinged the root to // render again lastPingedTime: ExpirationTime, + lastExpiredTime: ExpirationTime, |}; // The following attributes are only used by interaction tracing builds. @@ -132,6 +133,7 @@ function FiberRootNode(containerInfo, tag, hydrate) { this.lastSuspendedTime = NoWork; this.nextKnownPendingLevel = NoWork; this.lastPingedTime = NoWork; + this.lastExpiredTime = NoWork; if (enableSchedulerTracing) { this.interactionThreadID = unstable_getThreadID(); @@ -192,6 +194,10 @@ export function markRootSuspendedAtTime( if (expirationTime <= root.lastPingedTime) { root.lastPingedTime = NoWork; } + + if (expirationTime <= root.lastExpiredTime) { + root.lastExpiredTime = NoWork; + } } export function markRootUpdatedAtTime( @@ -247,4 +253,19 @@ export function markRootFinishedAtTime( // Clear the pinged time root.lastPingedTime = NoWork; } + + if (finishedExpirationTime <= root.lastExpiredTime) { + // Clear the expired time + root.lastExpiredTime = NoWork; + } +} + +export function markRootExpiredAtTime( + root: FiberRoot, + expirationTime: ExpirationTime, +): void { + const lastExpiredTime = root.lastExpiredTime; + if (lastExpiredTime === NoWork || lastExpiredTime > expirationTime) { + root.lastExpiredTime = expirationTime; + } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 84f6705164dea..df0d554f19ca3 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -68,6 +68,7 @@ import { markRootSuspendedAtTime, markRootFinishedAtTime, markRootUpdatedAtTime, + markRootExpiredAtTime, } from './ReactFiberRoot'; import { NoMode, @@ -521,10 +522,14 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { // Determines the next expiration time that the root should render, taking // into account levels that may be suspended, or levels that may have // received a ping. - // + + const lastExpiredTime = root.lastExpiredTime; + if (lastExpiredTime !== NoWork) { + return lastExpiredTime; + } + // "Pending" refers to any update that hasn't committed yet, including if it // suspended. The "suspended" range is therefore a subset. - const firstPendingTime = root.firstPendingTime; if (!isRootSuspendedAtTime(root, firstPendingTime)) { // The highest priority pending time is not suspended. Let's work on that. @@ -547,6 +552,17 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { // the next level that the root has work on. This function is called on every // update, and right before exiting a task. function ensureRootIsScheduled(root: FiberRoot) { + const lastExpiredTime = root.lastExpiredTime; + if (lastExpiredTime !== NoWork) { + // Special case: Expired work should flush synchronously. + root.callbackExpirationTime = Sync; + root.callbackPriority = ImmediatePriority; + root.callbackNode = scheduleSyncCallback( + performSyncWorkOnRoot.bind(null, root, lastExpiredTime), + ); + return; + } + const expirationTime = getNextRootExpirationTimeToWorkOn(root); const existingCallbackNode = root.callbackNode; if (expirationTime === NoWork) { @@ -621,20 +637,16 @@ function performConcurrentWorkOnRoot(root, didTimeout) { // event time. The next update will compute a new event time. currentEventTime = NoWork; + if (didTimeout) { + // An async update expired. + const currentTime = requestCurrentTime(); + markRootExpiredAtTime(root, currentTime); + } + // Determine the next expiration time to work on, using the fields stored // on the root. - let expirationTime = getNextRootExpirationTimeToWorkOn(root); + const expirationTime = getNextRootExpirationTimeToWorkOn(root); if (expirationTime !== NoWork) { - if (didTimeout) { - // An async update expired. There may be other expired updates on - // this root. - const currentTime = requestCurrentTime(); - if (currentTime < expirationTime) { - // Render all the expired work in a single batch. - expirationTime = currentTime; - } - } - const originalCallbackNode = root.callbackNode; try { renderRoot(root, expirationTime, didTimeout); @@ -673,7 +685,9 @@ export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) { 'means you attempted to commit from inside a lifecycle method.', ); } - performSyncWorkOnRoot(root, expirationTime); + markRootExpiredAtTime(root, expirationTime); + ensureRootIsScheduled(root); + flushSyncCallbackQueue(); } export function flushDiscreteUpdates() { @@ -741,9 +755,8 @@ function flushPendingDiscreteUpdates() { const roots = rootsWithPendingDiscreteUpdates; rootsWithPendingDiscreteUpdates = null; roots.forEach((expirationTime, root) => { - scheduleSyncCallback( - performSyncWorkOnRoot.bind(null, root, expirationTime), - ); + markRootExpiredAtTime(root, expirationTime); + ensureRootIsScheduled(root); }); // Now flush the immediate queue. flushSyncCallbackQueue(); @@ -1032,7 +1045,7 @@ function renderRoot( // synchronously, to see if the error goes away. If there are lower // priority updates, let's include those, too, in case they fix the // inconsistency. Render at Idle to include all updates. - performSyncWorkOnRoot(root, Idle); + markRootExpiredAtTime(root, Idle); return; } // Commit the root in its errored state.