Skip to content

Commit 0991647

Browse files
authoredFeb 17, 2021
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
1 parent e2fd460 commit 0991647

File tree

5 files changed

+318
-5
lines changed

5 files changed

+318
-5
lines changed
 

‎packages/scheduler/src/SchedulerFeatureFlags.js

+3
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@
99
export const enableSchedulerDebugging = false;
1010
export const enableIsInputPending = false;
1111
export const enableProfiling = __PROFILE__;
12+
13+
// TODO: enable to fix https://github.com/facebook/react/issues/20756.
14+
export const enableSetImmediate = __VARIANT__;
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import {
1212
enableSchedulerDebugging,
1313
enableProfiling,
14+
enableSetImmediate,
1415
} from '../SchedulerFeatureFlags';
1516

1617
import {push, pop, peek} from '../SchedulerMinHeap';
@@ -88,6 +89,7 @@ var isHostTimeoutScheduled = false;
8889
// Capture local references to native APIs, in case a polyfill overrides them.
8990
const setTimeout = window.setTimeout;
9091
const clearTimeout = window.clearTimeout;
92+
const setImmediate = window.setImmediate; // IE and Node.js + jsdom
9193

9294
if (typeof console !== 'undefined') {
9395
// TODO: Scheduler no longer requires these methods to be polyfilled. But
@@ -533,7 +535,7 @@ const performWorkUntilDeadline = () => {
533535
if (hasMoreWork) {
534536
// If there's more work, schedule the next message event at the end
535537
// of the preceding one.
536-
port.postMessage(null);
538+
schedulePerformWorkUntilDeadline();
537539
} else {
538540
isMessageLoopRunning = false;
539541
scheduledHostCallback = null;
@@ -547,15 +549,36 @@ const performWorkUntilDeadline = () => {
547549
needsPaint = false;
548550
};
549551

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+
}
553576

554577
function requestHostCallback(callback) {
555578
scheduledHostCallback = callback;
556579
if (!isMessageLoopRunning) {
557580
isMessageLoopRunning = true;
558-
port.postMessage(null);
581+
schedulePerformWorkUntilDeadline();
559582
}
560583
}
561584

‎packages/scheduler/src/forks/SchedulerFeatureFlags.www.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const {
1010
enableIsInputPending,
1111
enableSchedulerDebugging,
1212
enableProfiling: enableProfilingFeatureFlag,
13+
enableSetImmediate,
1314
} = require('SchedulerFeatureFlags');
1415

1516
export const enableProfiling = __PROFILE__ && enableProfilingFeatureFlag;

‎scripts/jest/TestFlags.js

+5
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ function getTestFlags() {
5555
// These are required on demand because some of our tests mutate them. We try
5656
// not to but there are exceptions.
5757
const featureFlags = require('shared/ReactFeatureFlags');
58+
const schedulerFeatureFlags = require('scheduler/src/SchedulerFeatureFlags');
5859

5960
// TODO: This is a heuristic to detect the release channel by checking a flag
6061
// that is known to only be enabled in www. What we should do instead is set
@@ -89,6 +90,10 @@ function getTestFlags() {
8990
// tests, Jest doesn't expose the API correctly. Fix then remove
9091
// this override.
9192
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,
9297
},
9398
{
9499
get(flags, flagName) {

0 commit comments

Comments
 (0)