Skip to content

[Scheduler] Add tests for isInputPending #22140

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

Merged
merged 1 commit into from
Aug 20, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 217 additions & 1 deletion packages/scheduler/src/__tests__/Scheduler-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ let runtime;
let performance;
let cancelCallback;
let scheduleCallback;
let requestPaint;
let NormalPriority;

// The Scheduler implementation uses browser APIs like `MessageChannel` and
Expand All @@ -40,6 +41,7 @@ describe('SchedulerBrowser', () => {
cancelCallback = Scheduler.unstable_cancelCallback;
scheduleCallback = Scheduler.unstable_scheduleCallback;
NormalPriority = Scheduler.unstable_NormalPriority;
requestPaint = Scheduler.unstable_requestPaint;
});

afterEach(() => {
Expand All @@ -52,6 +54,9 @@ describe('SchedulerBrowser', () => {

function installMockBrowserRuntime() {
let hasPendingMessageEvent = false;
let isFiringMessageEvent = false;
let hasPendingDiscreteEvent = false;
let hasPendingContinuousEvent = false;

let timerIDCounter = 0;
// let timerIDs = new Map();
Expand Down Expand Up @@ -94,6 +99,23 @@ describe('SchedulerBrowser', () => {
this.port2 = port2;
};

const scheduling = {
isInputPending(options) {
if (this !== scheduling) {
throw new Error(
'isInputPending called with incorrect `this` context',
);
}

return (
hasPendingDiscreteEvent ||
(options && options.includeContinuous && hasPendingContinuousEvent)
);
},
};

global.navigator = {scheduling};

function ensureLogIsEmpty() {
if (eventLog.length !== 0) {
throw Error('Log is not empty. Call assertLog before continuing.');
Expand All @@ -102,6 +124,9 @@ describe('SchedulerBrowser', () => {
function advanceTime(ms) {
currentTime += ms;
}
function resetTime() {
currentTime = 0;
}
function fireMessageEvent() {
ensureLogIsEmpty();
if (!hasPendingMessageEvent) {
Expand All @@ -110,7 +135,35 @@ describe('SchedulerBrowser', () => {
hasPendingMessageEvent = false;
const onMessage = port1.onmessage;
log('Message Event');
onMessage();

isFiringMessageEvent = true;
try {
onMessage();
} finally {
isFiringMessageEvent = false;
if (hasPendingDiscreteEvent) {
log('Discrete Event');
hasPendingDiscreteEvent = false;
}
if (hasPendingContinuousEvent) {
log('Continuous Event');
hasPendingContinuousEvent = false;
}
}
}
function scheduleDiscreteEvent() {
if (isFiringMessageEvent) {
hasPendingDiscreteEvent = true;
} else {
log('Discrete Event');
}
}
function scheduleContinuousEvent() {
if (isFiringMessageEvent) {
hasPendingContinuousEvent = true;
} else {
log('Continuous Event');
}
}
function log(val) {
eventLog.push(val);
Expand All @@ -125,10 +178,13 @@ describe('SchedulerBrowser', () => {
}
return {
advanceTime,
resetTime,
fireMessageEvent,
log,
isLogEmpty,
assertLog,
scheduleDiscreteEvent,
scheduleContinuousEvent,
};
}

Expand All @@ -144,6 +200,8 @@ describe('SchedulerBrowser', () => {
it('task with continuation', () => {
scheduleCallback(NormalPriority, () => {
runtime.log('Task');
// Request paint so that we yield at the end of the frame interval
requestPaint();
while (!Scheduler.unstable_shouldYield()) {
runtime.advanceTime(1);
}
Expand Down Expand Up @@ -259,4 +317,162 @@ describe('SchedulerBrowser', () => {
runtime.fireMessageEvent();
runtime.assertLog(['Message Event', 'B']);
});

it('when isInputPending is available, we can wait longer before yielding', () => {
function blockUntilSchedulerAsksToYield() {
while (!Scheduler.unstable_shouldYield()) {
runtime.advanceTime(1);
}
runtime.log(`Yield at ${performance.now()}ms`);
}

// First show what happens when we don't request a paint
scheduleCallback(NormalPriority, () => {
runtime.log('Task with no pending input');
blockUntilSchedulerAsksToYield();
});
runtime.assertLog(['Post Message']);

runtime.fireMessageEvent();
runtime.assertLog([
'Message Event',
'Task with no pending input',
// Even though there's no input, eventually Scheduler will yield
// regardless in case there's a pending main thread task we don't know
// about, like a network event.
gate(flags =>
flags.enableIsInputPending
? 'Yield at 300ms'
: // When isInputPending is disabled, we always yield quickly
'Yield at 5ms',
),
]);

runtime.resetTime();

// Now do the same thing, but while the task is running, simulate an
// input event.
scheduleCallback(NormalPriority, () => {
runtime.log('Task with pending input');
runtime.scheduleDiscreteEvent();
blockUntilSchedulerAsksToYield();
});
runtime.assertLog(['Post Message']);

runtime.fireMessageEvent();
runtime.assertLog([
'Message Event',
'Task with pending input',
// This time we yielded quickly to unblock the discrete event.
'Yield at 5ms',
'Discrete Event',
]);
});

it(
'isInputPending will also check for continuous inputs, but after a ' +
'slightly larger threshold',
() => {
function blockUntilSchedulerAsksToYield() {
while (!Scheduler.unstable_shouldYield()) {
runtime.advanceTime(1);
}
runtime.log(`Yield at ${performance.now()}ms`);
}

// First show what happens when we don't request a paint
scheduleCallback(NormalPriority, () => {
runtime.log('Task with no pending input');
blockUntilSchedulerAsksToYield();
});
runtime.assertLog(['Post Message']);

runtime.fireMessageEvent();
runtime.assertLog([
'Message Event',
'Task with no pending input',
// Even though there's no input, eventually Scheduler will yield
// regardless in case there's a pending main thread task we don't know
// about, like a network event.
gate(flags =>
flags.enableIsInputPending
? 'Yield at 300ms'
: // When isInputPending is disabled, we always yield quickly
'Yield at 5ms',
),
]);

runtime.resetTime();

// Now do the same thing, but while the task is running, simulate a
// continuous input event.
scheduleCallback(NormalPriority, () => {
runtime.log('Task with continuous input');
runtime.scheduleContinuousEvent();
blockUntilSchedulerAsksToYield();
});
runtime.assertLog(['Post Message']);

runtime.fireMessageEvent();
runtime.assertLog([
'Message Event',
'Task with continuous input',
// This time we yielded quickly to unblock the continuous event. But not
// as quickly as for a discrete event.
gate(flags =>
flags.enableIsInputPending
? 'Yield at 50ms'
: // When isInputPending is disabled, we always yield quickly
'Yield at 5ms',
),
'Continuous Event',
]);
},
);

it('requestPaint forces a yield at the end of the next frame interval', () => {
function blockUntilSchedulerAsksToYield() {
while (!Scheduler.unstable_shouldYield()) {
runtime.advanceTime(1);
}
runtime.log(`Yield at ${performance.now()}ms`);
}

// First show what happens when we don't request a paint
scheduleCallback(NormalPriority, () => {
runtime.log('Task with no paint');
blockUntilSchedulerAsksToYield();
});
runtime.assertLog(['Post Message']);

runtime.fireMessageEvent();
runtime.assertLog([
'Message Event',
'Task with no paint',
gate(flags =>
flags.enableIsInputPending
? 'Yield at 300ms'
: // When isInputPending is disabled, we always yield quickly
'Yield at 5ms',
),
]);

runtime.resetTime();

// Now do the same thing, but call requestPaint inside the task
scheduleCallback(NormalPriority, () => {
runtime.log('Task with paint');
requestPaint();
blockUntilSchedulerAsksToYield();
});
runtime.assertLog(['Post Message']);

runtime.fireMessageEvent();
runtime.assertLog([
'Message Event',
'Task with paint',
// This time we yielded quickly (5ms) because we requested a paint.
'Yield at 5ms',
]);
});
});