New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use setImmediate when available over MessageChannel #20834
Merged
Merged
Changes from 5 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
cb2a78c
Move direct port access into a function
gaearon 50bde0c
Fork based on presence of setImmediate
gaearon 02eb954
Copy SchedulerDOM-test into another file
gaearon 3f3cd83
Change the new test to use shimmed setImmediate
gaearon 4917e27
Clarify comment
gaearon a4682a2
Fix test to work with existing feature detection
gaearon 8d3f3db
Add flags
gaearon dd51cb7
Disable OSS flag and skip tests
gaearon ae20fe9
Use VARIANT to reenable tests
gaearon 2679450
lol
gaearon File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
264 changes: 264 additions & 0 deletions
264
packages/scheduler/src/__tests__/SchedulerDOMSetImmediate-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @emails react-core | ||
* @jest-environment node | ||
*/ | ||
|
||
/* eslint-disable no-for-of-loops/no-for-of-loops */ | ||
|
||
'use strict'; | ||
|
||
let Scheduler; | ||
let runtime; | ||
let performance; | ||
let cancelCallback; | ||
let scheduleCallback; | ||
let NormalPriority; | ||
|
||
// The Scheduler implementation uses browser APIs like `MessageChannel` and | ||
// `setTimeout` to schedule work on the main thread. Most of our tests treat | ||
// these as implementation details; however, the sequence and timing of these | ||
// APIs are not precisely specified, and can vary across browsers. | ||
// | ||
// To prevent regressions, we need the ability to simulate specific edge cases | ||
// that we may encounter in various browsers. | ||
// | ||
// This test suite mocks all browser methods used in our implementation. It | ||
// assumes as little as possible about the order and timing of events. | ||
describe('SchedulerDOMSetImmediate', () => { | ||
beforeEach(() => { | ||
jest.resetModules(); | ||
|
||
// Un-mock scheduler | ||
jest.mock('scheduler', () => require.requireActual('scheduler')); | ||
|
||
runtime = installMockBrowserRuntime(); | ||
performance = global.performance; | ||
Scheduler = require('scheduler'); | ||
cancelCallback = Scheduler.unstable_cancelCallback; | ||
scheduleCallback = Scheduler.unstable_scheduleCallback; | ||
NormalPriority = Scheduler.unstable_NormalPriority; | ||
}); | ||
|
||
afterEach(() => { | ||
delete global.performance; | ||
|
||
if (!runtime.isLogEmpty()) { | ||
throw Error('Test exited without clearing log.'); | ||
} | ||
}); | ||
|
||
function installMockBrowserRuntime() { | ||
let timerIDCounter = 0; | ||
// let timerIDs = new Map(); | ||
|
||
let eventLog = []; | ||
|
||
let currentTime = 0; | ||
|
||
global.performance = { | ||
now() { | ||
return currentTime; | ||
}, | ||
}; | ||
|
||
const window = {}; | ||
global.window = window; | ||
|
||
// TODO: Scheduler no longer requires these methods to be polyfilled. But | ||
// maybe we want to continue warning if they don't exist, to preserve the | ||
// option to rely on it in the future? | ||
window.requestAnimationFrame = window.cancelAnimationFrame = () => {}; | ||
|
||
window.setTimeout = (cb, delay) => { | ||
const id = timerIDCounter++; | ||
log(`Set Timer`); | ||
// TODO | ||
return id; | ||
}; | ||
window.clearTimeout = id => { | ||
// TODO | ||
}; | ||
|
||
// Unused: we expect setImmediate to be preferred. | ||
window.MessageChannel = function() {}; | ||
|
||
let pendingSetImmediateCallback = null; | ||
window.setImmediate = function(cb) { | ||
if (pendingSetImmediateCallback) { | ||
throw Error('Message event already scheduled'); | ||
} | ||
log('Set Immediate'); | ||
pendingSetImmediateCallback = cb; | ||
}; | ||
|
||
function ensureLogIsEmpty() { | ||
if (eventLog.length !== 0) { | ||
throw Error('Log is not empty. Call assertLog before continuing.'); | ||
} | ||
} | ||
function advanceTime(ms) { | ||
currentTime += ms; | ||
} | ||
function fireSetImmediate() { | ||
ensureLogIsEmpty(); | ||
if (!pendingSetImmediateCallback) { | ||
throw Error('No setImmediate was scheduled'); | ||
} | ||
const cb = pendingSetImmediateCallback; | ||
pendingSetImmediateCallback = null; | ||
log('setImmediate Callback'); | ||
cb(); | ||
} | ||
function log(val) { | ||
eventLog.push(val); | ||
} | ||
function isLogEmpty() { | ||
return eventLog.length === 0; | ||
} | ||
function assertLog(expected) { | ||
const actual = eventLog; | ||
eventLog = []; | ||
expect(actual).toEqual(expected); | ||
} | ||
return { | ||
advanceTime, | ||
fireSetImmediate, | ||
log, | ||
isLogEmpty, | ||
assertLog, | ||
}; | ||
} | ||
|
||
it('task that finishes before deadline', () => { | ||
scheduleCallback(NormalPriority, () => { | ||
runtime.log('Task'); | ||
}); | ||
runtime.assertLog(['Set Immediate']); | ||
runtime.fireSetImmediate(); | ||
runtime.assertLog(['setImmediate Callback', 'Task']); | ||
}); | ||
|
||
it('task with continuation', () => { | ||
scheduleCallback(NormalPriority, () => { | ||
runtime.log('Task'); | ||
while (!Scheduler.unstable_shouldYield()) { | ||
runtime.advanceTime(1); | ||
} | ||
runtime.log(`Yield at ${performance.now()}ms`); | ||
return () => { | ||
runtime.log('Continuation'); | ||
}; | ||
}); | ||
runtime.assertLog(['Set Immediate']); | ||
|
||
runtime.fireSetImmediate(); | ||
runtime.assertLog([ | ||
'setImmediate Callback', | ||
'Task', | ||
'Yield at 5ms', | ||
'Set Immediate', | ||
]); | ||
|
||
runtime.fireSetImmediate(); | ||
runtime.assertLog(['setImmediate Callback', 'Continuation']); | ||
}); | ||
|
||
it('multiple tasks', () => { | ||
scheduleCallback(NormalPriority, () => { | ||
runtime.log('A'); | ||
}); | ||
scheduleCallback(NormalPriority, () => { | ||
runtime.log('B'); | ||
}); | ||
runtime.assertLog(['Set Immediate']); | ||
runtime.fireSetImmediate(); | ||
runtime.assertLog(['setImmediate Callback', 'A', 'B']); | ||
}); | ||
|
||
it('multiple tasks with a yield in between', () => { | ||
scheduleCallback(NormalPriority, () => { | ||
runtime.log('A'); | ||
runtime.advanceTime(4999); | ||
}); | ||
scheduleCallback(NormalPriority, () => { | ||
runtime.log('B'); | ||
}); | ||
runtime.assertLog(['Set Immediate']); | ||
runtime.fireSetImmediate(); | ||
runtime.assertLog([ | ||
'setImmediate Callback', | ||
'A', | ||
// Ran out of time. Post a continuation event. | ||
'Set Immediate', | ||
]); | ||
runtime.fireSetImmediate(); | ||
runtime.assertLog(['setImmediate Callback', 'B']); | ||
}); | ||
|
||
it('cancels tasks', () => { | ||
const task = scheduleCallback(NormalPriority, () => { | ||
runtime.log('Task'); | ||
}); | ||
runtime.assertLog(['Set Immediate']); | ||
cancelCallback(task); | ||
runtime.assertLog([]); | ||
}); | ||
|
||
it('throws when a task errors then continues in a new event', () => { | ||
scheduleCallback(NormalPriority, () => { | ||
runtime.log('Oops!'); | ||
throw Error('Oops!'); | ||
}); | ||
scheduleCallback(NormalPriority, () => { | ||
runtime.log('Yay'); | ||
}); | ||
runtime.assertLog(['Set Immediate']); | ||
|
||
expect(() => runtime.fireSetImmediate()).toThrow('Oops!'); | ||
runtime.assertLog(['setImmediate Callback', 'Oops!', 'Set Immediate']); | ||
|
||
runtime.fireSetImmediate(); | ||
runtime.assertLog(['setImmediate Callback', 'Yay']); | ||
}); | ||
|
||
it('schedule new task after queue has emptied', () => { | ||
scheduleCallback(NormalPriority, () => { | ||
runtime.log('A'); | ||
}); | ||
|
||
runtime.assertLog(['Set Immediate']); | ||
runtime.fireSetImmediate(); | ||
runtime.assertLog(['setImmediate Callback', 'A']); | ||
|
||
scheduleCallback(NormalPriority, () => { | ||
runtime.log('B'); | ||
}); | ||
runtime.assertLog(['Set Immediate']); | ||
runtime.fireSetImmediate(); | ||
runtime.assertLog(['setImmediate Callback', 'B']); | ||
}); | ||
|
||
it('schedule new task after a cancellation', () => { | ||
const handle = scheduleCallback(NormalPriority, () => { | ||
runtime.log('A'); | ||
}); | ||
|
||
runtime.assertLog(['Set Immediate']); | ||
cancelCallback(handle); | ||
|
||
runtime.fireSetImmediate(); | ||
runtime.assertLog(['setImmediate Callback']); | ||
|
||
scheduleCallback(NormalPriority, () => { | ||
runtime.log('B'); | ||
}); | ||
runtime.assertLog(['Set Immediate']); | ||
runtime.fireSetImmediate(); | ||
runtime.assertLog(['setImmediate Callback', 'B']); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think ideally we would create two separate bundles and resolve to the correct one at the packaging level. Maybe we can tackle this when we switch to ES Modules.