@samitouri / QOS-React-2 / commits / 3af91eb8ce

[Scheduler] Use continuation pattern for posting host callback (#15910)

* [scheduler] Internal rename: Callback -> Task Rename Callback type to Task. Does not affect the public API, only internal names, though eventually we'll probably want to align with the WICG Main-thread Scheduling proposal (https://github.com/WICG/main-thread-scheduling). * [scheduler] flushFirstTask() -> flushTask(task) Pass task as an argument to `flushTask` instead of using a module- level variable. * [scheduler] Add startTime field This does not change any semantics, but in the future `startTime` may represent a future time, to support delayed tasks. * [Scheduler] Use continuation pattern for host cb As I prepare to implement integrated timers, I noticed some peculiarities in the Scheduler implementation that could afford to be cleaned up. This is a refactor and shouldn't affect any observable behavior; mostly it removes some concepts that existed in earlier iterations of Scheduler and are no longer needed. The main change is to how the DOM implementation schedules an additional callback before yielding to the main thread. It used to follow the same code path for scheduling task; now it has its own branch directly inside the message event handler. The special case for error handling — where we call `postMessage` immediately without waiting for rAF — has similarly been localized inside the catch block of the message event handler.

Andrew Clark committed Jun 19, 2019 at 15:57 UTC 3af91eb8cee7cbfb482e4bbee91b81822e264916
4 files changed +222 -301
packages/scheduler/src/Scheduler.js
+134 -188
@@ -11,7 +11,6 @@
11 import {enableSchedulerDebugging} from './SchedulerFeatureFlags';
12 import {
13 requestHostCallback,
14 - cancelHostCallback,
14 shouldYieldToHost,
15 getCurrentTime,
16 forceFrameRate,
@@ -38,130 +37,105 @@ var LOW_PRIORITY_TIMEOUT = 10000;
37 // Never times out
38 var IDLE_PRIORITY = maxSigned31BitInt;
39
41 -// Callbacks are stored as a circular, doubly linked list.
42 -var firstCallbackNode = null;
40 +// Tasks are stored as a circular, doubly linked list.
41 +var firstTask = null;
42
44 -var currentHostCallbackDidTimeout = false;
43 // Pausing the scheduler is useful for debugging.
44 var isSchedulerPaused = false;
45
46 +var currentTask = null;
47 var currentPriorityLevel = NormalPriority;
49 -var currentEventStartTime = -1;
50 -var currentExpirationTime = -1;
48
49 // This is set while performing work, to prevent re-entrancy.
50 var isPerformingWork = false;
51
52 var isHostCallbackScheduled = false;
53
57 -function scheduleHostCallbackIfNeeded() {
58 - if (isPerformingWork) {
59 - // Don't schedule work yet; wait until the next time we yield.
60 - return;
61 - }
62 - if (firstCallbackNode !== null) {
63 - // Schedule the host callback using the earliest expiration in the list.
64 - var expirationTime = firstCallbackNode.expirationTime;
65 - if (isHostCallbackScheduled) {
66 - // Cancel the existing host callback.
67 - cancelHostCallback();
68 - } else {
69 - isHostCallbackScheduled = true;
70 - }
71 - requestHostCallback(flushWork, expirationTime);
72 - }
73 -}
74 -
75 -function flushFirstCallback() {
76 - const currentlyFlushingCallback = firstCallbackNode;
77 -
78 - // Remove the node from the list before calling the callback. That way the
54 +function flushTask(task, currentTime) {
55 + // Remove the task from the list before calling the callback. That way the
56 // list is in a consistent state even if the callback throws.
80 - var next = firstCallbackNode.next;
81 - if (firstCallbackNode === next) {
82 - // This is the last callback in the list.
83 - firstCallbackNode = null;
84 - next = null;
57 + const next = task.next;
58 + if (next === task) {
59 + // This is the only scheduled task. Clear the list.
60 + firstTask = null;
61 } else {
86 - var lastCallbackNode = firstCallbackNode.previous;
87 - firstCallbackNode = lastCallbackNode.next = next;
88 - next.previous = lastCallbackNode;
62 + // Remove the task from its position in the list.
63 + if (task === firstTask) {
64 + firstTask = next;
65 + }
66 + const previous = task.previous;
67 + previous.next = next;
68 + next.previous = previous;
69 }
70 + task.next = task.previous = null;
71
91 - currentlyFlushingCallback.next = currentlyFlushingCallback.previous = null;
92 -
93 - // Now it's safe to call the callback.
94 - var callback = currentlyFlushingCallback.callback;
95 - var expirationTime = currentlyFlushingCallback.expirationTime;
96 - var priorityLevel = currentlyFlushingCallback.priorityLevel;
72 + // Now it's safe to execute the task.
73 + var callback = task.callback;
74 var previousPriorityLevel = currentPriorityLevel;
98 - var previousExpirationTime = currentExpirationTime;
99 - currentPriorityLevel = priorityLevel;
100 - currentExpirationTime = expirationTime;
75 + var previousTask = currentTask;
76 + currentPriorityLevel = task.priorityLevel;
77 + currentTask = task;
78 var continuationCallback;
79 try {
103 - const didUserCallbackTimeout =
104 - currentHostCallbackDidTimeout ||
105 - // Immediate priority callbacks are always called as if they timed out
106 - priorityLevel === ImmediatePriority;
80 + var didUserCallbackTimeout = task.expirationTime <= currentTime;
81 continuationCallback = callback(didUserCallbackTimeout);
82 } catch (error) {
83 throw error;
84 } finally {
85 currentPriorityLevel = previousPriorityLevel;
112 - currentExpirationTime = previousExpirationTime;
86 + currentTask = previousTask;
87 }
88
89 // A callback may return a continuation. The continuation should be scheduled
90 // with the same priority and expiration as the just-finished callback.
91 if (typeof continuationCallback === 'function') {
118 - var continuationNode: CallbackNode = {
92 + var expirationTime = task.expirationTime;
93 + var continuationTask = {
94 callback: continuationCallback,
120 - priorityLevel,
95 + priorityLevel: task.priorityLevel,
96 + startTime: task.startTime,
97 expirationTime,
98 next: null,
99 previous: null,
100 };
101
126 - // Insert the new callback into the list, sorted by its expiration. This is
102 + // Insert the new callback into the list, sorted by its timeout. This is
103 // almost the same as the code in `scheduleCallback`, except the callback
128 - // is inserted into the list *before* callbacks of equal expiration instead
104 + // is inserted into the list *before* callbacks of equal timeout instead
105 // of after.
130 - if (firstCallbackNode === null) {
106 + if (firstTask === null) {
107 // This is the first callback in the list.
132 - firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
108 + firstTask = continuationTask.next = continuationTask.previous = continuationTask;
109 } else {
110 var nextAfterContinuation = null;
135 - var node = firstCallbackNode;
111 + var t = firstTask;
112 do {
137 - if (node.expirationTime >= expirationTime) {
138 - // This callback expires at or after the continuation. We will insert
139 - // the continuation *before* this callback.
140 - nextAfterContinuation = node;
113 + if (expirationTime <= t.expirationTime) {
114 + // This task times out at or after the continuation. We will insert
115 + // the continuation *before* this task.
116 + nextAfterContinuation = t;
117 break;
118 }
143 - node = node.next;
144 - } while (node !== firstCallbackNode);
145 -
119 + t = t.next;
120 + } while (t !== firstTask);
121 if (nextAfterContinuation === null) {
147 - // No equal or lower priority callback was found, which means the new
148 - // callback is the lowest priority callback in the list.
149 - nextAfterContinuation = firstCallbackNode;
150 - } else if (nextAfterContinuation === firstCallbackNode) {
151 - // The new callback is the highest priority callback in the list.
152 - firstCallbackNode = continuationNode;
153 - scheduleHostCallbackIfNeeded();
122 + // No equal or lower priority task was found, which means the new task
123 + // is the lowest priority task in the list.
124 + nextAfterContinuation = firstTask;
125 + } else if (nextAfterContinuation === firstTask) {
126 + // The new task is the highest priority task in the list.
127 + firstTask = continuationTask;
128 }
129
156 - var previous = nextAfterContinuation.previous;
157 - previous.next = nextAfterContinuation.previous = continuationNode;
158 - continuationNode.next = nextAfterContinuation;
159 - continuationNode.previous = previous;
130 + const previous = nextAfterContinuation.previous;
131 + previous.next = nextAfterContinuation.previous = continuationTask;
132 + continuationTask.next = nextAfterContinuation;
133 + continuationTask.previous = previous;
134 }
135 }
136 }
137
164 -function flushWork(didUserCallbackTimeout) {
138 +function flushWork(hasTimeRemaining, initialTime) {
139 // Exit right away if we're currently paused
140 if (enableSchedulerDebugging && isSchedulerPaused) {
141 return;
@@ -171,48 +145,38 @@ function flushWork(didUserCallbackTimeout) {
145 isHostCallbackScheduled = false;
146
147 isPerformingWork = true;
174 - const previousDidTimeout = currentHostCallbackDidTimeout;
175 - currentHostCallbackDidTimeout = didUserCallbackTimeout;
148 try {
177 - if (didUserCallbackTimeout) {
149 + if (!hasTimeRemaining) {
150 // Flush all the expired callbacks without yielding.
151 + // TODO: Split flushWork into two separate functions instead of using
152 + // a boolean argument?
153 + let currentTime = initialTime;
154 while (
180 - firstCallbackNode !== null &&
155 + firstTask !== null &&
156 + firstTask.expirationTime <= currentTime &&
157 !(enableSchedulerDebugging && isSchedulerPaused)
158 ) {
183 - // TODO Wrap in feature flag
184 - // Read the current time. Flush all the callbacks that expire at or
185 - // earlier than that time. Then read the current time again and repeat.
186 - // This optimizes for as few performance.now calls as possible.
187 - var currentTime = getCurrentTime();
188 - if (firstCallbackNode.expirationTime <= currentTime) {
189 - do {
190 - flushFirstCallback();
191 - } while (
192 - firstCallbackNode !== null &&
193 - firstCallbackNode.expirationTime <= currentTime &&
194 - !(enableSchedulerDebugging && isSchedulerPaused)
195 - );
196 - continue;
197 - }
198 - break;
159 + flushTask(firstTask, currentTime);
160 + currentTime = getCurrentTime();
161 }
162 } else {
163 // Keep flushing callbacks until we run out of time in the frame.
202 - if (firstCallbackNode !== null) {
164 + let currentTime = initialTime;
165 + if (firstTask !== null) {
166 do {
204 - if (enableSchedulerDebugging && isSchedulerPaused) {
205 - break;
206 - }
207 - flushFirstCallback();
208 - } while (firstCallbackNode !== null && !shouldYieldToHost());
167 + flushTask(firstTask, currentTime);
168 + currentTime = getCurrentTime();
169 + } while (
170 + firstTask !== null &&
171 + !shouldYieldToHost() &&
172 + !(enableSchedulerDebugging && isSchedulerPaused)
173 + );
174 }
175 }
176 + // Return whether there's additional work
177 + return firstTask !== null;
178 } finally {
179 isPerformingWork = false;
213 - currentHostCallbackDidTimeout = previousDidTimeout;
214 - // There's still work remaining. Request another callback.
215 - scheduleHostCallbackIfNeeded();
180 }
181 }
182
@@ -229,24 +193,17 @@ function unstable_runWithPriority(priorityLevel, eventHandler) {
193 }
194
195 var previousPriorityLevel = currentPriorityLevel;
232 - var previousEventStartTime = currentEventStartTime;
196 currentPriorityLevel = priorityLevel;
234 - currentEventStartTime = getCurrentTime();
197
198 try {
199 return eventHandler();
238 - } catch (error) {
239 - // There's still work remaining. Request another callback.
240 - scheduleHostCallbackIfNeeded();
241 - throw error;
200 } finally {
201 currentPriorityLevel = previousPriorityLevel;
244 - currentEventStartTime = previousEventStartTime;
202 }
203 }
204
205 function unstable_next(eventHandler) {
249 - let priorityLevel;
206 + var priorityLevel;
207 switch (currentPriorityLevel) {
208 case ImmediatePriority:
209 case UserBlockingPriority:
@@ -261,19 +218,12 @@ function unstable_next(eventHandler) {
218 }
219
220 var previousPriorityLevel = currentPriorityLevel;
264 - var previousEventStartTime = currentEventStartTime;
221 currentPriorityLevel = priorityLevel;
266 - currentEventStartTime = getCurrentTime();
222
223 try {
224 return eventHandler();
270 - } catch (error) {
271 - // There's still work remaining. Request another callback.
272 - scheduleHostCallbackIfNeeded();
273 - throw error;
225 } finally {
226 currentPriorityLevel = previousPriorityLevel;
276 - currentEventStartTime = previousEventStartTime;
227 }
228 }
229
@@ -282,103 +232,98 @@ function unstable_wrapCallback(callback) {
232 return function() {
233 // This is a fork of runWithPriority, inlined for performance.
234 var previousPriorityLevel = currentPriorityLevel;
285 - var previousEventStartTime = currentEventStartTime;
235 currentPriorityLevel = parentPriorityLevel;
287 - currentEventStartTime = getCurrentTime();
236
237 try {
238 return callback.apply(this, arguments);
291 - } catch (error) {
292 - // There's still work remaining. Request another callback.
293 - scheduleHostCallbackIfNeeded();
294 - throw error;
239 } finally {
240 currentPriorityLevel = previousPriorityLevel;
297 - currentEventStartTime = previousEventStartTime;
241 }
242 };
243 }
244
302 -function unstable_scheduleCallback(
303 - priorityLevel,
304 - callback,
305 - deprecated_options,
306 -) {
307 - var startTime =
308 - currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
245 +function unstable_scheduleCallback(priorityLevel, callback, options) {
246 + var startTime = getCurrentTime();
247
310 - var expirationTime;
248 + var timeout;
249 if (
312 - typeof deprecated_options === 'object' &&
313 - deprecated_options !== null &&
314 - typeof deprecated_options.timeout === 'number'
250 + typeof options === 'object' &&
251 + options !== null &&
252 + typeof options.timeout === 'number'
253 ) {
316 - // FIXME: Remove this branch once we lift expiration times out of React.
317 - expirationTime = startTime + deprecated_options.timeout;
254 + timeout = options.timeout;
255 } else {
256 switch (priorityLevel) {
257 case ImmediatePriority:
321 - expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
258 + timeout = IMMEDIATE_PRIORITY_TIMEOUT;
259 break;
260 case UserBlockingPriority:
324 - expirationTime = startTime + USER_BLOCKING_PRIORITY;
261 + timeout = USER_BLOCKING_PRIORITY;
262 break;
263 case IdlePriority:
327 - expirationTime = startTime + IDLE_PRIORITY;
264 + timeout = IDLE_PRIORITY;
265 break;
266 case LowPriority:
330 - expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
267 + timeout = LOW_PRIORITY_TIMEOUT;
268 break;
269 case NormalPriority:
270 default:
334 - expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
271 + timeout = NORMAL_PRIORITY_TIMEOUT;
272 }
273 }
274
338 - var newNode = {
275 + var expirationTime = startTime + timeout;
276 +
277 + var newTask = {
278 callback,
340 - priorityLevel: priorityLevel,
279 + priorityLevel,
280 + startTime,
281 expirationTime,
282 next: null,
283 previous: null,
284 };
285
346 - // Insert the new callback into the list, ordered first by expiration, then
347 - // by insertion. So the new callback is inserted after any other callback
348 - // with equal expiration.
349 - if (firstCallbackNode === null) {
350 - // This is the first callback in the list.
351 - firstCallbackNode = newNode.next = newNode.previous = newNode;
352 - scheduleHostCallbackIfNeeded();
286 + // Insert the new task into the list, ordered first by its timeout, then by
287 + // insertion. So the new task is inserted after any other task the
288 + // same timeout
289 + if (firstTask === null) {
290 + // This is the first task in the list.
291 + firstTask = newTask.next = newTask.previous = newTask;
292 } else {
293 var next = null;
355 - var node = firstCallbackNode;
294 + var task = firstTask;
295 do {
357 - if (node.expirationTime > expirationTime) {
358 - // The new callback expires before this one.
359 - next = node;
296 + if (expirationTime < task.expirationTime) {
297 + // The new task times out before this one.
298 + next = task;
299 break;
300 }
362 - node = node.next;
363 - } while (node !== firstCallbackNode);
301 + task = task.next;
302 + } while (task !== firstTask);
303
304 if (next === null) {
366 - // No callback with a later expiration was found, which means the new
367 - // callback has the latest expiration in the list.
368 - next = firstCallbackNode;
369 - } else if (next === firstCallbackNode) {
370 - // The new callback has the earliest expiration in the entire list.
371 - firstCallbackNode = newNode;
372 - scheduleHostCallbackIfNeeded();
305 + // No task with a later timeout was found, which means the new task has
306 + // the latest timeout in the list.
307 + next = firstTask;
308 + } else if (next === firstTask) {
309 + // The new task has the earliest expiration in the entire list.
310 + firstTask = newTask;
311 }
312
313 var previous = next.previous;
376 - previous.next = next.previous = newNode;
377 - newNode.next = next;
378 - newNode.previous = previous;
314 + previous.next = next.previous = newTask;
315 + newTask.next = next;
316 + newTask.previous = previous;
317 + }
318 +
319 + // Schedule a host callback, if needed. If we're already performing work, wait
320 + // until the next time we yield.
321 + if (!isHostCallbackScheduled && !isPerformingWork) {
322 + isHostCallbackScheduled = true;
323 + requestHostCallback(flushWork);
324 }
325
381 - return newNode;
326 + return newTask;
327 }
328
329 function unstable_pauseExecution() {
@@ -387,36 +332,37 @@ function unstable_pauseExecution() {
332
333 function unstable_continueExecution() {
334 isSchedulerPaused = false;
390 - if (firstCallbackNode !== null) {
391 - scheduleHostCallbackIfNeeded();
335 + if (!isHostCallbackScheduled && !isPerformingWork) {
336 + isHostCallbackScheduled = true;
337 + requestHostCallback(flushWork);
338 }
339 }
340
341 function unstable_getFirstCallbackNode() {
396 - return firstCallbackNode;
342 + return firstTask;
343 }
344
399 -function unstable_cancelCallback(callbackNode) {
400 - var next = callbackNode.next;
345 +function unstable_cancelCallback(task) {
346 + var next = task.next;
347 if (next === null) {
348 // Already cancelled.
349 return;
350 }
351
406 - if (next === callbackNode) {
407 - // This is the only scheduled callback. Clear the list.
408 - firstCallbackNode = null;
352 + if (next === task) {
353 + // This is the only scheduled task. Clear the list.
354 + firstTask = null;
355 } else {
410 - // Remove the callback from its position in the list.
411 - if (callbackNode === firstCallbackNode) {
412 - firstCallbackNode = next;
356 + // Remove the task from its position in the list.
357 + if (task === firstTask) {
358 + firstTask = next;
359 }
414 - var previous = callbackNode.previous;
360 + var previous = task.previous;
361 previous.next = next;
362 next.previous = previous;
363 }
364
419 - callbackNode.next = callbackNode.previous = null;
365 + task.next = task.previous = null;
366 }
367
368 function unstable_getCurrentPriorityLevel() {
@@ -425,10 +371,10 @@ function unstable_getCurrentPriorityLevel() {
371
372 function unstable_shouldYield() {
373 return (
428 - !currentHostCallbackDidTimeout &&
429 - ((firstCallbackNode !== null &&
430 - firstCallbackNode.expirationTime < currentExpirationTime) ||
431 - shouldYieldToHost())
374 + (currentTask !== null &&
375 + firstTask !== null &&
376 + firstTask.expirationTime < currentTask.expirationTime) ||
377 + shouldYieldToHost()
378 );
379 }
380
packages/scheduler/src/__tests__/Scheduler-test.js
+2 -2
@@ -227,7 +227,7 @@ describe('Scheduler', () => {
227 });
228
229 it(
230 - 'continutations are interrupted by higher priority work scheduled ' +
230 + 'continuations are interrupted by higher priority work scheduled ' +
231 'inside an executing callback',
232 () => {
233 const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
@@ -237,7 +237,7 @@ describe('Scheduler', () => {
237 const [label, ms] = task;
238 Scheduler.advanceTime(ms);
239 Scheduler.yieldValue(label);
240 - if (task[0] === 'B') {
240 + if (label === 'B') {
241 // Schedule high pri work from inside another callback
242 Scheduler.yieldValue('Schedule high pri');
243 scheduleCallback(UserBlockingPriority, () => {
packages/scheduler/src/forks/SchedulerHostConfig.default.js
+43 -57
@@ -88,22 +88,26 @@ if (
88 // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
89 // fallback to a naive implementation.
90 let _callback = null;
91 - const _flushCallback = function(didTimeout) {
91 + const _flushCallback = function() {
92 if (_callback !== null) {
93 try {
94 - _callback(didTimeout);
95 - } finally {
94 + const currentTime = getCurrentTime();
95 + const hasRemainingTime = true;
96 + _callback(hasRemainingTime, currentTime);
97 _callback = null;
98 + } catch (e) {
99 + setTimeout(_flushCallback, 0);
100 + throw e;
101 }
102 }
103 };
100 - requestHostCallback = function(cb, ms) {
104 + requestHostCallback = function(cb) {
105 if (_callback !== null) {
106 // Protect against re-entrancy.
107 setTimeout(requestHostCallback, 0, cb);
108 } else {
109 _callback = cb;
106 - setTimeout(_flushCallback, 0, false);
110 + setTimeout(_flushCallback, 0);
111 }
112 };
113 cancelHostCallback = function() {
@@ -134,12 +138,9 @@ if (
138
139 let scheduledHostCallback = null;
140 let isMessageEventScheduled = false;
137 - let timeoutTime = -1;
141
142 let isAnimationFrameScheduled = false;
143
141 - let isFlushingHostCallback = false;
142 -
144 let frameDeadline = 0;
145 // We start out assuming that we run at 30fps but then the heuristic tracking
146 // will adjust this value to a faster fps if we get more frequent animation
@@ -175,42 +176,30 @@ if (
176 const port = channel.port2;
177 channel.port1.onmessage = function(event) {
178 isMessageEventScheduled = false;
178 -
179 - const prevScheduledCallback = scheduledHostCallback;
180 - const prevTimeoutTime = timeoutTime;
181 - scheduledHostCallback = null;
182 - timeoutTime = -1;
183 -
184 - const currentTime = getCurrentTime();
185 -
186 - let didTimeout = false;
187 - if (frameDeadline - currentTime <= 0) {
188 - // There's no time left in this idle period. Check if the callback has
189 - // a timeout and whether it's been exceeded.
190 - if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
191 - // Exceeded the timeout. Invoke the callback even though there's no
192 - // time left.
193 - didTimeout = true;
194 - } else {
195 - // No timeout.
196 - if (!isAnimationFrameScheduled) {
197 - // Schedule another animation callback so we retry later.
198 - isAnimationFrameScheduled = true;
199 - requestAnimationFrameWithTimeout(animationTick);
200 - }
201 - // Exit without invoking the callback.
202 - scheduledHostCallback = prevScheduledCallback;
203 - timeoutTime = prevTimeoutTime;
204 - return;
205 - }
206 - }
207 -
208 - if (prevScheduledCallback !== null) {
209 - isFlushingHostCallback = true;
179 + if (scheduledHostCallback !== null) {
180 + const currentTime = getCurrentTime();
181 + const hasTimeRemaining = frameDeadline - currentTime > 0;
182 try {
211 - prevScheduledCallback(didTimeout);
212 - } finally {
213 - isFlushingHostCallback = false;
183 + const hasMoreWork = scheduledHostCallback(
184 + hasTimeRemaining,
185 + currentTime,
186 + );
187 + if (hasMoreWork) {
188 + // Ensure the next frame is scheduled.
189 + if (!isAnimationFrameScheduled) {
190 + isAnimationFrameScheduled = true;
191 + requestAnimationFrameWithTimeout(animationTick);
192 + }
193 + } else {
194 + scheduledHostCallback = null;
195 + }
196 + } catch (error) {
197 + // If a scheduler task throws, exit the current browser task so the
198 + // error can be observed, and post a new task as soon as possible
199 + // so we can continue where we left off.
200 + isMessageEventScheduled = true;
201 + port.postMessage(undefined);
202 + throw error;
203 }
204 }
205 };
@@ -262,25 +251,22 @@ if (
251 }
252 };
253
265 - requestHostCallback = function(callback, absoluteTimeout) {
266 - scheduledHostCallback = callback;
267 - timeoutTime = absoluteTimeout;
268 - if (isFlushingHostCallback || absoluteTimeout < 0) {
269 - // Don't wait for the next frame. Continue working ASAP, in a new event.
270 - port.postMessage(undefined);
271 - } else if (!isAnimationFrameScheduled) {
272 - // If rAF didn't already schedule one, we need to schedule a frame.
273 - // TODO: If this rAF doesn't materialize because the browser throttles, we
274 - // might want to still have setTimeout trigger rIC as a backup to ensure
275 - // that we keep performing work.
276 - isAnimationFrameScheduled = true;
277 - requestAnimationFrameWithTimeout(animationTick);
254 + requestHostCallback = function(callback) {
255 + if (scheduledHostCallback === null) {
256 + scheduledHostCallback = callback;
257 + if (!isAnimationFrameScheduled) {
258 + // If rAF didn't already schedule one, we need to schedule a frame.
259 + // TODO: If this rAF doesn't materialize because the browser throttles,
260 + // we might want to still have setTimeout trigger rIC as a backup to
261 + // ensure that we keep performing work.
262 + isAnimationFrameScheduled = true;
263 + requestAnimationFrameWithTimeout(animationTick);
264 + }
265 }
266 };
267
268 cancelHostCallback = function() {
269 scheduledHostCallback = null;
270 isMessageEventScheduled = false;
284 - timeoutTime = -1;
271 };
272 }
packages/scheduler/src/forks/SchedulerHostConfig.mock.js
+43 -54
@@ -8,33 +8,25 @@
8 */
9
10 let currentTime: number = 0;
11 -let scheduledCallback: (boolean => void) | null = null;
12 -let scheduledCallbackExpiration: number = -1;
11 +let scheduledCallback: ((boolean, number) => void) | null = null;
12 let yieldedValues: Array<mixed> | null = null;
13 let expectedNumberOfYields: number = -1;
14 let didStop: boolean = false;
15 let isFlushing: boolean = false;
16
18 -export function requestHostCallback(
19 - callback: boolean => void,
20 - expiration: number,
21 -) {
17 +export function requestHostCallback(callback: boolean => void) {
18 scheduledCallback = callback;
23 - scheduledCallbackExpiration = expiration;
19 }
20
21 export function cancelHostCallback(): void {
22 scheduledCallback = null;
28 - scheduledCallbackExpiration = -1;
23 }
24
25 export function shouldYieldToHost(): boolean {
26 if (
33 - (expectedNumberOfYields !== -1 &&
34 - yieldedValues !== null &&
35 - yieldedValues.length >= expectedNumberOfYields) ||
36 - (scheduledCallbackExpiration !== -1 &&
37 - scheduledCallbackExpiration <= currentTime)
27 + expectedNumberOfYields !== -1 &&
28 + yieldedValues !== null &&
29 + yieldedValues.length >= expectedNumberOfYields
30 ) {
31 // We yielded at least as many values as expected. Stop flushing.
32 didStop = true;
@@ -57,7 +49,6 @@ export function reset() {
49 }
50 currentTime = 0;
51 scheduledCallback = null;
60 - scheduledCallbackExpiration = -1;
52 yieldedValues = null;
53 expectedNumberOfYields = -1;
54 didStop = false;
@@ -69,21 +60,23 @@ export function unstable_flushNumberOfYields(count: number): void {
60 if (isFlushing) {
61 throw new Error('Already flushing work.');
62 }
72 - expectedNumberOfYields = count;
73 - isFlushing = true;
74 - try {
75 - while (scheduledCallback !== null && !didStop) {
76 - const cb = scheduledCallback;
77 - scheduledCallback = null;
78 - const didTimeout =
79 - scheduledCallbackExpiration !== -1 &&
80 - scheduledCallbackExpiration <= currentTime;
81 - cb(didTimeout);
63 + if (scheduledCallback !== null) {
64 + const cb = scheduledCallback;
65 + expectedNumberOfYields = count;
66 + isFlushing = true;
67 + try {
68 + let hasMoreWork = true;
69 + do {
70 + hasMoreWork = cb(true, currentTime);
71 + } while (hasMoreWork && !didStop);
72 + if (!hasMoreWork) {
73 + scheduledCallback = null;
74 + }
75 + } finally {
76 + expectedNumberOfYields = -1;
77 + didStop = false;
78 + isFlushing = false;
79 }
83 - } finally {
84 - expectedNumberOfYields = -1;
85 - didStop = false;
86 - isFlushing = false;
80 }
81 }
82
@@ -92,11 +85,12 @@ export function unstable_flushExpired() {
85 throw new Error('Already flushing work.');
86 }
87 if (scheduledCallback !== null) {
95 - const cb = scheduledCallback;
96 - scheduledCallback = null;
88 isFlushing = true;
89 try {
99 - cb(true);
90 + const hasMoreWork = scheduledCallback(false, currentTime);
91 + if (!hasMoreWork) {
92 + scheduledCallback = null;
93 + }
94 } finally {
95 isFlushing = false;
96 }
@@ -104,27 +98,27 @@ export function unstable_flushExpired() {
98 }
99
100 export function unstable_flushWithoutYielding(): boolean {
101 + // Returns false if no work was flushed.
102 if (isFlushing) {
103 throw new Error('Already flushing work.');
104 }
110 - isFlushing = true;
111 - try {
112 - if (scheduledCallback === null) {
113 - return false;
114 - }
115 - while (scheduledCallback !== null) {
116 - const cb = scheduledCallback;
117 - scheduledCallback = null;
118 - const didTimeout =
119 - scheduledCallbackExpiration !== -1 &&
120 - scheduledCallbackExpiration <= currentTime;
121 - cb(didTimeout);
105 + if (scheduledCallback !== null) {
106 + const cb = scheduledCallback;
107 + isFlushing = true;
108 + try {
109 + let hasMoreWork = true;
110 + do {
111 + hasMoreWork = cb(true, currentTime);
112 + } while (hasMoreWork);
113 + if (!hasMoreWork) {
114 + scheduledCallback = null;
115 + }
116 + return true;
117 + } finally {
118 + isFlushing = false;
119 }
123 - return true;
124 - } finally {
125 - expectedNumberOfYields = -1;
126 - didStop = false;
127 - isFlushing = false;
120 + } else {
121 + return false;
122 }
123 }
124
@@ -164,12 +158,7 @@ export function yieldValue(value: mixed): void {
158
159 export function advanceTime(ms: number) {
160 currentTime += ms;
167 - // If the host callback timed out, flush the expired work.
168 - if (
169 - !isFlushing &&
170 - scheduledCallbackExpiration !== -1 &&
171 - scheduledCallbackExpiration <= currentTime
172 - ) {
161 + if (!isFlushing) {
162 unstable_flushExpired();
163 }
164 }