From a92dc96f49cb4c69ab11113c29b91104cbdebdc0 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 24 Sep 2018 17:17:32 -0700 Subject: [PATCH] [scheduler] Priority levels, continuations, and wrapped callbacks All of these features are based on features of React's internal scheduler. The eventual goal is to lift as much as possible out of the React internals into the Scheduler package. Includes some renaming of existing methods. - `scheduleWork` is now `scheduleCallback` - `cancelScheduledWork` is now `cancelCallback` Priority levels --------------- Adds the ability to schedule callbacks at different priority levels. The current levels are (final names TBD): - Immediate priority. Fires at the end of the outermost currently executing (similar to a microtask). - Interactive priority. Fires within a few hundred milliseconds. This should only be used to provide quick feedback to the user as a result of an interaction. - Normal priority. This is the default. Fires within several seconds. - "Maybe" priority. Only fires if there's nothing else to do. Used for prerendering or warming a cache. The priority is changed using `runWithPriority`: ```js runWithPriority(InteractivePriority, () => { scheduleCallback(callback); }); ``` Continuations ------------- Adds the ability for a callback to yield without losing its place in the queue, by returning a continuation. The continuation will have the same expiration as the callback that yielded. Wrapped callbacks ----------------- Adds the ability to wrap a callback so that, when it is called, it receives the priority of the current execution context. --- fixtures/scheduler/index.html | 154 ++++--- fixtures/tracing/script.js | 4 +- .../suspense/src/components/App.js | 4 +- .../unstable-async/time-slicing/src/index.js | 6 +- packages/react-art/src/ReactARTHostConfig.js | 4 +- .../src/client/ReactDOMHostConfig.js | 4 +- packages/react/src/ReactSharedInternals.js | 14 +- .../npm/umd/scheduler.development.js | 38 +- .../npm/umd/scheduler.production.min.js | 38 +- .../npm/umd/scheduler.profiling.min.js | 38 +- packages/scheduler/src/Scheduler.js | 297 ++++++++++-- .../src/__tests__/Scheduler-test.internal.js | 432 +++++++++++++++--- .../src/__tests__/SchedulerDOM-test.js | 172 +++---- .../SchedulerUMDBundle-test.internal.js | 3 +- packages/shared/forks/Scheduler.umd.js | 6 +- 15 files changed, 913 insertions(+), 301 deletions(-) diff --git a/fixtures/scheduler/index.html b/fixtures/scheduler/index.html index a9ae2ded7ef46..f522e2534240b 100644 --- a/fixtures/scheduler/index.html +++ b/fixtures/scheduler/index.html @@ -1,8 +1,9 @@ - - - Scheduler Test Page + + + + Scheduler Test Page - - -

Scheduler Fixture

-

- This fixture is for manual testing purposes, and the patterns used in - implementing it should not be used as a model. This is mainly for anyone - working on making changes to the `schedule` module. -

-

Tests:

-
    -
  1. - -

    Calls the callback within the frame when not blocked:

    -
    Expected:
    -
    -
    -
    -------------------------------------------------
    -
    If you see the same above and below it's correct. + + + +

    Scheduler Fixture

    +

    + This fixture is for manual testing purposes, and the patterns used in + implementing it should not be used as a model. This is mainly for anyone + working on making changes to the `schedule` module. +

    +

    Tests:

    +
      +
    1. + +

      Calls the callback within the frame when not blocked:

      +
      Expected:
      +
      +
      +
      -------------------------------------------------
      +
      If you see the same above and below it's correct.
      -------------------------------------------------
      Actual:
      -
    2. -
    3. -

      Accepts multiple callbacks and calls within frame when not blocked

      - -
      Expected:
      -
      -
      -
      -------------------------------------------------
      -
      If you see the same above and below it's correct. +
    4. +
    5. +

      Accepts multiple callbacks and calls within frame when not blocked

      + +
      Expected:
      +
      +
      +
      -------------------------------------------------
      +
      If you see the same above and below it's correct.
      -------------------------------------------------
      Actual:
      -
    6. -
    7. -

      Schedules callbacks in correct order when they use scheduleWork to schedule themselves

      - -
      Expected:
      -
      -
      -
      -------------------------------------------------
      -
      If you see the same above and below it's correct. +
    8. +
    9. +

      Schedules callbacks in correct order when they use scheduleWork to schedule themselves

      + +
      Expected:
      +
      +
      +
      -------------------------------------------------
      +
      If you see the same above and below it's correct.
      -------------------------------------------------
      Actual:
      -
    10. -
    11. -

      Calls timed out callbacks and then any more pending callbacks, defers others if time runs out

      - -
      Expected:
      -
      -
      -
      -------------------------------------------------
      -
      If you see the same above and below it's correct. +
    12. +
    13. +

      Calls timed out callbacks and then any more pending callbacks, defers others if time runs out

      + +
      Expected:
      +
      +
      +
      -------------------------------------------------
      +
      If you see the same above and below it's correct.
      -------------------------------------------------
      Actual:
      -
    14. -
    15. -

      When some callbacks throw errors, still calls them all within the same frame

      -

      IMPORTANT: Open the console when you run this! Inspect the logs there!

      - -
    16. -
    17. -

      When some callbacks throw errors and some also time out, still calls them all within the same frame

      -

      IMPORTANT: Open the console when you run this! Inspect the logs there!

      - -
    18. -
    19. -

      Continues calling callbacks even when user switches away from this tab

      - -
      Click the button above, observe the counter, then switch to - another tab and switch back:
      -
      -
      -
      If the counter advanced while you were away from this tab, it's correct.
      -
    20. -
    - - - - + + + - + \ No newline at end of file diff --git a/fixtures/tracing/script.js b/fixtures/tracing/script.js index 309efcf2a7725..48d98308eacbe 100644 --- a/fixtures/tracing/script.js +++ b/fixtures/tracing/script.js @@ -30,8 +30,8 @@ function checkSchedulerAPI() { if ( typeof Scheduler === 'undefined' || typeof Scheduler.unstable_now !== 'function' || - typeof Scheduler.unstable_scheduleWork !== 'function' || - typeof Scheduler.unstable_cancelScheduledWork !== 'function' + typeof Scheduler.unstable_scheduleCallback !== 'function' || + typeof Scheduler.unstable_cancelCallback !== 'function' ) { throw 'API is not defined'; } diff --git a/fixtures/unstable-async/suspense/src/components/App.js b/fixtures/unstable-async/suspense/src/components/App.js index 82198ce8d8c79..26d16f7d88c9e 100644 --- a/fixtures/unstable-async/suspense/src/components/App.js +++ b/fixtures/unstable-async/suspense/src/components/App.js @@ -1,5 +1,5 @@ import React, {Placeholder, PureComponent} from 'react'; -import {unstable_scheduleWork} from 'scheduler'; +import {unstable_scheduleCallback} from 'scheduler'; import { unstable_trace as trace, unstable_wrap as wrap, @@ -38,7 +38,7 @@ export default class App extends PureComponent { currentId: id, }) ); - unstable_scheduleWork( + unstable_scheduleCallback( wrap(() => trace(`View ${id} (low-pri)`, performance.now(), () => this.setState({ diff --git a/fixtures/unstable-async/time-slicing/src/index.js b/fixtures/unstable-async/time-slicing/src/index.js index 291bc8ab13fe5..0b1436e04eceb 100644 --- a/fixtures/unstable-async/time-slicing/src/index.js +++ b/fixtures/unstable-async/time-slicing/src/index.js @@ -1,6 +1,6 @@ import React, {PureComponent} from 'react'; import {flushSync, render} from 'react-dom'; -import {unstable_scheduleWork} from 'scheduler'; +import {unstable_scheduleCallback} from 'scheduler'; import _ from 'lodash'; import Charts from './Charts'; import Clock from './Clock'; @@ -67,7 +67,7 @@ class App extends PureComponent { } this._ignoreClick = true; - unstable_scheduleWork(() => { + unstable_scheduleCallback(() => { this.setState({showDemo: true}, () => { this._ignoreClick = false; }); @@ -107,7 +107,7 @@ class App extends PureComponent { this.debouncedHandleChange(value); break; case 'async': - unstable_scheduleWork(() => { + unstable_scheduleCallback(() => { this.setState({value}); }); break; diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index ea75201cb4872..bd7f8d043f4c5 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -7,8 +7,8 @@ export { unstable_now as now, - unstable_scheduleWork as scheduleDeferredCallback, - unstable_cancelScheduledWork as cancelDeferredCallback, + unstable_scheduleCallback as scheduleDeferredCallback, + unstable_cancelCallback as cancelDeferredCallback, } from 'scheduler'; import Transform from 'art/core/transform'; import Mode from 'art/modes/current'; diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index fedb694200c9a..a9dd23f2fac3c 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -62,8 +62,8 @@ export type NoTimeout = -1; export { unstable_now as now, - unstable_scheduleWork as scheduleDeferredCallback, - unstable_cancelScheduledWork as cancelDeferredCallback, + unstable_scheduleCallback as scheduleDeferredCallback, + unstable_cancelCallback as cancelDeferredCallback, } from 'scheduler'; let SUPPRESS_HYDRATION_WARNING; diff --git a/packages/react/src/ReactSharedInternals.js b/packages/react/src/ReactSharedInternals.js index 34db8d778ae71..c6cf7ded26b92 100644 --- a/packages/react/src/ReactSharedInternals.js +++ b/packages/react/src/ReactSharedInternals.js @@ -7,9 +7,12 @@ import assign from 'object-assign'; import { - unstable_cancelScheduledWork, + unstable_cancelCallback, unstable_now, - unstable_scheduleWork, + unstable_scheduleCallback, + unstable_runWithPriority, + unstable_wrapCallback, + unstable_getCurrentPriorityLevel, } from 'scheduler'; import { __interactionsRef, @@ -39,9 +42,12 @@ if (__UMD__) { // CJS bundles use the shared NPM package. Object.assign(ReactSharedInternals, { Scheduler: { - unstable_cancelScheduledWork, + unstable_cancelCallback, unstable_now, - unstable_scheduleWork, + unstable_scheduleCallback, + unstable_runWithPriority, + unstable_wrapCallback, + unstable_getCurrentPriorityLevel, }, SchedulerTracing: { __interactionsRef, diff --git a/packages/scheduler/npm/umd/scheduler.development.js b/packages/scheduler/npm/umd/scheduler.development.js index 67f9f903956d5..21c4c70dc1dc4 100644 --- a/packages/scheduler/npm/umd/scheduler.development.js +++ b/packages/scheduler/npm/umd/scheduler.development.js @@ -7,6 +7,8 @@ * LICENSE file in the root directory of this source tree. */ +/* eslint-disable max-len */ + 'use strict'; (function(global, factory) { @@ -23,15 +25,36 @@ ); } - function unstable_scheduleWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleWork.apply( + function unstable_scheduleCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleCallback.apply( + this, + arguments + ); + } + + function unstable_cancelCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelCallback.apply( + this, + arguments + ); + } + + function unstable_runWithPriority() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( + this, + arguments + ); + } + + function unstable_wrapCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply( this, arguments ); } - function unstable_cancelScheduledWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelScheduledWork.apply( + function unstable_getCurrentPriorityLevel() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getCurrentPriorityLevel.apply( this, arguments ); @@ -39,7 +62,10 @@ return Object.freeze({ unstable_now: unstable_now, - unstable_scheduleWork: unstable_scheduleWork, - unstable_cancelScheduledWork: unstable_cancelScheduledWork, + unstable_scheduleCallback: unstable_scheduleCallback, + unstable_cancelCallback: unstable_cancelCallback, + unstable_runWithPriority: unstable_runWithPriority, + unstable_wrapCallback: unstable_wrapCallback, + unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, }); }); diff --git a/packages/scheduler/npm/umd/scheduler.production.min.js b/packages/scheduler/npm/umd/scheduler.production.min.js index 67f9f903956d5..21c4c70dc1dc4 100644 --- a/packages/scheduler/npm/umd/scheduler.production.min.js +++ b/packages/scheduler/npm/umd/scheduler.production.min.js @@ -7,6 +7,8 @@ * LICENSE file in the root directory of this source tree. */ +/* eslint-disable max-len */ + 'use strict'; (function(global, factory) { @@ -23,15 +25,36 @@ ); } - function unstable_scheduleWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleWork.apply( + function unstable_scheduleCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleCallback.apply( + this, + arguments + ); + } + + function unstable_cancelCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelCallback.apply( + this, + arguments + ); + } + + function unstable_runWithPriority() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( + this, + arguments + ); + } + + function unstable_wrapCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply( this, arguments ); } - function unstable_cancelScheduledWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelScheduledWork.apply( + function unstable_getCurrentPriorityLevel() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getCurrentPriorityLevel.apply( this, arguments ); @@ -39,7 +62,10 @@ return Object.freeze({ unstable_now: unstable_now, - unstable_scheduleWork: unstable_scheduleWork, - unstable_cancelScheduledWork: unstable_cancelScheduledWork, + unstable_scheduleCallback: unstable_scheduleCallback, + unstable_cancelCallback: unstable_cancelCallback, + unstable_runWithPriority: unstable_runWithPriority, + unstable_wrapCallback: unstable_wrapCallback, + unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, }); }); diff --git a/packages/scheduler/npm/umd/scheduler.profiling.min.js b/packages/scheduler/npm/umd/scheduler.profiling.min.js index 67f9f903956d5..21c4c70dc1dc4 100644 --- a/packages/scheduler/npm/umd/scheduler.profiling.min.js +++ b/packages/scheduler/npm/umd/scheduler.profiling.min.js @@ -7,6 +7,8 @@ * LICENSE file in the root directory of this source tree. */ +/* eslint-disable max-len */ + 'use strict'; (function(global, factory) { @@ -23,15 +25,36 @@ ); } - function unstable_scheduleWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleWork.apply( + function unstable_scheduleCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleCallback.apply( + this, + arguments + ); + } + + function unstable_cancelCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelCallback.apply( + this, + arguments + ); + } + + function unstable_runWithPriority() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( + this, + arguments + ); + } + + function unstable_wrapCallback() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply( this, arguments ); } - function unstable_cancelScheduledWork() { - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelScheduledWork.apply( + function unstable_getCurrentPriorityLevel() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getCurrentPriorityLevel.apply( this, arguments ); @@ -39,7 +62,10 @@ return Object.freeze({ unstable_now: unstable_now, - unstable_scheduleWork: unstable_scheduleWork, - unstable_cancelScheduledWork: unstable_cancelScheduledWork, + unstable_scheduleCallback: unstable_scheduleCallback, + unstable_cancelCallback: unstable_cancelCallback, + unstable_runWithPriority: unstable_runWithPriority, + unstable_wrapCallback: unstable_wrapCallback, + unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, }); }); diff --git a/packages/scheduler/src/Scheduler.js b/packages/scheduler/src/Scheduler.js index 07bade1aee5b2..412f96930eb36 100644 --- a/packages/scheduler/src/Scheduler.js +++ b/packages/scheduler/src/Scheduler.js @@ -8,14 +8,34 @@ /* eslint-disable no-var */ -// TODO: Currently there's only a single priority level, Deferred. Will add -// additional priorities. -var DEFERRED_TIMEOUT = 5000; +// TODO: Use symbols? +var ImmediatePriority = 1; +var InteractivePriority = 2; +var NormalPriority = 3; +var WheneverPriority = 4; + +// Max 31 bit integer. The max integer size in V8 for 32-bit systems. +// Math.pow(2, 30) - 1 +// 0b111111111111111111111111111111 +var maxSigned31BitInt = 1073741823; + +// Times out immediately +var IMMEDIATE_PRIORITY_TIMEOUT = -1; +// Eventually times out +var INTERACTIVE_PRIORITY_TIMEOUT = 250; +var NORMAL_PRIORITY_TIMEOUT = 5000; +// Never times out +var WHENEVER_PRIORITY_TIMEOUT = maxSigned31BitInt; // Callbacks are stored as a circular, doubly linked list. var firstCallbackNode = null; -var isPerformingWork = false; +var currentPriorityLevel = NormalPriority; +var currentEventStartTime = -1; +var currentExpirationTime = -1; + +// This is set when a callback is being executed, to prevent re-entrancy. +var isExecutingCallback = false; var isHostCallbackScheduled = false; @@ -25,6 +45,14 @@ var hasNativePerformanceNow = var timeRemaining; if (hasNativePerformanceNow) { timeRemaining = function() { + if ( + firstCallbackNode !== null && + firstCallbackNode.expirationTime < currentExpirationTime + ) { + // A higher priority callback was scheduled. Yield so we can switch to + // working on that. + return 0; + } // 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. var remaining = getFrameDeadline() - performance.now(); @@ -33,6 +61,12 @@ if (hasNativePerformanceNow) { } else { timeRemaining = function() { // Fallback to Date.now() + if ( + firstCallbackNode !== null && + firstCallbackNode.expirationTime < currentExpirationTime + ) { + return 0; + } var remaining = getFrameDeadline() - Date.now(); return remaining > 0 ? remaining : 0; }; @@ -44,22 +78,22 @@ var deadlineObject = { }; function ensureHostCallbackIsScheduled() { - if (isPerformingWork) { + if (isExecutingCallback) { // Don't schedule work yet; wait until the next time we yield. return; } - // Schedule the host callback using the earliest timeout in the list. - var timesOutAt = firstCallbackNode.timesOutAt; + // Schedule the host callback using the earliest expiration in the list. + var expirationTime = firstCallbackNode.expirationTime; if (!isHostCallbackScheduled) { isHostCallbackScheduled = true; } else { // Cancel the existing host callback. - cancelCallback(); + cancelHostCallback(); } - requestCallback(flushWork, timesOutAt); + requestHostCallback(flushWork, expirationTime); } -function flushFirstCallback(node) { +function flushFirstCallback() { var flushedNode = firstCallbackNode; // Remove the node from the list before calling the callback. That way the @@ -70,35 +104,124 @@ function flushFirstCallback(node) { firstCallbackNode = null; next = null; } else { - var previous = firstCallbackNode.previous; - firstCallbackNode = previous.next = next; - next.previous = previous; + var lastCallbackNode = firstCallbackNode.previous; + firstCallbackNode = lastCallbackNode.next = next; + next.previous = lastCallbackNode; } flushedNode.next = flushedNode.previous = null; // Now it's safe to call the callback. var callback = flushedNode.callback; - callback(deadlineObject); + var expirationTime = flushedNode.expirationTime; + var priorityLevel = flushedNode.priorityLevel; + var previousPriorityLevel = currentPriorityLevel; + var previousExpirationTime = currentExpirationTime; + currentPriorityLevel = priorityLevel; + currentExpirationTime = expirationTime; + var continuationCallback; + try { + continuationCallback = callback(deadlineObject); + } finally { + currentPriorityLevel = previousPriorityLevel; + currentExpirationTime = previousExpirationTime; + } + + // A callback may return a continuation. The continuation should be scheduled + // with the same priority and expiration as the just-finished callback. + if (typeof continuationCallback === 'function') { + var continuationNode: CallbackNode = { + callback: continuationCallback, + priorityLevel, + expirationTime, + next: null, + previous: null, + }; + + // Insert the new callback into the list, sorted by its expiration. This is + // almost the same as the code in `scheduleCallback`, except the callback + // is inserted into the list *before* callbacks of equal expiration instead + // of after. + if (firstCallbackNode === null) { + // This is the first callback in the list. + firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode; + } else { + var nextAfterContinuation = null; + var node = firstCallbackNode; + do { + if (node.expirationTime >= expirationTime) { + // This callback expires at or after the continuation. We will insert + // the continuation *before* this callback. + nextAfterContinuation = node; + break; + } + node = node.next; + } while (node !== firstCallbackNode); + + if (nextAfterContinuation === null) { + // No equal or lower priority callback was found, which means the new + // callback is the lowest priority callback in the list. + nextAfterContinuation = firstCallbackNode; + } else if (nextAfterContinuation === firstCallbackNode) { + // The new callback is the highest priority callback in the list. + firstCallbackNode = continuationNode; + ensureHostCallbackIsScheduled(firstCallbackNode); + } + + var previous = nextAfterContinuation.previous; + previous.next = nextAfterContinuation.previous = continuationNode; + continuationNode.next = nextAfterContinuation; + continuationNode.previous = previous; + } + } +} + +function flushImmediateWork() { + if ( + // Confirm we've exited the outer most event handler + currentEventStartTime === -1 && + firstCallbackNode !== null && + firstCallbackNode.priorityLevel === ImmediatePriority + ) { + isExecutingCallback = true; + deadlineObject.didTimeout = true; + try { + do { + flushFirstCallback(); + } while ( + // Keep flushing until there are no more immediate callbacks + firstCallbackNode !== null && + firstCallbackNode.priorityLevel === ImmediatePriority + ); + } finally { + isExecutingCallback = false; + if (firstCallbackNode !== null) { + // There's still work remaining. Request another callback. + ensureHostCallbackIsScheduled(firstCallbackNode); + } else { + isHostCallbackScheduled = false; + } + } + } } function flushWork(didTimeout) { - isPerformingWork = true; + isExecutingCallback = true; deadlineObject.didTimeout = didTimeout; try { if (didTimeout) { - // Flush all the timed out callbacks without yielding. + // Flush all the expired callbacks without yielding. while (firstCallbackNode !== null) { // Read the current time. Flush all the callbacks that expire at or // earlier than that time. Then read the current time again and repeat. // This optimizes for as few performance.now calls as possible. var currentTime = getCurrentTime(); - if (firstCallbackNode.timesOutAt <= currentTime) { + if (firstCallbackNode.expirationTime <= currentTime) { do { flushFirstCallback(); } while ( firstCallbackNode !== null && - firstCallbackNode.timesOutAt <= currentTime + firstCallbackNode.expirationTime <= currentTime ); continue; } @@ -116,41 +239,104 @@ function flushWork(didTimeout) { } } } finally { - isPerformingWork = false; + isExecutingCallback = false; if (firstCallbackNode !== null) { // There's still work remaining. Request another callback. ensureHostCallbackIsScheduled(firstCallbackNode); } else { isHostCallbackScheduled = false; } + // Before exiting, flush all the immediate work that was scheduled. + flushImmediateWork(); } } -function unstable_scheduleWork(callback, options) { - var currentTime = getCurrentTime(); +function unstable_runWithPriority(priorityLevel, eventHandler) { + switch (priorityLevel) { + case ImmediatePriority: + case InteractivePriority: + case NormalPriority: + case WheneverPriority: + break; + default: + priorityLevel = NormalPriority; + } + + var previousPriorityLevel = currentPriorityLevel; + var previousEventStartTime = currentEventStartTime; + currentPriorityLevel = priorityLevel; + currentEventStartTime = getCurrentTime(); + + try { + return eventHandler(); + } finally { + currentPriorityLevel = previousPriorityLevel; + currentEventStartTime = previousEventStartTime; + + // Before exiting, flush all the immediate work that was scheduled. + flushImmediateWork(); + } +} - var timesOutAt; +function unstable_wrapCallback(callback) { + var parentPriorityLevel = currentPriorityLevel; + return function() { + // This is a fork of runWithPriority, inlined for performance. + var previousPriorityLevel = currentPriorityLevel; + var previousEventStartTime = currentEventStartTime; + currentPriorityLevel = parentPriorityLevel; + currentEventStartTime = getCurrentTime(); + + try { + return callback.apply(this, arguments); + } finally { + currentPriorityLevel = previousPriorityLevel; + currentEventStartTime = previousEventStartTime; + flushImmediateWork(); + } + }; +} + +function unstable_scheduleCallback(callback, deprecated_options) { + var startTime = + currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime(); + + var expirationTime; if ( - options !== undefined && - options !== null && - options.timeout !== null && - options.timeout !== undefined + typeof deprecated_options === 'object' && + deprecated_options !== null && + typeof deprecated_options.timeout === 'number' ) { - // Check for an explicit timeout - timesOutAt = currentTime + options.timeout; + // FIXME: Remove this branch once we lift expiration times out of React. + expirationTime = startTime + deprecated_options.timeout; } else { - // Compute an absolute timeout using the default constant. - timesOutAt = currentTime + DEFERRED_TIMEOUT; + switch (currentPriorityLevel) { + case ImmediatePriority: + expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT; + break; + case InteractivePriority: + expirationTime = startTime + INTERACTIVE_PRIORITY_TIMEOUT; + break; + case WheneverPriority: + expirationTime = startTime + WHENEVER_PRIORITY_TIMEOUT; + break; + case NormalPriority: + default: + expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT; + } } var newNode = { callback, - timesOutAt, + priorityLevel: currentPriorityLevel, + expirationTime, next: null, previous: null, }; - // Insert the new callback into the list, sorted by its timeout. + // Insert the new callback into the list, ordered first by expiration, then + // by insertion. So the new callback is inserted any other callback with + // equal expiration. if (firstCallbackNode === null) { // This is the first callback in the list. firstCallbackNode = newNode.next = newNode.previous = newNode; @@ -159,8 +345,8 @@ function unstable_scheduleWork(callback, options) { var next = null; var node = firstCallbackNode; do { - if (node.timesOutAt > timesOutAt) { - // The new callback times out before this one. + if (node.expirationTime > expirationTime) { + // The new callback expires before this one. next = node; break; } @@ -168,11 +354,11 @@ function unstable_scheduleWork(callback, options) { } while (node !== firstCallbackNode); if (next === null) { - // No callback with a later timeout was found, which means the new - // callback has the latest timeout in the list. + // No callback with a later expiration was found, which means the new + // callback has the latest expiration in the list. next = firstCallbackNode; } else if (next === firstCallbackNode) { - // The new callback has the earliest timeout in the entire list. + // The new callback has the earliest expiration in the entire list. firstCallbackNode = newNode; ensureHostCallbackIsScheduled(firstCallbackNode); } @@ -186,7 +372,7 @@ function unstable_scheduleWork(callback, options) { return newNode; } -function unstable_cancelScheduledWork(callbackNode) { +function unstable_cancelCallback(callbackNode) { var next = callbackNode.next; if (next === null) { // Already cancelled. @@ -209,6 +395,10 @@ function unstable_cancelScheduledWork(callbackNode) { callbackNode.next = callbackNode.previous = null; } +function unstable_getCurrentPriorityLevel() { + return currentPriorityLevel; +} + // The remaining code is essentially a polyfill for requestIdleCallback. It // works by scheduling a requestAnimationFrame, storing the time for the start // of the frame, then scheduling a postMessage which gets scheduled after paint. @@ -274,18 +464,18 @@ if (hasNativePerformanceNow) { }; } -var requestCallback; -var cancelCallback; +var requestHostCallback; +var cancelHostCallback; var getFrameDeadline; if (typeof window === 'undefined') { // If this accidentally gets imported in a non-browser environment, fallback // to a naive implementation. var timeoutID = -1; - requestCallback = function(callback, absoluteTimeout) { + requestHostCallback = function(callback, absoluteTimeout) { timeoutID = setTimeout(callback, 0, true); }; - cancelCallback = function() { + cancelHostCallback = function() { clearTimeout(timeoutID); }; getFrameDeadline = function() { @@ -294,11 +484,12 @@ if (typeof window === 'undefined') { } else if (window._schedMock) { // Dynamic injection, only for testing purposes. var impl = window._schedMock; - requestCallback = impl[0]; - cancelCallback = impl[1]; + requestHostCallback = impl[0]; + cancelHostCallback = impl[1]; getFrameDeadline = impl[2]; } else { if (typeof console !== 'undefined') { + // TODO: Remove fb.me link if (typeof localRequestAnimationFrame !== 'function') { console.error( "This browser doesn't support requestAnimationFrame. " + @@ -416,11 +607,10 @@ if (typeof window === 'undefined') { } }; - requestCallback = function(callback, absoluteTimeout) { + requestHostCallback = function(callback, absoluteTimeout) { scheduledCallback = callback; timeoutTime = absoluteTimeout; - if (isPerformingIdleWork) { - // If we're already performing idle work, an error must have been thrown. + if (isPerformingIdleWork || absoluteTimeout < 0) { // Don't wait for the next frame. Continue working ASAP, in a new event. window.postMessage(messageKey, '*'); } else if (!isAnimationFrameScheduled) { @@ -433,7 +623,7 @@ if (typeof window === 'undefined') { } }; - cancelCallback = function() { + cancelHostCallback = function() { scheduledCallback = null; isIdleScheduled = false; timeoutTime = -1; @@ -441,7 +631,14 @@ if (typeof window === 'undefined') { } export { - unstable_scheduleWork, - unstable_cancelScheduledWork, + ImmediatePriority as unstable_ImmediatePriority, + InteractivePriority as unstable_InteractivePriority, + NormalPriority as unstable_NormalPriority, + WheneverPriority as unstable_WheneverPriority, + unstable_runWithPriority, + unstable_scheduleCallback, + unstable_cancelCallback, + unstable_wrapCallback, + unstable_getCurrentPriorityLevel, getCurrentTime as unstable_now, }; diff --git a/packages/scheduler/src/__tests__/Scheduler-test.internal.js b/packages/scheduler/src/__tests__/Scheduler-test.internal.js index eaaae4ec911ce..f96b690a356ff 100644 --- a/packages/scheduler/src/__tests__/Scheduler-test.internal.js +++ b/packages/scheduler/src/__tests__/Scheduler-test.internal.js @@ -9,8 +9,14 @@ 'use strict'; -let scheduleWork; -let cancelScheduledWork; +let runWithPriority; +let ImmediatePriority; +let InteractivePriority; +let NormalPriority; +let scheduleCallback; +let cancelCallback; +let wrapCallback; +let getCurrentPriorityLevel; let flushWork; let advanceTime; let doWork; @@ -24,12 +30,16 @@ describe('Scheduler', () => { jest.resetModules(); let _flushWork = null; + let isFlushing = false; let timeoutID = -1; let endOfFrame = -1; let currentTime = 0; flushWork = frameSize => { + if (isFlushing) { + throw new Error('Already flushing work.'); + } if (frameSize === null || frameSize === undefined) { frameSize = Infinity; } @@ -39,8 +49,10 @@ describe('Scheduler', () => { timeoutID = -1; endOfFrame = currentTime + frameSize; try { - _flushWork(); + isFlushing = true; + _flushWork(false); } finally { + isFlushing = false; endOfFrame = -1; } const yields = yieldedValues; @@ -54,6 +66,9 @@ describe('Scheduler', () => { }; doWork = (label, timeCost) => { + if (typeof timeCost !== 'number') { + throw new Error('Second arg must be a number.'); + } advanceTime(timeCost); yieldValue(label); }; @@ -69,16 +84,32 @@ describe('Scheduler', () => { return yields; }; - function requestCallback(fw, absoluteTimeout) { + function onTimeout() { + if (_flushWork === null) { + return; + } + if (isFlushing) { + // Jest fires timers synchronously when jest.advanceTimersByTime is + // called. Use setImmediate to prevent re-entrancy. + setImmediate(onTimeout); + } else { + try { + isFlushing = true; + _flushWork(true); + } finally { + isFlushing = false; + } + } + } + + function requestHostCallback(fw, absoluteTimeout) { if (_flushWork !== null) { throw new Error('Work is already scheduled.'); } _flushWork = fw; - timeoutID = setTimeout(() => { - _flushWork(true); - }, absoluteTimeout - currentTime); + timeoutID = setTimeout(onTimeout, absoluteTimeout - currentTime); } - function cancelCallback() { + function cancelHostCallback() { if (_flushWork === null) { throw new Error('No work is scheduled.'); } @@ -91,19 +122,31 @@ describe('Scheduler', () => { // Override host implementation delete global.performance; - global.Date.now = () => currentTime; - window._schedMock = [requestCallback, cancelCallback, getTimeRemaining]; + global.Date.now = () => { + return currentTime; + }; + window._schedMock = [ + requestHostCallback, + cancelHostCallback, + getTimeRemaining, + ]; - const Scheduler = require('scheduler'); - scheduleWork = Scheduler.unstable_scheduleWork; - cancelScheduledWork = Scheduler.unstable_cancelScheduledWork; + const Schedule = require('scheduler'); + runWithPriority = Schedule.unstable_runWithPriority; + ImmediatePriority = Schedule.unstable_ImmediatePriority; + InteractivePriority = Schedule.unstable_InteractivePriority; + NormalPriority = Schedule.unstable_NormalPriority; + scheduleCallback = Schedule.unstable_scheduleCallback; + cancelCallback = Schedule.unstable_cancelCallback; + wrapCallback = Schedule.unstable_wrapCallback; + getCurrentPriorityLevel = Schedule.unstable_getCurrentPriorityLevel; }); it('flushes work incrementally', () => { - scheduleWork(() => doWork('A', 100)); - scheduleWork(() => doWork('B', 200)); - scheduleWork(() => doWork('C', 300)); - scheduleWork(() => doWork('D', 400)); + scheduleCallback(() => doWork('A', 100)); + scheduleCallback(() => doWork('B', 200)); + scheduleCallback(() => doWork('C', 300)); + scheduleCallback(() => doWork('D', 400)); expect(flushWork(300)).toEqual(['A', 'B']); expect(flushWork(300)).toEqual(['C']); @@ -111,11 +154,11 @@ describe('Scheduler', () => { }); it('cancels work', () => { - scheduleWork(() => doWork('A', 100)); - const callbackHandleB = scheduleWork(() => doWork('B', 200)); - scheduleWork(() => doWork('C', 300)); + scheduleCallback(() => doWork('A', 100)); + const callbackHandleB = scheduleCallback(() => doWork('B', 200)); + scheduleCallback(() => doWork('C', 300)); - cancelScheduledWork(callbackHandleB); + cancelCallback(callbackHandleB); expect(flushWork()).toEqual([ 'A', @@ -124,51 +167,336 @@ describe('Scheduler', () => { ]); }); - it('prioritizes callbacks according to their timeouts', () => { - scheduleWork(() => doWork('A', 10), {timeout: 5000}); - scheduleWork(() => doWork('B', 20), {timeout: 5000}); - scheduleWork(() => doWork('C', 30), {timeout: 1000}); - scheduleWork(() => doWork('D', 40), {timeout: 5000}); + it('executes the highest priority callbacks first', () => { + scheduleCallback(() => doWork('A', 100)); + scheduleCallback(() => doWork('B', 100)); + + // Yield before B is flushed + expect(flushWork(100)).toEqual(['A']); + + runWithPriority(InteractivePriority, () => { + scheduleCallback(() => doWork('C', 100)); + scheduleCallback(() => doWork('D', 100)); + }); + + // C and D should come first, because they are higher priority + expect(flushWork()).toEqual(['C', 'D', 'B']); + }); + + it('expires work', () => { + scheduleCallback(() => doWork('A', 100)); + runWithPriority(InteractivePriority, () => { + scheduleCallback(() => doWork('B', 100)); + }); + scheduleCallback(() => doWork('C', 100)); + runWithPriority(InteractivePriority, () => { + scheduleCallback(() => doWork('D', 100)); + }); + + // Advance time, but not by enough to expire any work + advanceTime(249); + expect(clearYieldedValues()).toEqual([]); + + // Advance by just a bit more to expire the high pri callbacks + advanceTime(1); + expect(clearYieldedValues()).toEqual(['B', 'D']); - // C should be first because it has the earliest timeout - expect(flushWork()).toEqual(['C', 'A', 'B', 'D']); + // Expire the rest + advanceTime(10000); + expect(clearYieldedValues()).toEqual(['A', 'C']); }); - it('times out work', () => { - scheduleWork(() => doWork('A', 100), {timeout: 5000}); - scheduleWork(() => doWork('B', 200), {timeout: 5000}); - scheduleWork(() => doWork('C', 300), {timeout: 1000}); - scheduleWork(() => doWork('D', 400), {timeout: 5000}); + it('has a default expiration of ~5 seconds', () => { + scheduleCallback(() => doWork('A', 100)); - // Advance time, but not by enough to flush any work - advanceTime(999); + advanceTime(4999); expect(clearYieldedValues()).toEqual([]); - // Advance by just a bit more to flush C advanceTime(1); - expect(clearYieldedValues()).toEqual(['C']); + expect(clearYieldedValues()).toEqual(['A']); + }); + + it('continues working on same task after yielding', () => { + scheduleCallback(() => doWork('A', 100)); + scheduleCallback(() => doWork('B', 100)); + + const tasks = [['C1', 100], ['C2', 100], ['C3', 100]]; + const C = deadline => { + while (tasks.length > 0) { + doWork(...tasks.shift()); + if ( + tasks.length > 0 && + !deadline.didTimeout && + deadline.timeRemaining() <= 0 + ) { + yieldValue('Yield!'); + return C; + } + } + }; + + scheduleCallback(C); + + scheduleCallback(() => doWork('D', 100)); + scheduleCallback(() => doWork('E', 100)); + + expect(flushWork(300)).toEqual(['A', 'B', 'C1', 'Yield!']); + + expect(flushWork()).toEqual(['C2', 'C3', 'D', 'E']); + }); + + it('continuation callbacks inherit the expiration of the previous callback', () => { + const tasks = [['A', 125], ['B', 125], ['C', 125], ['D', 125]]; + const work = deadline => { + while (tasks.length > 0) { + doWork(...tasks.shift()); + if ( + tasks.length > 0 && + !deadline.didTimeout && + deadline.timeRemaining() <= 0 + ) { + yieldValue('Yield!'); + return work; + } + } + }; + + // Schedule a high priority callback + runWithPriority(InteractivePriority, () => scheduleCallback(work)); + + // Flush until just before the expiration time + expect(flushWork(249)).toEqual(['A', 'B', 'Yield!']); + + // Advance time by just a bit more. This should expire all the remaining work. + advanceTime(1); + expect(clearYieldedValues()).toEqual(['C', 'D']); + }); + + it('nested callbacks inherit the priority of the currently executing callback', () => { + runWithPriority(InteractivePriority, () => { + scheduleCallback(() => { + doWork('Parent callback', 100); + scheduleCallback(() => { + doWork('Nested callback', 100); + }); + }); + }); - // Flush the rest - advanceTime(4000); - expect(clearYieldedValues()).toEqual(['A', 'B', 'D']); + expect(flushWork(100)).toEqual(['Parent callback']); + + // The nested callback has interactive priority, so it should + // expire quickly. + advanceTime(250 + 100); + expect(clearYieldedValues()).toEqual(['Nested callback']); }); - it('has a default timeout of 5 seconds', () => { - scheduleWork(() => doWork('A', 100)); - scheduleWork(() => doWork('B', 200)); - scheduleWork(() => doWork('C', 300), {timeout: 1000}); - scheduleWork(() => doWork('D', 400)); + it('continuations are interrupted by higher priority work', () => { + const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]]; + const work = deadline => { + while (tasks.length > 0) { + doWork(...tasks.shift()); + if ( + tasks.length > 0 && + !deadline.didTimeout && + deadline.timeRemaining() <= 0 + ) { + yieldValue('Yield!'); + return work; + } + } + }; + scheduleCallback(work); + expect(flushWork(100)).toEqual(['A', 'Yield!']); + + runWithPriority(InteractivePriority, () => { + scheduleCallback(() => doWork('High pri', 100)); + }); + + expect(flushWork()).toEqual(['High pri', 'B', 'C', 'D']); + }); - // Flush C - advanceTime(1000); - expect(clearYieldedValues()).toEqual(['C']); + it( + 'continutations are interrupted by higher priority work scheduled ' + + 'inside an executing callback', + () => { + const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]]; + const work = deadline => { + while (tasks.length > 0) { + const task = tasks.shift(); + doWork(...task); + if (task[0] === 'B') { + // Schedule high pri work from inside another callback + yieldValue('Schedule high pri'); + runWithPriority(InteractivePriority, () => + scheduleCallback(() => doWork('High pri', 100)), + ); + } + if ( + tasks.length > 0 && + !deadline.didTimeout && + deadline.timeRemaining() <= 0 + ) { + yieldValue('Yield!'); + return work; + } + } + }; + scheduleCallback(work); + expect(flushWork()).toEqual([ + 'A', + 'B', + 'Schedule high pri', + // Even though there's time left in the frame, the low pri callback + // should yield to the high pri callback + 'Yield!', + 'High pri', + // Continue low pri work + 'C', + 'D', + ]); + }, + ); - // Advance time until right before the rest of the work expires - advanceTime(3699); + it('immediate callbacks fire at the end of outermost event', () => { + runWithPriority(ImmediatePriority, () => { + scheduleCallback(() => yieldValue('A')); + scheduleCallback(() => yieldValue('B')); + // Nested event + runWithPriority(ImmediatePriority, () => { + scheduleCallback(() => yieldValue('C')); + // Nothing should have fired yet + expect(clearYieldedValues()).toEqual([]); + }); + // Nothing should have fired yet + expect(clearYieldedValues()).toEqual([]); + }); + // The callbacks were called at the end of the outer event + expect(clearYieldedValues()).toEqual(['A', 'B', 'C']); + }); + + it('wrapped callbacks have same signature as original callback', () => { + const wrappedCallback = wrapCallback((...args) => ({args})); + expect(wrappedCallback('a', 'b')).toEqual({args: ['a', 'b']}); + }); + + it('wrapped callbacks inherit the current priority', () => { + const wrappedCallback = wrapCallback(() => { + scheduleCallback(() => { + doWork('Normal', 100); + }); + }); + const wrappedInteractiveCallback = runWithPriority( + InteractivePriority, + () => + wrapCallback(() => { + scheduleCallback(() => { + doWork('Interactive', 100); + }); + }), + ); + + // This should schedule a normal callback + wrappedCallback(); + // This should schedule an interactive callback + wrappedInteractiveCallback(); + + advanceTime(249); expect(clearYieldedValues()).toEqual([]); + advanceTime(1); + expect(clearYieldedValues()).toEqual(['Interactive']); - // Now advance by just a bit more + advanceTime(10000); + expect(clearYieldedValues()).toEqual(['Normal']); + }); + + it('wrapped callbacks inherit the current priority even when nested', () => { + const wrappedCallback = wrapCallback(() => { + scheduleCallback(() => { + doWork('Normal', 100); + }); + }); + const wrappedInteractiveCallback = runWithPriority( + InteractivePriority, + () => + wrapCallback(() => { + scheduleCallback(() => { + doWork('Interactive', 100); + }); + }), + ); + + runWithPriority(InteractivePriority, () => { + // This should schedule a normal callback + wrappedCallback(); + // This should schedule an interactive callback + wrappedInteractiveCallback(); + }); + + advanceTime(249); + expect(clearYieldedValues()).toEqual([]); advanceTime(1); - expect(clearYieldedValues()).toEqual(['A', 'B', 'D']); + expect(clearYieldedValues()).toEqual(['Interactive']); + + advanceTime(10000); + expect(clearYieldedValues()).toEqual(['Normal']); + }); + + it('immediate callbacks fire at the end of callback', () => { + const immediateCallback = runWithPriority(ImmediatePriority, () => + wrapCallback(() => { + scheduleCallback(() => yieldValue('callback')); + }), + ); + immediateCallback(); + + // The callback was called at the end of the outer event + expect(clearYieldedValues()).toEqual(['callback']); + }); + + it("immediate callbacks fire even if there's an error", () => { + expect(() => { + runWithPriority(ImmediatePriority, () => { + scheduleCallback(() => { + yieldValue('A'); + throw new Error('Oops A'); + }); + scheduleCallback(() => { + yieldValue('B'); + }); + scheduleCallback(() => { + yieldValue('C'); + throw new Error('Oops C'); + }); + }); + }).toThrow('Oops A'); + + expect(clearYieldedValues()).toEqual(['A']); + + // B and C flush in a subsequent event. That way, the second error is not + // swallowed. + expect(() => flushWork(0)).toThrow('Oops C'); + expect(clearYieldedValues()).toEqual(['B', 'C']); + }); + + it('exposes the current priority level', () => { + yieldValue(getCurrentPriorityLevel()); + runWithPriority(ImmediatePriority, () => { + yieldValue(getCurrentPriorityLevel()); + runWithPriority(NormalPriority, () => { + yieldValue(getCurrentPriorityLevel()); + runWithPriority(InteractivePriority, () => { + yieldValue(getCurrentPriorityLevel()); + }); + }); + yieldValue(getCurrentPriorityLevel()); + }); + + expect(clearYieldedValues()).toEqual([ + NormalPriority, + ImmediatePriority, + NormalPriority, + InteractivePriority, + ImmediatePriority, + ]); }); }); diff --git a/packages/scheduler/src/__tests__/SchedulerDOM-test.js b/packages/scheduler/src/__tests__/SchedulerDOM-test.js index ae9c0826c46ee..b1253ffe9ddc3 100644 --- a/packages/scheduler/src/__tests__/SchedulerDOM-test.js +++ b/packages/scheduler/src/__tests__/SchedulerDOM-test.js @@ -97,11 +97,11 @@ describe('SchedulerDOM', () => { Scheduler = require('scheduler'); }); - describe('scheduleWork', () => { + describe('scheduleCallback', () => { it('calls the callback within the frame when not blocked', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const cb = jest.fn(); - scheduleWork(cb); + scheduleCallback(cb); advanceOneFrame({timeLeftInFrame: 15}); expect(cb).toHaveBeenCalledTimes(1); // should not have timed out and should include a timeRemaining method @@ -111,15 +111,15 @@ describe('SchedulerDOM', () => { describe('with multiple callbacks', () => { it('accepts multiple callbacks and calls within frame when not blocked', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => callbackLog.push('B')); - scheduleWork(callbackA); + scheduleCallback(callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); // waits while second callback is passed - scheduleWork(callbackB); + scheduleCallback(callbackB); expect(callbackLog).toEqual([]); // after a delay, calls as many callbacks as it has time for advanceOneFrame({timeLeftInFrame: 15}); @@ -137,17 +137,17 @@ describe('SchedulerDOM', () => { }); it("accepts callbacks betweeen animationFrame and postMessage and doesn't stall", () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => callbackLog.push('B')); const callbackC = jest.fn(() => callbackLog.push('C')); - scheduleWork(callbackA); + scheduleCallback(callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); runRAFCallbacks(); // this should schedule work *after* the requestAnimationFrame but before the message handler - scheduleWork(callbackB); + scheduleCallback(callbackB); expect(callbackLog).toEqual([]); // now it should drain the message queue and do all scheduled work runPostMessageCallbacks({timeLeftInFrame: 15}); @@ -157,7 +157,7 @@ describe('SchedulerDOM', () => { advanceOneFrame({timeLeftInFrame: 15}); // see if more work can be done now. - scheduleWork(callbackC); + scheduleCallback(callbackC); expect(callbackLog).toEqual(['A', 'B']); advanceOneFrame({timeLeftInFrame: 15}); expect(callbackLog).toEqual(['A', 'B', 'C']); @@ -167,11 +167,11 @@ describe('SchedulerDOM', () => { 'schedules callbacks in correct order and' + 'keeps calling them if there is time', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); - scheduleWork(callbackC); + scheduleCallback(callbackC); }); const callbackB = jest.fn(() => { callbackLog.push('B'); @@ -180,11 +180,11 @@ describe('SchedulerDOM', () => { callbackLog.push('C'); }); - scheduleWork(callbackA); + scheduleCallback(callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); // continues waiting while B is scheduled - scheduleWork(callbackB); + scheduleCallback(callbackB); expect(callbackLog).toEqual([]); // after a delay, calls the scheduled callbacks, // and also calls new callbacks scheduled by current callbacks @@ -193,18 +193,18 @@ describe('SchedulerDOM', () => { }, ); - it('schedules callbacks in correct order when callbacks have many nested scheduleWork calls', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + it('schedules callbacks in correct order when callbacks have many nested scheduleCallback calls', () => { + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); - scheduleWork(callbackC); - scheduleWork(callbackD); + scheduleCallback(callbackC); + scheduleCallback(callbackD); }); const callbackB = jest.fn(() => { callbackLog.push('B'); - scheduleWork(callbackE); - scheduleWork(callbackF); + scheduleCallback(callbackE); + scheduleCallback(callbackF); }); const callbackC = jest.fn(() => { callbackLog.push('C'); @@ -219,8 +219,8 @@ describe('SchedulerDOM', () => { callbackLog.push('F'); }); - scheduleWork(callbackA); - scheduleWork(callbackB); + scheduleCallback(callbackA); + scheduleCallback(callbackB); // initially waits to call the callback expect(callbackLog).toEqual([]); // while flushing callbacks, calls as many as it has time for @@ -228,23 +228,23 @@ describe('SchedulerDOM', () => { expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F']); }); - it('schedules callbacks in correct order when they use scheduleWork to schedule themselves', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + it('schedules callbacks in correct order when they use scheduleCallback to schedule themselves', () => { + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; let callbackAIterations = 0; const callbackA = jest.fn(() => { if (callbackAIterations < 1) { - scheduleWork(callbackA); + scheduleCallback(callbackA); } callbackLog.push('A' + callbackAIterations); callbackAIterations++; }); const callbackB = jest.fn(() => callbackLog.push('B')); - scheduleWork(callbackA); + scheduleCallback(callbackA); // initially waits to call the callback expect(callbackLog).toEqual([]); - scheduleWork(callbackB); + scheduleCallback(callbackB); expect(callbackLog).toEqual([]); // after a delay, calls the latest callback passed advanceOneFrame({timeLeftInFrame: 15}); @@ -260,7 +260,7 @@ describe('SchedulerDOM', () => { describe('when there is no more time left in the frame', () => { it('calls any callback which has timed out, waits for others', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; startOfLatestFrame = 1000000000000; currentTime = startOfLatestFrame - 10; const callbackLog = []; @@ -269,9 +269,9 @@ describe('SchedulerDOM', () => { const callbackB = jest.fn(() => callbackLog.push('B')); const callbackC = jest.fn(() => callbackLog.push('C')); - scheduleWork(callbackA); // won't time out - scheduleWork(callbackB, {timeout: 100}); // times out later - scheduleWork(callbackC, {timeout: 2}); // will time out fast + scheduleCallback(callbackA); // won't time out + scheduleCallback(callbackB, {timeout: 100}); // times out later + scheduleCallback(callbackC, {timeout: 2}); // will time out fast // push time ahead a bit so that we have no idle time advanceOneFrame({timePastFrameDeadline: 16}); @@ -295,7 +295,7 @@ describe('SchedulerDOM', () => { describe('when there is some time left in the frame', () => { it('calls timed out callbacks and then any more pending callbacks, defers others if time runs out', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; startOfLatestFrame = 1000000000000; currentTime = startOfLatestFrame - 10; const callbackLog = []; @@ -309,10 +309,10 @@ describe('SchedulerDOM', () => { const callbackC = jest.fn(() => callbackLog.push('C')); const callbackD = jest.fn(() => callbackLog.push('D')); - scheduleWork(callbackA, {timeout: 100}); // won't time out - scheduleWork(callbackB, {timeout: 100}); // times out later - scheduleWork(callbackC, {timeout: 2}); // will time out fast - scheduleWork(callbackD, {timeout: 200}); // won't time out + scheduleCallback(callbackA, {timeout: 100}); // won't time out + scheduleCallback(callbackB, {timeout: 100}); // times out later + scheduleCallback(callbackC, {timeout: 2}); // will time out fast + scheduleCallback(callbackD, {timeout: 200}); // won't time out advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks @@ -341,16 +341,16 @@ describe('SchedulerDOM', () => { }); }); - describe('cancelScheduledWork', () => { + describe('cancelCallback', () => { it('cancels the scheduled callback', () => { const { - unstable_scheduleWork: scheduleWork, - unstable_cancelScheduledWork: cancelScheduledWork, + unstable_scheduleCallback: scheduleCallback, + unstable_cancelCallback: cancelCallback, } = Scheduler; const cb = jest.fn(); - const callbackId = scheduleWork(cb); + const callbackId = scheduleCallback(cb); expect(cb).toHaveBeenCalledTimes(0); - cancelScheduledWork(callbackId); + cancelCallback(callbackId); advanceOneFrame({timeLeftInFrame: 15}); expect(cb).toHaveBeenCalledTimes(0); }); @@ -358,19 +358,19 @@ describe('SchedulerDOM', () => { describe('with multiple callbacks', () => { it('when called more than once', () => { const { - unstable_scheduleWork: scheduleWork, - unstable_cancelScheduledWork: cancelScheduledWork, + unstable_scheduleCallback: scheduleCallback, + unstable_cancelCallback: cancelCallback, } = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => callbackLog.push('B')); const callbackC = jest.fn(() => callbackLog.push('C')); - scheduleWork(callbackA); - const callbackId = scheduleWork(callbackB); - scheduleWork(callbackC); - cancelScheduledWork(callbackId); - cancelScheduledWork(callbackId); - cancelScheduledWork(callbackId); + scheduleCallback(callbackA); + const callbackId = scheduleCallback(callbackB); + scheduleCallback(callbackC); + cancelCallback(callbackId); + cancelCallback(callbackId); + cancelCallback(callbackId); // Initially doesn't call anything expect(callbackLog).toEqual([]); advanceOneFrame({timeLeftInFrame: 15}); @@ -382,18 +382,18 @@ describe('SchedulerDOM', () => { it('when one callback cancels the next one', () => { const { - unstable_scheduleWork: scheduleWork, - unstable_cancelScheduledWork: cancelScheduledWork, + unstable_scheduleCallback: scheduleCallback, + unstable_cancelCallback: cancelCallback, } = Scheduler; const callbackLog = []; let callbackBId; const callbackA = jest.fn(() => { callbackLog.push('A'); - cancelScheduledWork(callbackBId); + cancelCallback(callbackBId); }); const callbackB = jest.fn(() => callbackLog.push('B')); - scheduleWork(callbackA); - callbackBId = scheduleWork(callbackB); + scheduleCallback(callbackA); + callbackBId = scheduleCallback(callbackB); // Initially doesn't call anything expect(callbackLog).toEqual([]); advanceOneFrame({timeLeftInFrame: 15}); @@ -421,7 +421,7 @@ describe('SchedulerDOM', () => { * */ it('still calls all callbacks within same frame', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => callbackLog.push('A')); const callbackB = jest.fn(() => { @@ -434,11 +434,11 @@ describe('SchedulerDOM', () => { throw new Error('D error'); }); const callbackE = jest.fn(() => callbackLog.push('E')); - scheduleWork(callbackA); - scheduleWork(callbackB); - scheduleWork(callbackC); - scheduleWork(callbackD); - scheduleWork(callbackE); + scheduleCallback(callbackA); + scheduleCallback(callbackB); + scheduleCallback(callbackC); + scheduleCallback(callbackD); + scheduleCallback(callbackE); // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -467,7 +467,7 @@ describe('SchedulerDOM', () => { * */ it('and with some timed out callbacks, still calls all callbacks within same frame', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); @@ -480,11 +480,11 @@ describe('SchedulerDOM', () => { throw new Error('D error'); }); const callbackE = jest.fn(() => callbackLog.push('E')); - scheduleWork(callbackA); - scheduleWork(callbackB); - scheduleWork(callbackC, {timeout: 2}); // times out fast - scheduleWork(callbackD, {timeout: 2}); // times out fast - scheduleWork(callbackE, {timeout: 2}); // times out fast + scheduleCallback(callbackA); + scheduleCallback(callbackB); + scheduleCallback(callbackC, {timeout: 2}); // times out fast + scheduleCallback(callbackD, {timeout: 2}); // times out fast + scheduleCallback(callbackE, {timeout: 2}); // times out fast // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -513,7 +513,7 @@ describe('SchedulerDOM', () => { * */ it('still calls all callbacks within same frame', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); @@ -535,11 +535,11 @@ describe('SchedulerDOM', () => { callbackLog.push('E'); throw new Error('E error'); }); - scheduleWork(callbackA); - scheduleWork(callbackB); - scheduleWork(callbackC); - scheduleWork(callbackD); - scheduleWork(callbackE); + scheduleCallback(callbackA); + scheduleCallback(callbackB); + scheduleCallback(callbackC); + scheduleCallback(callbackD); + scheduleCallback(callbackE); // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -574,7 +574,7 @@ describe('SchedulerDOM', () => { * */ it('and with all timed out callbacks, still calls all callbacks within same frame', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; const callbackLog = []; const callbackA = jest.fn(() => { callbackLog.push('A'); @@ -596,11 +596,11 @@ describe('SchedulerDOM', () => { callbackLog.push('E'); throw new Error('E error'); }); - scheduleWork(callbackA, {timeout: 2}); // times out fast - scheduleWork(callbackB, {timeout: 2}); // times out fast - scheduleWork(callbackC, {timeout: 2}); // times out fast - scheduleWork(callbackD, {timeout: 2}); // times out fast - scheduleWork(callbackE, {timeout: 2}); // times out fast + scheduleCallback(callbackA, {timeout: 2}); // times out fast + scheduleCallback(callbackB, {timeout: 2}); // times out fast + scheduleCallback(callbackC, {timeout: 2}); // times out fast + scheduleCallback(callbackD, {timeout: 2}); // times out fast + scheduleCallback(callbackE, {timeout: 2}); // times out fast // Initially doesn't call anything expect(callbackLog).toEqual([]); catchPostMessageErrors = true; @@ -655,7 +655,7 @@ describe('SchedulerDOM', () => { * */ it('still calls all callbacks within same frame', () => { - const {unstable_scheduleWork: scheduleWork} = Scheduler; + const {unstable_scheduleCallback: scheduleCallback} = Scheduler; startOfLatestFrame = 1000000000000; currentTime = startOfLatestFrame - 10; catchPostMessageErrors = true; @@ -689,13 +689,13 @@ describe('SchedulerDOM', () => { }); const callbackG = jest.fn(() => callbackLog.push('G')); - scheduleWork(callbackA); - scheduleWork(callbackB); - scheduleWork(callbackC); - scheduleWork(callbackD); - scheduleWork(callbackE); - scheduleWork(callbackF); - scheduleWork(callbackG); + scheduleCallback(callbackA); + scheduleCallback(callbackB); + scheduleCallback(callbackC); + scheduleCallback(callbackD); + scheduleCallback(callbackE); + scheduleCallback(callbackF); + scheduleCallback(callbackG); // does nothing initially expect(callbackLog).toEqual([]); diff --git a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js index 5b6f04a2a1cc5..8fd78522480f1 100644 --- a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js +++ b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js @@ -17,7 +17,8 @@ describe('Scheduling UMD bundle', () => { }); function filterPrivateKeys(name) { - return !name.startsWith('_'); + // TODO: Figure out how to forward priority levels. + return !name.startsWith('_') && !name.endsWith('Priority'); } function validateForwardedAPIs(api, forwardedAPIs) { diff --git a/packages/shared/forks/Scheduler.umd.js b/packages/shared/forks/Scheduler.umd.js index 86ad51ac79735..66d3125c37106 100644 --- a/packages/shared/forks/Scheduler.umd.js +++ b/packages/shared/forks/Scheduler.umd.js @@ -12,9 +12,9 @@ import React from 'react'; const ReactInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; const { - unstable_cancelScheduledWork, + unstable_cancelCallback, unstable_now, - unstable_scheduleWork, + unstable_scheduleCallback, } = ReactInternals.Scheduler; -export {unstable_cancelScheduledWork, unstable_now, unstable_scheduleWork}; +export {unstable_cancelCallback, unstable_now, unstable_scheduleCallback};