From 8db47ad8d27b6ca9ab5c1570d2e5b383abb08ece Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 11:30:17 -0700 Subject: [PATCH 1/2] 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 18b33854e11a5..4a9d238ec0ec8 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -447,11 +447,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 dd62030d3b380..39493cb436764 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -28,11 +28,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 = Infinity; -const MAGIC_NUMBER_OFFSET = 2; +const MAGIC_NUMBER_OFFSET = 10; -const Never = Infinity; +exports.Done = Done; exports.Never = Infinity; // 1 unit of expiration time represents 10ms. @@ -63,11 +65,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)); @@ -77,7 +77,6 @@ function priorityToExpirationTime( case OffscreenPriority: return Never; default: - console.log(priorityLevel); invariant( false, 'Switch statement should be exhuastive. ' + @@ -95,16 +94,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; } // Keep this value in sync with priorityToExpirationTime. diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 5175b1781b4f8..b7c590afc562c 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -992,7 +992,7 @@ module.exports = function( // Read the current time from the host environment. const currentTime = recalculateCurrentTime(); - const minExpirationTime = priorityToExpirationTime( + const minExpirationTime = getExpirationTimeForPriority( currentTime, minPriorityLevel, ); @@ -1447,30 +1447,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 { @@ -1533,11 +1539,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 8972ae8e03b18f5cf89cd131ce5b48a5efcd153b Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 13:52:29 -0700 Subject: [PATCH 2/2] Use current time to calculate expiration time --- .../shared/fiber/ReactFiberExpirationTime.js | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index 39493cb436764..e05234a384e18 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -32,6 +32,7 @@ const Sync = 1; const Task = 2; const Never = Infinity; +const UNIT_SIZE = 10; const MAGIC_NUMBER_OFFSET = 10; exports.Done = Done; @@ -40,16 +41,27 @@ exports.Never = Infinity; // 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 expirationTimeToMs(expirationTime: ExpirationTime): number { - return (expirationTime - MAGIC_NUMBER_OFFSET) * 10; + return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE; } -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 @@ -68,12 +80,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: