Skip to content

Commit 44f0348

Browse files
committed
Use setImmediate when available over MessageChannel (facebook#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 8e5adfb commit 44f0348

File tree

2 files changed

+306
-6
lines changed

2 files changed

+306
-6
lines changed
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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+
jest.mock('scheduler/src/SchedulerHostConfig', () =>
39+
require.requireActual(
40+
'scheduler/src/forks/SchedulerHostConfig.default.js',
41+
),
42+
);
43+
44+
runtime = installMockBrowserRuntime();
45+
performance = global.performance;
46+
Scheduler = require('scheduler');
47+
cancelCallback = Scheduler.unstable_cancelCallback;
48+
scheduleCallback = Scheduler.unstable_scheduleCallback;
49+
NormalPriority = Scheduler.unstable_NormalPriority;
50+
});
51+
52+
afterEach(() => {
53+
delete global.performance;
54+
55+
if (!runtime.isLogEmpty()) {
56+
throw Error('Test exited without clearing log.');
57+
}
58+
});
59+
60+
function installMockBrowserRuntime() {
61+
let timerIDCounter = 0;
62+
// let timerIDs = new Map();
63+
64+
let eventLog = [];
65+
66+
let currentTime = 0;
67+
68+
global.performance = {
69+
now() {
70+
return currentTime;
71+
},
72+
};
73+
74+
const window = {};
75+
global.window = window;
76+
77+
// TODO: Scheduler no longer requires these methods to be polyfilled. But
78+
// maybe we want to continue warning if they don't exist, to preserve the
79+
// option to rely on it in the future?
80+
window.requestAnimationFrame = window.cancelAnimationFrame = () => {};
81+
82+
window.setTimeout = (cb, delay) => {
83+
const id = timerIDCounter++;
84+
log(`Set Timer`);
85+
// TODO
86+
return id;
87+
};
88+
window.clearTimeout = id => {
89+
// TODO
90+
};
91+
92+
// Unused: we expect setImmediate to be preferred.
93+
global.MessageChannel = function() {
94+
return {
95+
port1: {},
96+
port2: {
97+
postMessage() {
98+
throw Error('Should be unused');
99+
},
100+
},
101+
};
102+
};
103+
104+
let pendingSetImmediateCallback = null;
105+
window.setImmediate = function(cb) {
106+
if (pendingSetImmediateCallback) {
107+
throw Error('Message event already scheduled');
108+
}
109+
log('Set Immediate');
110+
pendingSetImmediateCallback = cb;
111+
};
112+
113+
function ensureLogIsEmpty() {
114+
if (eventLog.length !== 0) {
115+
throw Error('Log is not empty. Call assertLog before continuing.');
116+
}
117+
}
118+
function advanceTime(ms) {
119+
currentTime += ms;
120+
}
121+
function fireSetImmediate() {
122+
ensureLogIsEmpty();
123+
if (!pendingSetImmediateCallback) {
124+
throw Error('No setImmediate was scheduled');
125+
}
126+
const cb = pendingSetImmediateCallback;
127+
pendingSetImmediateCallback = null;
128+
log('setImmediate Callback');
129+
cb();
130+
}
131+
function log(val) {
132+
eventLog.push(val);
133+
}
134+
function isLogEmpty() {
135+
return eventLog.length === 0;
136+
}
137+
function assertLog(expected) {
138+
const actual = eventLog;
139+
eventLog = [];
140+
expect(actual).toEqual(expected);
141+
}
142+
return {
143+
advanceTime,
144+
fireSetImmediate,
145+
log,
146+
isLogEmpty,
147+
assertLog,
148+
};
149+
}
150+
151+
it('task that finishes before deadline', () => {
152+
scheduleCallback(NormalPriority, () => {
153+
runtime.log('Task');
154+
});
155+
runtime.assertLog(['Set Immediate']);
156+
runtime.fireSetImmediate();
157+
runtime.assertLog(['setImmediate Callback', 'Task']);
158+
});
159+
160+
it('task with continuation', () => {
161+
scheduleCallback(NormalPriority, () => {
162+
runtime.log('Task');
163+
while (!Scheduler.unstable_shouldYield()) {
164+
runtime.advanceTime(1);
165+
}
166+
runtime.log(`Yield at ${performance.now()}ms`);
167+
return () => {
168+
runtime.log('Continuation');
169+
};
170+
});
171+
runtime.assertLog(['Set Immediate']);
172+
173+
runtime.fireSetImmediate();
174+
runtime.assertLog([
175+
'setImmediate Callback',
176+
'Task',
177+
'Yield at 5ms',
178+
'Set Immediate',
179+
]);
180+
181+
runtime.fireSetImmediate();
182+
runtime.assertLog(['setImmediate Callback', 'Continuation']);
183+
});
184+
185+
it('multiple tasks', () => {
186+
scheduleCallback(NormalPriority, () => {
187+
runtime.log('A');
188+
});
189+
scheduleCallback(NormalPriority, () => {
190+
runtime.log('B');
191+
});
192+
runtime.assertLog(['Set Immediate']);
193+
runtime.fireSetImmediate();
194+
runtime.assertLog(['setImmediate Callback', 'A', 'B']);
195+
});
196+
197+
it('multiple tasks with a yield in between', () => {
198+
scheduleCallback(NormalPriority, () => {
199+
runtime.log('A');
200+
runtime.advanceTime(4999);
201+
});
202+
scheduleCallback(NormalPriority, () => {
203+
runtime.log('B');
204+
});
205+
runtime.assertLog(['Set Immediate']);
206+
runtime.fireSetImmediate();
207+
runtime.assertLog([
208+
'setImmediate Callback',
209+
'A',
210+
// Ran out of time. Post a continuation event.
211+
'Set Immediate',
212+
]);
213+
runtime.fireSetImmediate();
214+
runtime.assertLog(['setImmediate Callback', 'B']);
215+
});
216+
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+
it('throws when a task errors then continues in a new event', () => {
227+
scheduleCallback(NormalPriority, () => {
228+
runtime.log('Oops!');
229+
throw Error('Oops!');
230+
});
231+
scheduleCallback(NormalPriority, () => {
232+
runtime.log('Yay');
233+
});
234+
runtime.assertLog(['Set Immediate']);
235+
236+
expect(() => runtime.fireSetImmediate()).toThrow('Oops!');
237+
runtime.assertLog(['setImmediate Callback', 'Oops!', 'Set Immediate']);
238+
239+
runtime.fireSetImmediate();
240+
runtime.assertLog(['setImmediate Callback', 'Yay']);
241+
});
242+
243+
it('schedule new task after queue has emptied', () => {
244+
scheduleCallback(NormalPriority, () => {
245+
runtime.log('A');
246+
});
247+
248+
runtime.assertLog(['Set Immediate']);
249+
runtime.fireSetImmediate();
250+
runtime.assertLog(['setImmediate Callback', 'A']);
251+
252+
scheduleCallback(NormalPriority, () => {
253+
runtime.log('B');
254+
});
255+
runtime.assertLog(['Set Immediate']);
256+
runtime.fireSetImmediate();
257+
runtime.assertLog(['setImmediate Callback', 'B']);
258+
});
259+
260+
it('schedule new task after a cancellation', () => {
261+
const handle = scheduleCallback(NormalPriority, () => {
262+
runtime.log('A');
263+
});
264+
265+
runtime.assertLog(['Set Immediate']);
266+
cancelCallback(handle);
267+
268+
runtime.fireSetImmediate();
269+
runtime.assertLog(['setImmediate Callback']);
270+
271+
scheduleCallback(NormalPriority, () => {
272+
runtime.log('B');
273+
});
274+
runtime.assertLog(['Set Immediate']);
275+
runtime.fireSetImmediate();
276+
runtime.assertLog(['setImmediate Callback', 'B']);
277+
});
278+
});

packages/scheduler/src/forks/SchedulerHostConfig.default.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ if (
7878
// Capture local references to native APIs, in case a polyfill overrides them.
7979
const setTimeout = window.setTimeout;
8080
const clearTimeout = window.clearTimeout;
81+
const setImmediate = window.setImmediate; // IE and Node.js + jsdom
8182

8283
if (typeof console !== 'undefined') {
8384
// TODO: Scheduler no longer requires these methods to be polyfilled. But
@@ -201,12 +202,12 @@ if (
201202
} else {
202203
// If there's more work, schedule the next message event at the end
203204
// of the preceding one.
204-
port.postMessage(null);
205+
schedulePerformWorkUntilDeadline();
205206
}
206207
} catch (error) {
207208
// If a scheduler task throws, exit the current browser task so the
208209
// error can be observed.
209-
port.postMessage(null);
210+
schedulePerformWorkUntilDeadline();
210211
throw error;
211212
}
212213
} else {
@@ -217,15 +218,36 @@ if (
217218
needsPaint = false;
218219
};
219220

220-
const channel = new MessageChannel();
221-
const port = channel.port2;
222-
channel.port1.onmessage = performWorkUntilDeadline;
221+
let schedulePerformWorkUntilDeadline;
222+
if (typeof setImmediate === 'function') {
223+
// Node.js and old IE.
224+
// There's a few reasons for why we prefer setImmediate.
225+
//
226+
// Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
227+
// (Even though this is a DOM fork of the Scheduler, you could get here
228+
// with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
229+
// https://github.com/facebook/react/issues/20756
230+
//
231+
// But also, it runs earlier which is the semantic we want.
232+
// If other browsers ever implement it, it's better to use it.
233+
// Although both of these would be inferior to native scheduling.
234+
schedulePerformWorkUntilDeadline = () => {
235+
setImmediate(performWorkUntilDeadline);
236+
};
237+
} else {
238+
const channel = new MessageChannel();
239+
const port = channel.port2;
240+
channel.port1.onmessage = performWorkUntilDeadline;
241+
schedulePerformWorkUntilDeadline = () => {
242+
port.postMessage(null);
243+
};
244+
}
223245

224246
requestHostCallback = function(callback) {
225247
scheduledHostCallback = callback;
226248
if (!isMessageLoopRunning) {
227249
isMessageLoopRunning = true;
228-
port.postMessage(null);
250+
schedulePerformWorkUntilDeadline();
229251
}
230252
};
231253

0 commit comments

Comments
 (0)