[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
+};