Skip to content

Commit

Permalink
[Scheduler] Add tests for isInputPending (#22140)
Browse files Browse the repository at this point in the history
This feature was already implemented but we didn't have any tests. So
I wrote some.
  • Loading branch information
acdlite authored Aug 20, 2021
1 parent d54be90 commit 986d0e6
Showing 1 changed file with 217 additions and 1 deletion.
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',
]);
});
});

0 comments on commit 986d0e6

Please sign in to comment.