Skip to content

Commit 0898660

Browse files
authored
Add version of scheduler that only swaps MessageChannel for postTask (#20206)
* Fork SchedulerDOM to SchedulerPostTaskOnly * Swap in postTask for MessageChannel * Add SchedulerPostTaskOnly-test.js * Update getCurrentTime * Gate tests to source * Prettier
1 parent 393c452 commit 0898660

File tree

4 files changed

+892
-0
lines changed

4 files changed

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

0 commit comments

Comments
 (0)