From 033794eb2204c7d97f68ac30e85111a961af7d40 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 9 Aug 2017 16:04:08 -0700 Subject: [PATCH 01/10] [Work-in-progress] Assign expiration times to updates An expiration time represents a time in the future by which an update should flush. The priority of the update is related to the difference between the current clock time and the expiration time. This has the effect of increasing the priority of updates as time progresses, to prevent starvation. This lays the initial groundwork for expiration times without changing any behavior. Future commits will replace work priority with expiration times. --- src/renderers/art/ReactARTFiberEntry.js | 5 + src/renderers/dom/fiber/ReactDOMFiberEntry.js | 18 +++ .../native-rt/ReactNativeRTFiberRenderer.js | 5 + .../native/ReactNativeFiberRenderer.js | 5 + src/renderers/noop/ReactNoopEntry.js | 5 + .../shared/fiber/ReactFiberBeginWork.js | 3 + .../shared/fiber/ReactFiberClassComponent.js | 11 +- .../shared/fiber/ReactFiberExpirationTime.js | 104 ++++++++++++++++++ .../shared/fiber/ReactFiberReconciler.js | 6 +- .../shared/fiber/ReactFiberScheduler.js | 14 +++ .../shared/fiber/ReactFiberUpdateQueue.js | 75 +++++++++++-- .../__tests__/ReactFiberHostContext-test.js | 3 + .../testing/ReactTestRendererFiberEntry.js | 5 + 13 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 src/renderers/shared/fiber/ReactFiberExpirationTime.js diff --git a/src/renderers/art/ReactARTFiberEntry.js b/src/renderers/art/ReactARTFiberEntry.js index a4e340c2f75e..ea0c4d68649e 100644 --- a/src/renderers/art/ReactARTFiberEntry.js +++ b/src/renderers/art/ReactARTFiberEntry.js @@ -532,6 +532,11 @@ const ARTRenderer = ReactFiberReconciler({ ); }, + now(): number { + // TODO: Enable expiration by implementing this method. + return 0; + }, + useSyncScheduling: true, }); diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index d0dd466bfb4f..6076c61fcc19 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -162,6 +162,22 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean { return false; } +// TODO: Better polyfill +let now; +if ( + typeof window !== 'undefined' && + window.performance && + typeof window.performance.now === 'function' +) { + now = function() { + return performance.now(); + }; +} else { + now = function() { + return Date.now(); + }; +} + var DOMRenderer = ReactFiberReconciler({ getRootHostContext(rootContainerInstance: Container): HostContext { let type; @@ -437,6 +453,8 @@ var DOMRenderer = ReactFiberReconciler({ } }, + now: now, + canHydrateInstance( instance: Instance | TextInstance, type: string, diff --git a/src/renderers/native-rt/ReactNativeRTFiberRenderer.js b/src/renderers/native-rt/ReactNativeRTFiberRenderer.js index 799ee85c1f55..443c84b1fe83 100644 --- a/src/renderers/native-rt/ReactNativeRTFiberRenderer.js +++ b/src/renderers/native-rt/ReactNativeRTFiberRenderer.js @@ -227,6 +227,11 @@ const NativeRTRenderer = ReactFiberReconciler({ }, useSyncScheduling: true, + + now(): number { + // TODO: Enable expiration by implementing this method. + return 0; + }, }); module.exports = NativeRTRenderer; diff --git a/src/renderers/native/ReactNativeFiberRenderer.js b/src/renderers/native/ReactNativeFiberRenderer.js index 8f36d4886a51..859f422f4c2a 100644 --- a/src/renderers/native/ReactNativeFiberRenderer.js +++ b/src/renderers/native/ReactNativeFiberRenderer.js @@ -377,6 +377,11 @@ const NativeRenderer = ReactFiberReconciler({ }, useSyncScheduling: true, + + now(): number { + // TODO: Enable expiration by implementing this method. + return 0; + }, }); module.exports = NativeRenderer; diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index cc98ecb920d9..9a2dcadaa315 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -201,6 +201,11 @@ var NoopRenderer = ReactFiberReconciler({ prepareForCommit(): void {}, resetAfterCommit(): void {}, + + now(): number { + // TODO: Add an API to advance time. + return 0; + }, }); var rootContainers = new Map(); diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index ed81a1d77894..d308973277e4 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -17,6 +17,7 @@ import type {HydrationContext} from 'ReactFiberHydrationContext'; import type {FiberRoot} from 'ReactFiberRoot'; import type {HostConfig} from 'ReactFiberReconciler'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; var { mountChildFibersInPlace, @@ -73,6 +74,7 @@ module.exports = function( hydrationContext: HydrationContext, scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, + recalculateCurrentTime: () => ExpirationTime, ) { const { shouldSetTextContent, @@ -99,6 +101,7 @@ module.exports = function( getPriorityContext, memoizeProps, memoizeState, + recalculateCurrentTime, ); function reconcileChildren(current, workInProgress, nextChildren) { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 2e080e23bff4..4c72e6236b7c 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -12,6 +12,7 @@ import type {Fiber} from 'ReactFiber'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; var {Update} = require('ReactTypeOfSideEffect'); @@ -81,6 +82,7 @@ module.exports = function( getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, memoizeProps: (workInProgress: Fiber, props: any) => void, memoizeState: (workInProgress: Fiber, state: any) => void, + recalculateCurrentTime: () => ExpirationTime, ) { // Class component state updater const updater = { @@ -88,31 +90,34 @@ module.exports = function( enqueueSetState(instance, partialState, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); + const currentTime = recalculateCurrentTime(); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - addUpdate(fiber, partialState, callback, priorityLevel); + addUpdate(fiber, partialState, callback, priorityLevel, currentTime); scheduleUpdate(fiber, priorityLevel); }, enqueueReplaceState(instance, state, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); + const currentTime = recalculateCurrentTime(); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'replaceState'); } - addReplaceUpdate(fiber, state, callback, priorityLevel); + addReplaceUpdate(fiber, state, callback, priorityLevel, currentTime); scheduleUpdate(fiber, priorityLevel); }, enqueueForceUpdate(instance, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); + const currentTime = recalculateCurrentTime(); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'forceUpdate'); } - addForceUpdate(fiber, callback, priorityLevel); + addForceUpdate(fiber, callback, priorityLevel, currentTime); scheduleUpdate(fiber, priorityLevel); }, }; diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js new file mode 100644 index 000000000000..1f5d9c42c4e0 --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule ReactFiberExpirationTime + * @flow + */ + +'use strict'; + +import type {PriorityLevel} from 'ReactPriorityLevel'; +const { + NoWork, + SynchronousPriority, + TaskPriority, + HighPriority, + LowPriority, + OffscreenPriority, +} = require('ReactPriorityLevel'); + +const invariant = require('fbjs/lib/invariant'); + +// TODO: Use an opaque type once ESLint et al support the syntax +export type ExpirationTime = number; + +const Done = 0; +exports.Done = Done; + +const Never = Number.MAX_SAFE_INTEGER; +exports.Never = Never; + +// 1 unit of expiration time represents 10ms. +function msToExpirationTime(ms: number): ExpirationTime { + // Always add 1 so that we don't clash with the magic number for Done. + return Math.round(ms / 10) + 1; +} +exports.msToExpirationTime = msToExpirationTime; + +function ceiling(time: ExpirationTime, precision: number): ExpirationTime { + return Math.ceil(Math.ceil(time * precision) / precision); +} + +// Given the current clock time and a priority level, returns an expiration time +// that represents a point in the future by which some work should complete. +// The lower the priority, the further out the expiration time. We use rounding +// to batch like updates together. The further out the expiration time, the +// more we want to batch, so we use a larger precision when rounding. +function priorityToExpirationTime( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel, +): ExpirationTime { + switch (priorityLevel) { + case NoWork: + return Done; + case SynchronousPriority: + // Return a number lower than the current time, but higher than Done. + return 1; + case TaskPriority: + // Return the current time, so that this work completes in this batch. + return currentTime; + case HighPriority: + // Should complete within ~100ms. 120ms max. + return msToExpirationTime(ceiling(100, 20)); + case LowPriority: + // Should complete within ~1000ms. 1200ms max. + return msToExpirationTime(ceiling(1000, 200)); + case OffscreenPriority: + return Never; + default: + invariant( + false, + 'Switch statement should be exhuastive. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } +} +exports.priorityToExpirationTime = priorityToExpirationTime; + +// Given the current clock time and an expiration time, returns the +// corresponding priority level. The more time has advanced, the higher the +// priority level. +function expirationTimeToPriorityLevel( + currentTime: ExpirationTime, + expirationTime: ExpirationTime, +): PriorityLevel { + // First check for magic values + if (expirationTime === Done) { + return NoWork; + } + if (expirationTime === Never) { + return OffscreenPriority; + } + if (expirationTime < currentTime) { + return SynchronousPriority; + } + if (expirationTime === currentTime) { + return TaskPriority; + } + // TODO: We don't currently distinguish between high and low priority. + return LowPriority; +} +exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 4876d88f4607..e20fff429d69 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -122,6 +122,8 @@ export type HostConfig = { prepareForCommit(): void, resetAfterCommit(): void, + now(): number, + // Optional hydration canHydrateInstance?: (instance: I | TI, type: T, props: P) => boolean, canHydrateTextInstance?: (instance: I | TI, text: string) => boolean, @@ -235,6 +237,7 @@ module.exports = function( var { scheduleUpdate, getPriorityContext, + recalculateCurrentTime, batchedUpdates, unbatchedUpdates, flushSync, @@ -274,6 +277,7 @@ module.exports = function( element.type.prototype != null && (element.type.prototype: any).unstable_isAsyncReactComponent === true; const priorityLevel = getPriorityContext(current, forceAsync); + const currentTime = recalculateCurrentTime(); const nextState = {element}; callback = callback === undefined ? null : callback; if (__DEV__) { @@ -284,7 +288,7 @@ module.exports = function( callback, ); } - addTopLevelUpdate(current, nextState, callback, priorityLevel); + addTopLevelUpdate(current, nextState, callback, priorityLevel, currentTime); scheduleUpdate(current, priorityLevel); } diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index cc3800d129ea..4509708d0fbf 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -15,6 +15,7 @@ import type {FiberRoot} from 'ReactFiberRoot'; import type {HostConfig, Deadline} from 'ReactFiberReconciler'; import type {PriorityLevel} from 'ReactPriorityLevel'; import type {HydrationContext} from 'ReactFiberHydrationContext'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; export type CapturedError = { componentName: ?string, @@ -62,6 +63,8 @@ var { OffscreenPriority, } = require('ReactPriorityLevel'); +var {msToExpirationTime} = require('ReactFiberExpirationTime'); + var {AsyncUpdates} = require('ReactTypeOfInternalContext'); var { @@ -166,6 +169,7 @@ module.exports = function( hydrationContext, scheduleUpdate, getPriorityContext, + recalculateCurrentTime, ); const {completeWork} = ReactFiberCompleteWork( config, @@ -181,12 +185,16 @@ module.exports = function( commitDetachRef, } = ReactFiberCommitWork(config, captureError); const { + now, scheduleDeferredCallback, useSyncScheduling, prepareForCommit, resetAfterCommit, } = config; + // Represents the current time in ms. + let currentTime: ExpirationTime = msToExpirationTime(now()); + // The priority level to use when scheduling an update. We use NoWork to // represent the default priority. // TODO: Should we change this to an array instead of using the call stack? @@ -1513,6 +1521,11 @@ module.exports = function( scheduleUpdateImpl(fiber, TaskPriority, true); } + function recalculateCurrentTime(): ExpirationTime { + currentTime = msToExpirationTime(now()); + return currentTime; + } + function batchedUpdates(fn: (a: A) => R, a: A): R { const previousIsBatchingUpdates = isBatchingUpdates; isBatchingUpdates = true; @@ -1575,6 +1588,7 @@ module.exports = function( return { scheduleUpdate: scheduleUpdate, getPriorityContext: getPriorityContext, + recalculateCurrentTime: recalculateCurrentTime, batchedUpdates: batchedUpdates, unbatchedUpdates: unbatchedUpdates, flushSync: flushSync, diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 39f483b7d78d..4b40e0e565d7 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -12,6 +12,7 @@ import type {Fiber} from 'ReactFiber'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect'); @@ -21,6 +22,8 @@ const { TaskPriority, } = require('ReactPriorityLevel'); +const {Done, priorityToExpirationTime} = require('ReactFiberExpirationTime'); + const {ClassComponent, HostRoot} = require('ReactTypeOfWork'); const invariant = require('fbjs/lib/invariant'); @@ -35,8 +38,9 @@ type PartialState = // Callbacks are not validated until invocation type Callback = mixed; -type Update = { +export type Update = { priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, partialState: PartialState, callback: Callback | null, isReplace: boolean, @@ -104,6 +108,7 @@ function createUpdateQueue(): UpdateQueue { function cloneUpdate(update: Update): Update { return { priorityLevel: update.priorityLevel, + expirationTime: update.expirationTime, partialState: update.partialState, callback: update.callback, isReplace: update.isReplace, @@ -113,19 +118,41 @@ function cloneUpdate(update: Update): Update { }; } +const COALESCENCE_THRESHOLD: ExpirationTime = 10; + function insertUpdateIntoQueue( queue: UpdateQueue, update: Update, insertAfter: Update | null, insertBefore: Update | null, + currentTime: ExpirationTime, ) { + const priorityLevel = update.priorityLevel; + + let coalescedTime: ExpirationTime | null = null; if (insertAfter !== null) { insertAfter.next = update; + // If we receive multiple updates to the same fiber at the same priority + // level, we coalesce them by assigning the same expiration time, so that + // they all flush at the same time. Because this causes an interruption, it + // could lead to starvation, so we stop coalescing once the time until the + // expiration time reaches a certain threshold. + if (insertAfter !== null && insertAfter.priorityLevel === priorityLevel) { + const expirationTime = insertAfter.expirationTime; + if (expirationTime - currentTime > COALESCENCE_THRESHOLD) { + coalescedTime = expirationTime; + } + } } else { // This is the first item in the queue. update.next = queue.first; queue.first = update; } + update.expirationTime = coalescedTime !== null + ? coalescedTime + : // If we don't coalesce, calculate the expiration time using the + // current time. + priorityToExpirationTime(currentTime, priorityLevel); if (insertBefore !== null) { update.next = insertBefore; @@ -213,7 +240,11 @@ function ensureUpdateQueues(fiber: Fiber) { // we shouldn't make a copy. // // If the update is cloned, it returns the cloned update. -function insertUpdate(fiber: Fiber, update: Update): Update | null { +function insertUpdate( + fiber: Fiber, + update: Update, + currentTime: ExpirationTime, +): Update | null { // We'll have at least one and at most two distinct update queues. ensureUpdateQueues(fiber); const queue1 = _queue1; @@ -240,7 +271,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { if (queue2 === null) { // If there's no alternate queue, there's nothing else to do but insert. - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1); + insertUpdateIntoQueue( + queue1, + update, + insertAfter1, + insertBefore1, + currentTime, + ); return null; } @@ -252,7 +289,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { // Now we can insert into the first queue. This must come after finding both // insertion positions because it mutates the list. - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1); + insertUpdateIntoQueue( + queue1, + update, + insertAfter1, + insertBefore1, + currentTime, + ); // See if the insertion positions are equal. Be careful to only compare // non-null values. @@ -275,7 +318,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { // The insertion positions are different, so we need to clone the update and // insert the clone into the alternate queue. const update2 = cloneUpdate(update); - insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2); + insertUpdateIntoQueue( + queue2, + update2, + insertAfter2, + insertBefore2, + currentTime, + ); return update2; } } @@ -285,9 +334,11 @@ function addUpdate( partialState: PartialState | null, callback: mixed, priorityLevel: PriorityLevel, + currentTime: ExpirationTime, ): void { const update = { priorityLevel, + expirationTime: Done, partialState, callback, isReplace: false, @@ -295,7 +346,7 @@ function addUpdate( isTopLevelUnmount: false, next: null, }; - insertUpdate(fiber, update); + insertUpdate(fiber, update, currentTime); } exports.addUpdate = addUpdate; @@ -304,9 +355,11 @@ function addReplaceUpdate( state: any | null, callback: Callback | null, priorityLevel: PriorityLevel, + currentTime: ExpirationTime, ): void { const update = { priorityLevel, + expirationTime: Done, partialState: state, callback, isReplace: true, @@ -314,7 +367,7 @@ function addReplaceUpdate( isTopLevelUnmount: false, next: null, }; - insertUpdate(fiber, update); + insertUpdate(fiber, update, currentTime); } exports.addReplaceUpdate = addReplaceUpdate; @@ -322,9 +375,11 @@ function addForceUpdate( fiber: Fiber, callback: Callback | null, priorityLevel: PriorityLevel, + currentTime: ExpirationTime, ): void { const update = { priorityLevel, + expirationTime: Done, partialState: null, callback, isReplace: false, @@ -332,7 +387,7 @@ function addForceUpdate( isTopLevelUnmount: false, next: null, }; - insertUpdate(fiber, update); + insertUpdate(fiber, update, currentTime); } exports.addForceUpdate = addForceUpdate; @@ -353,11 +408,13 @@ function addTopLevelUpdate( partialState: PartialState, callback: Callback | null, priorityLevel: PriorityLevel, + currentTime: ExpirationTime, ): void { const isTopLevelUnmount = partialState.element === null; const update = { priorityLevel, + expirationTime: Done, partialState, callback, isReplace: false, @@ -365,7 +422,7 @@ function addTopLevelUpdate( isTopLevelUnmount, next: null, }; - const update2 = insertUpdate(fiber, update); + const update2 = insertUpdate(fiber, update, currentTime); if (isTopLevelUnmount) { // TODO: Redesign the top-level mount/update/unmount API to avoid this diff --git a/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js b/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js index 7c00d20f50f7..60416632715f 100644 --- a/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js @@ -46,6 +46,9 @@ describe('ReactFiberHostContext', () => { appendChildToContainer: function() { return null; }, + now: function() { + return 0; + }, useSyncScheduling: true, }); diff --git a/src/renderers/testing/ReactTestRendererFiberEntry.js b/src/renderers/testing/ReactTestRendererFiberEntry.js index 6779c0dc3662..2a6d9e1ea576 100644 --- a/src/renderers/testing/ReactTestRendererFiberEntry.js +++ b/src/renderers/testing/ReactTestRendererFiberEntry.js @@ -247,6 +247,11 @@ var TestRenderer = ReactFiberReconciler({ useSyncScheduling: true, getPublicInstance, + + now(): number { + // Test renderer does not use expiration + return 0; + }, }); var defaultTestOptions = { From 3e31bcf92c38898434befdb87af4f6dd88456d56 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 10 Aug 2017 14:48:23 -0700 Subject: [PATCH 02/10] Replace pendingWorkPriority with expiration times Instead of a priority, a fiber has an expiration time that represents a point in the future by which it should render. Pending updates still have priorities so that they can be coalesced. We use a host config method to read the current time. This commit implements everything except that method, which currently returns a constant value. So this just proves that expiration times work the same as priorities when time is frozen. Subsequent commits will show the effect of advancing time. --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 33 +- src/renderers/noop/ReactNoopEntry.js | 6 +- src/renderers/shared/fiber/ReactChildFiber.js | 214 +++++++----- src/renderers/shared/fiber/ReactFiber.js | 37 +- .../shared/fiber/ReactFiberBeginWork.js | 131 ++++--- .../shared/fiber/ReactFiberClassComponent.js | 63 +++- .../shared/fiber/ReactFiberCompleteWork.js | 23 +- .../shared/fiber/ReactFiberExpirationTime.js | 17 +- .../shared/fiber/ReactFiberReconciler.js | 16 +- .../shared/fiber/ReactFiberScheduler.js | 326 ++++++++++-------- .../shared/fiber/ReactFiberUpdateQueue.js | 94 ++--- 11 files changed, 566 insertions(+), 394 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 6076c61fcc19..5dbda35eb0b3 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -163,20 +163,20 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean { } // TODO: Better polyfill -let now; -if ( - typeof window !== 'undefined' && - window.performance && - typeof window.performance.now === 'function' -) { - now = function() { - return performance.now(); - }; -} else { - now = function() { - return Date.now(); - }; -} +// let now; +// if ( +// typeof window !== 'undefined' && +// window.performance && +// typeof window.performance.now === 'function' +// ) { +// now = function() { +// return performance.now(); +// }; +// } else { +// now = function() { +// return Date.now(); +// }; +// } var DOMRenderer = ReactFiberReconciler({ getRootHostContext(rootContainerInstance: Container): HostContext { @@ -453,7 +453,10 @@ var DOMRenderer = ReactFiberReconciler({ } }, - now: now, + now() { + // TODO: Use performance.now to enable expiration + return 0; + }, canHydrateInstance( instance: Instance | TextInstance, diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index 9a2dcadaa315..afd4eac99d19 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -405,7 +405,7 @@ var ReactNoop = { ' '.repeat(depth + 1) + '~', firstUpdate && firstUpdate.partialState, firstUpdate.callback ? 'with callback' : '', - '[' + firstUpdate.priorityLevel + ']', + '[' + firstUpdate.expirationTime + ']', ); var next; while ((next = firstUpdate.next)) { @@ -413,7 +413,7 @@ var ReactNoop = { ' '.repeat(depth + 1) + '~', next.partialState, next.callback ? 'with callback' : '', - '[' + firstUpdate.priorityLevel + ']', + '[' + firstUpdate.expirationTime + ']', ); } } @@ -423,7 +423,7 @@ var ReactNoop = { ' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'), - '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']', + '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']', ); if (fiber.updateQueue) { logUpdateQueue(fiber.updateQueue, depth); diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 763ee6d9f3c9..935b897b3ca1 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -13,7 +13,7 @@ import type {ReactElement} from 'ReactElementType'; import type {ReactCoroutine, ReactPortal, ReactYield} from 'ReactTypes'; import type {Fiber} from 'ReactFiber'; -import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; var {REACT_COROUTINE_TYPE, REACT_YIELD_TYPE} = require('ReactCoroutine'); var {REACT_PORTAL_TYPE} = require('ReactPortal'); @@ -288,19 +288,19 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return existingChildren; } - function useFiber(fiber: Fiber, priority: PriorityLevel): Fiber { + function useFiber(fiber: Fiber, expirationTime: ExpirationTime): Fiber { // We currently set sibling to null and index to 0 here because it is easy // to forget to do before returning it. E.g. for the single child case. if (shouldClone) { - const clone = createWorkInProgress(fiber, priority); + const clone = createWorkInProgress(fiber, expirationTime); clone.index = 0; clone.sibling = null; return clone; } else { - // We override the pending priority even if it is higher, because if - // we're reconciling at a lower priority that means that this was + // We override the expiration time even if it is earlier, because if + // we're reconciling at a later time that means that this was // down-prioritized. - fiber.pendingWorkPriority = priority; + fiber.expirationTime = expirationTime; fiber.effectTag = NoEffect; fiber.index = 0; fiber.sibling = null; @@ -349,20 +349,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, textContent: string, - priority: PriorityLevel, + expirationTime: ExpirationTime, ) { if (current === null || current.tag !== HostText) { // Insert const created = createFiberFromText( textContent, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = textContent; existing.return = returnFiber; return existing; @@ -373,21 +373,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, element: ReactElement, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if (current === null || current.type !== element.type) { // Insert const created = createFiberFromElement( element, returnFiber.internalContextTag, - priority, + expirationTime, ); created.ref = coerceRef(current, element); created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.ref = coerceRef(current, element); existing.pendingProps = element.props; existing.return = returnFiber; @@ -403,7 +403,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, coroutine: ReactCoroutine, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { // TODO: Should this also compare handler to determine whether to reuse? if (current === null || current.tag !== CoroutineComponent) { @@ -411,13 +411,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( coroutine, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = coroutine; existing.return = returnFiber; return existing; @@ -428,21 +428,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, yieldNode: ReactYield, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if (current === null || current.tag !== YieldComponent) { // Insert const created = createFiberFromYield( yieldNode, returnFiber.internalContextTag, - priority, + expirationTime, ); created.type = yieldNode.value; created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.type = yieldNode.value; existing.return = returnFiber; return existing; @@ -453,7 +453,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, portal: ReactPortal, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if ( current === null || @@ -465,13 +465,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( portal, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = portal.children || []; existing.return = returnFiber; return existing; @@ -482,20 +482,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, fragment: Iterable<*>, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if (current === null || current.tag !== Fragment) { // Insert const created = createFiberFromFragment( fragment, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = fragment; existing.return = returnFiber; return existing; @@ -505,7 +505,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { function createChild( returnFiber: Fiber, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes doesn't have keys. If the previous node is implicitly keyed @@ -514,7 +514,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromText( '' + newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -526,7 +526,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromElement( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.ref = coerceRef(null, newChild); created.return = returnFiber; @@ -537,7 +537,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -547,7 +547,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromYield( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.type = newChild.value; created.return = returnFiber; @@ -558,7 +558,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -569,7 +569,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromFragment( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -591,7 +591,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { // Update the fiber if the keys match, otherwise return null. @@ -604,14 +604,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (key !== null) { return null; } - return updateTextNode(returnFiber, oldFiber, '' + newChild, priority); + return updateTextNode( + returnFiber, + oldFiber, + '' + newChild, + expirationTime, + ); } if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { if (newChild.key === key) { - return updateElement(returnFiber, oldFiber, newChild, priority); + return updateElement( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); } else { return null; } @@ -619,7 +629,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { case REACT_COROUTINE_TYPE: { if (newChild.key === key) { - return updateCoroutine(returnFiber, oldFiber, newChild, priority); + return updateCoroutine( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); } else { return null; } @@ -630,7 +645,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // we can continue to replace it without aborting even if it is not a // yield. if (key === null) { - return updateYield(returnFiber, oldFiber, newChild, priority); + return updateYield(returnFiber, oldFiber, newChild, expirationTime); } else { return null; } @@ -638,7 +653,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { case REACT_PORTAL_TYPE: { if (newChild.key === key) { - return updatePortal(returnFiber, oldFiber, newChild, priority); + return updatePortal( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); } else { return null; } @@ -651,7 +671,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (key !== null) { return null; } - return updateFragment(returnFiber, oldFiber, newChild, priority); + return updateFragment(returnFiber, oldFiber, newChild, expirationTime); } throwOnInvalidObjectType(returnFiber, newChild); @@ -671,13 +691,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, newIdx: number, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes doesn't have keys, so we neither have to check the old nor // new node for the key. If both are text nodes, they match. const matchedFiber = existingChildren.get(newIdx) || null; - return updateTextNode(returnFiber, matchedFiber, '' + newChild, priority); + return updateTextNode( + returnFiber, + matchedFiber, + '' + newChild, + expirationTime, + ); } if (typeof newChild === 'object' && newChild !== null) { @@ -687,7 +712,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updateElement(returnFiber, matchedFiber, newChild, priority); + return updateElement( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } case REACT_COROUTINE_TYPE: { @@ -695,14 +725,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updateCoroutine(returnFiber, matchedFiber, newChild, priority); + return updateCoroutine( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } case REACT_YIELD_TYPE: { // Yields doesn't have keys, so we neither have to check the old nor // new node for the key. If both are yields, they match. const matchedFiber = existingChildren.get(newIdx) || null; - return updateYield(returnFiber, matchedFiber, newChild, priority); + return updateYield( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } case REACT_PORTAL_TYPE: { @@ -710,13 +750,23 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updatePortal(returnFiber, matchedFiber, newChild, priority); + return updatePortal( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } } if (isArray(newChild) || getIteratorFn(newChild)) { const matchedFiber = existingChildren.get(newIdx) || null; - return updateFragment(returnFiber, matchedFiber, newChild, priority); + return updateFragment( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } throwOnInvalidObjectType(returnFiber, newChild); @@ -782,7 +832,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<*>, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { // This algorithm can't optimize by searching from boths ends since we // don't have backpointers on fibers. I'm trying to see how far we can get @@ -830,7 +880,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, oldFiber, newChildren[newIdx], - priority, + expirationTime, ); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's @@ -877,7 +927,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const newFiber = createChild( returnFiber, newChildren[newIdx], - priority, + expirationTime, ); if (!newFiber) { continue; @@ -904,7 +954,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, newIdx, newChildren[newIdx], - priority, + expirationTime, ); if (newFiber) { if (shouldTrackSideEffects) { @@ -941,7 +991,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChildrenIterable: Iterable<*>, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { // This is the same implementation as reconcileChildrenArray(), // but using the iterator instead. @@ -1005,7 +1055,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } else { nextOldFiber = oldFiber.sibling; } - const newFiber = updateSlot(returnFiber, oldFiber, step.value, priority); + const newFiber = updateSlot( + returnFiber, + oldFiber, + step.value, + expirationTime, + ); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's // unfortunate because it triggers the slow path all the time. We need @@ -1048,7 +1103,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // If we don't have any more existing children we can choose a fast path // since the rest will all be insertions. for (; !step.done; newIdx++, (step = newChildren.next())) { - const newFiber = createChild(returnFiber, step.value, priority); + const newFiber = createChild(returnFiber, step.value, expirationTime); if (newFiber === null) { continue; } @@ -1074,7 +1129,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, newIdx, step.value, - priority, + expirationTime, ); if (newFiber !== null) { if (shouldTrackSideEffects) { @@ -1111,7 +1166,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, textContent: string, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { // There's no need to check for keys on text nodes since we don't have a // way to define them. @@ -1119,7 +1174,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // We already have an existing node so let's just update it and delete // the rest. deleteRemainingChildren(returnFiber, currentFirstChild.sibling); - const existing = useFiber(currentFirstChild, priority); + const existing = useFiber(currentFirstChild, expirationTime); existing.pendingProps = textContent; existing.return = returnFiber; return existing; @@ -1130,7 +1185,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromText( textContent, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -1140,7 +1195,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const key = element.key; let child = currentFirstChild; @@ -1150,7 +1205,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (child.key === key) { if (child.type === element.type) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, expirationTime); existing.ref = coerceRef(child, element); existing.pendingProps = element.props; existing.return = returnFiber; @@ -1172,7 +1227,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromElement( element, returnFiber.internalContextTag, - priority, + expirationTime, ); created.ref = coerceRef(currentFirstChild, element); created.return = returnFiber; @@ -1183,7 +1238,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, coroutine: ReactCoroutine, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const key = coroutine.key; let child = currentFirstChild; @@ -1193,7 +1248,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (child.key === key) { if (child.tag === CoroutineComponent) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, expirationTime); existing.pendingProps = coroutine; existing.return = returnFiber; return existing; @@ -1210,7 +1265,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( coroutine, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -1220,14 +1275,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, yieldNode: ReactYield, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { // There's no need to check for keys on yields since they're stateless. let child = currentFirstChild; if (child !== null) { if (child.tag === YieldComponent) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, expirationTime); existing.type = yieldNode.value; existing.return = returnFiber; return existing; @@ -1239,7 +1294,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromYield( yieldNode, returnFiber.internalContextTag, - priority, + expirationTime, ); created.type = yieldNode.value; created.return = returnFiber; @@ -1250,7 +1305,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, portal: ReactPortal, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const key = portal.key; let child = currentFirstChild; @@ -1264,7 +1319,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { child.stateNode.implementation === portal.implementation ) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, expirationTime); existing.pendingProps = portal.children || []; existing.return = returnFiber; return existing; @@ -1281,7 +1336,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( portal, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -1294,7 +1349,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { // This function is not recursive. // If the top level item is an array, we treat it as a set of children, @@ -1304,8 +1359,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // Handle object types const isObject = typeof newChild === 'object' && newChild !== null; if (isObject) { - // Support only the subset of return types that Stack supports. Treat - // everything else as empty, but log a warning. switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild( @@ -1313,7 +1366,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); @@ -1323,17 +1376,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); - case REACT_YIELD_TYPE: return placeSingleChild( reconcileSingleYield( returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); @@ -1343,7 +1395,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); } @@ -1355,7 +1407,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, '' + newChild, - priority, + expirationTime, ), ); } @@ -1365,7 +1417,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ); } @@ -1374,7 +1426,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ); } @@ -1446,7 +1498,7 @@ exports.cloneChildFibers = function( let currentChild = workInProgress.child; let newChild = createWorkInProgress( currentChild, - currentChild.pendingWorkPriority, + currentChild.expirationTime, ); // TODO: Pass this as an argument, since it's easy to forget. newChild.pendingProps = currentChild.pendingProps; @@ -1457,7 +1509,7 @@ exports.cloneChildFibers = function( currentChild = currentChild.sibling; newChild = newChild.sibling = createWorkInProgress( currentChild, - currentChild.pendingWorkPriority, + currentChild.expirationTime, ); newChild.pendingProps = currentChild.pendingProps; newChild.return = workInProgress; diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index dae77d55cc79..faf40a2d6526 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -21,6 +21,7 @@ import type {TypeOfWork} from 'ReactTypeOfWork'; import type {TypeOfInternalContext} from 'ReactTypeOfInternalContext'; import type {TypeOfSideEffect} from 'ReactTypeOfSideEffect'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; import type {UpdateQueue} from 'ReactFiberUpdateQueue'; var { @@ -37,6 +38,8 @@ var { var {NoWork} = require('ReactPriorityLevel'); +var {Done} = require('ReactFiberExpirationTime'); + var {NoContext} = require('ReactTypeOfInternalContext'); var {NoEffect} = require('ReactTypeOfSideEffect'); @@ -134,8 +137,9 @@ export type Fiber = {| firstEffect: Fiber | null, lastEffect: Fiber | null, - // This will be used to quickly determine if a subtree has no pending changes. - pendingWorkPriority: PriorityLevel, + // Represents a time in the future by which this work should be completed. + // This is also used to quickly determine if a subtree has no pending changes. + expirationTime: ExpirationTime, // This is a pooled version of a Fiber. Every fiber that gets updated will // eventually have a pair. There are cases when we can clean up pairs to save @@ -189,7 +193,7 @@ function FiberNode( this.firstEffect = null; this.lastEffect = null; - this.pendingWorkPriority = NoWork; + this.expirationTime = Done; this.alternate = null; @@ -233,7 +237,7 @@ function shouldConstruct(Component) { // This is used to create an alternate fiber to do work on. exports.createWorkInProgress = function( current: Fiber, - renderPriority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { let workInProgress = current.alternate; if (workInProgress === null) { @@ -270,7 +274,7 @@ exports.createWorkInProgress = function( workInProgress.lastEffect = null; } - workInProgress.pendingWorkPriority = renderPriority; + workInProgress.expirationTime = expirationTime; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; @@ -296,7 +300,7 @@ exports.createHostRootFiber = function(): Fiber { exports.createFiberFromElement = function( element: ReactElement, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { let owner = null; if (__DEV__) { @@ -310,7 +314,7 @@ exports.createFiberFromElement = function( owner, ); fiber.pendingProps = element.props; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; if (__DEV__) { fiber._debugSource = element._source; @@ -323,24 +327,24 @@ exports.createFiberFromElement = function( exports.createFiberFromFragment = function( elements: ReactFragment, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { // TODO: Consider supporting keyed fragments. Technically, we accidentally // support that in the existing React. const fiber = createFiber(Fragment, null, internalContextTag); fiber.pendingProps = elements; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; return fiber; }; exports.createFiberFromText = function( content: string, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber(HostText, null, internalContextTag); fiber.pendingProps = content; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; return fiber; }; @@ -411,7 +415,7 @@ exports.createFiberFromHostInstanceForDeletion = function(): Fiber { exports.createFiberFromCoroutine = function( coroutine: ReactCoroutine, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber( CoroutineComponent, @@ -420,27 +424,28 @@ exports.createFiberFromCoroutine = function( ); fiber.type = coroutine.handler; fiber.pendingProps = coroutine; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; return fiber; }; exports.createFiberFromYield = function( yieldNode: ReactYield, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber(YieldComponent, null, internalContextTag); + fiber.expirationTime = expirationTime; return fiber; }; exports.createFiberFromPortal = function( portal: ReactPortal, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber(HostPortal, portal.key, internalContextTag); fiber.pendingProps = portal.children || []; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; fiber.stateNode = { containerInfo: portal.containerInfo, implementation: portal.implementation, diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index d308973277e4..5d49bf7e89dd 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -48,7 +48,7 @@ var { YieldComponent, Fragment, } = ReactTypeOfWork; -var {NoWork, OffscreenPriority} = require('ReactPriorityLevel'); +var {Done, Never} = require('ReactFiberExpirationTime'); var { PerformedWork, Placement, @@ -72,9 +72,16 @@ module.exports = function( config: HostConfig, hostContext: HostContext, hydrationContext: HydrationContext, - scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, - getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, + scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void, + getPriorityContext: ( + fiber: Fiber, + forceAsync: boolean, + ) => PriorityLevel | null, recalculateCurrentTime: () => ExpirationTime, + getExpirationTimeForPriority: ( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel | null, + ) => ExpirationTime, ) { const { shouldSetTextContent, @@ -102,23 +109,24 @@ module.exports = function( memoizeProps, memoizeState, recalculateCurrentTime, + getExpirationTimeForPriority, ); + // TODO: Remove this and use reconcileChildrenAtExpirationTime directly. function reconcileChildren(current, workInProgress, nextChildren) { - const priorityLevel = workInProgress.pendingWorkPriority; - reconcileChildrenAtPriority( + reconcileChildrenAtExpirationTime( current, workInProgress, nextChildren, - priorityLevel, + workInProgress.expirationTime, ); } - function reconcileChildrenAtPriority( + function reconcileChildrenAtExpirationTime( current, workInProgress, nextChildren, - priorityLevel, + renderExpirationTime, ) { if (current === null) { // If this is a fresh new component that hasn't been rendered yet, we @@ -129,7 +137,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); } else if (current.child === workInProgress.child) { // If the current child is the same as the work in progress, it means that @@ -142,7 +150,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); } else { // If, on the other hand, it is already using a clone, that means we've @@ -152,7 +160,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); } } @@ -226,7 +234,7 @@ module.exports = function( function updateClassComponent( current: Fiber | null, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ) { // Push context providers early to prevent context stack mismatches. // During mounting we don't know the child context yet as the instance doesn't exist. @@ -238,18 +246,18 @@ module.exports = function( if (!workInProgress.stateNode) { // In the initial pass we might need to construct the instance. constructClassInstance(workInProgress, workInProgress.pendingProps); - mountClassInstance(workInProgress, priorityLevel); + mountClassInstance(workInProgress, renderExpirationTime); shouldUpdate = true; } else { invariant(false, 'Resuming work not yet implemented.'); // In a resume, we'll already have an instance we can reuse. - // shouldUpdate = resumeMountClassInstance(workInProgress, priorityLevel); + // shouldUpdate = resumeMountClassInstance(workInProgress, renderExpirationTime); } } else { shouldUpdate = updateClassInstance( current, workInProgress, - priorityLevel, + renderExpirationTime, ); } return finishClassComponent( @@ -321,7 +329,8 @@ module.exports = function( pushHostContainer(workInProgress, root.containerInfo); } - function updateHostRoot(current, workInProgress, priorityLevel) { + function updateHostRoot(current, workInProgress, renderExpirationTime) { + const root = (workInProgress.stateNode: FiberRoot); pushHostRootContext(workInProgress); const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { @@ -333,11 +342,11 @@ module.exports = function( null, prevState, null, - priorityLevel, + renderExpirationTime, ); if (prevState === state) { // If the state is the same as before, that's a bailout because we had - // no work matching this priority. + // no work that expires at this time. resetHydrationState(); return bailoutOnAlreadyFinishedWork(current, workInProgress); } @@ -364,7 +373,7 @@ module.exports = function( workInProgress, workInProgress.child, element, - priorityLevel, + renderExpirationTime, ); } else { // Otherwise reset hydration state in case we aborted and resumed another @@ -380,7 +389,7 @@ module.exports = function( return bailoutOnAlreadyFinishedWork(current, workInProgress); } - function updateHostComponent(current, workInProgress, renderPriority) { + function updateHostComponent(current, workInProgress, renderExpirationTime) { pushHostContext(workInProgress); if (current === null) { @@ -426,13 +435,13 @@ module.exports = function( // Check the host config to see if the children are offscreen/hidden. if ( - renderPriority !== OffscreenPriority && + renderExpirationTime !== Never && !useSyncScheduling && shouldDeprioritizeSubtree(type, nextProps) ) { // Down-prioritize the children. - workInProgress.pendingWorkPriority = OffscreenPriority; - // Bailout and come back to this fiber later at OffscreenPriority. + workInProgress.expirationTime = Never; + // Bailout and come back to this fiber later. return null; } @@ -455,7 +464,11 @@ module.exports = function( return null; } - function mountIndeterminateComponent(current, workInProgress, priorityLevel) { + function mountIndeterminateComponent( + current, + workInProgress, + renderExpirationTime, + ) { invariant( current === null, 'An indeterminate component should never have mounted. This error is ' + @@ -490,7 +503,7 @@ module.exports = function( // We will invalidate the child context in finishClassComponent() right after rendering. const hasContext = pushContextProvider(workInProgress); adoptClassInstance(workInProgress, value); - mountClassInstance(workInProgress, priorityLevel); + mountClassInstance(workInProgress, renderExpirationTime); return finishClassComponent(current, workInProgress, true, hasContext); } else { // Proceed under the assumption that this is a functional component @@ -535,7 +548,11 @@ module.exports = function( } } - function updateCoroutineComponent(current, workInProgress) { + function updateCoroutineComponent( + current, + workInProgress, + renderExpirationTime, + ) { var nextCoroutine = (workInProgress.pendingProps: null | ReactCoroutine); if (hasContextChanged()) { // Normally we can bail out on props equality but if context has changed @@ -559,30 +576,29 @@ module.exports = function( } const nextChildren = nextCoroutine.children; - const priorityLevel = workInProgress.pendingWorkPriority; - // The following is a fork of reconcileChildrenAtPriority but using + // The following is a fork of reconcileChildrenAtExpirationTime but using // stateNode to store the child. if (current === null) { workInProgress.stateNode = mountChildFibersInPlace( workInProgress, workInProgress.stateNode, nextChildren, - priorityLevel, + renderExpirationTime, ); } else if (current.child === workInProgress.child) { workInProgress.stateNode = reconcileChildFibers( workInProgress, workInProgress.stateNode, nextChildren, - priorityLevel, + renderExpirationTime, ); } else { workInProgress.stateNode = reconcileChildFibersInPlace( workInProgress, workInProgress.stateNode, nextChildren, - priorityLevel, + renderExpirationTime, ); } @@ -592,9 +608,12 @@ module.exports = function( return workInProgress.stateNode; } - function updatePortalComponent(current, workInProgress) { + function updatePortalComponent( + current, + workInProgress, + renderExpirationTime, + ) { pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); - const priorityLevel = workInProgress.pendingWorkPriority; let nextChildren = workInProgress.pendingProps; if (hasContextChanged()) { // Normally we can bail out on props equality but if context has changed @@ -624,7 +643,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); memoizeProps(workInProgress, nextChildren); } else { @@ -719,11 +738,11 @@ module.exports = function( function beginWork( current: Fiber | null, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): Fiber | null { if ( - workInProgress.pendingWorkPriority === NoWork || - workInProgress.pendingWorkPriority > priorityLevel + workInProgress.expirationTime === Done || + workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); } @@ -733,16 +752,24 @@ module.exports = function( return mountIndeterminateComponent( current, workInProgress, - priorityLevel, + renderExpirationTime, ); case FunctionalComponent: return updateFunctionalComponent(current, workInProgress); case ClassComponent: - return updateClassComponent(current, workInProgress, priorityLevel); + return updateClassComponent( + current, + workInProgress, + renderExpirationTime, + ); case HostRoot: - return updateHostRoot(current, workInProgress, priorityLevel); + return updateHostRoot(current, workInProgress, renderExpirationTime); case HostComponent: - return updateHostComponent(current, workInProgress, priorityLevel); + return updateHostComponent( + current, + workInProgress, + renderExpirationTime, + ); case HostText: return updateHostText(current, workInProgress); case CoroutineHandlerPhase: @@ -750,13 +777,21 @@ module.exports = function( workInProgress.tag = CoroutineComponent; // Intentionally fall through since this is now the same. case CoroutineComponent: - return updateCoroutineComponent(current, workInProgress); + return updateCoroutineComponent( + current, + workInProgress, + renderExpirationTime, + ); case YieldComponent: // A yield component is just a placeholder, we can just run through the // next one immediately. return null; case HostPortal: - return updatePortalComponent(current, workInProgress); + return updatePortalComponent( + current, + workInProgress, + renderExpirationTime, + ); case Fragment: return updateFragment(current, workInProgress); default: @@ -771,7 +806,7 @@ module.exports = function( function beginFailedWork( current: Fiber | null, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ) { // Push context providers here to avoid a push/pop context mismatch. switch (workInProgress.tag) { @@ -804,8 +839,8 @@ module.exports = function( } if ( - workInProgress.pendingWorkPriority === NoWork || - workInProgress.pendingWorkPriority > priorityLevel + workInProgress.expirationTime === Done || + workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); } @@ -817,11 +852,11 @@ module.exports = function( // Unmount the current children as if the component rendered null const nextChildren = null; - reconcileChildrenAtPriority( + reconcileChildrenAtExpirationTime( current, workInProgress, nextChildren, - priorityLevel, + renderExpirationTime, ); if (workInProgress.tag === ClassComponent) { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 4c72e6236b7c..62f35742b025 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -78,11 +78,18 @@ if (__DEV__) { } module.exports = function( - scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, - getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, + scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void, + getPriorityContext: ( + fiber: Fiber, + forceAsync: boolean, + ) => PriorityLevel | null, memoizeProps: (workInProgress: Fiber, props: any) => void, memoizeState: (workInProgress: Fiber, state: any) => void, recalculateCurrentTime: () => ExpirationTime, + getExpirationTimeForPriority: ( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel | null, + ) => ExpirationTime, ) { // Class component state updater const updater = { @@ -91,34 +98,66 @@ module.exports = function( const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - addUpdate(fiber, partialState, callback, priorityLevel, currentTime); - scheduleUpdate(fiber, priorityLevel); + addUpdate( + fiber, + partialState, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(fiber, expirationTime); }, enqueueReplaceState(instance, state, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'replaceState'); } - addReplaceUpdate(fiber, state, callback, priorityLevel, currentTime); - scheduleUpdate(fiber, priorityLevel); + addReplaceUpdate( + fiber, + state, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(fiber, expirationTime); }, enqueueForceUpdate(instance, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'forceUpdate'); } - addForceUpdate(fiber, callback, priorityLevel, currentTime); - scheduleUpdate(fiber, priorityLevel); + addForceUpdate( + fiber, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(fiber, expirationTime); }, }; @@ -388,7 +427,7 @@ module.exports = function( // Invokes the mount life-cycles on a previously never rendered instance. function mountClassInstance( workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): void { const current = workInProgress.alternate; @@ -435,7 +474,7 @@ module.exports = function( instance, state, props, - priorityLevel, + renderExpirationTime, ); } } @@ -553,7 +592,7 @@ module.exports = function( function updateClassInstance( current: Fiber, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): boolean { const instance = workInProgress.stateNode; resetInputPointers(workInProgress, instance); @@ -602,7 +641,7 @@ module.exports = function( instance, oldState, newProps, - priorityLevel, + renderExpirationTime, ); } else { newState = oldState; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 451e379965da..edb6048e9133 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -12,7 +12,7 @@ import type {ReactCoroutine} from 'ReactTypes'; import type {Fiber} from 'ReactFiber'; -import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; import type {HostContext} from 'ReactFiberHostContext'; import type {HydrationContext} from 'ReactFiberHydrationContext'; import type {FiberRoot} from 'ReactFiberRoot'; @@ -25,7 +25,7 @@ var { } = require('ReactFiberContext'); var ReactTypeOfWork = require('ReactTypeOfWork'); var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); -var ReactPriorityLevel = require('ReactPriorityLevel'); +var ReactFiberExpirationTime = require('ReactFiberExpirationTime'); var { IndeterminateComponent, FunctionalComponent, @@ -40,7 +40,7 @@ var { Fragment, } = ReactTypeOfWork; var {Placement, Ref, Update} = ReactTypeOfSideEffect; -var {OffscreenPriority} = ReactPriorityLevel; +var {Never} = ReactFiberExpirationTime; var invariant = require('fbjs/lib/invariant'); @@ -113,6 +113,7 @@ module.exports = function( function moveCoroutineToHandlerPhase( current: Fiber | null, workInProgress: Fiber, + renderExpirationTime: ExpirationTime, ) { var coroutine = (workInProgress.memoizedProps: ?ReactCoroutine); invariant( @@ -139,13 +140,11 @@ module.exports = function( var nextChildren = fn(props, yields); var currentFirstChild = current !== null ? current.child : null; - // Inherit the priority of the returnFiber. - const priority = workInProgress.pendingWorkPriority; workInProgress.child = reconcileChildFibers( workInProgress, currentFirstChild, nextChildren, - priority, + renderExpirationTime, ); return workInProgress.child; } @@ -181,15 +180,15 @@ module.exports = function( function completeWork( current: Fiber | null, workInProgress: Fiber, - renderPriority: PriorityLevel, + renderExpirationTime: ExpirationTime, ): Fiber | null { // Get the latest props. let newProps = workInProgress.pendingProps; if (newProps === null) { newProps = workInProgress.memoizedProps; } else if ( - workInProgress.pendingWorkPriority !== OffscreenPriority || - renderPriority === OffscreenPriority + workInProgress.expirationTime !== Never || + renderExpirationTime === Never ) { // Reset the pending props, unless this was a down-prioritization. workInProgress.pendingProps = null; @@ -358,7 +357,11 @@ module.exports = function( return null; } case CoroutineComponent: - return moveCoroutineToHandlerPhase(current, workInProgress); + return moveCoroutineToHandlerPhase( + current, + workInProgress, + renderExpirationTime, + ); case CoroutineHandlerPhase: // Reset the tag to now be a first phase coroutine. workInProgress.tag = CoroutineComponent; diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index 1f5d9c42c4e0..e520995a7e9e 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -28,13 +28,15 @@ export type ExpirationTime = number; const Done = 0; exports.Done = Done; +const MAGIC_NUMBER_OFFSET = 2; + const Never = Number.MAX_SAFE_INTEGER; exports.Never = Never; // 1 unit of expiration time represents 10ms. function msToExpirationTime(ms: number): ExpirationTime { - // Always add 1 so that we don't clash with the magic number for Done. - return Math.round(ms / 10) + 1; + // Always add an offset so that we don't clash with the magic number for Done. + return Math.round(ms / 10) + MAGIC_NUMBER_OFFSET; } exports.msToExpirationTime = msToExpirationTime; @@ -56,7 +58,7 @@ function priorityToExpirationTime( return Done; case SynchronousPriority: // Return a number lower than the current time, but higher than Done. - return 1; + return MAGIC_NUMBER_OFFSET - 1; case TaskPriority: // Return the current time, so that this work completes in this batch. return currentTime; @@ -69,6 +71,7 @@ function priorityToExpirationTime( case OffscreenPriority: return Never; default: + console.log(priorityLevel); invariant( false, 'Switch statement should be exhuastive. ' + @@ -102,3 +105,11 @@ function expirationTimeToPriorityLevel( return LowPriority; } exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel; + +function earlierExpirationTime( + t1: ExpirationTime, + t2: ExpirationTime, +): ExpirationTime { + return t1 !== Done && (t2 === Done || t2 > t1) ? t1 : t2; +} +exports.earlierExpirationTime = earlierExpirationTime; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index e20fff429d69..c672fe86a5c5 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -237,6 +237,7 @@ module.exports = function( var { scheduleUpdate, getPriorityContext, + getExpirationTimeForPriority, recalculateCurrentTime, batchedUpdates, unbatchedUpdates, @@ -278,6 +279,10 @@ module.exports = function( (element.type.prototype: any).unstable_isAsyncReactComponent === true; const priorityLevel = getPriorityContext(current, forceAsync); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); const nextState = {element}; callback = callback === undefined ? null : callback; if (__DEV__) { @@ -288,8 +293,15 @@ module.exports = function( callback, ); } - addTopLevelUpdate(current, nextState, callback, priorityLevel, currentTime); - scheduleUpdate(current, priorityLevel); + addTopLevelUpdate( + current, + nextState, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(current, expirationTime); } return { diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 4509708d0fbf..9fcbf2064bbd 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -51,7 +51,7 @@ var ReactFiberHydrationContext = require('ReactFiberHydrationContext'); var {ReactCurrentOwner} = require('ReactGlobalSharedState'); var getComponentName = require('getComponentName'); -var {createWorkInProgress, largerPriority} = require('ReactFiber'); +var {createWorkInProgress} = require('ReactFiber'); var {onCommitRoot} = require('ReactFiberDevToolsHook'); var { @@ -63,7 +63,14 @@ var { OffscreenPriority, } = require('ReactPriorityLevel'); -var {msToExpirationTime} = require('ReactFiberExpirationTime'); +var { + Done, + Never, + msToExpirationTime, + earlierExpirationTime, + priorityToExpirationTime, + expirationTimeToPriorityLevel, +} = require('ReactFiberExpirationTime'); var {AsyncUpdates} = require('ReactTypeOfInternalContext'); @@ -86,7 +93,7 @@ var { ClassComponent, } = require('ReactTypeOfWork'); -var {getUpdatePriority} = require('ReactFiberUpdateQueue'); +var {getUpdateExpirationTime} = require('ReactFiberUpdateQueue'); var {resetContext} = require('ReactFiberContext'); @@ -170,6 +177,7 @@ module.exports = function( scheduleUpdate, getPriorityContext, recalculateCurrentTime, + getExpirationTimeForPriority, ); const {completeWork} = ReactFiberCompleteWork( config, @@ -193,12 +201,10 @@ module.exports = function( } = config; // Represents the current time in ms. - let currentTime: ExpirationTime = msToExpirationTime(now()); + let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(now()); // The priority level to use when scheduling an update. We use NoWork to // represent the default priority. - // TODO: Should we change this to an array instead of using the call stack? - // Might be less confusing. let priorityContext: PriorityLevel = NoWork; // Keeps track of whether we're currently in a work loop. @@ -216,7 +222,8 @@ module.exports = function( // The next work in progress fiber that we're currently working on. let nextUnitOfWork: Fiber | null = null; - let nextPriorityLevel: PriorityLevel = NoWork; + // The time at which we're currently rendering work. + let nextRenderExpirationTime: ExpirationTime = Done; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; @@ -259,14 +266,11 @@ module.exports = function( resetHostContainer(); } - // resetNextUnitOfWork mutates the current priority context. It is reset after - // after the workLoop exits, so never call resetNextUnitOfWork from outside - // the work loop. function resetNextUnitOfWork() { // Clear out roots with no more work on them, or if they have uncaught errors while ( nextScheduledRoot !== null && - nextScheduledRoot.current.pendingWorkPriority === NoWork + nextScheduledRoot.current.expirationTime === Done ) { // Unschedule this root. nextScheduledRoot.isScheduled = false; @@ -278,7 +282,7 @@ module.exports = function( if (nextScheduledRoot === lastScheduledRoot) { nextScheduledRoot = null; lastScheduledRoot = null; - nextPriorityLevel = NoWork; + nextRenderExpirationTime = Done; return null; } // Continue with the next root. @@ -287,22 +291,22 @@ module.exports = function( } let root = nextScheduledRoot; - let highestPriorityRoot = null; - let highestPriorityLevel = NoWork; + let earliestExpirationRoot = null; + let earliestExpirationTime = Done; while (root !== null) { if ( - root.current.pendingWorkPriority !== NoWork && - (highestPriorityLevel === NoWork || - highestPriorityLevel > root.current.pendingWorkPriority) + root.current.expirationTime !== Done && + (earliestExpirationTime === Done || + earliestExpirationTime > root.current.expirationTime) ) { - highestPriorityLevel = root.current.pendingWorkPriority; - highestPriorityRoot = root; + 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 (highestPriorityRoot !== null) { - nextPriorityLevel = highestPriorityLevel; + 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 @@ -311,18 +315,18 @@ module.exports = function( resetContextStack(); nextUnitOfWork = createWorkInProgress( - highestPriorityRoot.current, - highestPriorityLevel, + earliestExpirationRoot.current, + earliestExpirationTime, ); - if (highestPriorityRoot !== nextRenderedTree) { + if (earliestExpirationRoot !== nextRenderedTree) { // We've switched trees. Reset the nested update counter. nestedUpdateCount = 0; - nextRenderedTree = highestPriorityRoot; + nextRenderedTree = earliestExpirationRoot; } return; } - nextPriorityLevel = NoWork; + nextRenderExpirationTime = Done; nextUnitOfWork = null; nextRenderedTree = null; return; @@ -400,7 +404,6 @@ module.exports = function( while (nextEffect !== null) { const effectTag = nextEffect.effectTag; - // Use Task priority for lifecycle updates if (effectTag & (Update | Callback)) { if (__DEV__) { recordEffect(); @@ -454,10 +457,7 @@ module.exports = function( 'in React. Please file an issue.', ); - if ( - nextPriorityLevel === SynchronousPriority || - nextPriorityLevel === TaskPriority - ) { + if (nextRenderExpirationTime <= mostRecentCurrentTime) { // Keep track of the number of iterations to prevent an infinite // update loop. nestedUpdateCount++; @@ -591,35 +591,36 @@ module.exports = function( commitPhaseBoundaries = null; } - // This tree is done. Reset the unit of work pointer to the next highest - // priority root. If there's no more work left, the pointer is set to 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(); } - function resetWorkPriority( + function resetExpirationTime( workInProgress: Fiber, - renderPriority: PriorityLevel, + renderTime: ExpirationTime, ) { - if ( - workInProgress.pendingWorkPriority !== NoWork && - workInProgress.pendingWorkPriority > renderPriority - ) { - // This was a down-prioritization. Don't bubble priority from children. + if (renderTime !== Never && workInProgress.expirationTime === Never) { + // The children of this component are hidden. Don't bubble their + // expiration times. return; } - // Check for pending update priority. - let newPriority = getUpdatePriority(workInProgress); + // Check for pending updates. + let newExpirationTime = getUpdateExpirationTime(workInProgress); // TODO: Coroutines need to visit stateNode + // Bubble up the earliest expiration time. let child = workInProgress.child; while (child !== null) { - // Ensure that remaining work priority bubbles up. - newPriority = largerPriority(newPriority, child.pendingWorkPriority); + newExpirationTime = earlierExpirationTime( + newExpirationTime, + child.expirationTime, + ); child = child.sibling; } - workInProgress.pendingWorkPriority = newPriority; + workInProgress.expirationTime = newExpirationTime; } function completeUnitOfWork(workInProgress: Fiber): Fiber | null { @@ -632,7 +633,11 @@ module.exports = function( if (__DEV__) { ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } - const next = completeWork(current, workInProgress, nextPriorityLevel); + const next = completeWork( + current, + workInProgress, + nextRenderExpirationTime, + ); if (__DEV__) { ReactDebugCurrentFiber.resetCurrentFiber(); } @@ -640,7 +645,7 @@ module.exports = function( const returnFiber = workInProgress.return; const siblingFiber = workInProgress.sibling; - resetWorkPriority(workInProgress, nextPriorityLevel); + resetExpirationTime(workInProgress, nextRenderExpirationTime); if (next !== null) { if (__DEV__) { @@ -728,7 +733,7 @@ module.exports = function( startWorkTimer(workInProgress); ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } - let next = beginWork(current, workInProgress, nextPriorityLevel); + let next = beginWork(current, workInProgress, nextRenderExpirationTime); if (__DEV__) { ReactDebugCurrentFiber.resetCurrentFiber(); } @@ -758,7 +763,11 @@ module.exports = function( startWorkTimer(workInProgress); ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } - let next = beginFailedWork(current, workInProgress, nextPriorityLevel); + let next = beginFailedWork( + current, + workInProgress, + nextRenderExpirationTime, + ); if (__DEV__) { ReactDebugCurrentFiber.resetCurrentFiber(); } @@ -793,7 +802,8 @@ module.exports = function( if ( capturedErrors !== null && capturedErrors.size > 0 && - nextPriorityLevel === TaskPriority + nextRenderExpirationTime !== Done && + nextRenderExpirationTime <= mostRecentCurrentTime ) { while (nextUnitOfWork !== null) { if (hasCapturedError(nextUnitOfWork)) { @@ -809,14 +819,12 @@ module.exports = function( 'a bug in React. Please file an issue.', ); // We just completed a root. Commit it now. - priorityContext = TaskPriority; commitAllWork(pendingCommit); - priorityContext = nextPriorityLevel; - if ( capturedErrors === null || capturedErrors.size === 0 || - nextPriorityLevel !== TaskPriority + nextRenderExpirationTime === Done || + 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 @@ -830,28 +838,26 @@ module.exports = function( } function workLoop( - minPriorityLevel: PriorityLevel, + minExpirationTime: ExpirationTime, deadline: Deadline | null, ) { if (pendingCommit !== null) { - priorityContext = TaskPriority; commitAllWork(pendingCommit); handleCommitPhaseErrors(); } else if (nextUnitOfWork === null) { resetNextUnitOfWork(); } - if (nextPriorityLevel === NoWork || nextPriorityLevel > minPriorityLevel) { + if ( + nextRenderExpirationTime === Done || + nextRenderExpirationTime > minExpirationTime + ) { return; } - // During the render phase, updates should have the same priority at which - // we're rendering. - priorityContext = nextPriorityLevel; - loop: do { - if (nextPriorityLevel <= TaskPriority) { - // Flush all synchronous and task work. + if (nextRenderExpirationTime <= mostRecentCurrentTime) { + // Flush all expired work. while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); if (nextUnitOfWork === null) { @@ -861,24 +867,22 @@ module.exports = function( 'a bug in React. Please file an issue.', ); // We just completed a root. Commit it now. - priorityContext = TaskPriority; commitAllWork(pendingCommit); - priorityContext = nextPriorityLevel; // Clear any errors that were scheduled during the commit phase. handleCommitPhaseErrors(); - // The priority level may have changed. Check again. + // The render time may have changed. Check again. if ( - nextPriorityLevel === NoWork || - nextPriorityLevel > minPriorityLevel || - nextPriorityLevel > TaskPriority + nextRenderExpirationTime === Done || + nextRenderExpirationTime > minExpirationTime || + nextRenderExpirationTime > mostRecentCurrentTime ) { - // The priority level does not match. + // We've completed all the expired work. break; } } } } else if (deadline !== null) { - // Flush asynchronous work until the deadline expires. + // Flush asynchronous work until the deadline runs out of time. while (nextUnitOfWork !== null && !deadlineHasExpired) { if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); @@ -895,18 +899,16 @@ module.exports = function( // 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) { - priorityContext = TaskPriority; commitAllWork(pendingCommit); - priorityContext = nextPriorityLevel; // Clear any errors that were scheduled during the commit phase. handleCommitPhaseErrors(); - // The priority level may have changed. Check again. + // The render time may have changed. Check again. if ( - nextPriorityLevel === NoWork || - nextPriorityLevel > minPriorityLevel || - nextPriorityLevel < HighPriority + nextRenderExpirationTime === Done || + nextRenderExpirationTime > minExpirationTime || + nextRenderExpirationTime <= mostRecentCurrentTime ) { - // The priority level does not match. + // We've completed all the async work. break; } } else { @@ -921,12 +923,16 @@ module.exports = function( // There might be work left. Depending on the priority, we should // either perform it now or schedule a callback to perform it later. - switch (nextPriorityLevel) { + const currentTime = recalculateCurrentTime(); + switch (expirationTimeToPriorityLevel( + currentTime, + nextRenderExpirationTime, + )) { case SynchronousPriority: case TaskPriority: // We have remaining synchronous or task work. Keep performing it, // regardless of whether we're inside a callback. - if (nextPriorityLevel <= minPriorityLevel) { + if (nextRenderExpirationTime <= minExpirationTime) { continue loop; } break loop; @@ -940,7 +946,10 @@ module.exports = function( break loop; } // We are inside a callback. - if (!deadlineHasExpired && nextPriorityLevel <= minPriorityLevel) { + if ( + !deadlineHasExpired && + nextRenderExpirationTime <= minExpirationTime + ) { // We still have time. Keep working. continue loop; } @@ -962,7 +971,7 @@ module.exports = function( function performWorkCatchBlock( failedWork: Fiber, boundary: Fiber, - minPriorityLevel: PriorityLevel, + minExpirationTime: ExpirationTime, deadline: Deadline | null, ) { // We're going to restart the error boundary that captured the error. @@ -978,7 +987,7 @@ module.exports = function( nextUnitOfWork = performFailedUnitOfWork(boundary); // Continue working. - workLoop(minPriorityLevel, deadline); + workLoop(minExpirationTime, deadline); } function performWork( @@ -996,21 +1005,32 @@ module.exports = function( ); isPerformingWork = true; - // The priority context changes during the render phase. We'll need to - // reset it at the end. + // Updates that occur during the commit phase should have task priority + // by default. (Render phase updates are special; getPriorityContext + // accounts for their behavior.) const previousPriorityContext = priorityContext; + priorityContext = TaskPriority; + + // Read the current time from the host environment. + const currentTime = recalculateCurrentTime(); + const minExpirationTime = priorityToExpirationTime( + currentTime, + minPriorityLevel, + ); + + nestedUpdateCount = 0; let didError = false; let error = null; if (__DEV__) { - invokeGuardedCallback(null, workLoop, null, minPriorityLevel, deadline); + invokeGuardedCallback(null, workLoop, null, minExpirationTime, deadline); if (hasCaughtError()) { didError = true; error = clearCaughtError(); } } else { try { - workLoop(minPriorityLevel, deadline); + workLoop(minExpirationTime, deadline); } catch (e) { didError = true; error = e; @@ -1057,7 +1077,7 @@ module.exports = function( null, failedWork, boundary, - minPriorityLevel, + minExpirationTime, deadline, ); if (hasCaughtError()) { @@ -1070,7 +1090,7 @@ module.exports = function( performWorkCatchBlock( failedWork, boundary, - minPriorityLevel, + minExpirationTime, deadline, ); error = null; @@ -1084,19 +1104,21 @@ module.exports = function( break; } - // Reset the priority context to its previous value. - priorityContext = previousPriorityContext; - // 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 (nextPriorityLevel > TaskPriority && !isCallbackScheduled) { + if ( + nextRenderExpirationTime > mostRecentCurrentTime && + !isCallbackScheduled + ) { scheduleDeferredCallback(performDeferredWork); isCallbackScheduled = true; } + priorityContext = previousPriorityContext; + const errorToThrow = firstUncaughtError; // We're done performing work. Time to clean up. @@ -1361,8 +1383,8 @@ module.exports = function( } } - function scheduleRoot(root: FiberRoot, priorityLevel: PriorityLevel) { - if (priorityLevel === NoWork) { + function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) { + if (expirationTime === Done) { return; } @@ -1380,13 +1402,13 @@ module.exports = function( } } - function scheduleUpdate(fiber: Fiber, priorityLevel: PriorityLevel) { - return scheduleUpdateImpl(fiber, priorityLevel, false); + function scheduleUpdate(fiber: Fiber, expirationTime: ExpirationTime) { + return scheduleUpdateImpl(fiber, expirationTime, false); } function scheduleUpdateImpl( fiber: Fiber, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, isErrorRecovery: boolean, ) { if (__DEV__) { @@ -1404,7 +1426,7 @@ module.exports = function( ); } - if (!isPerformingWork && priorityLevel <= nextPriorityLevel) { + 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. @@ -1421,59 +1443,58 @@ 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 priority. Once - // we reach a node whose priority matches (and whose alternate's priority - // matches) we can exit safely knowing that the rest of the path is correct. + // 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; if ( - node.pendingWorkPriority === NoWork || - node.pendingWorkPriority > priorityLevel + node.expirationTime === Done || + node.expirationTime > expirationTime ) { - // Priority did not match. Update and keep going. + // Expiration time did not match. Update and keep going. shouldContinue = true; - node.pendingWorkPriority = priorityLevel; + node.expirationTime = expirationTime; } if (node.alternate !== null) { if ( - node.alternate.pendingWorkPriority === NoWork || - node.alternate.pendingWorkPriority > priorityLevel + node.alternate.expirationTime === Done || + node.alternate.expirationTime > expirationTime ) { - // Priority did not match. Update and keep going. + // Expiration time did not match. Update and keep going. shouldContinue = true; - node.alternate.pendingWorkPriority = priorityLevel; + node.alternate.expirationTime = expirationTime; } } if (node.return === null) { if (node.tag === HostRoot) { const root: FiberRoot = (node.stateNode: any); - scheduleRoot(root, priorityLevel); + scheduleRoot(root, expirationTime); if (!isPerformingWork) { - switch (priorityLevel) { - case SynchronousPriority: - // Perform this update now. - if (isUnbatchingUpdates) { - // We're inside unbatchedUpdates, which is inside either - // batchedUpdates or a lifecycle. We should only flush - // synchronous work, not task work. - performWork(SynchronousPriority, null); - } else { - // Flush both synchronous and task work. - performWork(TaskPriority, null); - } - break; - case TaskPriority: - invariant( - isBatchingUpdates, - 'Task updates can only be scheduled as a nested update or ' + - 'inside batchedUpdates.', - ); - break; - default: - // Schedule a callback to perform the work later. - if (!isCallbackScheduled) { - scheduleDeferredCallback(performDeferredWork); - isCallbackScheduled = true; - } + if (expirationTime < mostRecentCurrentTime) { + // This update is synchronous. Perform it now. + if (isUnbatchingUpdates) { + // We're inside unbatchedUpdates, which is inside either + // batchedUpdates or a lifecycle. We should only flush + // synchronous work, not task work. + performWork(SynchronousPriority, null); + } else { + // Flush both synchronous and task work. + performWork(TaskPriority, null); + } + } else if (expirationTime === mostRecentCurrentTime) { + 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.', + ); + } else { + // This update is async. Schedule a callback. + if (!isCallbackScheduled) { + scheduleDeferredCallback(performDeferredWork); + isCallbackScheduled = true; + } } } } else { @@ -1492,7 +1513,13 @@ module.exports = function( function getPriorityContext( fiber: Fiber, forceAsync: boolean, - ): PriorityLevel { + ): PriorityLevel | null { + if (isPerformingWork && !isCommitting) { + // Updates during the render phase should expire at the same time as + // the work that is being rendered. Return null to indicate. + return null; + } + let priorityLevel = priorityContext; if (priorityLevel === NoWork) { if ( @@ -1517,13 +1544,29 @@ module.exports = function( return priorityLevel; } + function getExpirationTimeForPriority( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel | null, + ): ExpirationTime { + if (priorityLevel === null) { + // A priorityLevel of null indicates that this update should expire at + // the same time as whatever is currently being rendered. + return nextRenderExpirationTime; + } + return priorityToExpirationTime(currentTime, priorityLevel); + } + function scheduleErrorRecovery(fiber: Fiber) { - scheduleUpdateImpl(fiber, TaskPriority, true); + scheduleUpdateImpl( + fiber, + priorityToExpirationTime(mostRecentCurrentTime, TaskPriority), + true, + ); } function recalculateCurrentTime(): ExpirationTime { - currentTime = msToExpirationTime(now()); - return currentTime; + mostRecentCurrentTime = msToExpirationTime(now()); + return mostRecentCurrentTime; } function batchedUpdates(fn: (a: A) => R, a: A): R { @@ -1589,6 +1632,7 @@ module.exports = function( scheduleUpdate: scheduleUpdate, getPriorityContext: getPriorityContext, recalculateCurrentTime: recalculateCurrentTime, + getExpirationTimeForPriority: getExpirationTimeForPriority, batchedUpdates: batchedUpdates, unbatchedUpdates: unbatchedUpdates, flushSync: flushSync, diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 4b40e0e565d7..03d1532e168b 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -16,13 +16,7 @@ import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect'); -const { - NoWork, - SynchronousPriority, - TaskPriority, -} = require('ReactPriorityLevel'); - -const {Done, priorityToExpirationTime} = require('ReactFiberExpirationTime'); +const {Done} = require('ReactFiberExpirationTime'); const {ClassComponent, HostRoot} = require('ReactTypeOfWork'); @@ -39,7 +33,7 @@ type PartialState = type Callback = mixed; export type Update = { - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, expirationTime: ExpirationTime, partialState: PartialState, callback: Callback | null, @@ -73,25 +67,6 @@ export type UpdateQueue = { let _queue1; let _queue2; -function comparePriority(a: PriorityLevel, b: PriorityLevel): number { - // When comparing update priorities, treat sync and Task work as equal. - // TODO: Could we avoid the need for this by always coercing sync priority - // to Task when scheduling an update? - if ( - (a === TaskPriority || a === SynchronousPriority) && - (b === TaskPriority || b === SynchronousPriority) - ) { - return 0; - } - if (a === NoWork && b !== NoWork) { - return -255; - } - if (a !== NoWork && b === NoWork) { - return 255; - } - return a - b; -} - function createUpdateQueue(): UpdateQueue { const queue: UpdateQueue = { first: null, @@ -127,9 +102,6 @@ function insertUpdateIntoQueue( insertBefore: Update | null, currentTime: ExpirationTime, ) { - const priorityLevel = update.priorityLevel; - - let coalescedTime: ExpirationTime | null = null; if (insertAfter !== null) { insertAfter.next = update; // If we receive multiple updates to the same fiber at the same priority @@ -137,10 +109,13 @@ function insertUpdateIntoQueue( // they all flush at the same time. Because this causes an interruption, it // could lead to starvation, so we stop coalescing once the time until the // expiration time reaches a certain threshold. - if (insertAfter !== null && insertAfter.priorityLevel === priorityLevel) { - const expirationTime = insertAfter.expirationTime; - if (expirationTime - currentTime > COALESCENCE_THRESHOLD) { - coalescedTime = expirationTime; + if ( + insertAfter !== null && + insertAfter.priorityLevel === update.priorityLevel + ) { + const coalescedTime = insertAfter.expirationTime; + if (coalescedTime - currentTime > COALESCENCE_THRESHOLD) { + update.expirationTime = coalescedTime; } } } else { @@ -148,11 +123,6 @@ function insertUpdateIntoQueue( update.next = queue.first; queue.first = update; } - update.expirationTime = coalescedTime !== null - ? coalescedTime - : // If we don't coalesce, calculate the expiration time using the - // current time. - priorityToExpirationTime(currentTime, priorityLevel); if (insertBefore !== null) { update.next = insertBefore; @@ -165,13 +135,10 @@ function insertUpdateIntoQueue( // Returns the update after which the incoming update should be inserted into // the queue, or null if it should be inserted at beginning. function findInsertionPosition(queue, update): Update | null { - const priorityLevel = update.priorityLevel; + const expirationTime = update.expirationTime; let insertAfter = null; let insertBefore = null; - if ( - queue.last !== null && - comparePriority(queue.last.priorityLevel, priorityLevel) <= 0 - ) { + if (queue.last !== null && queue.last.expirationTime <= expirationTime) { // Fast path for the common case where the update should be inserted at // the end of the queue. insertAfter = queue.last; @@ -179,7 +146,7 @@ function findInsertionPosition(queue, update): Update | null { insertBefore = queue.first; while ( insertBefore !== null && - comparePriority(insertBefore.priorityLevel, priorityLevel) <= 0 + insertBefore.expirationTime <= expirationTime ) { insertAfter = insertBefore; insertBefore = insertBefore.next; @@ -333,12 +300,13 @@ function addUpdate( fiber: Fiber, partialState: PartialState | null, callback: mixed, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState, callback, isReplace: false, @@ -354,12 +322,13 @@ function addReplaceUpdate( fiber: Fiber, state: any | null, callback: Callback | null, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState: state, callback, isReplace: true, @@ -374,12 +343,13 @@ exports.addReplaceUpdate = addReplaceUpdate; function addForceUpdate( fiber: Fiber, callback: Callback | null, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState: null, callback, isReplace: false, @@ -391,30 +361,31 @@ function addForceUpdate( } exports.addForceUpdate = addForceUpdate; -function getUpdatePriority(fiber: Fiber): PriorityLevel { +function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { const updateQueue = fiber.updateQueue; if (updateQueue === null) { - return NoWork; + return Done; } if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) { - return NoWork; + return Done; } - return updateQueue.first !== null ? updateQueue.first.priorityLevel : NoWork; + return updateQueue.first !== null ? updateQueue.first.expirationTime : Done; } -exports.getUpdatePriority = getUpdatePriority; +exports.getUpdateExpirationTime = getUpdateExpirationTime; function addTopLevelUpdate( fiber: Fiber, partialState: PartialState, callback: Callback | null, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const isTopLevelUnmount = partialState.element === null; const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState, callback, isReplace: false, @@ -461,7 +432,7 @@ function beginUpdateQueue( instance: any, prevState: any, props: any, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): any { if (current !== null && current.updateQueue === queue) { // We need to create a work-in-progress queue, by cloning the current queue. @@ -491,10 +462,7 @@ function beginUpdateQueue( let state = prevState; let dontMutatePrevState = true; let update = queue.first; - while ( - update !== null && - comparePriority(update.priorityLevel, priorityLevel) <= 0 - ) { + while (update !== null && update.expirationTime <= renderExpirationTime) { // Remove each update from the queue right before it is processed. That way // if setState is called from inside an updater function, the new update // will be inserted in the correct position. From ec3435af0ee875df7560ca70de744842b7e40d0b Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 09:03:49 -0700 Subject: [PATCH 03/10] Triangle Demo should use a class shouldComponentUpdate was removed from functional components. Running the demo shows, now that expiration is enabled, the demo does not starve. (Still won't run smoothly until we add back the ability to resume interrupted work.) --- fixtures/fiber-triangle/index.html | 81 ++++++++++--------- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 31 +++---- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/fixtures/fiber-triangle/index.html b/fixtures/fiber-triangle/index.html index d2541eee1e93..1b5941f09b31 100644 --- a/fixtures/fiber-triangle/index.html +++ b/fixtures/fiber-triangle/index.html @@ -76,51 +76,54 @@

Fiber Example

} } - function SierpinskiTriangle({ x, y, s, children }) { - if (s <= targetSize) { - return ( - + class SierpinskiTriangle extends React.Component { + shouldComponentUpdate(nextProps) { + var o = this.props; + var n = nextProps; + return !( + o.x === n.x && + o.y === n.y && + o.s === n.s && + o.children === n.children ); - return r; } - var newSize = s / 2; - var slowDown = true; - if (slowDown) { - var e = performance.now() + 0.8; - while (performance.now() < e) { - // Artificially long execution time. + render() { + let {x, y, s, children} = this.props; + if (s <= targetSize) { + return ( + + ); + return r; + } + var newSize = s / 2; + var slowDown = true; + if (slowDown) { + var e = performance.now() + 0.8; + while (performance.now() < e) { + // Artificially long execution time. + } } - } - s /= 2; + s /= 2; - return [ - - {children} - , - - {children} - , - - {children} - , - ]; + return [ + + {children} + , + + {children} + , + + {children} + , + ]; + } } - SierpinskiTriangle.shouldComponentUpdate = function(oldProps, newProps) { - var o = oldProps; - var n = newProps; - return !( - o.x === n.x && - o.y === n.y && - o.s === n.s && - o.children === n.children - ); - }; class ExampleApplication extends React.Component { constructor() { diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 5dbda35eb0b3..0b09818ee60e 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -163,20 +163,20 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean { } // TODO: Better polyfill -// let now; -// if ( -// typeof window !== 'undefined' && -// window.performance && -// typeof window.performance.now === 'function' -// ) { -// now = function() { -// return performance.now(); -// }; -// } else { -// now = function() { -// return Date.now(); -// }; -// } +let now; +if ( + typeof window !== 'undefined' && + window.performance && + typeof window.performance.now === 'function' +) { + now = function() { + return performance.now(); + }; +} else { + now = function() { + return Date.now(); + }; +} var DOMRenderer = ReactFiberReconciler({ getRootHostContext(rootContainerInstance: Container): HostContext { @@ -455,7 +455,8 @@ var DOMRenderer = ReactFiberReconciler({ now() { // TODO: Use performance.now to enable expiration - return 0; + // return 0; + return now(); }, canHydrateInstance( From cc675f95d5f2afee6eb7c6075dba790e2f67f10e Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 11:30:17 -0700 Subject: [PATCH 04/10] Use a magic value for task expiration time There are a few cases related to sync mode where we need to distinguish between work that is scheduled as task and work that is treated like task because it expires. For example, batchedUpdates. We don't want to perform any work until the end of the batch, regardless of how much time has elapsed. --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 6 +- .../shared/fiber/ReactFiberExpirationTime.js | 36 ++++++----- .../shared/fiber/ReactFiberScheduler.js | 64 ++++++++++--------- 3 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 0b09818ee60e..8250d1773b43 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -453,11 +453,7 @@ var DOMRenderer = ReactFiberReconciler({ } }, - now() { - // TODO: Use performance.now to enable expiration - // return 0; - return now(); - }, + now, canHydrateInstance( instance: Instance | TextInstance, diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index e520995a7e9e..9fd65f1c758f 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -26,11 +26,13 @@ const invariant = require('fbjs/lib/invariant'); export type ExpirationTime = number; const Done = 0; -exports.Done = Done; +const Sync = 1; +const Task = 2; +const Never = Number.MAX_SAFE_INTEGER; -const MAGIC_NUMBER_OFFSET = 2; +const MAGIC_NUMBER_OFFSET = 10; -const Never = Number.MAX_SAFE_INTEGER; +exports.Done = Done; exports.Never = Never; // 1 unit of expiration time represents 10ms. @@ -57,11 +59,9 @@ function priorityToExpirationTime( case NoWork: return Done; case SynchronousPriority: - // Return a number lower than the current time, but higher than Done. - return MAGIC_NUMBER_OFFSET - 1; + return Sync; case TaskPriority: - // Return the current time, so that this work completes in this batch. - return currentTime; + return Task; case HighPriority: // Should complete within ~100ms. 120ms max. return msToExpirationTime(ceiling(100, 20)); @@ -71,7 +71,6 @@ function priorityToExpirationTime( case OffscreenPriority: return Never; default: - console.log(priorityLevel); invariant( false, 'Switch statement should be exhuastive. ' + @@ -89,16 +88,19 @@ function expirationTimeToPriorityLevel( expirationTime: ExpirationTime, ): PriorityLevel { // First check for magic values - if (expirationTime === Done) { - return NoWork; - } - if (expirationTime === Never) { - return OffscreenPriority; - } - if (expirationTime < currentTime) { - return SynchronousPriority; + switch (expirationTime) { + case Done: + return NoWork; + case Sync: + return SynchronousPriority; + case Task: + return TaskPriority; + case Never: + return OffscreenPriority; + default: + break; } - if (expirationTime === currentTime) { + if (expirationTime <= currentTime) { return TaskPriority; } // TODO: We don't currently distinguish between high and low priority. diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 9fcbf2064bbd..f7506449e942 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -1013,7 +1013,7 @@ module.exports = function( // Read the current time from the host environment. const currentTime = recalculateCurrentTime(); - const minExpirationTime = priorityToExpirationTime( + const minExpirationTime = getExpirationTimeForPriority( currentTime, minPriorityLevel, ); @@ -1471,30 +1471,36 @@ module.exports = function( const root: FiberRoot = (node.stateNode: any); scheduleRoot(root, expirationTime); if (!isPerformingWork) { - if (expirationTime < mostRecentCurrentTime) { - // This update is synchronous. Perform it now. - if (isUnbatchingUpdates) { - // We're inside unbatchedUpdates, which is inside either - // batchedUpdates or a lifecycle. We should only flush - // synchronous work, not task work. - performWork(SynchronousPriority, null); - } else { - // Flush both synchronous and task work. - performWork(TaskPriority, null); - } - } else if (expirationTime === mostRecentCurrentTime) { - 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.', - ); - } else { - // This update is async. Schedule a callback. - if (!isCallbackScheduled) { - scheduleDeferredCallback(performDeferredWork); - isCallbackScheduled = true; - } + const priorityLevel = expirationTimeToPriorityLevel( + mostRecentCurrentTime, + expirationTime, + ); + switch (priorityLevel) { + case SynchronousPriority: + if (isUnbatchingUpdates) { + // We're inside unbatchedUpdates, which is inside either + // batchedUpdates or a lifecycle. We should only flush + // synchronous work, not task work. + performWork(SynchronousPriority, null); + } else { + // Flush both synchronous and task work. + performWork(TaskPriority, null); + } + break; + case TaskPriority: + 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; + } } } } else { @@ -1557,11 +1563,11 @@ module.exports = function( } function scheduleErrorRecovery(fiber: Fiber) { - scheduleUpdateImpl( - fiber, - priorityToExpirationTime(mostRecentCurrentTime, TaskPriority), - true, + const taskTime = getExpirationTimeForPriority( + mostRecentCurrentTime, + TaskPriority, ); + scheduleUpdateImpl(fiber, taskTime, true); } function recalculateCurrentTime(): ExpirationTime { From 3913a05aef63e113424fd712e007397900251f4f Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 13:52:29 -0700 Subject: [PATCH 05/10] Use current time to calculate expiration time --- .../shared/fiber/ReactFiberExpirationTime.js | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index 9fd65f1c758f..851316454e5f 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -30,6 +30,7 @@ const Sync = 1; const Task = 2; const Never = Number.MAX_SAFE_INTEGER; +const UNIT_SIZE = 10; const MAGIC_NUMBER_OFFSET = 10; exports.Done = Done; @@ -38,12 +39,23 @@ exports.Never = Never; // 1 unit of expiration time represents 10ms. function msToExpirationTime(ms: number): ExpirationTime { // Always add an offset so that we don't clash with the magic number for Done. - return Math.round(ms / 10) + MAGIC_NUMBER_OFFSET; + return Math.round(ms / UNIT_SIZE) + MAGIC_NUMBER_OFFSET; } exports.msToExpirationTime = msToExpirationTime; -function ceiling(time: ExpirationTime, precision: number): ExpirationTime { - return Math.ceil(Math.ceil(time * precision) / precision); +function ceiling(num: number, precision: number): number { + return Math.ceil(Math.ceil(num * precision) / precision); +} + +function bucket( + currentTime: ExpirationTime, + expirationInMs: number, + precisionInMs: number, +): ExpirationTime { + return ceiling( + currentTime + expirationInMs / UNIT_SIZE, + precisionInMs / UNIT_SIZE, + ); } // Given the current clock time and a priority level, returns an expiration time @@ -62,12 +74,14 @@ function priorityToExpirationTime( return Sync; case TaskPriority: return Task; - case HighPriority: + case HighPriority: { // Should complete within ~100ms. 120ms max. - return msToExpirationTime(ceiling(100, 20)); - case LowPriority: + return bucket(currentTime, 100, 20); + } + case LowPriority: { // Should complete within ~1000ms. 1200ms max. - return msToExpirationTime(ceiling(1000, 200)); + return bucket(currentTime, 1000, 200); + } case OffscreenPriority: return Never; default: From 608e5b4f8e13c918b5c0ecf29d742ab1f3c21a07 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 14:44:54 -0700 Subject: [PATCH 06/10] Add unit tests for expiration and coalescing --- src/renderers/noop/ReactNoopEntry.js | 13 +- .../fiber/__tests__/ReactExpiration-test.js | 125 ++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/renderers/shared/fiber/__tests__/ReactExpiration-test.js diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index afd4eac99d19..6a5ed0b11ed3 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -83,6 +83,8 @@ function removeChild( parentInstance.children.splice(index, 1); } +let elapsedTimeInMs = 0; + var NoopRenderer = ReactFiberReconciler({ getRootHostContext() { if (failInBeginPhase) { @@ -203,8 +205,7 @@ var NoopRenderer = ReactFiberReconciler({ resetAfterCommit(): void {}, now(): number { - // TODO: Add an API to advance time. - return 0; + return elapsedTimeInMs; }, }); @@ -341,6 +342,14 @@ var ReactNoop = { expect(actual).toEqual(expected); }, + expire(ms: number): void { + elapsedTimeInMs += ms; + }, + + flushExpired(): Array { + return ReactNoop.flushUnitsOfWork(0); + }, + yield(value: mixed) { if (yieldedValues === null) { yieldedValues = [value]; diff --git a/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js new file mode 100644 index 000000000000..a8c35a1e0501 --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +var React; +var ReactNoop; + +describe('ReactExpiration', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactNoop = require('react-noop-renderer'); + }); + + function span(prop) { + return {type: 'span', children: [], prop}; + } + + it('increases priority of updates as time progresses', () => { + ReactNoop.render(); + + expect(ReactNoop.getChildren()).toEqual([]); + + // Nothing has expired yet because time hasn't advanced. + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance by 300ms, not enough to expire the low pri update. + ReactNoop.expire(300); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance by another second. Now the update should expire and flush. + ReactNoop.expire(1000); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span('done')]); + }); + + it('coalesces updates to the same component', () => { + const foos = []; + class Foo extends React.Component { + constructor() { + super(); + this.state = {step: 0}; + foos.push(this); + } + render() { + return ; + } + } + + ReactNoop.render([, ]); + ReactNoop.flush(); + const [a, b] = foos; + + a.setState({step: 1}); + + // Advance time by 500ms. + ReactNoop.expire(500); + + // Update A again. This update should coalesce with the previous update. + a.setState({step: 2}); + // Update B. This is the first update, so it has nothing to coalesce with. + b.setState({step: 1}); + + // Advance time. This should be enough to flush both updates to A, but not + // the update to B. If only the first update to A flushes, but not the + // second, then it wasn't coalesced properly. + ReactNoop.expire(500); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(2), span(0)]); + + // Now expire the update to B. + ReactNoop.expire(500); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(2), span(1)]); + }); + + it('stops coalescing after a certain threshold', () => { + let instance; + class Foo extends React.Component { + state = {step: 0}; + render() { + instance = this; + return ; + } + } + + ReactNoop.render(); + ReactNoop.flush(); + + instance.setState({step: 1}); + + // Advance time by 500 ms. + ReactNoop.expire(500); + + // Update again. This update should coalesce with the previous update. + instance.setState({step: 2}); + + // Advance time by 480ms. Not enough to expire the updates. + ReactNoop.expire(480); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(0)]); + + // Update again. This update should NOT be coalesced, because the + // previous updates have almost expired. + instance.setState({step: 3}); + + // Advance time. This should expire the first two updates, + // but not the third. + ReactNoop.expire(500); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(2)]); + + // Now expire the remaining update. + ReactNoop.expire(1000); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(3)]); + }); +}); From 91bd584df02c68b3a540a3c1847bf0cf806dab66 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 15:36:54 -0700 Subject: [PATCH 07/10] Delete unnecessary abstraction --- .../shared/fiber/ReactFiberExpirationTime.js | 8 -------- src/renderers/shared/fiber/ReactFiberScheduler.js | 11 ++++++----- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index 851316454e5f..789eeb89ab4e 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -121,11 +121,3 @@ function expirationTimeToPriorityLevel( return LowPriority; } exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel; - -function earlierExpirationTime( - t1: ExpirationTime, - t2: ExpirationTime, -): ExpirationTime { - return t1 !== Done && (t2 === Done || t2 > t1) ? t1 : t2; -} -exports.earlierExpirationTime = earlierExpirationTime; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index f7506449e942..e45d0776e1ce 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -67,7 +67,6 @@ var { Done, Never, msToExpirationTime, - earlierExpirationTime, priorityToExpirationTime, expirationTimeToPriorityLevel, } = require('ReactFiberExpirationTime'); @@ -614,10 +613,12 @@ module.exports = function( // Bubble up the earliest expiration time. let child = workInProgress.child; while (child !== null) { - newExpirationTime = earlierExpirationTime( - newExpirationTime, - child.expirationTime, - ); + if ( + child.expirationTime !== Done && + (newExpirationTime === Done || newExpirationTime > child.expirationTime) + ) { + newExpirationTime = child.expirationTime; + } child = child.sibling; } workInProgress.expirationTime = newExpirationTime; From a4c602c241ba6d9425f001dd55d019a5d0ae574d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 16:16:25 -0700 Subject: [PATCH 08/10] Move performance.now polyfill to ReactDOMFrameScheduling --- src/renderers/art/ReactARTFiberEntry.js | 5 +-- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 18 +------- .../shared/ReactDOMFrameScheduling.js | 45 +++++++++++++------ 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/renderers/art/ReactARTFiberEntry.js b/src/renderers/art/ReactARTFiberEntry.js index ea0c4d68649e..ca97c71e8d7e 100644 --- a/src/renderers/art/ReactARTFiberEntry.js +++ b/src/renderers/art/ReactARTFiberEntry.js @@ -532,10 +532,7 @@ const ARTRenderer = ReactFiberReconciler({ ); }, - now(): number { - // TODO: Enable expiration by implementing this method. - return 0; - }, + now: ReactDOMFrameScheduling.now, useSyncScheduling: true, }); diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 8250d1773b43..01090873ad72 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -162,22 +162,6 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean { return false; } -// TODO: Better polyfill -let now; -if ( - typeof window !== 'undefined' && - window.performance && - typeof window.performance.now === 'function' -) { - now = function() { - return performance.now(); - }; -} else { - now = function() { - return Date.now(); - }; -} - var DOMRenderer = ReactFiberReconciler({ getRootHostContext(rootContainerInstance: Container): HostContext { let type; @@ -453,7 +437,7 @@ var DOMRenderer = ReactFiberReconciler({ } }, - now, + now: ReactDOMFrameScheduling.now, canHydrateInstance( instance: Instance | TextInstance, diff --git a/src/renderers/shared/ReactDOMFrameScheduling.js b/src/renderers/shared/ReactDOMFrameScheduling.js index de8d0da98f37..5a2fc01ca989 100644 --- a/src/renderers/shared/ReactDOMFrameScheduling.js +++ b/src/renderers/shared/ReactDOMFrameScheduling.js @@ -37,6 +37,20 @@ if (__DEV__) { } } +const hasNativePerformanceNow = + typeof performance === 'object' && typeof performance.now === 'function'; + +let now; +if (hasNativePerformanceNow) { + now = function() { + return performance.now(); + }; +} else { + now = function() { + return Date.now(); + }; +} + // TODO: There's no way to cancel, because Fiber doesn't atm. let rIC: (callback: (deadline: Deadline) => void) => number; @@ -67,19 +81,23 @@ if (!ExecutionEnvironment.canUseDOM) { var previousFrameTime = 33; var activeFrameTime = 33; - var frameDeadlineObject = { - timeRemaining: typeof performance === 'object' && - typeof performance.now === 'function' - ? function() { - // We assume that if we have a performance timer that the rAF callback - // gets a performance timer value. Not sure if this is always true. - return frameDeadline - performance.now(); - } - : function() { - // As a fallback we use Date.now. - return frameDeadline - Date.now(); - }, - }; + var frameDeadlineObject; + if (hasNativePerformanceNow) { + frameDeadlineObject = { + timeRemaining() { + // We assume that if we have a performance timer that the rAF callback + // gets a performance timer value. Not sure if this is always true. + return frameDeadline - performance.now(); + }, + }; + } else { + frameDeadlineObject = { + timeRemaining() { + // Fallback to Date.now() + return frameDeadline - Date.now(); + }, + }; + } // We use the postMessage trick to defer idle work until after the repaint. var messageKey = '__reactIdleCallback$' + Math.random().toString(36).slice(2); @@ -153,4 +171,5 @@ if (!ExecutionEnvironment.canUseDOM) { rIC = requestIdleCallback; } +exports.now = now; exports.rIC = rIC; From d89d6a3e259622ee9788bb89a1484628b4bdb6f9 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 20:32:30 -0700 Subject: [PATCH 09/10] Add expiration to fuzz tester --- .../__tests__/ReactIncrementalTriangle-test.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js index fdeefaa3d975..d5cdd22ae431 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js @@ -54,6 +54,14 @@ describe('ReactIncrementalTriangle', () => { }; } + const EXPIRE = 'EXPIRE'; + function expire(ms) { + return { + type: EXPIRE, + ms, + }; + } + function TriangleSimulator() { let triangles = []; let leafTriangles = []; @@ -212,6 +220,9 @@ describe('ReactIncrementalTriangle', () => { targetTriangle.activate(); } break; + case EXPIRE: + ReactNoop.expire(action.ms); + break; default: break; } @@ -251,7 +262,7 @@ describe('ReactIncrementalTriangle', () => { } function randomAction() { - switch (randomInteger(0, 4)) { + switch (randomInteger(0, 5)) { case 0: return flush(randomInteger(0, totalTriangles * 1.5)); case 1: @@ -260,6 +271,8 @@ describe('ReactIncrementalTriangle', () => { return interrupt(); case 3: return toggle(randomInteger(0, totalChildren)); + case 4: + return expire(randomInteger(0, 1500)); default: throw new Error('Switch statement should be exhaustive'); } @@ -290,6 +303,9 @@ describe('ReactIncrementalTriangle', () => { case TOGGLE: result += `toggle(${action.childIndex})`; break; + case EXPIRE: + result += `expire(${action.ms})`; + break; default: throw new Error('Switch statement should be exhaustive'); } From 721fdfcef3090a48b19439b0018cee435f8642a4 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 7 Oct 2017 20:34:07 -0700 Subject: [PATCH 10/10] Expiration nits - Rename Done -> NoWork - Use max int32 instead of max safe int - Use bitwise operations instead of Math functions --- src/renderers/shared/fiber/ReactFiber.js | 8 ++-- .../shared/fiber/ReactFiberBeginWork.js | 7 ++- .../shared/fiber/ReactFiberExpirationTime.js | 20 ++++----- .../shared/fiber/ReactFiberScheduler.js | 43 ++++++++++--------- .../shared/fiber/ReactFiberUpdateQueue.js | 8 ++-- .../fiber/__tests__/ReactExpiration-test.js | 2 +- 6 files changed, 45 insertions(+), 43 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index faf40a2d6526..8bbffcc48b6b 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -36,9 +36,9 @@ var { Fragment, } = require('ReactTypeOfWork'); -var {NoWork} = require('ReactPriorityLevel'); +var {NoWork: NoWorkPriority} = require('ReactPriorityLevel'); -var {Done} = require('ReactFiberExpirationTime'); +var {NoWork} = require('ReactFiberExpirationTime'); var {NoContext} = require('ReactTypeOfInternalContext'); @@ -193,7 +193,7 @@ function FiberNode( this.firstEffect = null; this.lastEffect = null; - this.expirationTime = Done; + this.expirationTime = NoWork; this.alternate = null; @@ -457,5 +457,5 @@ exports.largerPriority = function( p1: PriorityLevel, p2: PriorityLevel, ): PriorityLevel { - return p1 !== NoWork && (p2 === NoWork || p2 > p1) ? p1 : p2; + return p1 !== NoWorkPriority && (p2 === NoWorkPriority || p2 > p1) ? p1 : p2; }; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 5d49bf7e89dd..0627bc7f9b35 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -48,7 +48,7 @@ var { YieldComponent, Fragment, } = ReactTypeOfWork; -var {Done, Never} = require('ReactFiberExpirationTime'); +var {NoWork, Never} = require('ReactFiberExpirationTime'); var { PerformedWork, Placement, @@ -330,7 +330,6 @@ module.exports = function( } function updateHostRoot(current, workInProgress, renderExpirationTime) { - const root = (workInProgress.stateNode: FiberRoot); pushHostRootContext(workInProgress); const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { @@ -741,7 +740,7 @@ module.exports = function( renderExpirationTime: ExpirationTime, ): Fiber | null { if ( - workInProgress.expirationTime === Done || + workInProgress.expirationTime === NoWork || workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); @@ -839,7 +838,7 @@ module.exports = function( } if ( - workInProgress.expirationTime === Done || + workInProgress.expirationTime === NoWork || workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index 789eeb89ab4e..0413f8a5994d 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -12,7 +12,7 @@ import type {PriorityLevel} from 'ReactPriorityLevel'; const { - NoWork, + NoWork: NoWorkPriority, SynchronousPriority, TaskPriority, HighPriority, @@ -25,26 +25,26 @@ const invariant = require('fbjs/lib/invariant'); // TODO: Use an opaque type once ESLint et al support the syntax export type ExpirationTime = number; -const Done = 0; +const NoWork = 0; const Sync = 1; const Task = 2; -const Never = Number.MAX_SAFE_INTEGER; +const Never = 2147483647; // Max int32: Math.pow(2, 31) - 1 const UNIT_SIZE = 10; -const MAGIC_NUMBER_OFFSET = 10; +const MAGIC_NUMBER_OFFSET = 3; -exports.Done = Done; +exports.NoWork = NoWork; exports.Never = Never; // 1 unit of expiration time represents 10ms. function msToExpirationTime(ms: number): ExpirationTime { - // Always add an offset so that we don't clash with the magic number for Done. - return Math.round(ms / UNIT_SIZE) + MAGIC_NUMBER_OFFSET; + // Always add an offset so that we don't clash with the magic number for NoWork. + return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET; } exports.msToExpirationTime = msToExpirationTime; function ceiling(num: number, precision: number): number { - return Math.ceil(Math.ceil(num * precision) / precision); + return (((((num * precision) | 0) + 1) / precision) | 0) + 1; } function bucket( @@ -69,7 +69,7 @@ function priorityToExpirationTime( ): ExpirationTime { switch (priorityLevel) { case NoWork: - return Done; + return NoWorkPriority; case SynchronousPriority: return Sync; case TaskPriority: @@ -103,7 +103,7 @@ function expirationTimeToPriorityLevel( ): PriorityLevel { // First check for magic values switch (expirationTime) { - case Done: + case NoWorkPriority: return NoWork; case Sync: return SynchronousPriority; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index e45d0776e1ce..52e6ebee2627 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -64,7 +64,6 @@ var { } = require('ReactPriorityLevel'); var { - Done, Never, msToExpirationTime, priorityToExpirationTime, @@ -200,7 +199,8 @@ module.exports = function( } = config; // Represents the current time in ms. - let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(now()); + const startTime = now(); + let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(0); // The priority level to use when scheduling an update. We use NoWork to // represent the default priority. @@ -222,7 +222,7 @@ module.exports = function( // The next work in progress fiber that we're currently working on. let nextUnitOfWork: Fiber | null = null; // The time at which we're currently rendering work. - let nextRenderExpirationTime: ExpirationTime = Done; + let nextRenderExpirationTime: ExpirationTime = NoWork; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; @@ -269,7 +269,7 @@ module.exports = function( // Clear out roots with no more work on them, or if they have uncaught errors while ( nextScheduledRoot !== null && - nextScheduledRoot.current.expirationTime === Done + nextScheduledRoot.current.expirationTime === NoWork ) { // Unschedule this root. nextScheduledRoot.isScheduled = false; @@ -281,7 +281,7 @@ module.exports = function( if (nextScheduledRoot === lastScheduledRoot) { nextScheduledRoot = null; lastScheduledRoot = null; - nextRenderExpirationTime = Done; + nextRenderExpirationTime = NoWork; return null; } // Continue with the next root. @@ -291,11 +291,11 @@ module.exports = function( let root = nextScheduledRoot; let earliestExpirationRoot = null; - let earliestExpirationTime = Done; + let earliestExpirationTime = NoWork; while (root !== null) { if ( - root.current.expirationTime !== Done && - (earliestExpirationTime === Done || + root.current.expirationTime !== NoWork && + (earliestExpirationTime === NoWork || earliestExpirationTime > root.current.expirationTime) ) { earliestExpirationTime = root.current.expirationTime; @@ -325,7 +325,7 @@ module.exports = function( return; } - nextRenderExpirationTime = Done; + nextRenderExpirationTime = NoWork; nextUnitOfWork = null; nextRenderedTree = null; return; @@ -614,8 +614,9 @@ module.exports = function( let child = workInProgress.child; while (child !== null) { if ( - child.expirationTime !== Done && - (newExpirationTime === Done || newExpirationTime > child.expirationTime) + child.expirationTime !== NoWork && + (newExpirationTime === NoWork || + newExpirationTime > child.expirationTime) ) { newExpirationTime = child.expirationTime; } @@ -803,7 +804,7 @@ module.exports = function( if ( capturedErrors !== null && capturedErrors.size > 0 && - nextRenderExpirationTime !== Done && + nextRenderExpirationTime !== NoWork && nextRenderExpirationTime <= mostRecentCurrentTime ) { while (nextUnitOfWork !== null) { @@ -824,7 +825,7 @@ module.exports = function( if ( capturedErrors === null || capturedErrors.size === 0 || - nextRenderExpirationTime === Done || + nextRenderExpirationTime === NoWork || nextRenderExpirationTime > mostRecentCurrentTime ) { // There are no more unhandled errors. We can exit this special @@ -850,7 +851,7 @@ module.exports = function( } if ( - nextRenderExpirationTime === Done || + nextRenderExpirationTime === NoWork || nextRenderExpirationTime > minExpirationTime ) { return; @@ -873,7 +874,7 @@ module.exports = function( handleCommitPhaseErrors(); // The render time may have changed. Check again. if ( - nextRenderExpirationTime === Done || + nextRenderExpirationTime === NoWork || nextRenderExpirationTime > minExpirationTime || nextRenderExpirationTime > mostRecentCurrentTime ) { @@ -905,7 +906,7 @@ module.exports = function( handleCommitPhaseErrors(); // The render time may have changed. Check again. if ( - nextRenderExpirationTime === Done || + nextRenderExpirationTime === NoWork || nextRenderExpirationTime > minExpirationTime || nextRenderExpirationTime <= mostRecentCurrentTime ) { @@ -1385,7 +1386,7 @@ module.exports = function( } function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) { - if (expirationTime === Done) { + if (expirationTime === NoWork) { return; } @@ -1450,7 +1451,7 @@ module.exports = function( // rest of the path is correct. shouldContinue = false; if ( - node.expirationTime === Done || + node.expirationTime === NoWork || node.expirationTime > expirationTime ) { // Expiration time did not match. Update and keep going. @@ -1459,7 +1460,7 @@ module.exports = function( } if (node.alternate !== null) { if ( - node.alternate.expirationTime === Done || + node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime ) { // Expiration time did not match. Update and keep going. @@ -1572,7 +1573,9 @@ module.exports = function( } function recalculateCurrentTime(): ExpirationTime { - mostRecentCurrentTime = msToExpirationTime(now()); + // Subtract initial time so it fits inside 32bits + const ms = now() - startTime; + mostRecentCurrentTime = msToExpirationTime(ms); return mostRecentCurrentTime; } diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 03d1532e168b..596ed9d41c98 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -16,7 +16,7 @@ import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect'); -const {Done} = require('ReactFiberExpirationTime'); +const {NoWork} = require('ReactFiberExpirationTime'); const {ClassComponent, HostRoot} = require('ReactTypeOfWork'); @@ -364,12 +364,12 @@ exports.addForceUpdate = addForceUpdate; function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { const updateQueue = fiber.updateQueue; if (updateQueue === null) { - return Done; + return NoWork; } if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) { - return Done; + return NoWork; } - return updateQueue.first !== null ? updateQueue.first.expirationTime : Done; + return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork; } exports.getUpdateExpirationTime = getUpdateExpirationTime; diff --git a/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js index a8c35a1e0501..7b9121f19a20 100644 --- a/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js @@ -71,7 +71,7 @@ describe('ReactExpiration', () => { // Advance time. This should be enough to flush both updates to A, but not // the update to B. If only the first update to A flushes, but not the // second, then it wasn't coalesced properly. - ReactNoop.expire(500); + ReactNoop.expire(600); ReactNoop.flushExpired(); expect(ReactNoop.getChildren()).toEqual([span(2), span(0)]);