@samitouri / QOS-React / commits / 8c67bbf183

[scheduler] Deadline object -> shouldYield (#14025)

* [scheduler] Deadline object -> shouldYield Instead of using a requestIdleCallback-style deadline object, expose a method Scheduler.shouldYield that returns true if there's a higher priority event in the queue. * Nits

Andrew Clark committed Oct 30, 2018 at 13:47 UTC 8c67bbf183cc5ae1cab15a8265c612daf80cd86f
18 files changed +215 -249
packages/react-art/src/ReactARTHostConfig.js
+1
@@ -8,6 +8,7 @@
8 export {
9 unstable_now as now,
10 unstable_scheduleCallback as scheduleDeferredCallback,
11 + unstable_shouldYield as shouldYield,
12 unstable_cancelCallback as cancelDeferredCallback,
13 } from 'scheduler';
14 import Transform from 'art/core/transform';
packages/react-dom/src/client/ReactDOMHostConfig.js
+1
@@ -68,6 +68,7 @@ export type NoTimeout = -1;
68 export {
69 unstable_now as now,
70 unstable_scheduleCallback as scheduleDeferredCallback,
71 + unstable_shouldYield as shouldYield,
72 unstable_cancelCallback as cancelDeferredCallback,
73 } from 'scheduler';
74
packages/react-native-renderer/src/ReactFabricHostConfig.js
+1
@@ -332,6 +332,7 @@ export const scheduleDeferredCallback =
332 ReactNativeFrameScheduling.scheduleDeferredCallback;
333 export const cancelDeferredCallback =
334 ReactNativeFrameScheduling.cancelDeferredCallback;
335 +export const shouldYield = ReactNativeFrameScheduling.shouldYield;
336
337 export const scheduleTimeout = setTimeout;
338 export const cancelTimeout = clearTimeout;
packages/react-native-renderer/src/ReactNativeFrameScheduling.js
+8 -13
@@ -7,8 +7,6 @@
7 * @flow
8 */
9
10 -import type {Deadline} from 'react-reconciler/src/ReactFiberScheduler';
11 -
10 const hasNativePerformanceNow =
11 typeof performance === 'object' && typeof performance.now === 'function';
12
@@ -16,16 +14,9 @@ const now = hasNativePerformanceNow
14 ? () => performance.now()
15 : () => Date.now();
16
19 -type Callback = (deadline: Deadline) => void;
20 -
21 -let scheduledCallback: Callback | null = null;
17 +let scheduledCallback: (() => mixed) | null = null;
18 let frameDeadline: number = 0;
19
24 -const frameDeadlineObject: Deadline = {
25 - timeRemaining: () => frameDeadline - now(),
26 - didTimeout: false,
27 -};
28 -
20 function setTimeoutCallback() {
21 // TODO (bvaughn) Hard-coded 5ms unblocks initial async testing.
22 // React API probably changing to boolean rather than time remaining.
@@ -36,7 +27,7 @@ function setTimeoutCallback() {
27 const callback = scheduledCallback;
28 scheduledCallback = null;
29 if (callback !== null) {
39 - callback(frameDeadlineObject);
30 + callback();
31 }
32 }
33
@@ -44,7 +35,7 @@ function setTimeoutCallback() {
35 // This implementation is only intended for short-term use anyway.
36 // We also don't implement cancel functionality b'c Fiber doesn't currently need it.
37 function scheduleDeferredCallback(
47 - callback: Callback,
38 + callback: () => mixed,
39 options?: {timeout: number},
40 ): number {
41 // We assume only one callback is scheduled at a time b'c that's how Fiber works.
@@ -58,4 +49,8 @@ function cancelDeferredCallback(callbackID: number) {
49 clearTimeout((callbackID: any)); // Timeouts are always numbers on RN
50 }
51
61 -export {now, scheduleDeferredCallback, cancelDeferredCallback};
52 +function shouldYield() {
53 + return frameDeadline <= now();
54 +}
55 +
56 +export {now, scheduleDeferredCallback, cancelDeferredCallback, shouldYield};
packages/react-native-renderer/src/ReactNativeHostConfig.js
+1
@@ -238,6 +238,7 @@ export const scheduleDeferredCallback =
238 ReactNativeFrameScheduling.scheduleDeferredCallback;
239 export const cancelDeferredCallback =
240 ReactNativeFrameScheduling.cancelDeferredCallback;
241 +export const shouldYield = ReactNativeFrameScheduling.shouldYield;
242
243 export const scheduleTimeout = setTimeout;
244 export const cancelTimeout = clearTimeout;
packages/react-noop-renderer/src/createReactNoop.js
+37 -26
@@ -319,7 +319,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
319 scheduledCallbackTimeout === -1 ||
320 scheduledCallbackTimeout > newTimeout
321 ) {
322 - scheduledCallbackTimeout = newTimeout;
322 + scheduledCallbackTimeout = elapsedTimeInMs + newTimeout;
323 }
324 }
325 return 0;
@@ -333,6 +333,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
333 scheduledCallbackTimeout = -1;
334 },
335
336 + shouldYield,
337 +
338 scheduleTimeout: setTimeout,
339 cancelTimeout: clearTimeout,
340 noTimeout: -1,
@@ -512,35 +514,44 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
514
515 let yieldedValues = null;
516
517 + let didYield;
518 let unitsRemaining;
519
517 - function* flushUnitsOfWork(n: number): Generator<Array<mixed>, void, void> {
518 - let didStop = false;
519 - while (!didStop && scheduledCallback !== null) {
520 - let cb = scheduledCallback;
521 - scheduledCallback = null;
522 - unitsRemaining = n;
523 - cb({
524 - timeRemaining() {
525 - if (yieldedValues !== null) {
526 - return 0;
527 - }
528 - if (unitsRemaining-- > 0) {
529 - return 999;
530 - }
531 - didStop = true;
532 - return 0;
533 - },
534 - didTimeout:
535 - scheduledCallbackTimeout !== -1 &&
536 - elapsedTimeInMs > scheduledCallbackTimeout,
537 - });
520 + function shouldYield() {
521 + if (
522 + scheduledCallbackTimeout === -1 ||
523 + elapsedTimeInMs > scheduledCallbackTimeout
524 + ) {
525 + return false;
526 + } else {
527 + if (didYield || yieldedValues !== null) {
528 + return true;
529 + }
530 + if (unitsRemaining-- > 0) {
531 + return false;
532 + }
533 + didYield = true;
534 + return true;
535 + }
536 + }
537
539 - if (yieldedValues !== null) {
540 - const values = yieldedValues;
541 - yieldedValues = null;
542 - yield values;
538 + function* flushUnitsOfWork(n: number): Generator<Array<mixed>, void, void> {
539 + unitsRemaining = n + 1;
540 + didYield = false;
541 + try {
542 + while (!didYield && scheduledCallback !== null) {
543 + let cb = scheduledCallback;
544 + scheduledCallback = null;
545 + cb();
546 + if (yieldedValues !== null) {
547 + const values = yieldedValues;
548 + yieldedValues = null;
549 + yield values;
550 + }
551 }
552 + } finally {
553 + unitsRemaining = -1;
554 + didYield = false;
555 }
556 }
557
packages/react-reconciler/src/ReactFiberScheduler.js
+64 -85
@@ -66,18 +66,16 @@ import getComponentName from 'shared/getComponentName';
66 import invariant from 'shared/invariant';
67 import warningWithoutStack from 'shared/warningWithoutStack';
68
69 -import {
70 - scheduleTimeout,
71 - cancelTimeout,
72 - noTimeout,
73 -} from './ReactFiberHostConfig';
74 -
69 import ReactFiberInstrumentation from './ReactFiberInstrumentation';
70 import * as ReactCurrentFiber from './ReactCurrentFiber';
71 import {
72 now,
73 scheduleDeferredCallback,
74 cancelDeferredCallback,
75 + shouldYield,
76 + scheduleTimeout,
77 + cancelTimeout,
78 + noTimeout,
79 prepareForCommit,
80 resetAfterCommit,
81 } from './ReactFiberHostConfig';
@@ -168,11 +166,6 @@ import {
166 } from './ReactFiberCommitWork';
167 import {Dispatcher} from './ReactFiberDispatcher';
168
171 -export type Deadline = {
172 - timeRemaining(): number,
173 - didTimeout: boolean,
174 -};
175 -
169 export type Thenable = {
170 then(resolve: () => mixed, reject?: () => mixed): mixed,
171 };
@@ -1205,18 +1198,14 @@ function workLoop(isYieldy) {
1198 nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
1199 }
1200 } else {
1208 - // Flush asynchronous work until the deadline runs out of time.
1209 - while (nextUnitOfWork !== null && !shouldYield()) {
1201 + // Flush asynchronous work until there's a higher priority event
1202 + while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {
1203 nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
1204 }
1205 }
1206 }
1207
1215 -function renderRoot(
1216 - root: FiberRoot,
1217 - isYieldy: boolean,
1218 - isExpired: boolean,
1219 -): void {
1208 +function renderRoot(root: FiberRoot, isYieldy: boolean): void {
1209 invariant(
1210 !isWorking,
1211 'renderRoot was called recursively. This error is likely caused ' +
@@ -1445,7 +1434,7 @@ function renderRoot(
1434 // similar to a suspend, but without a timeout because we're not waiting
1435 // for a promise to resolve.
1436 !root.didError &&
1448 - !isExpired
1437 + isYieldy
1438 ) {
1439 root.didError = true;
1440 const suspendedExpirationTime = (root.nextExpirationTimeToWorkOn = expirationTime);
@@ -1461,7 +1450,7 @@ function renderRoot(
1450 }
1451 }
1452
1464 - if (!isExpired && nextLatestAbsoluteTimeoutMs !== -1) {
1453 + if (isYieldy && nextLatestAbsoluteTimeoutMs !== -1) {
1454 // The tree was suspended.
1455 const suspendedExpirationTime = expirationTime;
1456 markSuspendedPriorityLevel(root, suspendedExpirationTime);
@@ -1891,10 +1880,8 @@ let isRendering: boolean = false;
1880 let nextFlushedRoot: FiberRoot | null = null;
1881 let nextFlushedExpirationTime: ExpirationTime = NoWork;
1882 let lowestPriorityPendingInteractiveExpirationTime: ExpirationTime = NoWork;
1894 -let deadlineDidExpire: boolean = false;
1883 let hasUnhandledError: boolean = false;
1884 let unhandledError: mixed | null = null;
1897 -let deadline: Deadline | null = null;
1885
1886 let isBatchingUpdates: boolean = false;
1887 let isUnbatchingUpdates: boolean = false;
@@ -1913,8 +1900,6 @@ const NESTED_UPDATE_LIMIT = 50;
1900 let nestedUpdateCount: number = 0;
1901 let lastCommittedRootDuringThisBatch: FiberRoot | null = null;
1902
1916 -const timeHeuristicForUnitOfWork = 1;
1917 -
1903 function recomputeCurrentRendererTime() {
1904 const currentTimeMs = now() - originalStartTimeMs;
1905 currentRendererTime = msToExpirationTime(currentTimeMs);
@@ -1973,7 +1958,7 @@ function onSuspend(
1958 msUntilTimeout: number,
1959 ): void {
1960 root.expirationTime = rootExpirationTime;
1976 - if (msUntilTimeout === 0 && !shouldYield()) {
1961 + if (msUntilTimeout === 0 && !shouldYieldToRenderer()) {
1962 // Don't wait an additional tick. Commit the tree immediately.
1963 root.pendingCommitExpirationTime = suspendedExpirationTime;
1964 root.finishedWork = finishedWork;
@@ -2068,7 +2053,7 @@ function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
2053 // flush it now.
2054 nextFlushedRoot = root;
2055 nextFlushedExpirationTime = Sync;
2071 - performWorkOnRoot(root, Sync, true);
2056 + performWorkOnRoot(root, Sync, false);
2057 }
2058 return;
2059 }
@@ -2176,38 +2161,55 @@ function findHighestPriorityRoot() {
2161 nextFlushedExpirationTime = highestPriorityWork;
2162 }
2163
2179 -function performAsyncWork(dl) {
2180 - if (dl.didTimeout) {
2181 - // The callback timed out. That means at least one update has expired.
2182 - // Iterate through the root schedule. If they contain expired work, set
2183 - // the next render expiration time to the current time. This has the effect
2184 - // of flushing all expired work in a single batch, instead of flushing each
2185 - // level one at a time.
2186 - if (firstScheduledRoot !== null) {
2187 - recomputeCurrentRendererTime();
2188 - let root: FiberRoot = firstScheduledRoot;
2189 - do {
2190 - didExpireAtExpirationTime(root, currentRendererTime);
2191 - // The root schedule is circular, so this is never null.
2192 - root = (root.nextScheduledRoot: any);
2193 - } while (root !== firstScheduledRoot);
2164 +// TODO: This wrapper exists because many of the older tests (the ones that use
2165 +// flushDeferredPri) rely on the number of times `shouldYield` is called. We
2166 +// should get rid of it.
2167 +let didYield: boolean = false;
2168 +function shouldYieldToRenderer() {
2169 + if (didYield) {
2170 + return true;
2171 + }
2172 + if (shouldYield()) {
2173 + didYield = true;
2174 + return true;
2175 + }
2176 + return false;
2177 +}
2178 +
2179 +function performAsyncWork() {
2180 + try {
2181 + if (!shouldYieldToRenderer()) {
2182 + // The callback timed out. That means at least one update has expired.
2183 + // Iterate through the root schedule. If they contain expired work, set
2184 + // the next render expiration time to the current time. This has the effect
2185 + // of flushing all expired work in a single batch, instead of flushing each
2186 + // level one at a time.
2187 + if (firstScheduledRoot !== null) {
2188 + recomputeCurrentRendererTime();
2189 + let root: FiberRoot = firstScheduledRoot;
2190 + do {
2191 + didExpireAtExpirationTime(root, currentRendererTime);
2192 + // The root schedule is circular, so this is never null.
2193 + root = (root.nextScheduledRoot: any);
2194 + } while (root !== firstScheduledRoot);
2195 + }
2196 }
2197 + performWork(NoWork, true);
2198 + } finally {
2199 + didYield = false;
2200 }
2196 - performWork(NoWork, dl);
2201 }
2202
2203 function performSyncWork() {
2200 - performWork(Sync, null);
2204 + performWork(Sync, false);
2205 }
2206
2203 -function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
2204 - deadline = dl;
2205 -
2206 - // Keep working on roots until there's no more work, or until we reach
2207 - // the deadline.
2207 +function performWork(minExpirationTime: ExpirationTime, isYieldy: boolean) {
2208 + // Keep working on roots until there's no more work, or until there's a higher
2209 + // priority event.
2210 findHighestPriorityRoot();
2211
2210 - if (deadline !== null) {
2212 + if (isYieldy) {
2213 recomputeCurrentRendererTime();
2214 currentSchedulerTime = currentRendererTime;
2215
@@ -2222,12 +2224,12 @@ function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
2224 nextFlushedExpirationTime !== NoWork &&
2225 (minExpirationTime === NoWork ||
2226 minExpirationTime >= nextFlushedExpirationTime) &&
2225 - (!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)
2227 + !(didYield && currentRendererTime < nextFlushedExpirationTime)
2228 ) {
2229 performWorkOnRoot(
2230 nextFlushedRoot,
2231 nextFlushedExpirationTime,
2230 - currentRendererTime >= nextFlushedExpirationTime,
2232 + currentRendererTime < nextFlushedExpirationTime,
2233 );
2234 findHighestPriorityRoot();
2235 recomputeCurrentRendererTime();
@@ -2240,7 +2242,7 @@ function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
2242 (minExpirationTime === NoWork ||
2243 minExpirationTime >= nextFlushedExpirationTime)
2244 ) {
2243 - performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
2245 + performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
2246 findHighestPriorityRoot();
2247 }
2248 }
@@ -2249,7 +2251,7 @@ function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
2251 // or there's no more work left with sufficient priority.
2252
2253 // If we're inside a callback, set this to false since we just completed it.
2252 - if (deadline !== null) {
2254 + if (isYieldy) {
2255 callbackExpirationTime = NoWork;
2256 callbackID = null;
2257 }
@@ -2262,9 +2264,6 @@ function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
2264 }
2265
2266 // Clean-up.
2265 - deadline = null;
2266 - deadlineDidExpire = false;
2267 -
2267 finishRendering();
2268 }
2269
@@ -2279,7 +2278,7 @@ function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
2278 // including the given time.
2279 nextFlushedRoot = root;
2280 nextFlushedExpirationTime = expirationTime;
2282 - performWorkOnRoot(root, expirationTime, true);
2281 + performWorkOnRoot(root, expirationTime, false);
2282 // Flush any sync work that was scheduled by lifecycles
2283 performSyncWork();
2284 }
@@ -2315,7 +2314,7 @@ function finishRendering() {
2314 function performWorkOnRoot(
2315 root: FiberRoot,
2316 expirationTime: ExpirationTime,
2318 - isExpired: boolean,
2317 + isYieldy: boolean,
2318 ) {
2319 invariant(
2320 !isRendering,
@@ -2326,7 +2325,7 @@ function performWorkOnRoot(
2325 isRendering = true;
2326
2327 // Check if this is async work or sync/expired work.
2329 - if (deadline === null || isExpired) {
2328 + if (!isYieldy) {
2329 // Flush work without yielding.
2330 // TODO: Non-yieldy work does not necessarily imply expired work. A renderer
2331 // may want to perform some work without yielding, but also without
@@ -2346,8 +2345,7 @@ function performWorkOnRoot(
2345 // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
2346 cancelTimeout(timeoutHandle);
2347 }
2349 - const isYieldy = false;
2350 - renderRoot(root, isYieldy, isExpired);
2348 + renderRoot(root, isYieldy);
2349 finishedWork = root.finishedWork;
2350 if (finishedWork !== null) {
2351 // We've completed the root. Commit it.
@@ -2370,13 +2368,12 @@ function performWorkOnRoot(
2368 // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
2369 cancelTimeout(timeoutHandle);
2370 }
2373 - const isYieldy = true;
2374 - renderRoot(root, isYieldy, isExpired);
2371 + renderRoot(root, isYieldy);
2372 finishedWork = root.finishedWork;
2373 if (finishedWork !== null) {
2377 - // We've completed the root. Check the deadline one more time
2374 + // We've completed the root. Check the if we should yield one more time
2375 // before committing.
2379 - if (!shouldYield()) {
2376 + if (!shouldYieldToRenderer()) {
2377 // Still time left. Commit the root.
2378 completeRoot(root, finishedWork, expirationTime);
2379 } else {
@@ -2430,24 +2427,6 @@ function completeRoot(
2427 commitRoot(root, finishedWork);
2428 }
2429
2433 -// When working on async work, the reconciler asks the renderer if it should
2434 -// yield execution. For DOM, we implement this with requestIdleCallback.
2435 -function shouldYield() {
2436 - if (deadlineDidExpire) {
2437 - return true;
2438 - }
2439 - if (
2440 - deadline === null ||
2441 - deadline.timeRemaining() > timeHeuristicForUnitOfWork
2442 - ) {
2443 - // Disregard deadline.didTimeout. Only expired work should be flushed
2444 - // during a timeout. This path is only hit for non-expired work.
2445 - return false;
2446 - }
2447 - deadlineDidExpire = true;
2448 - return true;
2449 -}
2450 -
2430 function onUncaughtError(error: mixed) {
2431 invariant(
2432 nextFlushedRoot !== null,
@@ -2524,7 +2503,7 @@ function interactiveUpdates<A, B, R>(fn: (A, B) => R, a: A, b: B): R {
2503 lowestPriorityPendingInteractiveExpirationTime !== NoWork
2504 ) {
2505 // Synchronously flush pending interactive updates.
2527 - performWork(lowestPriorityPendingInteractiveExpirationTime, null);
2506 + performWork(lowestPriorityPendingInteractiveExpirationTime, false);
2507 lowestPriorityPendingInteractiveExpirationTime = NoWork;
2508 }
2509 const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
@@ -2548,7 +2527,7 @@ function flushInteractiveUpdates() {
2527 lowestPriorityPendingInteractiveExpirationTime !== NoWork
2528 ) {
2529 // Synchronously flush pending interactive updates.
2551 - performWork(lowestPriorityPendingInteractiveExpirationTime, null);
2530 + performWork(lowestPriorityPendingInteractiveExpirationTime, false);
2531 lowestPriorityPendingInteractiveExpirationTime = NoWork;
2532 }
2533 }
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+1
@@ -52,6 +52,7 @@ export const shouldDeprioritizeSubtree =
52 export const createTextInstance = $$$hostConfig.createTextInstance;
53 export const scheduleDeferredCallback = $$$hostConfig.scheduleDeferredCallback;
54 export const cancelDeferredCallback = $$$hostConfig.cancelDeferredCallback;
55 +export const shouldYield = $$$hostConfig.shouldYield;
56 export const scheduleTimeout = $$$hostConfig.setTimeout;
57 export const cancelTimeout = $$$hostConfig.clearTimeout;
58 export const noTimeout = $$$hostConfig.noTimeout;
packages/react-test-renderer/src/ReactTestHostConfig.js
+1
@@ -202,6 +202,7 @@ export const scheduleDeferredCallback =
202 TestRendererScheduling.scheduleDeferredCallback;
203 export const cancelDeferredCallback =
204 TestRendererScheduling.cancelDeferredCallback;
205 +export const shouldYield = TestRendererScheduling.shouldYield;
206
207 export const scheduleTimeout = setTimeout;
208 export const cancelTimeout = clearTimeout;
packages/react-test-renderer/src/ReactTestRendererScheduling.js
+32 -36
@@ -7,15 +7,16 @@
7 * @flow
8 */
9
10 -import type {Deadline} from 'react-reconciler/src/ReactFiberScheduler';
11 -
10 // Current virtual time
11 export let nowImplementation = () => 0;
14 -export let scheduledCallback: ((deadline: Deadline) => mixed) | null = null;
12 +export let scheduledCallback: (() => mixed) | null = null;
13 export let yieldedValues: Array<mixed> = [];
14
15 +let didStop: boolean = false;
16 +let expectedNumberOfYields: number = -1;
17 +
18 export function scheduleDeferredCallback(
18 - callback: (deadline: Deadline) => mixed,
19 + callback: () => mixed,
20 options?: {timeout: number},
21 ): number {
22 scheduledCallback = callback;
@@ -31,21 +32,25 @@ export function setNowImplementation(implementation: () => number): void {
32 nowImplementation = implementation;
33 }
34
35 +export function shouldYield() {
36 + if (
37 + expectedNumberOfYields !== -1 &&
38 + yieldedValues.length >= expectedNumberOfYields
39 + ) {
40 + // We yielded at least as many values as expected. Stop rendering.
41 + didStop = true;
42 + return true;
43 + }
44 + // Keep rendering.
45 + return false;
46 +}
47 +
48 export function flushAll(): Array<mixed> {
49 yieldedValues = [];
50 while (scheduledCallback !== null) {
51 const cb = scheduledCallback;
52 scheduledCallback = null;
39 - cb({
40 - timeRemaining() {
41 - // Keep rendering until there's no more work
42 - return 999;
43 - },
44 - // React's scheduler has its own way of keeping track of expired
45 - // work and doesn't read this, so don't bother setting it to the
46 - // correct value.
47 - didTimeout: false,
48 - });
53 + cb();
54 }
55 const values = yieldedValues;
56 yieldedValues = [];
@@ -53,30 +58,21 @@ export function flushAll(): Array<mixed> {
58 }
59
60 export function flushNumberOfYields(count: number): Array<mixed> {
56 - let didStop = false;
61 + expectedNumberOfYields = count;
62 + didStop = false;
63 yieldedValues = [];
58 - while (scheduledCallback !== null && !didStop) {
59 - const cb = scheduledCallback;
60 - scheduledCallback = null;
61 - cb({
62 - timeRemaining() {
63 - if (yieldedValues.length >= count) {
64 - // We at least as many values as expected. Stop rendering.
65 - didStop = true;
66 - return 0;
67 - }
68 - // Keep rendering.
69 - return 999;
70 - },
71 - // React's scheduler has its own way of keeping track of expired
72 - // work and doesn't read this, so don't bother setting it to the
73 - // correct value.
74 - didTimeout: false,
75 - });
64 + try {
65 + while (scheduledCallback !== null && !didStop) {
66 + const cb = scheduledCallback;
67 + scheduledCallback = null;
68 + cb();
69 + }
70 + return yieldedValues;
71 + } finally {
72 + expectedNumberOfYields = -1;
73 + didStop = false;
74 + yieldedValues = [];
75 }
77 - const values = yieldedValues;
78 - yieldedValues = [];
79 - return values;
76 }
77
78 export function yieldValue(value: mixed): void {
packages/react/src/ReactSharedInternals.js
+2
@@ -8,6 +8,7 @@
8 import assign from 'object-assign';
9 import {
10 unstable_cancelCallback,
11 + unstable_shouldYield,
12 unstable_now,
13 unstable_scheduleCallback,
14 unstable_runWithPriority,
@@ -43,6 +44,7 @@ if (__UMD__) {
44 Object.assign(ReactSharedInternals, {
45 Scheduler: {
46 unstable_cancelCallback,
47 + unstable_shouldYield,
48 unstable_now,
49 unstable_scheduleCallback,
50 unstable_runWithPriority,
packages/scheduler/npm/umd/scheduler.development.js
+8
@@ -39,6 +39,13 @@
39 );
40 }
41
42 + function unstable_shouldYield() {
43 + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_shouldYield.apply(
44 + this,
45 + arguments
46 + );
47 + }
48 +
49 function unstable_runWithPriority() {
50 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
51 this,
@@ -64,6 +71,7 @@
71 unstable_now: unstable_now,
72 unstable_scheduleCallback: unstable_scheduleCallback,
73 unstable_cancelCallback: unstable_cancelCallback,
74 + unstable_shouldYield: unstable_shouldYield,
75 unstable_runWithPriority: unstable_runWithPriority,
76 unstable_wrapCallback: unstable_wrapCallback,
77 unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel,
packages/scheduler/npm/umd/scheduler.production.min.js
+8
@@ -39,6 +39,13 @@
39 );
40 }
41
42 + function unstable_shouldYield() {
43 + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_shouldYield.apply(
44 + this,
45 + arguments
46 + );
47 + }
48 +
49 function unstable_runWithPriority() {
50 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
51 this,
@@ -64,6 +71,7 @@
71 unstable_now: unstable_now,
72 unstable_scheduleCallback: unstable_scheduleCallback,
73 unstable_cancelCallback: unstable_cancelCallback,
74 + unstable_shouldYield: unstable_shouldYield,
75 unstable_runWithPriority: unstable_runWithPriority,
76 unstable_wrapCallback: unstable_wrapCallback,
77 unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel,
packages/scheduler/npm/umd/scheduler.profiling.min.js
+8
@@ -39,6 +39,13 @@
39 );
40 }
41
42 + function unstable_shouldYield() {
43 + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_shouldYield.apply(
44 + this,
45 + arguments
46 + );
47 + }
48 +
49 function unstable_runWithPriority() {
50 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
51 this,
@@ -64,6 +71,7 @@
71 unstable_now: unstable_now,
72 unstable_scheduleCallback: unstable_scheduleCallback,
73 unstable_cancelCallback: unstable_cancelCallback,
74 + unstable_shouldYield: unstable_shouldYield,
75 unstable_runWithPriority: unstable_runWithPriority,
76 unstable_wrapCallback: unstable_wrapCallback,
77 unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel,
packages/scheduler/src/Scheduler.js
+22 -48
@@ -30,6 +30,7 @@ var IDLE_PRIORITY = maxSigned31BitInt;
30 // Callbacks are stored as a circular, doubly linked list.
31 var firstCallbackNode = null;
32
33 +var currentDidTimeout = false;
34 var currentPriorityLevel = NormalPriority;
35 var currentEventStartTime = -1;
36 var currentExpirationTime = -1;
@@ -42,41 +43,6 @@ var isHostCallbackScheduled = false;
43 var hasNativePerformanceNow =
44 typeof performance === 'object' && typeof performance.now === 'function';
45
45 -var timeRemaining;
46 -if (hasNativePerformanceNow) {
47 - timeRemaining = function() {
48 - if (
49 - firstCallbackNode !== null &&
50 - firstCallbackNode.expirationTime < currentExpirationTime
51 - ) {
52 - // A higher priority callback was scheduled. Yield so we can switch to
53 - // working on that.
54 - return 0;
55 - }
56 - // We assume that if we have a performance timer that the rAF callback
57 - // gets a performance timer value. Not sure if this is always true.
58 - var remaining = getFrameDeadline() - performance.now();
59 - return remaining > 0 ? remaining : 0;
60 - };
61 -} else {
62 - timeRemaining = function() {
63 - // Fallback to Date.now()
64 - if (
65 - firstCallbackNode !== null &&
66 - firstCallbackNode.expirationTime < currentExpirationTime
67 - ) {
68 - return 0;
69 - }
70 - var remaining = getFrameDeadline() - Date.now();
71 - return remaining > 0 ? remaining : 0;
72 - };
73 -}
74 -
75 -var deadlineObject = {
76 - timeRemaining,
77 - didTimeout: false,
78 -};
79 -
46 function ensureHostCallbackIsScheduled() {
47 if (isExecutingCallback) {
48 // Don't schedule work yet; wait until the next time we yield.
@@ -121,7 +87,7 @@ function flushFirstCallback() {
87 currentExpirationTime = expirationTime;
88 var continuationCallback;
89 try {
124 - continuationCallback = callback(deadlineObject);
90 + continuationCallback = callback();
91 } finally {
92 currentPriorityLevel = previousPriorityLevel;
93 currentExpirationTime = previousExpirationTime;
@@ -184,7 +150,6 @@ function flushImmediateWork() {
150 firstCallbackNode.priorityLevel === ImmediatePriority
151 ) {
152 isExecutingCallback = true;
187 - deadlineObject.didTimeout = true;
153 try {
154 do {
155 flushFirstCallback();
@@ -207,7 +172,8 @@ function flushImmediateWork() {
172
173 function flushWork(didTimeout) {
174 isExecutingCallback = true;
210 - deadlineObject.didTimeout = didTimeout;
175 + const previousDidTimeout = currentDidTimeout;
176 + currentDidTimeout = didTimeout;
177 try {
178 if (didTimeout) {
179 // Flush all the expired callbacks without yielding.
@@ -232,14 +198,12 @@ function flushWork(didTimeout) {
198 if (firstCallbackNode !== null) {
199 do {
200 flushFirstCallback();
235 - } while (
236 - firstCallbackNode !== null &&
237 - getFrameDeadline() - getCurrentTime() > 0
238 - );
201 + } while (firstCallbackNode !== null && !shouldYieldToHost());
202 }
203 }
204 } finally {
205 isExecutingCallback = false;
206 + currentDidTimeout = previousDidTimeout;
207 if (firstCallbackNode !== null) {
208 // There's still work remaining. Request another callback.
209 ensureHostCallbackIsScheduled();
@@ -399,6 +363,15 @@ function unstable_getCurrentPriorityLevel() {
363 return currentPriorityLevel;
364 }
365
366 +function unstable_shouldYield() {
367 + return (
368 + !currentDidTimeout &&
369 + ((firstCallbackNode !== null &&
370 + firstCallbackNode.expirationTime < currentExpirationTime) ||
371 + shouldYieldToHost())
372 + );
373 +}
374 +
375 // The remaining code is essentially a polyfill for requestIdleCallback. It
376 // works by scheduling a requestAnimationFrame, storing the time for the start
377 // of the frame, then scheduling a postMessage which gets scheduled after paint.
@@ -466,14 +439,14 @@ if (hasNativePerformanceNow) {
439
440 var requestHostCallback;
441 var cancelHostCallback;
469 -var getFrameDeadline;
442 +var shouldYieldToHost;
443
444 if (typeof window !== 'undefined' && window._schedMock) {
445 // Dynamic injection, only for testing purposes.
446 var impl = window._schedMock;
447 requestHostCallback = impl[0];
448 cancelHostCallback = impl[1];
476 - getFrameDeadline = impl[2];
449 + shouldYieldToHost = impl[2];
450 } else if (
451 // If Scheduler runs in a non-DOM environment, it falls back to a naive
452 // implementation using setTimeout.
@@ -509,8 +482,8 @@ if (typeof window !== 'undefined' && window._schedMock) {
482 cancelHostCallback = function() {
483 _callback = null;
484 };
512 - getFrameDeadline = function() {
513 - return Infinity;
485 + shouldYieldToHost = function() {
486 + return false;
487 };
488 getCurrentTime = function() {
489 return _currentTime === -1 ? 0 : _currentTime;
@@ -549,8 +522,8 @@ if (typeof window !== 'undefined' && window._schedMock) {
522 var previousFrameTime = 33;
523 var activeFrameTime = 33;
524
552 - getFrameDeadline = function() {
553 - return frameDeadline;
525 + shouldYieldToHost = function() {
526 + return frameDeadline <= getCurrentTime();
527 };
528
529 // We use the postMessage trick to defer idle work until after the repaint.
@@ -687,5 +660,6 @@ export {
660 unstable_cancelCallback,
661 unstable_wrapCallback,
662 unstable_getCurrentPriorityLevel,
663 + unstable_shouldYield,
664 getCurrentTime as unstable_now,
665 };
packages/scheduler/src/__tests__/Scheduler-test.internal.js
+13 -27
@@ -17,6 +17,7 @@ let scheduleCallback;
17 let cancelCallback;
18 let wrapCallback;
19 let getCurrentPriorityLevel;
20 +let shouldYield;
21 let flushWork;
22 let advanceTime;
23 let doWork;
@@ -119,8 +120,8 @@ describe('Scheduler', () => {
120 _flushWork = null;
121 clearTimeout(timeoutID);
122 }
122 - function getTimeRemaining() {
123 - return endOfFrame;
123 + function shouldYieldToHost() {
124 + return endOfFrame <= currentTime;
125 }
126
127 // Override host implementation
@@ -131,7 +132,7 @@ describe('Scheduler', () => {
132 window._schedMock = [
133 requestHostCallback,
134 cancelHostCallback,
134 - getTimeRemaining,
135 + shouldYieldToHost,
136 ];
137
138 const Schedule = require('scheduler');
@@ -143,6 +144,7 @@ describe('Scheduler', () => {
144 cancelCallback = Schedule.unstable_cancelCallback;
145 wrapCallback = Schedule.unstable_wrapCallback;
146 getCurrentPriorityLevel = Schedule.unstable_getCurrentPriorityLevel;
147 + shouldYield = Schedule.unstable_shouldYield;
148 });
149
150 it('flushes work incrementally', () => {
@@ -273,14 +275,10 @@ describe('Scheduler', () => {
275 scheduleCallback(() => doWork('B', 100));
276
277 const tasks = [['C1', 100], ['C2', 100], ['C3', 100]];
276 - const C = deadline => {
278 + const C = () => {
279 while (tasks.length > 0) {
280 doWork(...tasks.shift());
279 - if (
280 - tasks.length > 0 &&
281 - !deadline.didTimeout &&
282 - deadline.timeRemaining() <= 0
283 - ) {
281 + if (shouldYield()) {
282 yieldValue('Yield!');
283 return C;
284 }
@@ -299,14 +297,10 @@ describe('Scheduler', () => {
297
298 it('continuation callbacks inherit the expiration of the previous callback', () => {
299 const tasks = [['A', 125], ['B', 124], ['C', 100], ['D', 100]];
302 - const work = deadline => {
300 + const work = () => {
301 while (tasks.length > 0) {
302 doWork(...tasks.shift());
305 - if (
306 - tasks.length > 0 &&
307 - !deadline.didTimeout &&
308 - deadline.timeRemaining() <= 0
309 - ) {
303 + if (shouldYield()) {
304 yieldValue('Yield!');
305 return work;
306 }
@@ -344,14 +338,10 @@ describe('Scheduler', () => {
338
339 it('continuations are interrupted by higher priority work', () => {
340 const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
347 - const work = deadline => {
341 + const work = () => {
342 while (tasks.length > 0) {
343 doWork(...tasks.shift());
350 - if (
351 - tasks.length > 0 &&
352 - !deadline.didTimeout &&
353 - deadline.timeRemaining() <= 0
354 - ) {
344 + if (tasks.length > 0 && shouldYield()) {
345 yieldValue('Yield!');
346 return work;
347 }
@@ -372,7 +362,7 @@ describe('Scheduler', () => {
362 'inside an executing callback',
363 () => {
364 const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
375 - const work = deadline => {
365 + const work = () => {
366 while (tasks.length > 0) {
367 const task = tasks.shift();
368 doWork(...task);
@@ -383,11 +373,7 @@ describe('Scheduler', () => {
373 scheduleCallback(() => doWork('High pri', 100)),
374 );
375 }
386 - if (
387 - tasks.length > 0 &&
388 - !deadline.didTimeout &&
389 - deadline.timeRemaining() <= 0
390 - ) {
376 + if (tasks.length > 0 && shouldYield()) {
377 yieldValue('Yield!');
378 return work;
379 }
packages/scheduler/src/__tests__/SchedulerDOM-test.js
-13
@@ -105,9 +105,6 @@ describe('SchedulerDOM', () => {
105 scheduleCallback(cb);
106 advanceOneFrame({timeLeftInFrame: 15});
107 expect(cb).toHaveBeenCalledTimes(1);
108 - // should not have timed out and should include a timeRemaining method
109 - expect(cb.mock.calls[0][0].didTimeout).toBe(false);
110 - expect(typeof cb.mock.calls[0][0].timeRemaining()).toBe('number');
108 });
109
110 it('inserts its rAF callback as early into the queue as possible', () => {
@@ -144,16 +141,6 @@ describe('SchedulerDOM', () => {
141 // after a delay, calls as many callbacks as it has time for
142 advanceOneFrame({timeLeftInFrame: 15});
143 expect(callbackLog).toEqual(['A', 'B']);
147 - // callbackA should not have timed out and should include a timeRemaining method
148 - expect(callbackA.mock.calls[0][0].didTimeout).toBe(false);
149 - expect(typeof callbackA.mock.calls[0][0].timeRemaining()).toBe(
150 - 'number',
151 - );
152 - // callbackA should not have timed out and should include a timeRemaining method
153 - expect(callbackB.mock.calls[0][0].didTimeout).toBe(false);
154 - expect(typeof callbackB.mock.calls[0][0].timeRemaining()).toBe(
155 - 'number',
156 - );
144 });
145
146 it("accepts callbacks betweeen animationFrame and postMessage and doesn't stall", () => {
packages/shared/forks/Scheduler.umd.js
+7 -1
@@ -15,6 +15,12 @@ const {
15 unstable_cancelCallback,
16 unstable_now,
17 unstable_scheduleCallback,
18 + unstable_shouldYield,
19 } = ReactInternals.Scheduler;
20
20 -export {unstable_cancelCallback, unstable_now, unstable_scheduleCallback};
21 +export {
22 + unstable_cancelCallback,
23 + unstable_now,
24 + unstable_scheduleCallback,
25 + unstable_shouldYield,
26 +};