Use setImmediate when available over MessageChannel (#20834)
* Move direct port access into a function * Fork based on presence of setImmediate * Copy SchedulerDOM-test into another file * Change the new test to use shimmed setImmediate * Clarify comment * Fix test to work with existing feature detection * Add flags * Disable OSS flag and skip tests * Use VARIANT to reenable tests * lol
Dan Abramov committed
Feb 17, 2021 at 21:00 UTC
09916479219a61ae86d2ec8ce159a161337b9007
5 files changed
+318
-5
packages/scheduler/src/SchedulerFeatureFlags.js
+3
@@ -9,3 +9,6 @@
9
export const enableSchedulerDebugging = false;
10
export const enableIsInputPending = false;
11
export const enableProfiling = __PROFILE__;
12
+
13
+// TODO: enable to fix https://github.com/facebook/react/issues/20756.
14
+export const enableSetImmediate = __VARIANT__;
packages/scheduler/src/__tests__/SchedulerDOMSetImmediate-test.js
new
+281
@@ -0,0 +1,281 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ * @jest-environment node
9
+ */
10
+
11
+/* eslint-disable no-for-of-loops/no-for-of-loops */
12
+
13
+'use strict';
14
+
15
+let Scheduler;
16
+let runtime;
17
+let performance;
18
+let cancelCallback;
19
+let scheduleCallback;
20
+let NormalPriority;
21
+
22
+// The Scheduler implementation uses browser APIs like `MessageChannel` and
23
+// `setTimeout` to schedule work on the main thread. Most of our tests treat
24
+// these as implementation details; however, the sequence and timing of these
25
+// APIs are not precisely specified, and can vary across browsers.
26
+//
27
+// To prevent regressions, we need the ability to simulate specific edge cases
28
+// that we may encounter in various browsers.
29
+//
30
+// This test suite mocks all browser methods used in our implementation. It
31
+// assumes as little as possible about the order and timing of events.
32
+describe('SchedulerDOMSetImmediate', () => {
33
+ beforeEach(() => {
34
+ jest.resetModules();
35
+
36
+ // Un-mock scheduler
37
+ jest.mock('scheduler', () => require.requireActual('scheduler'));
38
+
39
+ runtime = installMockBrowserRuntime();
40
+ performance = global.performance;
41
+ Scheduler = require('scheduler');
42
+ cancelCallback = Scheduler.unstable_cancelCallback;
43
+ scheduleCallback = Scheduler.unstable_scheduleCallback;
44
+ NormalPriority = Scheduler.unstable_NormalPriority;
45
+ });
46
+
47
+ afterEach(() => {
48
+ delete global.performance;
49
+
50
+ if (!runtime.isLogEmpty()) {
51
+ throw Error('Test exited without clearing log.');
52
+ }
53
+ });
54
+
55
+ function installMockBrowserRuntime() {
56
+ let timerIDCounter = 0;
57
+ // let timerIDs = new Map();
58
+
59
+ let eventLog = [];
60
+
61
+ let currentTime = 0;
62
+
63
+ global.performance = {
64
+ now() {
65
+ return currentTime;
66
+ },
67
+ };
68
+
69
+ const window = {};
70
+ global.window = window;
71
+
72
+ // TODO: Scheduler no longer requires these methods to be polyfilled. But
73
+ // maybe we want to continue warning if they don't exist, to preserve the
74
+ // option to rely on it in the future?
75
+ window.requestAnimationFrame = window.cancelAnimationFrame = () => {};
76
+
77
+ window.setTimeout = (cb, delay) => {
78
+ const id = timerIDCounter++;
79
+ log(`Set Timer`);
80
+ // TODO
81
+ return id;
82
+ };
83
+ window.clearTimeout = id => {
84
+ // TODO
85
+ };
86
+
87
+ // Unused: we expect setImmediate to be preferred.
88
+ global.MessageChannel = function() {
89
+ return {
90
+ port1: {},
91
+ port2: {
92
+ postMessage() {
93
+ throw Error('Should be unused');
94
+ },
95
+ },
96
+ };
97
+ };
98
+
99
+ let pendingSetImmediateCallback = null;
100
+ window.setImmediate = function(cb) {
101
+ if (pendingSetImmediateCallback) {
102
+ throw Error('Message event already scheduled');
103
+ }
104
+ log('Set Immediate');
105
+ pendingSetImmediateCallback = cb;
106
+ };
107
+
108
+ function ensureLogIsEmpty() {
109
+ if (eventLog.length !== 0) {
110
+ throw Error('Log is not empty. Call assertLog before continuing.');
111
+ }
112
+ }
113
+ function advanceTime(ms) {
114
+ currentTime += ms;
115
+ }
116
+ function fireSetImmediate() {
117
+ ensureLogIsEmpty();
118
+ if (!pendingSetImmediateCallback) {
119
+ throw Error('No setImmediate was scheduled');
120
+ }
121
+ const cb = pendingSetImmediateCallback;
122
+ pendingSetImmediateCallback = null;
123
+ log('setImmediate Callback');
124
+ cb();
125
+ }
126
+ function log(val) {
127
+ eventLog.push(val);
128
+ }
129
+ function isLogEmpty() {
130
+ return eventLog.length === 0;
131
+ }
132
+ function assertLog(expected) {
133
+ const actual = eventLog;
134
+ eventLog = [];
135
+ expect(actual).toEqual(expected);
136
+ }
137
+ return {
138
+ advanceTime,
139
+ fireSetImmediate,
140
+ log,
141
+ isLogEmpty,
142
+ assertLog,
143
+ };
144
+ }
145
+
146
+ // @gate enableSchedulerSetImmediate
147
+ it('task that finishes before deadline', () => {
148
+ scheduleCallback(NormalPriority, () => {
149
+ runtime.log('Task');
150
+ });
151
+ runtime.assertLog(['Set Immediate']);
152
+ runtime.fireSetImmediate();
153
+ runtime.assertLog(['setImmediate Callback', 'Task']);
154
+ });
155
+
156
+ // @gate enableSchedulerSetImmediate
157
+ it('task with continuation', () => {
158
+ scheduleCallback(NormalPriority, () => {
159
+ runtime.log('Task');
160
+ while (!Scheduler.unstable_shouldYield()) {
161
+ runtime.advanceTime(1);
162
+ }
163
+ runtime.log(`Yield at ${performance.now()}ms`);
164
+ return () => {
165
+ runtime.log('Continuation');
166
+ };
167
+ });
168
+ runtime.assertLog(['Set Immediate']);
169
+
170
+ runtime.fireSetImmediate();
171
+ runtime.assertLog([
172
+ 'setImmediate Callback',
173
+ 'Task',
174
+ 'Yield at 5ms',
175
+ 'Set Immediate',
176
+ ]);
177
+
178
+ runtime.fireSetImmediate();
179
+ runtime.assertLog(['setImmediate Callback', 'Continuation']);
180
+ });
181
+
182
+ // @gate enableSchedulerSetImmediate
183
+ it('multiple tasks', () => {
184
+ scheduleCallback(NormalPriority, () => {
185
+ runtime.log('A');
186
+ });
187
+ scheduleCallback(NormalPriority, () => {
188
+ runtime.log('B');
189
+ });
190
+ runtime.assertLog(['Set Immediate']);
191
+ runtime.fireSetImmediate();
192
+ runtime.assertLog(['setImmediate Callback', 'A', 'B']);
193
+ });
194
+
195
+ // @gate enableSchedulerSetImmediate
196
+ it('multiple tasks with a yield in between', () => {
197
+ scheduleCallback(NormalPriority, () => {
198
+ runtime.log('A');
199
+ runtime.advanceTime(4999);
200
+ });
201
+ scheduleCallback(NormalPriority, () => {
202
+ runtime.log('B');
203
+ });
204
+ runtime.assertLog(['Set Immediate']);
205
+ runtime.fireSetImmediate();
206
+ runtime.assertLog([
207
+ 'setImmediate Callback',
208
+ 'A',
209
+ // Ran out of time. Post a continuation event.
210
+ 'Set Immediate',
211
+ ]);
212
+ runtime.fireSetImmediate();
213
+ runtime.assertLog(['setImmediate Callback', 'B']);
214
+ });
215
+
216
+ // @gate enableSchedulerSetImmediate
217
+ it('cancels tasks', () => {
218
+ const task = scheduleCallback(NormalPriority, () => {
219
+ runtime.log('Task');
220
+ });
221
+ runtime.assertLog(['Set Immediate']);
222
+ cancelCallback(task);
223
+ runtime.assertLog([]);
224
+ });
225
+
226
+ // @gate enableSchedulerSetImmediate
227
+ it('throws when a task errors then continues in a new event', () => {
228
+ scheduleCallback(NormalPriority, () => {
229
+ runtime.log('Oops!');
230
+ throw Error('Oops!');
231
+ });
232
+ scheduleCallback(NormalPriority, () => {
233
+ runtime.log('Yay');
234
+ });
235
+ runtime.assertLog(['Set Immediate']);
236
+
237
+ expect(() => runtime.fireSetImmediate()).toThrow('Oops!');
238
+ runtime.assertLog(['setImmediate Callback', 'Oops!', 'Set Immediate']);
239
+
240
+ runtime.fireSetImmediate();
241
+ runtime.assertLog(['setImmediate Callback', 'Yay']);
242
+ });
243
+
244
+ // @gate enableSchedulerSetImmediate
245
+ it('schedule new task after queue has emptied', () => {
246
+ scheduleCallback(NormalPriority, () => {
247
+ runtime.log('A');
248
+ });
249
+
250
+ runtime.assertLog(['Set Immediate']);
251
+ runtime.fireSetImmediate();
252
+ runtime.assertLog(['setImmediate Callback', 'A']);
253
+
254
+ scheduleCallback(NormalPriority, () => {
255
+ runtime.log('B');
256
+ });
257
+ runtime.assertLog(['Set Immediate']);
258
+ runtime.fireSetImmediate();
259
+ runtime.assertLog(['setImmediate Callback', 'B']);
260
+ });
261
+
262
+ // @gate enableSchedulerSetImmediate
263
+ it('schedule new task after a cancellation', () => {
264
+ const handle = scheduleCallback(NormalPriority, () => {
265
+ runtime.log('A');
266
+ });
267
+
268
+ runtime.assertLog(['Set Immediate']);
269
+ cancelCallback(handle);
270
+
271
+ runtime.fireSetImmediate();
272
+ runtime.assertLog(['setImmediate Callback']);
273
+
274
+ scheduleCallback(NormalPriority, () => {
275
+ runtime.log('B');
276
+ });
277
+ runtime.assertLog(['Set Immediate']);
278
+ runtime.fireSetImmediate();
279
+ runtime.assertLog(['setImmediate Callback', 'B']);
280
+ });
281
+});
packages/scheduler/src/forks/SchedulerDOM.js
+28
-5
@@ -11,6 +11,7 @@
11
import {
12
enableSchedulerDebugging,
13
enableProfiling,
14
+ enableSetImmediate,
15
} from '../SchedulerFeatureFlags';
16
17
import {push, pop, peek} from '../SchedulerMinHeap';
@@ -88,6 +89,7 @@ var isHostTimeoutScheduled = false;
89
// Capture local references to native APIs, in case a polyfill overrides them.
90
const setTimeout = window.setTimeout;
91
const clearTimeout = window.clearTimeout;
92
+const setImmediate = window.setImmediate; // IE and Node.js + jsdom
93
94
if (typeof console !== 'undefined') {
95
// TODO: Scheduler no longer requires these methods to be polyfilled. But
@@ -533,7 +535,7 @@ const performWorkUntilDeadline = () => {
535
if (hasMoreWork) {
536
// If there's more work, schedule the next message event at the end
537
// of the preceding one.
536
- port.postMessage(null);
538
+ schedulePerformWorkUntilDeadline();
539
} else {
540
isMessageLoopRunning = false;
541
scheduledHostCallback = null;
@@ -547,15 +549,36 @@ const performWorkUntilDeadline = () => {
549
needsPaint = false;
550
};
551
550
-const channel = new MessageChannel();
551
-const port = channel.port2;
552
-channel.port1.onmessage = performWorkUntilDeadline;
552
+let schedulePerformWorkUntilDeadline;
553
+if (enableSetImmediate && typeof setImmediate === 'function') {
554
+ // Node.js and old IE.
555
+ // There's a few reasons for why we prefer setImmediate.
556
+ //
557
+ // Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
558
+ // (Even though this is a DOM fork of the Scheduler, you could get here
559
+ // with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
560
+ // https://github.com/facebook/react/issues/20756
561
+ //
562
+ // But also, it runs earlier which is the semantic we want.
563
+ // If other browsers ever implement it, it's better to use it.
564
+ // Although both of these would be inferior to native scheduling.
565
+ schedulePerformWorkUntilDeadline = () => {
566
+ setImmediate(performWorkUntilDeadline);
567
+ };
568
+} else {
569
+ const channel = new MessageChannel();
570
+ const port = channel.port2;
571
+ channel.port1.onmessage = performWorkUntilDeadline;
572
+ schedulePerformWorkUntilDeadline = () => {
573
+ port.postMessage(null);
574
+ };
575
+}
576
577
function requestHostCallback(callback) {
578
scheduledHostCallback = callback;
579
if (!isMessageLoopRunning) {
580
isMessageLoopRunning = true;
558
- port.postMessage(null);
581
+ schedulePerformWorkUntilDeadline();
582
}
583
}
584
packages/scheduler/src/forks/SchedulerFeatureFlags.www.js
+1
@@ -10,6 +10,7 @@ export const {
10
enableIsInputPending,
11
enableSchedulerDebugging,
12
enableProfiling: enableProfilingFeatureFlag,
13
+ enableSetImmediate,
14
} = require('SchedulerFeatureFlags');
15
16
export const enableProfiling = __PROFILE__ && enableProfilingFeatureFlag;
scripts/jest/TestFlags.js
+5
@@ -55,6 +55,7 @@ function getTestFlags() {
55
// These are required on demand because some of our tests mutate them. We try
56
// not to but there are exceptions.
57
const featureFlags = require('shared/ReactFeatureFlags');
58
+ const schedulerFeatureFlags = require('scheduler/src/SchedulerFeatureFlags');
59
60
// TODO: This is a heuristic to detect the release channel by checking a flag
61
// that is known to only be enabled in www. What we should do instead is set
@@ -89,6 +90,10 @@ function getTestFlags() {
90
// tests, Jest doesn't expose the API correctly. Fix then remove
91
// this override.
92
enableCache: __EXPERIMENTAL__,
93
+
94
+ // This is from SchedulerFeatureFlags. Needed because there's no equivalent
95
+ // of ReactFeatureFlags-www.dynamic for it. Remove when enableSetImmediate is gone.
96
+ enableSchedulerSetImmediate: schedulerFeatureFlags.enableSetImmediate,
97
},
98
{
99
get(flags, flagName) {