@samitouri / QOS-React-2 / commits / 0991647921

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) {