diff --git a/fixtures/scheduler/index.html b/fixtures/scheduler/index.html index bdf1ffe00940a..553d79970d894 100644 --- a/fixtures/scheduler/index.html +++ b/fixtures/scheduler/index.html @@ -90,6 +90,21 @@

Tests:

If the counter advanced while you were away from this tab, it's correct.
+
  • +

    Can pause execution, dump scheduled callbacks, and continue where it left off

    + +
    Click the button above, press "continue" to finish the test after it pauses:
    + +
    Expected:
    +
    +
    +
    -------------------------------------------------
    +
    If the test didn't progress until you hit "continue" and
    +
    you see the same above and below afterwards it's correct. +
    -------------------------------------------------
    +
    Actual:
    +
    +
  • @@ -98,7 +113,10 @@

    Tests:

    const { unstable_scheduleCallback: scheduleCallback, unstable_cancelCallback: cancelCallback, - unstable_now: now + unstable_now: now, + unstable_getFirstCallbackNode: getFirstCallbackNode, + unstable_pauseExecution: pauseExecution, + unstable_continueExecution: continueExecution, } = Scheduler; function displayTestResult(testNumber) { const expectationNode = document.getElementById('test-' + testNumber + '-expected'); @@ -215,6 +233,16 @@

    Tests:

    [ // ... TODO ], + [], + [], + // Test 8 + [ + 'Queue size: 0.', + 'Pausing... press continue to resume.', + 'Queue size: 2.', + 'Finishing...', + 'Done!', + ], ]; function runTestOne() { // Test 1 @@ -496,6 +524,51 @@

    Tests:

    } scheduleCallback(incrementCounterAndScheduleNextCallback); } + +function runTestEight() { + // Test 8 + // Pauses execution, dumps the queue, and continues execution + clearTestResult(8); + + function countNodesInStack(firstCallbackNode) { + var node = firstCallbackNode; + var count = 0; + if (node !== null) { + do { + count = count + 1; + node = node.next; + } while (node !== firstCallbackNode); + } + return count; + } + + scheduleCallback(() => { + + // size should be 0 + updateTestResult(8, `Queue size: ${countNodesInStack(getFirstCallbackNode())}.`); + updateTestResult(8, 'Pausing... press continue to resume.'); + pauseExecution(); + + scheduleCallback(function () { + updateTestResult(8, 'Finishing...'); + displayTestResult(8); + }) + scheduleCallback(function () { + updateTestResult(8, 'Done!'); + displayTestResult(8); + checkTestResult(8); + }) + + // new size should be 2 now + updateTestResult(8, `Queue size: ${countNodesInStack(getFirstCallbackNode())}.`); + displayTestResult(8); + }); +} + +function continueTestEight() { + continueExecution(); +} + \ No newline at end of file diff --git a/packages/react/src/ReactSharedInternals.js b/packages/react/src/ReactSharedInternals.js index 0181f090cdbfb..5b1f69929db79 100644 --- a/packages/react/src/ReactSharedInternals.js +++ b/packages/react/src/ReactSharedInternals.js @@ -12,6 +12,9 @@ import { unstable_now, unstable_scheduleCallback, unstable_runWithPriority, + unstable_getFirstCallbackNode, + unstable_pauseExecution, + unstable_continueExecution, unstable_wrapCallback, unstable_getCurrentPriorityLevel, } from 'scheduler'; @@ -49,6 +52,9 @@ if (__UMD__) { unstable_scheduleCallback, unstable_runWithPriority, unstable_wrapCallback, + unstable_getFirstCallbackNode, + unstable_pauseExecution, + unstable_continueExecution, unstable_getCurrentPriorityLevel, }, SchedulerTracing: { diff --git a/packages/scheduler/npm/umd/scheduler.development.js b/packages/scheduler/npm/umd/scheduler.development.js index 52d08c5cba4fe..41ac8e437bbd6 100644 --- a/packages/scheduler/npm/umd/scheduler.development.js +++ b/packages/scheduler/npm/umd/scheduler.development.js @@ -68,6 +68,27 @@ ); } + function unstable_getFirstCallbackNode() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getFirstCallbackNode.apply( + this, + arguments + ); + } + + function unstable_pauseExecution() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_pauseExecution.apply( + this, + arguments + ); + } + + function unstable_continueExecution() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_continueExecution.apply( + this, + arguments + ); + } + return Object.freeze({ unstable_now: unstable_now, unstable_scheduleCallback: unstable_scheduleCallback, @@ -76,5 +97,8 @@ unstable_runWithPriority: unstable_runWithPriority, unstable_wrapCallback: unstable_wrapCallback, unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, + unstable_continueExecution: unstable_continueExecution, + unstable_pauseExecution: unstable_pauseExecution, + unstable_getFirstCallbackNode: unstable_getFirstCallbackNode, }); }); diff --git a/packages/scheduler/npm/umd/scheduler.production.min.js b/packages/scheduler/npm/umd/scheduler.production.min.js index 52d08c5cba4fe..cea54f4da3cba 100644 --- a/packages/scheduler/npm/umd/scheduler.production.min.js +++ b/packages/scheduler/npm/umd/scheduler.production.min.js @@ -68,6 +68,21 @@ ); } + function unstable_getFirstCallbackNode() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getFirstCallbackNode.apply( + this, + arguments + ); + } + + function unstable_pauseExecution() { + return undefined; + } + + function unstable_continueExecution() { + return undefined; + } + return Object.freeze({ unstable_now: unstable_now, unstable_scheduleCallback: unstable_scheduleCallback, @@ -76,5 +91,8 @@ unstable_runWithPriority: unstable_runWithPriority, unstable_wrapCallback: unstable_wrapCallback, unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, + unstable_continueExecution: unstable_continueExecution, + unstable_pauseExecution: unstable_pauseExecution, + unstable_getFirstCallbackNode: unstable_getFirstCallbackNode, }); }); diff --git a/packages/scheduler/npm/umd/scheduler.profiling.min.js b/packages/scheduler/npm/umd/scheduler.profiling.min.js index 52d08c5cba4fe..cea54f4da3cba 100644 --- a/packages/scheduler/npm/umd/scheduler.profiling.min.js +++ b/packages/scheduler/npm/umd/scheduler.profiling.min.js @@ -68,6 +68,21 @@ ); } + function unstable_getFirstCallbackNode() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getFirstCallbackNode.apply( + this, + arguments + ); + } + + function unstable_pauseExecution() { + return undefined; + } + + function unstable_continueExecution() { + return undefined; + } + return Object.freeze({ unstable_now: unstable_now, unstable_scheduleCallback: unstable_scheduleCallback, @@ -76,5 +91,8 @@ unstable_runWithPriority: unstable_runWithPriority, unstable_wrapCallback: unstable_wrapCallback, unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, + unstable_continueExecution: unstable_continueExecution, + unstable_pauseExecution: unstable_pauseExecution, + unstable_getFirstCallbackNode: unstable_getFirstCallbackNode, }); }); diff --git a/packages/scheduler/src/Scheduler.js b/packages/scheduler/src/Scheduler.js index 6d310896e1891..ee34cd297e5f7 100644 --- a/packages/scheduler/src/Scheduler.js +++ b/packages/scheduler/src/Scheduler.js @@ -8,6 +8,8 @@ /* eslint-disable no-var */ +import {enableSchedulerDebugging} from 'shared/ReactFeatureFlags'; + // TODO: Use symbols? var ImmediatePriority = 1; var UserBlockingPriority = 2; @@ -33,6 +35,9 @@ var IDLE_PRIORITY = maxSigned31BitInt; var firstCallbackNode = null; var currentDidTimeout = false; +// Pausing the scheduler is useful for debugging. +var isSchedulerPaused = false; + var currentPriorityLevel = NormalPriority; var currentEventStartTime = -1; var currentExpirationTime = -1; @@ -173,13 +178,23 @@ function flushImmediateWork() { } function flushWork(didTimeout) { + // Exit right away if we're currently paused + + if (enableSchedulerDebugging && isSchedulerPaused) { + return; + } + isExecutingCallback = true; const previousDidTimeout = currentDidTimeout; currentDidTimeout = didTimeout; try { if (didTimeout) { // Flush all the expired callbacks without yielding. - while (firstCallbackNode !== null) { + while ( + firstCallbackNode !== null && + !(enableSchedulerDebugging && isSchedulerPaused) + ) { + // TODO Wrap i nfeature flag // 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. @@ -189,7 +204,8 @@ function flushWork(didTimeout) { flushFirstCallback(); } while ( firstCallbackNode !== null && - firstCallbackNode.expirationTime <= currentTime + firstCallbackNode.expirationTime <= currentTime && + !(enableSchedulerDebugging && isSchedulerPaused) ); continue; } @@ -199,6 +215,9 @@ function flushWork(didTimeout) { // Keep flushing callbacks until we run out of time in the frame. if (firstCallbackNode !== null) { do { + if (enableSchedulerDebugging && isSchedulerPaused) { + break; + } flushFirstCallback(); } while (firstCallbackNode !== null && !shouldYieldToHost()); } @@ -342,6 +361,21 @@ function unstable_scheduleCallback(callback, deprecated_options) { return newNode; } +function unstable_pauseExecution() { + isSchedulerPaused = true; +} + +function unstable_continueExecution() { + isSchedulerPaused = false; + if (firstCallbackNode !== null) { + ensureHostCallbackIsScheduled(); + } +} + +function unstable_getFirstCallbackNode() { + return firstCallbackNode; +} + function unstable_cancelCallback(callbackNode) { var next = callbackNode.next; if (next === null) { @@ -659,5 +693,8 @@ export { unstable_wrapCallback, unstable_getCurrentPriorityLevel, unstable_shouldYield, + unstable_continueExecution, + unstable_pauseExecution, + unstable_getFirstCallbackNode, getCurrentTime as unstable_now, }; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 9b7b557ac65fd..27fcfb8c21d8c 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -33,7 +33,10 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; // Only used in www builds. -export const enableSuspenseServerRenderer = false; +export const enableSuspenseServerRenderer = false; // TODO: __DEV__? Here it might just be false. + +// Only used in www builds. +export const enableSchedulerDebugging = __DEV__; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js index 19b826610e693..b049c09112368 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js @@ -24,6 +24,7 @@ export const enableSuspenseServerRenderer = false; export const disableInputAttributeSyncing = false; export const enableStableConcurrentModeAPIs = false; export const warnAboutShorthandPropertyCollision = false; +export const enableSchedulerDebugging = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js index f5a8fe9192ac7..ac9f5e1b93258 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js @@ -24,6 +24,7 @@ export const enableSuspenseServerRenderer = false; export const disableInputAttributeSyncing = false; export const enableStableConcurrentModeAPIs = false; export const warnAboutShorthandPropertyCollision = false; +export const enableSchedulerDebugging = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 7d36941075602..7e6332cf67490 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -29,6 +29,7 @@ export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableStableConcurrentModeAPIs = false; export const warnAboutShorthandPropertyCollision = false; +export const enableSchedulerDebugging = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 01153dfc927dd..7e24ece367466 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -24,6 +24,7 @@ export const enableSuspenseServerRenderer = false; export const disableInputAttributeSyncing = false; export const enableStableConcurrentModeAPIs = false; export const warnAboutShorthandPropertyCollision = false; +export const enableSchedulerDebugging = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index dc43add41035c..caa763c94c7e7 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -24,6 +24,7 @@ export const enableSuspenseServerRenderer = false; export const disableInputAttributeSyncing = false; export const enableStableConcurrentModeAPIs = false; export const warnAboutShorthandPropertyCollision = false; +export const enableSchedulerDebugging = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 3979848d6a18c..f55f419a6d491 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -24,6 +24,7 @@ export const enableSuspenseServerRenderer = false; export const disableInputAttributeSyncing = false; export const enableStableConcurrentModeAPIs = false; export const warnAboutShorthandPropertyCollision = false; +export const enableSchedulerDebugging = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 5d080f2f7bdf1..516f5738d5a3f 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -22,6 +22,7 @@ export const enableProfilerTimer = false; export const enableSchedulerTracing = false; export const enableSuspenseServerRenderer = false; export const enableStableConcurrentModeAPIs = false; +export const enableSchedulerDebugging = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index c52f380f9bbcc..225dee98a36e9 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -34,6 +34,7 @@ export let enableUserTimingAPI = __DEV__; export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; +export const enableSchedulerDebugging = __DEV__; // TODO or just true export const enableStableConcurrentModeAPIs = false; diff --git a/packages/shared/forks/Scheduler.umd.js b/packages/shared/forks/Scheduler.umd.js index 5abd34001c4f2..c33b2d0667416 100644 --- a/packages/shared/forks/Scheduler.umd.js +++ b/packages/shared/forks/Scheduler.umd.js @@ -16,6 +16,9 @@ const { unstable_now, unstable_scheduleCallback, unstable_shouldYield, + unstable_getFirstCallbackNode, + unstable_continueExecution, + unstable_pauseExecution, } = ReactInternals.Scheduler; export { @@ -23,4 +26,7 @@ export { unstable_now, unstable_scheduleCallback, unstable_shouldYield, + unstable_getFirstCallbackNode, + unstable_continueExecution, + unstable_pauseExecution, };