Add new mock build of Scheduler with flush, yield API (#14964)

* Add new mock build of Scheduler with flush, yield API

Test environments need a way to take control of the Scheduler queue and
incrementally flush work. Our current tests accomplish this either using
dynamic injection, or by using Jest's fake timers feature. Both of these
options are fragile and rely too much on implementation details.

In this new approach, we have a separate build of Scheduler that is
specifically designed for test environments. We mock the default
implementation like we would any other module; in our case, via Jest.
This special build has methods like `flushAll` and `yieldValue` that
control when work is flushed. These methods are based on equivalent
methods we've been using to write incremental React tests. Eventually
we may want to migrate the React tests to interact with the mock
Scheduler directly, instead of going through the host config like we
currently do.

For now, I'm using our custom static injection infrastructure to create
the two builds of Scheduler — a default build for DOM (which falls back
to a naive timer based implementation), and the new mock build. I did it
this way because it allows me to share most of the implementation, which
isn't specific to a host environment — e.g. everything related to the
priority queue. It may be better to duplicate the shared code instead,
especially considering that future environments (like React Native) may
have entirely forked implementations. I'd prefer to wait until the
implementation stabilizes before worrying about that, but I'm open to
changing this now if we decide it's important enough.

* Mock Scheduler in bundle tests, too

* Remove special case by making regex more restrictive
This commit is contained in:
Andrew Clark 2019-02-26 20:51:17 -08:00 committed by GitHub
parent 4186952a6f
commit 00748c53e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1075 additions and 1003 deletions

View File

@ -1,7 +1,3 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/jest-mock-scheduler.production.min.js');
} else {
module.exports = require('./cjs/jest-mock-scheduler.development.js');
}
module.exports = require('scheduler/unstable_mock');

View File

@ -1,61 +0,0 @@
/**
* 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.
*/
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
const maxSigned31BitInt = 1073741823;
export function mockRestore() {
delete global._schedMock;
}
let callback = null;
let currentTime = -1;
function flushCallback(didTimeout, ms) {
if (callback !== null) {
let cb = callback;
callback = null;
try {
currentTime = ms;
cb(didTimeout);
} finally {
currentTime = -1;
}
}
}
function requestHostCallback(cb, ms) {
if (currentTime !== -1) {
// Protect against re-entrancy.
setTimeout(requestHostCallback, 0, cb, ms);
} else {
callback = cb;
setTimeout(flushCallback, ms, true, ms);
setTimeout(flushCallback, maxSigned31BitInt, false, maxSigned31BitInt);
}
}
function cancelHostCallback() {
callback = null;
}
function shouldYieldToHost() {
return false;
}
function getCurrentTime() {
return currentTime === -1 ? 0 : currentTime;
}
global._schedMock = [
requestHostCallback,
cancelHostCallback,
shouldYieldToHost,
getCurrentTime,
];

View File

@ -13,6 +13,7 @@ const React = require('react');
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
let ReactDOM;
let Scheduler;
const ConcurrentMode = React.unstable_ConcurrentMode;
@ -25,33 +26,10 @@ describe('ReactDOMFiberAsync', () => {
let container;
beforeEach(() => {
// TODO pull this into helper method, reduce repetition.
// mock the browser APIs which are used in schedule:
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
// - calling 'window.postMessage' should actually fire postmessage handlers
global.requestAnimationFrame = function(cb) {
return setTimeout(() => {
cb(Date.now());
});
};
const originalAddEventListener = global.addEventListener;
let postMessageCallback;
global.addEventListener = function(eventName, callback, useCapture) {
if (eventName === 'message') {
postMessageCallback = callback;
} else {
originalAddEventListener(eventName, callback, useCapture);
}
};
global.postMessage = function(messageKey, targetOrigin) {
const postMessageEvent = {source: window, data: messageKey};
if (postMessageCallback) {
postMessageCallback(postMessageEvent);
}
};
jest.resetModules();
container = document.createElement('div');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
document.body.appendChild(container);
});
@ -124,6 +102,7 @@ describe('ReactDOMFiberAsync', () => {
// Should flush both updates now.
jest.runAllTimers();
Scheduler.flushAll();
expect(asyncValueRef.current.textContent).toBe('hello');
expect(syncValueRef.current.textContent).toBe('hello');
});
@ -133,6 +112,7 @@ describe('ReactDOMFiberAsync', () => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});
it('renders synchronously', () => {
@ -160,18 +140,19 @@ describe('ReactDOMFiberAsync', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});
it('createRoot makes the entire tree async', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<div>Hi</div>);
expect(container.textContent).toEqual('');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
root.render(<div>Bye</div>);
expect(container.textContent).toEqual('Hi');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Bye');
});
@ -188,12 +169,12 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<Component />);
expect(container.textContent).toEqual('');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('0');
instance.setState({step: 1});
expect(container.textContent).toEqual('0');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});
@ -213,11 +194,11 @@ describe('ReactDOMFiberAsync', () => {
</ConcurrentMode>,
container,
);
jest.runAllTimers();
Scheduler.flushAll();
instance.setState({step: 1});
expect(container.textContent).toEqual('0');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});
@ -239,11 +220,11 @@ describe('ReactDOMFiberAsync', () => {
</div>,
container,
);
jest.runAllTimers();
Scheduler.flushAll();
instance.setState({step: 1});
expect(container.textContent).toEqual('0');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});
@ -369,7 +350,7 @@ describe('ReactDOMFiberAsync', () => {
</ConcurrentMode>,
container,
);
jest.runAllTimers();
Scheduler.flushAll();
// Updates are async by default
instance.push('A');
@ -392,7 +373,7 @@ describe('ReactDOMFiberAsync', () => {
expect(ops).toEqual(['BC']);
// Flush the async updates
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('ABCD');
expect(ops).toEqual(['BC', 'ABCD']);
});
@ -419,7 +400,7 @@ describe('ReactDOMFiberAsync', () => {
// Test that a normal update is async
inst.increment();
expect(container.textContent).toEqual('0');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('1');
let ops = [];
@ -525,7 +506,7 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<Form />);
// Flush
jest.runAllTimers();
Scheduler.flushAll();
let disableButton = disableButtonRef.current;
expect(disableButton.tagName).toBe('BUTTON');
@ -592,7 +573,7 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<Form />);
// Flush
jest.runAllTimers();
Scheduler.flushAll();
let disableButton = disableButtonRef.current;
expect(disableButton.tagName).toBe('BUTTON');
@ -652,7 +633,7 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<Form />);
// Flush
jest.runAllTimers();
Scheduler.flushAll();
let enableButton = enableButtonRef.current;
expect(enableButton.tagName).toBe('BUTTON');

View File

@ -11,6 +11,7 @@
let React;
let ReactDOM;
let Scheduler;
describe('ReactDOMHooks', () => {
let container;
@ -20,6 +21,7 @@ describe('ReactDOMHooks', () => {
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
container = document.createElement('div');
document.body.appendChild(container);
@ -55,7 +57,7 @@ describe('ReactDOMHooks', () => {
expect(container.textContent).toBe('1');
expect(container2.textContent).toBe('');
expect(container3.textContent).toBe('');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toBe('1');
expect(container2.textContent).toBe('2');
expect(container3.textContent).toBe('3');
@ -64,7 +66,7 @@ describe('ReactDOMHooks', () => {
expect(container.textContent).toBe('2');
expect(container2.textContent).toBe('2'); // Not flushed yet
expect(container3.textContent).toBe('3'); // Not flushed yet
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toBe('2');
expect(container2.textContent).toBe('4');
expect(container3.textContent).toBe('6');
@ -166,14 +168,14 @@ describe('ReactDOMHooks', () => {
</React.unstable_ConcurrentMode>,
);
jest.runAllTimers();
Scheduler.flushAll();
inputRef.current.value = 'abc';
inputRef.current.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
jest.runAllTimers();
Scheduler.flushAll();
expect(labelRef.current.innerHTML).toBe('abc');
});

View File

@ -12,74 +12,36 @@
let React = require('react');
let ReactDOM = require('react-dom');
let ReactDOMServer = require('react-dom/server');
let Scheduler = require('scheduler');
let ConcurrentMode = React.unstable_ConcurrentMode;
describe('ReactDOMRoot', () => {
let container;
let advanceCurrentTime;
beforeEach(() => {
container = document.createElement('div');
// TODO pull this into helper method, reduce repetition.
// mock the browser APIs which are used in schedule:
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
// - calling 'window.postMessage' should actually fire postmessage handlers
// - must allow artificially changing time returned by Date.now
// Performance.now is not supported in the test environment
const originalDateNow = Date.now;
let advancedTime = null;
global.Date.now = function() {
if (advancedTime) {
return originalDateNow() + advancedTime;
}
return originalDateNow();
};
advanceCurrentTime = function(amount) {
advancedTime = amount;
};
global.requestAnimationFrame = function(cb) {
return setTimeout(() => {
cb(Date.now());
});
};
const originalAddEventListener = global.addEventListener;
let postMessageCallback;
global.addEventListener = function(eventName, callback, useCapture) {
if (eventName === 'message') {
postMessageCallback = callback;
} else {
originalAddEventListener(eventName, callback, useCapture);
}
};
global.postMessage = function(messageKey, targetOrigin) {
const postMessageEvent = {source: window, data: messageKey};
if (postMessageCallback) {
postMessageCallback(postMessageEvent);
}
};
jest.resetModules();
container = document.createElement('div');
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
ConcurrentMode = React.unstable_ConcurrentMode;
});
it('renders children', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<div>Hi</div>);
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
});
it('unmounts children', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<div>Hi</div>);
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
root.unmount();
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('');
});
@ -91,7 +53,7 @@ describe('ReactDOMRoot', () => {
ops.push('inside callback: ' + container.textContent);
});
ops.push('before committing: ' + container.textContent);
jest.runAllTimers();
Scheduler.flushAll();
ops.push('after committing: ' + container.textContent);
expect(ops).toEqual([
'before committing: ',
@ -104,7 +66,7 @@ describe('ReactDOMRoot', () => {
it('resolves `work.then` callback synchronously if the work already committed', () => {
const root = ReactDOM.unstable_createRoot(container);
const work = root.render(<ConcurrentMode>Hi</ConcurrentMode>);
jest.runAllTimers();
Scheduler.flushAll();
let ops = [];
work.then(() => {
ops.push('inside callback');
@ -132,7 +94,7 @@ describe('ReactDOMRoot', () => {
<span />
</div>,
);
jest.runAllTimers();
Scheduler.flushAll();
// Accepts `hydrate` option
const container2 = document.createElement('div');
@ -143,7 +105,7 @@ describe('ReactDOMRoot', () => {
<span />
</div>,
);
expect(jest.runAllTimers).toWarnDev('Extra attributes', {
expect(() => Scheduler.flushAll()).toWarnDev('Extra attributes', {
withoutStack: true,
});
});
@ -157,7 +119,7 @@ describe('ReactDOMRoot', () => {
<span>d</span>
</div>,
);
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('abcd');
root.render(
<div>
@ -165,7 +127,7 @@ describe('ReactDOMRoot', () => {
<span>c</span>
</div>,
);
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('abdc');
});
@ -201,7 +163,7 @@ describe('ReactDOMRoot', () => {
</ConcurrentMode>,
);
jest.runAllTimers();
Scheduler.flushAll();
// Hasn't updated yet
expect(container.textContent).toEqual('');
@ -230,7 +192,7 @@ describe('ReactDOMRoot', () => {
const batch = root.createBatch();
batch.render(<Foo>Hi</Foo>);
// Flush all async work.
jest.runAllTimers();
Scheduler.flushAll();
// Root should complete without committing.
expect(ops).toEqual(['Foo']);
expect(container.textContent).toEqual('');
@ -248,7 +210,7 @@ describe('ReactDOMRoot', () => {
const batch = root.createBatch();
batch.render(<ConcurrentMode>Foo</ConcurrentMode>);
jest.runAllTimers();
Scheduler.flushAll();
// Hasn't updated yet
expect(container.textContent).toEqual('');
@ -288,7 +250,7 @@ describe('ReactDOMRoot', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<ConcurrentMode>1</ConcurrentMode>);
advanceCurrentTime(2000);
Scheduler.advanceTime(2000);
// This batch has a later expiration time than the earlier update.
const batch = root.createBatch();
@ -296,7 +258,7 @@ describe('ReactDOMRoot', () => {
batch.commit();
expect(container.textContent).toEqual('');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});
@ -323,7 +285,7 @@ describe('ReactDOMRoot', () => {
batch1.render(1);
// This batch has a later expiration time
advanceCurrentTime(2000);
Scheduler.advanceTime(2000);
const batch2 = root.createBatch();
batch2.render(2);
@ -342,7 +304,7 @@ describe('ReactDOMRoot', () => {
batch1.render(1);
// This batch has a later expiration time
advanceCurrentTime(2000);
Scheduler.advanceTime(2000);
const batch2 = root.createBatch();
batch2.render(2);
@ -352,7 +314,7 @@ describe('ReactDOMRoot', () => {
expect(container.textContent).toEqual('2');
batch1.commit();
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});
@ -378,7 +340,7 @@ describe('ReactDOMRoot', () => {
it('warns when rendering with legacy API into createRoot() container', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<div>Hi</div>);
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
expect(() => {
ReactDOM.render(<div>Bye</div>, container);
@ -393,7 +355,7 @@ describe('ReactDOMRoot', () => {
],
{withoutStack: true},
);
jest.runAllTimers();
Scheduler.flushAll();
// This works now but we could disallow it:
expect(container.textContent).toEqual('Bye');
});
@ -401,7 +363,7 @@ describe('ReactDOMRoot', () => {
it('warns when hydrating with legacy API into createRoot() container', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<div>Hi</div>);
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
expect(() => {
ReactDOM.hydrate(<div>Hi</div>, container);
@ -421,7 +383,7 @@ describe('ReactDOMRoot', () => {
it('warns when unmounting with legacy API (no previous content)', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<div>Hi</div>);
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
let unmounted = false;
expect(() => {
@ -437,10 +399,10 @@ describe('ReactDOMRoot', () => {
{withoutStack: true},
);
expect(unmounted).toBe(false);
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
root.unmount();
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('');
});
@ -450,17 +412,17 @@ describe('ReactDOMRoot', () => {
// The rest is the same as test above.
const root = ReactDOM.unstable_createRoot(container);
root.render(<div>Hi</div>);
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
let unmounted = false;
expect(() => {
unmounted = ReactDOM.unmountComponentAtNode(container);
}).toWarnDev('Did you mean to call root.unmount()?', {withoutStack: true});
expect(unmounted).toBe(false);
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
root.unmount();
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('');
});

View File

@ -12,6 +12,7 @@
let React;
let ReactDOM;
let ReactDOMServer;
let Scheduler;
let ReactFeatureFlags;
let Suspense;
let act;
@ -27,6 +28,7 @@ describe('ReactDOMServerPartialHydration', () => {
ReactDOM = require('react-dom');
act = require('react-dom/test-utils').act;
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
Suspense = React.Suspense;
});
@ -72,6 +74,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);
Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@ -80,6 +83,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = false;
resolve();
await promise;
Scheduler.flushAll();
jest.runAllTimers();
// We should now have hydrated with a ref on the existing span.
@ -238,6 +242,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App text="Hello" className="hello" />);
Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@ -253,6 +258,7 @@ describe('ReactDOMServerPartialHydration', () => {
// Flushing both of these in the same batch won't be able to hydrate so we'll
// probably throw away the existing subtree.
Scheduler.flushAll();
jest.runAllTimers();
// Pick up the new span. In an ideal implementation this might be the same span
@ -305,15 +311,17 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App text="Hello" className="hello" />);
Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
// Render an update, but leave it still suspended.
root.render(<App text="Hi" className="hi" />);
Scheduler.flushAll();
jest.runAllTimers();
// Flushing now should delete the existing content and show the fallback.
jest.runAllTimers();
expect(container.getElementsByTagName('span').length).toBe(0);
expect(ref.current).toBe(null);
@ -324,6 +332,7 @@ describe('ReactDOMServerPartialHydration', () => {
resolve();
await promise;
Scheduler.flushAll();
jest.runAllTimers();
let span = container.getElementsByTagName('span')[0];
@ -375,6 +384,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App text="Hello" className="hello" />);
Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@ -383,6 +393,7 @@ describe('ReactDOMServerPartialHydration', () => {
root.render(<App text="Hi" className="hi" />);
// Flushing now should delete the existing content and show the fallback.
Scheduler.flushAll();
jest.runAllTimers();
expect(container.getElementsByTagName('span').length).toBe(0);
@ -394,6 +405,7 @@ describe('ReactDOMServerPartialHydration', () => {
resolve();
await promise;
Scheduler.flushAll();
jest.runAllTimers();
let span = container.getElementsByTagName('span')[0];
@ -444,6 +456,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App text="Hello" className="hello" />);
Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@ -452,6 +465,7 @@ describe('ReactDOMServerPartialHydration', () => {
root.render(<App text="Hi" className="hi" />);
// Flushing now should delete the existing content and show the fallback.
Scheduler.flushAll();
jest.runAllTimers();
expect(container.getElementsByTagName('span').length).toBe(0);
@ -463,6 +477,7 @@ describe('ReactDOMServerPartialHydration', () => {
resolve();
await promise;
Scheduler.flushAll();
jest.runAllTimers();
let span = container.getElementsByTagName('span')[0];
@ -522,6 +537,7 @@ describe('ReactDOMServerPartialHydration', () => {
<App />
</Context.Provider>,
);
Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@ -541,6 +557,7 @@ describe('ReactDOMServerPartialHydration', () => {
// Flushing both of these in the same batch won't be able to hydrate so we'll
// probably throw away the existing subtree.
Scheduler.flushAll();
jest.runAllTimers();
// Pick up the new span. In an ideal implementation this might be the same span
@ -603,6 +620,7 @@ describe('ReactDOMServerPartialHydration', () => {
<App />
</Context.Provider>,
);
Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@ -615,6 +633,7 @@ describe('ReactDOMServerPartialHydration', () => {
);
// Flushing now should delete the existing content and show the fallback.
Scheduler.flushAll();
jest.runAllTimers();
expect(container.getElementsByTagName('span').length).toBe(0);
@ -626,6 +645,7 @@ describe('ReactDOMServerPartialHydration', () => {
resolve();
await promise;
Scheduler.flushAll();
jest.runAllTimers();
let span = container.getElementsByTagName('span')[0];
@ -674,6 +694,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = false;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);
Scheduler.flushAll();
jest.runAllTimers();
expect(container.textContent).toBe('Hello');
@ -746,6 +767,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = false;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);
Scheduler.flushAll();
jest.runAllTimers();
// We're still loading because we're waiting for the server to stream more content.
@ -761,6 +783,7 @@ describe('ReactDOMServerPartialHydration', () => {
// But it is not yet hydrated.
expect(ref.current).toBe(null);
Scheduler.flushAll();
jest.runAllTimers();
// Now it's hydrated.
@ -837,6 +860,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = false;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);
Scheduler.flushAll();
jest.runAllTimers();
// We're still loading because we're waiting for the server to stream more content.
@ -850,6 +874,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('Loading...');
expect(ref.current).toBe(null);
Scheduler.flushAll();
jest.runAllTimers();
// Hydrating should've generated an error and replaced the suspense boundary.

View File

@ -13,6 +13,7 @@ let PropTypes;
let React;
let ReactDOM;
let ReactFeatureFlags;
let Scheduler;
describe('ReactErrorBoundaries', () => {
let log;
@ -44,6 +45,7 @@ describe('ReactErrorBoundaries', () => {
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
ReactDOM = require('react-dom');
React = require('react');
Scheduler = require('scheduler');
log = [];
@ -1839,9 +1841,8 @@ describe('ReactErrorBoundaries', () => {
expect(container.firstChild.textContent).toBe('Initial value');
log.length = 0;
jest.runAllTimers();
// Flush passive effects and handle the error
Scheduler.flushAll();
expect(log).toEqual([
'BrokenUseEffect useEffect [!]',
// Handle the error

View File

@ -12,6 +12,7 @@
const React = require('react');
let ReactDOM = require('react-dom');
let ReactFeatureFlags;
let Scheduler;
const setUntrackedChecked = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
@ -484,6 +485,7 @@ describe('ChangeEventPlugin', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});
it('text input', () => {
const root = ReactDOM.unstable_createRoot(container);
@ -515,7 +517,7 @@ describe('ChangeEventPlugin', () => {
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
jest.runAllTimers();
Scheduler.flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
@ -565,7 +567,7 @@ describe('ChangeEventPlugin', () => {
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
jest.runAllTimers();
Scheduler.flushAll();
expect(ops).toEqual(['render: false']);
expect(input.checked).toBe(false);
@ -581,7 +583,7 @@ describe('ChangeEventPlugin', () => {
// Now let's make sure we're using the controlled value.
root.render(<ControlledInput reverse={true} />);
jest.runAllTimers();
Scheduler.flushAll();
ops = [];
@ -624,7 +626,7 @@ describe('ChangeEventPlugin', () => {
expect(ops).toEqual([]);
expect(textarea).toBe(undefined);
// Flush callbacks.
jest.runAllTimers();
Scheduler.flushAll();
expect(ops).toEqual(['render: initial']);
expect(textarea.value).toBe('initial');
@ -675,7 +677,7 @@ describe('ChangeEventPlugin', () => {
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
jest.runAllTimers();
Scheduler.flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
@ -726,7 +728,7 @@ describe('ChangeEventPlugin', () => {
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
jest.runAllTimers();
Scheduler.flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
@ -741,7 +743,7 @@ describe('ChangeEventPlugin', () => {
expect(input.value).toBe('initial');
// Flush callbacks.
jest.runAllTimers();
Scheduler.flushAll();
// Now the click update has flushed.
expect(ops).toEqual(['render: ']);
expect(input.value).toBe('');

View File

@ -13,6 +13,7 @@ describe('SimpleEventPlugin', function() {
let React;
let ReactDOM;
let ReactFeatureFlags;
let Scheduler;
let onClick;
let container;
@ -35,33 +36,10 @@ describe('SimpleEventPlugin', function() {
}
beforeEach(function() {
// TODO pull this into helper method, reduce repetition.
// mock the browser APIs which are used in schedule:
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
// - calling 'window.postMessage' should actually fire postmessage handlers
global.requestAnimationFrame = function(cb) {
return setTimeout(() => {
cb(Date.now());
});
};
const originalAddEventListener = global.addEventListener;
let postMessageCallback;
global.addEventListener = function(eventName, callback, useCapture) {
if (eventName === 'message') {
postMessageCallback = callback;
} else {
originalAddEventListener(eventName, callback, useCapture);
}
};
global.postMessage = function(messageKey, targetOrigin) {
const postMessageEvent = {source: window, data: messageKey};
if (postMessageCallback) {
postMessageCallback(postMessageEvent);
}
};
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
onClick = jest.fn();
});
@ -258,6 +236,7 @@ describe('SimpleEventPlugin', function() {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});
it('flushes pending interactive work before extracting event handler', () => {
@ -296,7 +275,7 @@ describe('SimpleEventPlugin', function() {
expect(ops).toEqual([]);
expect(button).toBe(undefined);
// Flush async work
jest.runAllTimers();
Scheduler.flushAll();
expect(ops).toEqual(['render button: enabled']);
ops = [];
@ -336,7 +315,7 @@ describe('SimpleEventPlugin', function() {
click();
click();
click();
jest.runAllTimers();
Scheduler.flushAll();
expect(ops).toEqual([]);
});
@ -367,7 +346,7 @@ describe('SimpleEventPlugin', function() {
// Should not have flushed yet because it's async
expect(button).toBe(undefined);
// Flush async work
jest.runAllTimers();
Scheduler.flushAll();
expect(button.textContent).toEqual('Count: 0');
function click() {
@ -390,7 +369,7 @@ describe('SimpleEventPlugin', function() {
click();
// Flush the remaining work
jest.runAllTimers();
Scheduler.flushAll();
// The counter should equal the total number of clicks
expect(button.textContent).toEqual('Count: 7');
});
@ -459,7 +438,7 @@ describe('SimpleEventPlugin', function() {
click();
// Flush the remaining work
jest.runAllTimers();
Scheduler.flushAll();
// Both counters should equal the total number of clicks
expect(button.textContent).toEqual('High-pri count: 7, Low-pri count: 7');
});

View File

@ -13,34 +13,9 @@ let React;
let ReactFeatureFlags;
let ReactDOM;
let SchedulerTracing;
let Scheduler;
let ReactCache;
function initEnvForAsyncTesting() {
// Boilerplate copied from ReactDOMRoot-test
// TODO pull this into helper method, reduce repetition.
// TODO remove `requestAnimationFrame` when upgrading to Jest 24 with Lolex
global.requestAnimationFrame = function(cb) {
return setTimeout(() => {
cb(Date.now());
});
};
const originalAddEventListener = global.addEventListener;
let postMessageCallback;
global.addEventListener = function(eventName, callback, useCapture) {
if (eventName === 'message') {
postMessageCallback = callback;
} else {
originalAddEventListener(eventName, callback, useCapture);
}
};
global.postMessage = function(messageKey, targetOrigin) {
const postMessageEvent = {source: window, data: messageKey};
if (postMessageCallback) {
postMessageCallback(postMessageEvent);
}
};
}
function loadModules() {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffects = false;
@ -51,6 +26,7 @@ function loadModules() {
React = require('react');
SchedulerTracing = require('scheduler/tracing');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
ReactCache = require('react-cache');
}
@ -61,7 +37,6 @@ describe('ProfilerDOM', () => {
let onInteractionTraced;
beforeEach(() => {
initEnvForAsyncTesting();
loadModules();
onInteractionScheduledWorkCompleted = jest.fn();
@ -114,7 +89,7 @@ describe('ProfilerDOM', () => {
batch = root.createBatch();
batch.render(
<React.Suspense maxDuration={100} fallback={<Text text="Loading..." />}>
<AsyncText text="Text" ms={200} />
<AsyncText text="Text" ms={2000} />
</React.Suspense>,
);
batch.then(
@ -125,9 +100,12 @@ describe('ProfilerDOM', () => {
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
jest.runAllTimers();
resourcePromise.then(
SchedulerTracing.unstable_wrap(() => {
jest.runAllTimers();
Scheduler.flushAll();
expect(element.textContent).toBe('Text');
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@ -154,6 +132,8 @@ describe('ProfilerDOM', () => {
);
}),
);
Scheduler.flushAll();
});
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@ -161,7 +141,7 @@ describe('ProfilerDOM', () => {
interaction,
);
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
jest.runAllTimers();
Scheduler.flushAll();
jest.advanceTimersByTime(500);
});
});

View File

@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/scheduler-unstable_mock.production.min.js');
} else {
module.exports = require('./cjs/scheduler-unstable_mock.development.js');
}

View File

@ -27,6 +27,7 @@
"index.js",
"tracing.js",
"tracing-profiling.js",
"unstable_mock.js",
"cjs/",
"umd/"
],

View File

@ -9,6 +9,12 @@
/* eslint-disable no-var */
import {enableSchedulerDebugging} from './SchedulerFeatureFlags';
import {
requestHostCallback,
cancelHostCallback,
shouldYieldToHost,
getCurrentTime,
} from './SchedulerHostConfig';
// TODO: Use symbols?
var ImmediatePriority = 1;
@ -47,9 +53,6 @@ var isExecutingCallback = false;
var isHostCallbackScheduled = false;
var hasNativePerformanceNow =
typeof performance === 'object' && typeof performance.now === 'function';
function ensureHostCallbackIsScheduled() {
if (isExecutingCallback) {
// Don't schedule work yet; wait until the next time we yield.
@ -443,275 +446,6 @@ function unstable_shouldYield() {
);
}
// The remaining code is essentially a polyfill for requestIdleCallback. It
// works by scheduling a requestAnimationFrame, storing the time for the start
// of the frame, then scheduling a postMessage which gets scheduled after paint.
// Within the postMessage handler do as much work as possible until time + frame
// rate. By separating the idle call into a separate event tick we ensure that
// layout, paint and other browser work is counted against the available time.
// The frame rate is dynamically adjusted.
// We capture a local reference to any global, in case it gets polyfilled after
// this module is initially evaluated. We want to be using a
// consistent implementation.
var localDate = Date;
// This initialization code may run even on server environments if a component
// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
// have setTimeout or clearTimeout. However, we always expect them to be defined
// on the client. https://github.com/facebook/react/pull/13088
var localSetTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;
var localClearTimeout =
typeof clearTimeout === 'function' ? clearTimeout : undefined;
// We don't expect either of these to necessarily be defined, but we will error
// later if they are missing on the client.
var localRequestAnimationFrame =
typeof requestAnimationFrame === 'function'
? requestAnimationFrame
: undefined;
var localCancelAnimationFrame =
typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
var getCurrentTime;
// requestAnimationFrame does not run when the tab is in the background. If
// we're backgrounded we prefer for that work to happen so that the page
// continues to load in the background. So we also schedule a 'setTimeout' as
// a fallback.
// TODO: Need a better heuristic for backgrounded work.
var ANIMATION_FRAME_TIMEOUT = 100;
var rAFID;
var rAFTimeoutID;
var requestAnimationFrameWithTimeout = function(callback) {
// schedule rAF and also a setTimeout
rAFID = localRequestAnimationFrame(function(timestamp) {
// cancel the setTimeout
localClearTimeout(rAFTimeoutID);
callback(timestamp);
});
rAFTimeoutID = localSetTimeout(function() {
// cancel the requestAnimationFrame
localCancelAnimationFrame(rAFID);
callback(getCurrentTime());
}, ANIMATION_FRAME_TIMEOUT);
};
if (hasNativePerformanceNow) {
var Performance = performance;
getCurrentTime = function() {
return Performance.now();
};
} else {
getCurrentTime = function() {
return localDate.now();
};
}
var requestHostCallback;
var cancelHostCallback;
var shouldYieldToHost;
var globalValue = null;
if (typeof window !== 'undefined') {
globalValue = window;
} else if (typeof global !== 'undefined') {
globalValue = global;
}
if (globalValue && globalValue._schedMock) {
// Dynamic injection, only for testing purposes.
var globalImpl = globalValue._schedMock;
requestHostCallback = globalImpl[0];
cancelHostCallback = globalImpl[1];
shouldYieldToHost = globalImpl[2];
getCurrentTime = globalImpl[3];
} else if (
// If Scheduler runs in a non-DOM environment, it falls back to a naive
// implementation using setTimeout.
typeof window === 'undefined' ||
// Check if MessageChannel is supported, too.
typeof MessageChannel !== 'function'
) {
// If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
// fallback to a naive implementation.
var _callback = null;
var _flushCallback = function(didTimeout) {
if (_callback !== null) {
try {
_callback(didTimeout);
} finally {
_callback = null;
}
}
};
requestHostCallback = function(cb, ms) {
if (_callback !== null) {
// Protect against re-entrancy.
setTimeout(requestHostCallback, 0, cb);
} else {
_callback = cb;
setTimeout(_flushCallback, 0, false);
}
};
cancelHostCallback = function() {
_callback = null;
};
shouldYieldToHost = function() {
return false;
};
} else {
if (typeof console !== 'undefined') {
// TODO: Remove fb.me link
if (typeof localRequestAnimationFrame !== 'function') {
console.error(
"This browser doesn't support requestAnimationFrame. " +
'Make sure that you load a ' +
'polyfill in older browsers. https://fb.me/react-polyfills',
);
}
if (typeof localCancelAnimationFrame !== 'function') {
console.error(
"This browser doesn't support cancelAnimationFrame. " +
'Make sure that you load a ' +
'polyfill in older browsers. https://fb.me/react-polyfills',
);
}
}
var scheduledHostCallback = null;
var isMessageEventScheduled = false;
var timeoutTime = -1;
var isAnimationFrameScheduled = false;
var isFlushingHostCallback = false;
var frameDeadline = 0;
// We start out assuming that we run at 30fps but then the heuristic tracking
// will adjust this value to a faster fps if we get more frequent animation
// frames.
var previousFrameTime = 33;
var activeFrameTime = 33;
shouldYieldToHost = function() {
return frameDeadline <= getCurrentTime();
};
// We use the postMessage trick to defer idle work until after the repaint.
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = function(event) {
isMessageEventScheduled = false;
var prevScheduledCallback = scheduledHostCallback;
var prevTimeoutTime = timeoutTime;
scheduledHostCallback = null;
timeoutTime = -1;
var currentTime = getCurrentTime();
var didTimeout = false;
if (frameDeadline - currentTime <= 0) {
// There's no time left in this idle period. Check if the callback has
// a timeout and whether it's been exceeded.
if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
// Exceeded the timeout. Invoke the callback even though there's no
// time left.
didTimeout = true;
} else {
// No timeout.
if (!isAnimationFrameScheduled) {
// Schedule another animation callback so we retry later.
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
// Exit without invoking the callback.
scheduledHostCallback = prevScheduledCallback;
timeoutTime = prevTimeoutTime;
return;
}
}
if (prevScheduledCallback !== null) {
isFlushingHostCallback = true;
try {
prevScheduledCallback(didTimeout);
} finally {
isFlushingHostCallback = false;
}
}
};
var animationTick = function(rafTime) {
if (scheduledHostCallback !== null) {
// Eagerly schedule the next animation callback at the beginning of the
// frame. If the scheduler queue is not empty at the end of the frame, it
// will continue flushing inside that callback. If the queue *is* empty,
// then it will exit immediately. Posting the callback at the start of the
// frame ensures it's fired within the earliest possible frame. If we
// waited until the end of the frame to post the callback, we risk the
// browser skipping a frame and not firing the callback until the frame
// after that.
requestAnimationFrameWithTimeout(animationTick);
} else {
// No pending work. Exit.
isAnimationFrameScheduled = false;
return;
}
var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
if (
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime
) {
if (nextFrameTime < 8) {
// Defensive coding. We don't support higher frame rates than 120hz.
// If the calculated frame time gets lower than 8, it is probably a bug.
nextFrameTime = 8;
}
// If one frame goes long, then the next one can be short to catch up.
// If two frames are short in a row, then that's an indication that we
// actually have a higher frame rate than what we're currently optimizing.
// We adjust our heuristic dynamically accordingly. For example, if we're
// running on 120hz display or 90hz VR display.
// Take the max of the two in case one of them was an anomaly due to
// missed frame deadlines.
activeFrameTime =
nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
} else {
previousFrameTime = nextFrameTime;
}
frameDeadline = rafTime + activeFrameTime;
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
port.postMessage(undefined);
}
};
requestHostCallback = function(callback, absoluteTimeout) {
scheduledHostCallback = callback;
timeoutTime = absoluteTimeout;
if (isFlushingHostCallback || absoluteTimeout < 0) {
// Don't wait for the next frame. Continue working ASAP, in a new event.
port.postMessage(undefined);
} else if (!isAnimationFrameScheduled) {
// If rAF didn't already schedule one, we need to schedule a frame.
// TODO: If this rAF doesn't materialize because the browser throttles, we
// might want to still have setTimeout trigger rIC as a backup to ensure
// that we keep performing work.
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
};
cancelHostCallback = function() {
scheduledHostCallback = null;
isMessageEventScheduled = false;
timeoutTime = -1;
};
}
export {
ImmediatePriority as unstable_ImmediatePriority,
UserBlockingPriority as unstable_UserBlockingPriority,

View File

@ -3,6 +3,8 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from './src/JestMockScheduler';
throw new Error('This module must be shimmed by a specific build.');

View File

@ -9,6 +9,7 @@
'use strict';
let Scheduler;
let runWithPriority;
let ImmediatePriority;
let UserBlockingPriority;
@ -18,211 +19,44 @@ let cancelCallback;
let wrapCallback;
let getCurrentPriorityLevel;
let shouldYield;
let flushWork;
let advanceTime;
let doWork;
let yieldedValues;
let yieldValue;
let clearYieldedValues;
describe('Scheduler', () => {
beforeEach(() => {
jest.useFakeTimers();
jest.resetModules();
jest.mock('scheduler', () => require('scheduler/unstable_mock'));
const JestMockScheduler = require('jest-mock-scheduler');
JestMockScheduler.mockRestore();
Scheduler = require('scheduler');
let _flushWork = null;
let isFlushing = false;
let timeoutID = -1;
let endOfFrame = -1;
let hasMicrotask = false;
let currentTime = 0;
flushWork = frameSize => {
if (isFlushing) {
throw new Error('Already flushing work.');
}
if (frameSize === null || frameSize === undefined) {
frameSize = Infinity;
}
if (_flushWork === null) {
throw new Error('No work is scheduled.');
}
timeoutID = -1;
endOfFrame = currentTime + frameSize;
try {
isFlushing = true;
_flushWork(false);
} finally {
isFlushing = false;
endOfFrame = -1;
if (hasMicrotask) {
onTimeout();
}
}
const yields = yieldedValues;
yieldedValues = [];
return yields;
};
advanceTime = ms => {
currentTime += ms;
jest.advanceTimersByTime(ms);
};
doWork = (label, timeCost) => {
if (typeof timeCost !== 'number') {
throw new Error('Second arg must be a number.');
}
advanceTime(timeCost);
yieldValue(label);
};
yieldedValues = [];
yieldValue = value => {
yieldedValues.push(value);
};
clearYieldedValues = () => {
const yields = yieldedValues;
yieldedValues = [];
return yields;
};
function onTimeout() {
if (_flushWork === null) {
return;
}
if (isFlushing) {
hasMicrotask = true;
} else {
try {
isFlushing = true;
_flushWork(true);
} finally {
hasMicrotask = false;
isFlushing = false;
}
}
}
function requestHostCallback(fw, absoluteTimeout) {
if (_flushWork !== null) {
throw new Error('Work is already scheduled.');
}
_flushWork = fw;
timeoutID = setTimeout(onTimeout, absoluteTimeout - currentTime);
}
function cancelHostCallback() {
if (_flushWork === null) {
throw new Error('No work is scheduled.');
}
_flushWork = null;
clearTimeout(timeoutID);
}
function shouldYieldToHost() {
return endOfFrame <= currentTime;
}
function getCurrentTime() {
return currentTime;
}
// Override host implementation
delete global.performance;
global.Date.now = () => {
return currentTime;
};
window._schedMock = [
requestHostCallback,
cancelHostCallback,
shouldYieldToHost,
getCurrentTime,
];
const Schedule = require('scheduler');
runWithPriority = Schedule.unstable_runWithPriority;
ImmediatePriority = Schedule.unstable_ImmediatePriority;
UserBlockingPriority = Schedule.unstable_UserBlockingPriority;
NormalPriority = Schedule.unstable_NormalPriority;
scheduleCallback = Schedule.unstable_scheduleCallback;
cancelCallback = Schedule.unstable_cancelCallback;
wrapCallback = Schedule.unstable_wrapCallback;
getCurrentPriorityLevel = Schedule.unstable_getCurrentPriorityLevel;
shouldYield = Schedule.unstable_shouldYield;
runWithPriority = Scheduler.unstable_runWithPriority;
ImmediatePriority = Scheduler.unstable_ImmediatePriority;
UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
NormalPriority = Scheduler.unstable_NormalPriority;
scheduleCallback = Scheduler.unstable_scheduleCallback;
cancelCallback = Scheduler.unstable_cancelCallback;
wrapCallback = Scheduler.unstable_wrapCallback;
getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
shouldYield = Scheduler.unstable_shouldYield;
});
it('flushes work incrementally', () => {
scheduleCallback(() => doWork('A', 100));
scheduleCallback(() => doWork('B', 200));
scheduleCallback(() => doWork('C', 300));
scheduleCallback(() => doWork('D', 400));
scheduleCallback(() => Scheduler.yieldValue('A'));
scheduleCallback(() => Scheduler.yieldValue('B'));
scheduleCallback(() => Scheduler.yieldValue('C'));
scheduleCallback(() => Scheduler.yieldValue('D'));
expect(flushWork(300)).toEqual(['A', 'B']);
expect(flushWork(300)).toEqual(['C']);
expect(flushWork(400)).toEqual(['D']);
});
it('flushes work until framesize reached', () => {
scheduleCallback(() => doWork('A1_100', 100));
scheduleCallback(() => doWork('A2_200', 200));
scheduleCallback(() => doWork('B1_100', 100));
scheduleCallback(() => doWork('B2_200', 200));
scheduleCallback(() => doWork('C1_300', 300));
scheduleCallback(() => doWork('C2_300', 300));
scheduleCallback(() => doWork('D_3000', 3000));
scheduleCallback(() => doWork('E1_300', 300));
scheduleCallback(() => doWork('E2_200', 200));
scheduleCallback(() => doWork('F1_200', 200));
scheduleCallback(() => doWork('F2_200', 200));
scheduleCallback(() => doWork('F3_300', 300));
scheduleCallback(() => doWork('F4_500', 500));
scheduleCallback(() => doWork('F5_200', 200));
scheduleCallback(() => doWork('F6_20', 20));
expect(Date.now()).toEqual(0);
// No time left after A1_100 and A2_200 are run
expect(flushWork(300)).toEqual(['A1_100', 'A2_200']);
expect(Date.now()).toEqual(300);
// B2_200 is started as there is still time left after B1_100
expect(flushWork(101)).toEqual(['B1_100', 'B2_200']);
expect(Date.now()).toEqual(600);
// C1_300 is started as there is even a little frame time
expect(flushWork(1)).toEqual(['C1_300']);
expect(Date.now()).toEqual(900);
// C2_300 is started even though there is no frame time
expect(flushWork(0)).toEqual(['C2_300']);
expect(Date.now()).toEqual(1200);
// D_3000 is very slow, but won't affect next flushes (if no
// timeouts happen)
expect(flushWork(100)).toEqual(['D_3000']);
expect(Date.now()).toEqual(4200);
expect(flushWork(400)).toEqual(['E1_300', 'E2_200']);
expect(Date.now()).toEqual(4700);
// Default timeout is 5000, so during F2_200, work will timeout and are done
// in reverse, including F2_200
expect(flushWork(1000)).toEqual([
'F1_200',
'F2_200',
'F3_300',
'F4_500',
'F5_200',
'F6_20',
]);
expect(Date.now()).toEqual(6120);
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
expect(Scheduler).toFlushAndYieldThrough(['C']);
expect(Scheduler).toFlushAndYield(['D']);
});
it('cancels work', () => {
scheduleCallback(() => doWork('A', 100));
const callbackHandleB = scheduleCallback(() => doWork('B', 200));
scheduleCallback(() => doWork('C', 300));
scheduleCallback(() => Scheduler.yieldValue('A'));
const callbackHandleB = scheduleCallback(() => Scheduler.yieldValue('B'));
scheduleCallback(() => Scheduler.yieldValue('C'));
cancelCallback(callbackHandleB);
expect(flushWork()).toEqual([
expect(Scheduler).toFlushAndYield([
'A',
// B should have been cancelled
'C',
@ -230,86 +64,100 @@ describe('Scheduler', () => {
});
it('executes the highest priority callbacks first', () => {
scheduleCallback(() => doWork('A', 100));
scheduleCallback(() => doWork('B', 100));
scheduleCallback(() => Scheduler.yieldValue('A'));
scheduleCallback(() => Scheduler.yieldValue('B'));
// Yield before B is flushed
expect(flushWork(100)).toEqual(['A']);
expect(Scheduler).toFlushAndYieldThrough(['A']);
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(() => doWork('C', 100));
scheduleCallback(() => doWork('D', 100));
scheduleCallback(() => Scheduler.yieldValue('C'));
scheduleCallback(() => Scheduler.yieldValue('D'));
});
// C and D should come first, because they are higher priority
expect(flushWork()).toEqual(['C', 'D', 'B']);
expect(Scheduler).toFlushAndYield(['C', 'D', 'B']);
});
it('expires work', () => {
scheduleCallback(didTimeout =>
doWork(`A (did timeout: ${didTimeout})`, 100),
);
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(didTimeout =>
doWork(`B (did timeout: ${didTimeout})`, 100),
);
scheduleCallback(didTimeout => {
Scheduler.advanceTime(100);
Scheduler.yieldValue(`A (did timeout: ${didTimeout})`);
});
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(didTimeout =>
doWork(`C (did timeout: ${didTimeout})`, 100),
);
scheduleCallback(didTimeout => {
Scheduler.advanceTime(100);
Scheduler.yieldValue(`B (did timeout: ${didTimeout})`);
});
});
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(didTimeout => {
Scheduler.advanceTime(100);
Scheduler.yieldValue(`C (did timeout: ${didTimeout})`);
});
});
// Advance time, but not by enough to expire any work
advanceTime(249);
expect(clearYieldedValues()).toEqual([]);
Scheduler.advanceTime(249);
expect(Scheduler).toHaveYielded([]);
// Schedule a few more callbacks
scheduleCallback(didTimeout =>
doWork(`D (did timeout: ${didTimeout})`, 100),
);
scheduleCallback(didTimeout =>
doWork(`E (did timeout: ${didTimeout})`, 100),
);
scheduleCallback(didTimeout => {
Scheduler.advanceTime(100);
Scheduler.yieldValue(`D (did timeout: ${didTimeout})`);
});
scheduleCallback(didTimeout => {
Scheduler.advanceTime(100);
Scheduler.yieldValue(`E (did timeout: ${didTimeout})`);
});
// Advance by just a bit more to expire the user blocking callbacks
advanceTime(1);
expect(clearYieldedValues()).toEqual([
Scheduler.advanceTime(1);
expect(Scheduler).toHaveYielded([
'B (did timeout: true)',
'C (did timeout: true)',
]);
// Expire A
advanceTime(4600);
expect(clearYieldedValues()).toEqual(['A (did timeout: true)']);
Scheduler.advanceTime(4600);
expect(Scheduler).toHaveYielded(['A (did timeout: true)']);
// Flush the rest without expiring
expect(flushWork()).toEqual([
expect(Scheduler).toFlushAndYield([
'D (did timeout: false)',
'E (did timeout: false)',
'E (did timeout: true)',
]);
});
it('has a default expiration of ~5 seconds', () => {
scheduleCallback(() => doWork('A', 100));
scheduleCallback(() => Scheduler.yieldValue('A'));
advanceTime(4999);
expect(clearYieldedValues()).toEqual([]);
Scheduler.advanceTime(4999);
expect(Scheduler).toHaveYielded([]);
advanceTime(1);
expect(clearYieldedValues()).toEqual(['A']);
Scheduler.advanceTime(1);
expect(Scheduler).toHaveYielded(['A']);
});
it('continues working on same task after yielding', () => {
scheduleCallback(() => doWork('A', 100));
scheduleCallback(() => doWork('B', 100));
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('A');
});
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('B');
});
let didYield = false;
const tasks = [['C1', 100], ['C2', 100], ['C3', 100]];
const C = () => {
while (tasks.length > 0) {
doWork(...tasks.shift());
const [label, ms] = tasks.shift();
Scheduler.advanceTime(ms);
Scheduler.yieldValue(label);
if (shouldYield()) {
yieldValue('Yield!');
didYield = true;
return C;
}
}
@ -317,21 +165,32 @@ describe('Scheduler', () => {
scheduleCallback(C);
scheduleCallback(() => doWork('D', 100));
scheduleCallback(() => doWork('E', 100));
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('D');
});
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('E');
});
expect(flushWork(300)).toEqual(['A', 'B', 'C1', 'Yield!']);
// Flush, then yield while in the middle of C.
expect(didYield).toBe(false);
expect(Scheduler).toFlushAndYieldThrough(['A', 'B', 'C1']);
expect(didYield).toBe(true);
expect(flushWork()).toEqual(['C2', 'C3', 'D', 'E']);
// When we resume, we should continue working on C.
expect(Scheduler).toFlushAndYield(['C2', 'C3', 'D', 'E']);
});
it('continuation callbacks inherit the expiration of the previous callback', () => {
const tasks = [['A', 125], ['B', 124], ['C', 100], ['D', 100]];
const work = () => {
while (tasks.length > 0) {
doWork(...tasks.shift());
const [label, ms] = tasks.shift();
Scheduler.advanceTime(ms);
Scheduler.yieldValue(label);
if (shouldYield()) {
yieldValue('Yield!');
return work;
}
}
@ -341,50 +200,56 @@ describe('Scheduler', () => {
runWithPriority(UserBlockingPriority, () => scheduleCallback(work));
// Flush until just before the expiration time
expect(flushWork(249)).toEqual(['A', 'B', 'Yield!']);
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
// Advance time by just a bit more. This should expire all the remaining work.
advanceTime(1);
expect(clearYieldedValues()).toEqual(['C', 'D']);
Scheduler.advanceTime(1);
expect(Scheduler).toHaveYielded(['C', 'D']);
});
it('nested callbacks inherit the priority of the currently executing callback', () => {
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(() => {
doWork('Parent callback', 100);
Scheduler.advanceTime(100);
Scheduler.yieldValue('Parent callback');
scheduleCallback(() => {
doWork('Nested callback', 100);
Scheduler.advanceTime(100);
Scheduler.yieldValue('Nested callback');
});
});
});
expect(flushWork(100)).toEqual(['Parent callback']);
expect(Scheduler).toFlushAndYieldThrough(['Parent callback']);
// The nested callback has user-blocking priority, so it should
// expire quickly.
advanceTime(250 + 100);
expect(clearYieldedValues()).toEqual(['Nested callback']);
Scheduler.advanceTime(250 + 100);
expect(Scheduler).toHaveYielded(['Nested callback']);
});
it('continuations are interrupted by higher priority work', () => {
const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
const work = () => {
while (tasks.length > 0) {
doWork(...tasks.shift());
const [label, ms] = tasks.shift();
Scheduler.advanceTime(ms);
Scheduler.yieldValue(label);
if (tasks.length > 0 && shouldYield()) {
yieldValue('Yield!');
return work;
}
}
};
scheduleCallback(work);
expect(flushWork(100)).toEqual(['A', 'Yield!']);
expect(Scheduler).toFlushAndYieldThrough(['A']);
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(() => doWork('High pri', 100));
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('High pri');
});
});
expect(flushWork()).toEqual(['High pri', 'B', 'C', 'D']);
expect(Scheduler).toFlushAndYield(['High pri', 'B', 'C', 'D']);
});
it(
@ -395,22 +260,27 @@ describe('Scheduler', () => {
const work = () => {
while (tasks.length > 0) {
const task = tasks.shift();
doWork(...task);
const [label, ms] = task;
Scheduler.advanceTime(ms);
Scheduler.yieldValue(label);
if (task[0] === 'B') {
// Schedule high pri work from inside another callback
yieldValue('Schedule high pri');
Scheduler.yieldValue('Schedule high pri');
runWithPriority(UserBlockingPriority, () =>
scheduleCallback(() => doWork('High pri', 100)),
scheduleCallback(() => {
Scheduler.advanceTime(100);
Scheduler.yieldValue('High pri');
}),
);
}
if (tasks.length > 0 && shouldYield()) {
yieldValue('Yield!');
Scheduler.yieldValue('Yield!');
return work;
}
}
};
scheduleCallback(work);
expect(flushWork()).toEqual([
expect(Scheduler).toFlushAndYield([
'A',
'B',
'Schedule high pri',
@ -427,19 +297,19 @@ describe('Scheduler', () => {
it('immediate callbacks fire at the end of outermost event', () => {
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => yieldValue('A'));
scheduleCallback(() => yieldValue('B'));
scheduleCallback(() => Scheduler.yieldValue('A'));
scheduleCallback(() => Scheduler.yieldValue('B'));
// Nested event
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => yieldValue('C'));
scheduleCallback(() => Scheduler.yieldValue('C'));
// Nothing should have fired yet
expect(clearYieldedValues()).toEqual([]);
expect(Scheduler).toHaveYielded([]);
});
// Nothing should have fired yet
expect(clearYieldedValues()).toEqual([]);
expect(Scheduler).toHaveYielded([]);
});
// The callbacks were called at the end of the outer event
expect(clearYieldedValues()).toEqual(['A', 'B', 'C']);
expect(Scheduler).toHaveYielded(['A', 'B', 'C']);
});
it('wrapped callbacks have same signature as original callback', () => {
@ -450,7 +320,8 @@ describe('Scheduler', () => {
it('wrapped callbacks inherit the current priority', () => {
const wrappedCallback = wrapCallback(() => {
scheduleCallback(() => {
doWork('Normal', 100);
Scheduler.advanceTime(100);
Scheduler.yieldValue('Normal');
});
});
const wrappedInteractiveCallback = runWithPriority(
@ -458,7 +329,8 @@ describe('Scheduler', () => {
() =>
wrapCallback(() => {
scheduleCallback(() => {
doWork('User-blocking', 100);
Scheduler.advanceTime(100);
Scheduler.yieldValue('User-blocking');
});
}),
);
@ -468,19 +340,20 @@ describe('Scheduler', () => {
// This should schedule an user-blocking callback
wrappedInteractiveCallback();
advanceTime(249);
expect(clearYieldedValues()).toEqual([]);
advanceTime(1);
expect(clearYieldedValues()).toEqual(['User-blocking']);
Scheduler.advanceTime(249);
expect(Scheduler).toHaveYielded([]);
Scheduler.advanceTime(1);
expect(Scheduler).toHaveYielded(['User-blocking']);
advanceTime(10000);
expect(clearYieldedValues()).toEqual(['Normal']);
Scheduler.advanceTime(10000);
expect(Scheduler).toHaveYielded(['Normal']);
});
it('wrapped callbacks inherit the current priority even when nested', () => {
const wrappedCallback = wrapCallback(() => {
scheduleCallback(() => {
doWork('Normal', 100);
Scheduler.advanceTime(100);
Scheduler.yieldValue('Normal');
});
});
const wrappedInteractiveCallback = runWithPriority(
@ -488,7 +361,8 @@ describe('Scheduler', () => {
() =>
wrapCallback(() => {
scheduleCallback(() => {
doWork('User-blocking', 100);
Scheduler.advanceTime(100);
Scheduler.yieldValue('User-blocking');
});
}),
);
@ -500,66 +374,66 @@ describe('Scheduler', () => {
wrappedInteractiveCallback();
});
advanceTime(249);
expect(clearYieldedValues()).toEqual([]);
advanceTime(1);
expect(clearYieldedValues()).toEqual(['User-blocking']);
Scheduler.advanceTime(249);
expect(Scheduler).toHaveYielded([]);
Scheduler.advanceTime(1);
expect(Scheduler).toHaveYielded(['User-blocking']);
advanceTime(10000);
expect(clearYieldedValues()).toEqual(['Normal']);
Scheduler.advanceTime(10000);
expect(Scheduler).toHaveYielded(['Normal']);
});
it('immediate callbacks fire at the end of callback', () => {
const immediateCallback = runWithPriority(ImmediatePriority, () =>
wrapCallback(() => {
scheduleCallback(() => yieldValue('callback'));
scheduleCallback(() => Scheduler.yieldValue('callback'));
}),
);
immediateCallback();
// The callback was called at the end of the outer event
expect(clearYieldedValues()).toEqual(['callback']);
expect(Scheduler).toHaveYielded(['callback']);
});
it("immediate callbacks fire even if there's an error", () => {
expect(() => {
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => {
yieldValue('A');
Scheduler.yieldValue('A');
throw new Error('Oops A');
});
scheduleCallback(() => {
yieldValue('B');
Scheduler.yieldValue('B');
});
scheduleCallback(() => {
yieldValue('C');
Scheduler.yieldValue('C');
throw new Error('Oops C');
});
});
}).toThrow('Oops A');
expect(clearYieldedValues()).toEqual(['A']);
expect(Scheduler).toHaveYielded(['A']);
// B and C flush in a subsequent event. That way, the second error is not
// swallowed.
expect(() => flushWork(0)).toThrow('Oops C');
expect(clearYieldedValues()).toEqual(['B', 'C']);
expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops C');
expect(Scheduler).toHaveYielded(['B', 'C']);
});
it('exposes the current priority level', () => {
yieldValue(getCurrentPriorityLevel());
Scheduler.yieldValue(getCurrentPriorityLevel());
runWithPriority(ImmediatePriority, () => {
yieldValue(getCurrentPriorityLevel());
Scheduler.yieldValue(getCurrentPriorityLevel());
runWithPriority(NormalPriority, () => {
yieldValue(getCurrentPriorityLevel());
Scheduler.yieldValue(getCurrentPriorityLevel());
runWithPriority(UserBlockingPriority, () => {
yieldValue(getCurrentPriorityLevel());
Scheduler.yieldValue(getCurrentPriorityLevel());
});
});
yieldValue(getCurrentPriorityLevel());
Scheduler.yieldValue(getCurrentPriorityLevel());
});
expect(clearYieldedValues()).toEqual([
expect(Scheduler).toHaveYielded([
NormalPriority,
ImmediatePriority,
NormalPriority,

View File

@ -89,8 +89,13 @@ describe('SchedulerDOM', () => {
};
jest.resetModules();
const JestMockScheduler = require('jest-mock-scheduler');
JestMockScheduler.mockRestore();
// Un-mock scheduler
jest.mock('scheduler', () => require.requireActual('scheduler'));
jest.mock('scheduler/src/SchedulerHostConfig', () =>
require.requireActual(
'scheduler/src/forks/SchedulerHostConfig.default.js',
),
);
Scheduler = require('scheduler');
});

View File

@ -19,10 +19,17 @@ describe('SchedulerNoDOM', () => {
// implementation using setTimeout. This only meant to be used for testing
// purposes, like with jest's fake timer API.
beforeEach(() => {
jest.useFakeTimers();
jest.resetModules();
// Delete addEventListener to force us into the fallback mode.
window.addEventListener = undefined;
jest.useFakeTimers();
// Un-mock scheduler
jest.mock('scheduler', () => require.requireActual('scheduler'));
jest.mock('scheduler/src/SchedulerHostConfig', () =>
require.requireActual(
'scheduler/src/forks/SchedulerHostConfig.default.js',
),
);
const Scheduler = require('scheduler');
scheduleCallback = Scheduler.unstable_scheduleCallback;
runWithPriority = Scheduler.unstable_runWithPriority;
@ -69,36 +76,6 @@ describe('SchedulerNoDOM', () => {
expect(log).toEqual(['C', 'D', 'A', 'B']);
});
it('advanceTimersByTime expires callbacks incrementally', () => {
let log = [];
scheduleCallback(() => {
log.push('A');
});
scheduleCallback(() => {
log.push('B');
});
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(() => {
log.push('C');
});
scheduleCallback(() => {
log.push('D');
});
});
expect(log).toEqual([]);
jest.advanceTimersByTime(249);
expect(log).toEqual([]);
jest.advanceTimersByTime(1);
expect(log).toEqual(['C', 'D']);
log = [];
jest.runAllTimers();
expect(log).toEqual(['A', 'B']);
});
it('calls immediate callbacks immediately', () => {
let log = [];

View File

@ -14,6 +14,13 @@ describe('Scheduling UMD bundle', () => {
global.__UMD__ = true;
jest.resetModules();
jest.mock('scheduler', () => require.requireActual('scheduler'));
jest.mock('scheduler/src/SchedulerHostConfig', () =>
require.requireActual(
'scheduler/src/forks/SchedulerHostConfig.default.js',
),
);
});
function filterPrivateKeys(name) {

View File

@ -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.
*/
// The DOM Scheduler implementation is similar to requestIdleCallback. It
// works by scheduling a requestAnimationFrame, storing the time for the start
// of the frame, then scheduling a postMessage which gets scheduled after paint.
// Within the postMessage handler do as much work as possible until time + frame
// rate. By separating the idle call into a separate event tick we ensure that
// layout, paint and other browser work is counted against the available time.
// The frame rate is dynamically adjusted.
export let requestHostCallback;
export let cancelHostCallback;
export let shouldYieldToHost;
export let getCurrentTime;
const hasNativePerformanceNow =
typeof performance === 'object' && typeof performance.now === 'function';
// We capture a local reference to any global, in case it gets polyfilled after
// this module is initially evaluated. We want to be using a
// consistent implementation.
const localDate = Date;
// This initialization code may run even on server environments if a component
// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
// have setTimeout or clearTimeout. However, we always expect them to be defined
// on the client. https://github.com/facebook/react/pull/13088
const localSetTimeout =
typeof setTimeout === 'function' ? setTimeout : undefined;
const localClearTimeout =
typeof clearTimeout === 'function' ? clearTimeout : undefined;
// We don't expect either of these to necessarily be defined, but we will error
// later if they are missing on the client.
const localRequestAnimationFrame =
typeof requestAnimationFrame === 'function'
? requestAnimationFrame
: undefined;
const localCancelAnimationFrame =
typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
// requestAnimationFrame does not run when the tab is in the background. If
// we're backgrounded we prefer for that work to happen so that the page
// continues to load in the background. So we also schedule a 'setTimeout' as
// a fallback.
// TODO: Need a better heuristic for backgrounded work.
const ANIMATION_FRAME_TIMEOUT = 100;
let rAFID;
let rAFTimeoutID;
const requestAnimationFrameWithTimeout = function(callback) {
// schedule rAF and also a setTimeout
rAFID = localRequestAnimationFrame(function(timestamp) {
// cancel the setTimeout
localClearTimeout(rAFTimeoutID);
callback(timestamp);
});
rAFTimeoutID = localSetTimeout(function() {
// cancel the requestAnimationFrame
localCancelAnimationFrame(rAFID);
callback(getCurrentTime());
}, ANIMATION_FRAME_TIMEOUT);
};
if (hasNativePerformanceNow) {
const Performance = performance;
getCurrentTime = function() {
return Performance.now();
};
} else {
getCurrentTime = function() {
return localDate.now();
};
}
if (
// If Scheduler runs in a non-DOM environment, it falls back to a naive
// implementation using setTimeout.
typeof window === 'undefined' ||
// Check if MessageChannel is supported, too.
typeof MessageChannel !== 'function'
) {
// If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
// fallback to a naive implementation.
let _callback = null;
const _flushCallback = function(didTimeout) {
if (_callback !== null) {
try {
_callback(didTimeout);
} finally {
_callback = null;
}
}
};
requestHostCallback = function(cb, ms) {
if (_callback !== null) {
// Protect against re-entrancy.
setTimeout(requestHostCallback, 0, cb);
} else {
_callback = cb;
setTimeout(_flushCallback, 0, false);
}
};
cancelHostCallback = function() {
_callback = null;
};
shouldYieldToHost = function() {
return false;
};
} else {
if (typeof console !== 'undefined') {
// TODO: Remove fb.me link
if (typeof localRequestAnimationFrame !== 'function') {
console.error(
"This browser doesn't support requestAnimationFrame. " +
'Make sure that you load a ' +
'polyfill in older browsers. https://fb.me/react-polyfills',
);
}
if (typeof localCancelAnimationFrame !== 'function') {
console.error(
"This browser doesn't support cancelAnimationFrame. " +
'Make sure that you load a ' +
'polyfill in older browsers. https://fb.me/react-polyfills',
);
}
}
let scheduledHostCallback = null;
let isMessageEventScheduled = false;
let timeoutTime = -1;
let isAnimationFrameScheduled = false;
let isFlushingHostCallback = false;
let frameDeadline = 0;
// We start out assuming that we run at 30fps but then the heuristic tracking
// will adjust this value to a faster fps if we get more frequent animation
// frames.
let previousFrameTime = 33;
let activeFrameTime = 33;
shouldYieldToHost = function() {
return frameDeadline <= getCurrentTime();
};
// We use the postMessage trick to defer idle work until after the repaint.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = function(event) {
isMessageEventScheduled = false;
const prevScheduledCallback = scheduledHostCallback;
const prevTimeoutTime = timeoutTime;
scheduledHostCallback = null;
timeoutTime = -1;
const currentTime = getCurrentTime();
let didTimeout = false;
if (frameDeadline - currentTime <= 0) {
// There's no time left in this idle period. Check if the callback has
// a timeout and whether it's been exceeded.
if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
// Exceeded the timeout. Invoke the callback even though there's no
// time left.
didTimeout = true;
} else {
// No timeout.
if (!isAnimationFrameScheduled) {
// Schedule another animation callback so we retry later.
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
// Exit without invoking the callback.
scheduledHostCallback = prevScheduledCallback;
timeoutTime = prevTimeoutTime;
return;
}
}
if (prevScheduledCallback !== null) {
isFlushingHostCallback = true;
try {
prevScheduledCallback(didTimeout);
} finally {
isFlushingHostCallback = false;
}
}
};
const animationTick = function(rafTime) {
if (scheduledHostCallback !== null) {
// Eagerly schedule the next animation callback at the beginning of the
// frame. If the scheduler queue is not empty at the end of the frame, it
// will continue flushing inside that callback. If the queue *is* empty,
// then it will exit immediately. Posting the callback at the start of the
// frame ensures it's fired within the earliest possible frame. If we
// waited until the end of the frame to post the callback, we risk the
// browser skipping a frame and not firing the callback until the frame
// after that.
requestAnimationFrameWithTimeout(animationTick);
} else {
// No pending work. Exit.
isAnimationFrameScheduled = false;
return;
}
let nextFrameTime = rafTime - frameDeadline + activeFrameTime;
if (
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime
) {
if (nextFrameTime < 8) {
// Defensive coding. We don't support higher frame rates than 120hz.
// If the calculated frame time gets lower than 8, it is probably a bug.
nextFrameTime = 8;
}
// If one frame goes long, then the next one can be short to catch up.
// If two frames are short in a row, then that's an indication that we
// actually have a higher frame rate than what we're currently optimizing.
// We adjust our heuristic dynamically accordingly. For example, if we're
// running on 120hz display or 90hz VR display.
// Take the max of the two in case one of them was an anomaly due to
// missed frame deadlines.
activeFrameTime =
nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
} else {
previousFrameTime = nextFrameTime;
}
frameDeadline = rafTime + activeFrameTime;
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
port.postMessage(undefined);
}
};
requestHostCallback = function(callback, absoluteTimeout) {
scheduledHostCallback = callback;
timeoutTime = absoluteTimeout;
if (isFlushingHostCallback || absoluteTimeout < 0) {
// Don't wait for the next frame. Continue working ASAP, in a new event.
port.postMessage(undefined);
} else if (!isAnimationFrameScheduled) {
// If rAF didn't already schedule one, we need to schedule a frame.
// TODO: If this rAF doesn't materialize because the browser throttles, we
// might want to still have setTimeout trigger rIC as a backup to ensure
// that we keep performing work.
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
};
cancelHostCallback = function() {
scheduledHostCallback = null;
isMessageEventScheduled = false;
timeoutTime = -1;
};
}

View File

@ -0,0 +1,167 @@
/**
* 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.
*
* @flow
*/
let currentTime: number = 0;
let scheduledCallback: (boolean => void) | null = null;
let scheduledCallbackExpiration: number = -1;
let yieldedValues: Array<mixed> | null = null;
let expectedNumberOfYields: number = -1;
let didStop: boolean = false;
let isFlushing: boolean = false;
export function requestHostCallback(
callback: boolean => void,
expiration: number,
) {
scheduledCallback = callback;
scheduledCallbackExpiration = expiration;
}
export function cancelHostCallback(): void {
scheduledCallback = null;
scheduledCallbackExpiration = -1;
}
export function shouldYieldToHost(): boolean {
if (
(expectedNumberOfYields !== -1 &&
yieldedValues !== null &&
yieldedValues.length >= expectedNumberOfYields) ||
(scheduledCallbackExpiration !== -1 &&
scheduledCallbackExpiration <= currentTime)
) {
// We yielded at least as many values as expected. Stop flushing.
didStop = true;
return true;
}
return false;
}
export function getCurrentTime(): number {
return currentTime;
}
export function reset() {
if (isFlushing) {
throw new Error('Cannot reset while already flushing work.');
}
currentTime = 0;
scheduledCallback = null;
scheduledCallbackExpiration = -1;
yieldedValues = null;
expectedNumberOfYields = -1;
didStop = false;
isFlushing = false;
}
// Should only be used via an assertion helper that inspects the yielded values.
export function unstable_flushNumberOfYields(count: number): void {
if (isFlushing) {
throw new Error('Already flushing work.');
}
expectedNumberOfYields = count;
isFlushing = true;
try {
while (scheduledCallback !== null && !didStop) {
const cb = scheduledCallback;
scheduledCallback = null;
const didTimeout =
scheduledCallbackExpiration !== -1 &&
scheduledCallbackExpiration <= currentTime;
cb(didTimeout);
}
} finally {
expectedNumberOfYields = -1;
didStop = false;
isFlushing = false;
}
}
export function unstable_flushExpired() {
if (isFlushing) {
throw new Error('Already flushing work.');
}
if (scheduledCallback !== null) {
const cb = scheduledCallback;
scheduledCallback = null;
isFlushing = true;
try {
cb(true);
} finally {
isFlushing = false;
}
}
}
export function unstable_flushWithoutYielding(): void {
if (isFlushing) {
throw new Error('Already flushing work.');
}
isFlushing = true;
try {
while (scheduledCallback !== null) {
const cb = scheduledCallback;
scheduledCallback = null;
const didTimeout =
scheduledCallbackExpiration !== -1 &&
scheduledCallbackExpiration <= currentTime;
cb(didTimeout);
}
} finally {
expectedNumberOfYields = -1;
didStop = false;
isFlushing = false;
}
}
export function unstable_clearYields(): Array<mixed> {
if (yieldedValues === null) {
return [];
}
const values = yieldedValues;
yieldedValues = null;
return values;
}
export function flushAll(): void {
if (yieldedValues !== null) {
throw new Error(
'Log is not empty. Assert on the log of yielded values before ' +
'flushing additional work.',
);
}
unstable_flushWithoutYielding();
if (yieldedValues !== null) {
throw new Error(
'While flushing work, something yielded a value. Use an ' +
'assertion helper to assert on the log of yielded values, e.g. ' +
'expect(Scheduler).toFlushAndYield([...])',
);
}
}
export function yieldValue(value: mixed): void {
if (yieldedValues === null) {
yieldedValues = [value];
} else {
yieldedValues.push(value);
}
}
export function advanceTime(ms: number) {
currentTime += ms;
// If the host callback timed out, flush the expired work.
if (
!isFlushing &&
scheduledCallbackExpiration !== -1 &&
scheduledCallbackExpiration <= currentTime
) {
unstable_flushExpired();
}
}

View File

@ -0,0 +1,20 @@
/**
* 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.
*/
'use strict';
export * from './src/Scheduler';
export {
unstable_flushWithoutYielding,
unstable_flushNumberOfYields,
unstable_flushExpired,
unstable_clearYields,
flushAll,
yieldValue,
advanceTime,
} from './src/SchedulerHostConfig.js';

View File

@ -10,6 +10,18 @@
'use strict';
describe('ReactDOMFrameScheduling', () => {
beforeEach(() => {
jest.resetModules();
// Un-mock scheduler
jest.mock('scheduler', () => require.requireActual('scheduler'));
jest.mock('scheduler/src/SchedulerHostConfig', () =>
require.requireActual(
'scheduler/src/forks/SchedulerHostConfig.default.js',
),
);
});
it('warns when requestAnimationFrame is not polyfilled in the browser', () => {
const previousRAF = global.requestAnimationFrame;
const previousMessageChannel = global.MessageChannel;
@ -21,11 +33,6 @@ describe('ReactDOMFrameScheduling', () => {
port2: {},
};
};
jest.resetModules();
const JestMockScheduler = require('jest-mock-scheduler');
JestMockScheduler.mockRestore();
spyOnDevAndProd(console, 'error');
require('react-dom');
expect(console.error.calls.count()).toEqual(1);

View File

@ -35,7 +35,7 @@ packages.forEach(name => {
moduleNameMapper[`^${name}$`] = `<rootDir>/build/node_modules/${name}`;
// Named entry points
moduleNameMapper[
`^${name}/(.*)$`
`^${name}\/([^\/]+)$`
] = `<rootDir>/build/node_modules/${name}/$1`;
});
@ -46,4 +46,8 @@ module.exports = Object.assign({}, baseConfig, {
testPathIgnorePatterns: ['/node_modules/', '-test.internal.js$'],
// Exclude the build output from transforms
transformIgnorePatterns: ['/node_modules/', '<rootDir>/build/'],
setupFiles: [
...baseConfig.setupFiles,
require.resolve('./setupTests.build.js'),
],
});

View File

@ -1,6 +1,7 @@
'use strict';
const JestReact = require('jest-react');
const SchedulerMatchers = require('./schedulerTestMatchers');
function captureAssertion(fn) {
// Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
@ -18,6 +19,10 @@ function captureAssertion(fn) {
return {pass: true};
}
function isScheduler(obj) {
return typeof obj.unstable_scheduleCallback === 'function';
}
function isReactNoop(obj) {
return typeof obj.hasScheduledCallback === 'function';
}
@ -33,6 +38,9 @@ function assertYieldsWereCleared(ReactNoop) {
}
function toFlushAndYield(ReactNoop, expectedYields) {
if (isScheduler(ReactNoop)) {
return SchedulerMatchers.toFlushAndYield(ReactNoop, expectedYields);
}
if (!isReactNoop(ReactNoop)) {
return JestReact.unstable_toFlushAndYield(ReactNoop, expectedYields);
}
@ -44,6 +52,9 @@ function toFlushAndYield(ReactNoop, expectedYields) {
}
function toFlushAndYieldThrough(ReactNoop, expectedYields) {
if (isScheduler(ReactNoop)) {
return SchedulerMatchers.toFlushAndYieldThrough(ReactNoop, expectedYields);
}
if (!isReactNoop(ReactNoop)) {
return JestReact.unstable_toFlushAndYieldThrough(ReactNoop, expectedYields);
}
@ -57,6 +68,9 @@ function toFlushAndYieldThrough(ReactNoop, expectedYields) {
}
function toFlushWithoutYielding(ReactNoop) {
if (isScheduler(ReactNoop)) {
return SchedulerMatchers.toFlushWithoutYielding(ReactNoop);
}
if (!isReactNoop(ReactNoop)) {
return JestReact.unstable_toFlushWithoutYielding(ReactNoop);
}
@ -64,6 +78,9 @@ function toFlushWithoutYielding(ReactNoop) {
}
function toHaveYielded(ReactNoop, expectedYields) {
if (isScheduler(ReactNoop)) {
return SchedulerMatchers.toHaveYielded(ReactNoop, expectedYields);
}
if (!isReactNoop(ReactNoop)) {
return JestReact.unstable_toHaveYielded(ReactNoop, expectedYields);
}
@ -74,6 +91,9 @@ function toHaveYielded(ReactNoop, expectedYields) {
}
function toFlushAndThrow(ReactNoop, ...rest) {
if (isScheduler(ReactNoop)) {
return SchedulerMatchers.toFlushAndThrow(ReactNoop, ...rest);
}
if (!isReactNoop(ReactNoop)) {
return JestReact.unstable_toFlushAndThrow(ReactNoop, ...rest);
}

View File

@ -0,0 +1,73 @@
'use strict';
function captureAssertion(fn) {
// Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
// assertion; if it throws, we capture the error and return it, so the stack
// trace presented to the user points to the original assertion in the
// test file.
try {
fn();
} catch (error) {
return {
pass: false,
message: () => error.message,
};
}
return {pass: true};
}
function assertYieldsWereCleared(Scheduler) {
const actualYields = Scheduler.unstable_clearYields();
if (actualYields.length !== 0) {
throw new Error(
'Log of yielded values is not empty. ' +
'Call expect(Scheduler).toHaveYielded(...) first.'
);
}
}
function toFlushAndYield(Scheduler, expectedYields) {
assertYieldsWereCleared(Scheduler);
Scheduler.unstable_flushWithoutYielding();
const actualYields = Scheduler.unstable_clearYields();
return captureAssertion(() => {
expect(actualYields).toEqual(expectedYields);
});
}
function toFlushAndYieldThrough(Scheduler, expectedYields) {
assertYieldsWereCleared(Scheduler);
Scheduler.unstable_flushNumberOfYields(expectedYields.length);
const actualYields = Scheduler.unstable_clearYields();
return captureAssertion(() => {
expect(actualYields).toEqual(expectedYields);
});
}
function toFlushWithoutYielding(Scheduler) {
return toFlushAndYield(Scheduler, []);
}
function toHaveYielded(Scheduler, expectedYields) {
return captureAssertion(() => {
const actualYields = Scheduler.unstable_clearYields();
expect(actualYields).toEqual(expectedYields);
});
}
function toFlushAndThrow(Scheduler, ...rest) {
assertYieldsWereCleared(Scheduler);
return captureAssertion(() => {
expect(() => {
Scheduler.unstable_flushWithoutYielding();
}).toThrow(...rest);
});
}
module.exports = {
toFlushAndYield,
toFlushAndYieldThrough,
toFlushWithoutYielding,
toHaveYielded,
toFlushAndThrow,
};

View File

@ -111,3 +111,8 @@ inlinedHostConfigs.forEach(rendererInfo => {
jest.mock('shared/ReactSharedInternals', () =>
require.requireActual('react/src/ReactSharedInternals')
);
jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));
jest.mock('scheduler/src/SchedulerHostConfig', () =>
require.requireActual('scheduler/src/forks/SchedulerHostConfig.mock.js')
);

View File

@ -0,0 +1,6 @@
'use strict';
jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));
jest.mock('scheduler/src/SchedulerHostConfig', () =>
require.requireActual('scheduler/src/forks/SchedulerHostConfig.mock.js')
);

View File

@ -49,8 +49,6 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
...require('./matchers/reactTestMatchers'),
});
require('jest-mock-scheduler');
// We have a Babel transform that inserts guards against infinite loops.
// If a loop runs for too many iterations, we throw an error and set this
// global variable. The global lets us detect an infinite loop even if

View File

@ -453,6 +453,15 @@ const bundles = [
externals: [],
},
/******* React Scheduler Mock (experimental) *******/
{
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
moduleType: ISOMORPHIC,
entry: 'scheduler/unstable_mock',
global: 'SchedulerMock',
externals: [],
},
/******* Jest React (experimental) *******/
{
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
@ -462,15 +471,6 @@ const bundles = [
externals: [],
},
/******* Jest Scheduler (experimental) *******/
{
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
moduleType: ISOMORPHIC,
entry: 'jest-mock-scheduler',
global: 'JestMockScheduler',
externals: [],
},
/******* ESLint Plugin for Hooks (proposal) *******/
{
// TODO: it's awkward to create a bundle for this

View File

@ -159,16 +159,22 @@ const forks = Object.freeze({
'scheduler/src/SchedulerFeatureFlags': (bundleType, entry, dependencies) => {
if (
entry === 'scheduler' &&
(bundleType === FB_WWW_DEV ||
bundleType === FB_WWW_PROD ||
bundleType === FB_WWW_PROFILING)
bundleType === FB_WWW_DEV ||
bundleType === FB_WWW_PROD ||
bundleType === FB_WWW_PROFILING
) {
return 'scheduler/src/forks/SchedulerFeatureFlags.www.js';
}
return 'scheduler/src/SchedulerFeatureFlags';
},
'scheduler/src/SchedulerHostConfig': (bundleType, entry, dependencies) => {
if (entry === 'scheduler/unstable_mock') {
return 'scheduler/src/forks/SchedulerHostConfig.mock';
}
return 'scheduler/src/forks/SchedulerHostConfig.default';
},
// This logic is forked on www to fork the formatting function.
'shared/invariant': (bundleType, entry) => {
switch (bundleType) {

View File

@ -4,22 +4,22 @@
"filename": "react.development.js",
"bundleType": "UMD_DEV",
"packageName": "react",
"size": 101989,
"gzip": 26428
"size": 102032,
"gzip": 26457
},
{
"filename": "react.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react",
"size": 12548,
"gzip": 4823
"size": 12564,
"gzip": 4826
},
{
"filename": "react.development.js",
"bundleType": "NODE_DEV",
"packageName": "react",
"size": 63522,
"gzip": 17094
"size": 63704,
"gzip": 17181
},
{
"filename": "react.production.min.js",
@ -46,29 +46,29 @@
"filename": "react-dom.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 783692,
"gzip": 178609
"size": 791682,
"gzip": 179962
},
{
"filename": "react-dom.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 107842,
"gzip": 34729
"size": 107808,
"gzip": 34704
},
{
"filename": "react-dom.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 778167,
"gzip": 177083
"size": 786187,
"gzip": 178415
},
{
"filename": "react-dom.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 108009,
"gzip": 34209
"size": 108031,
"gzip": 34186
},
{
"filename": "ReactDOM-dev.js",
@ -165,29 +165,29 @@
"filename": "react-dom-server.browser.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 129798,
"gzip": 34602
"size": 130425,
"gzip": 34746
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 19117,
"gzip": 7343
"size": 19319,
"gzip": 7379
},
{
"filename": "react-dom-server.browser.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 125836,
"gzip": 33642
"size": 126463,
"gzip": 33795
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 19037,
"gzip": 7325
"size": 19239,
"gzip": 7368
},
{
"filename": "ReactDOMServer-dev.js",
@ -207,43 +207,43 @@
"filename": "react-dom-server.node.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 127943,
"gzip": 34197
"size": 128570,
"gzip": 34348
},
{
"filename": "react-dom-server.node.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 19930,
"gzip": 7641
"size": 20132,
"gzip": 7685
},
{
"filename": "react-art.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-art",
"size": 554932,
"gzip": 120585
"size": 562387,
"gzip": 121860
},
{
"filename": "react-art.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-art",
"size": 99655,
"gzip": 30575
"size": 99616,
"gzip": 30538
},
{
"filename": "react-art.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-art",
"size": 484327,
"gzip": 102945
"size": 491818,
"gzip": 104175
},
{
"filename": "react-art.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-art",
"size": 63856,
"gzip": 19480
"size": 63873,
"gzip": 19446
},
{
"filename": "ReactART-dev.js",
@ -291,29 +291,29 @@
"filename": "react-test-renderer.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-test-renderer",
"size": 496691,
"gzip": 105367
"size": 503952,
"gzip": 106548
},
{
"filename": "react-test-renderer.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-test-renderer",
"size": 65252,
"gzip": 19980
"size": 65222,
"gzip": 19962
},
{
"filename": "react-test-renderer.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-test-renderer",
"size": 490997,
"gzip": 104031
"size": 498258,
"gzip": 105207
},
{
"filename": "react-test-renderer.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-test-renderer",
"size": 64908,
"gzip": 19647
"size": 64873,
"gzip": 19614
},
{
"filename": "ReactTestRenderer-dev.js",
@ -361,43 +361,43 @@
"filename": "react-noop-renderer.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-noop-renderer",
"size": 32858,
"gzip": 7514
"size": 28240,
"gzip": 6740
},
{
"filename": "react-noop-renderer.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-noop-renderer",
"size": 11486,
"gzip": 3766
"size": 10081,
"gzip": 3367
},
{
"filename": "react-reconciler.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
"size": 481582,
"gzip": 101249
"size": 489120,
"gzip": 102507
},
{
"filename": "react-reconciler.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
"size": 65118,
"gzip": 19259
"size": 65080,
"gzip": 19234
},
{
"filename": "react-reconciler-persistent.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
"size": 479920,
"gzip": 100594
"size": 487276,
"gzip": 101780
},
{
"filename": "react-reconciler-persistent.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
"size": 65129,
"gzip": 19264
"size": 65091,
"gzip": 19239
},
{
"filename": "react-reconciler-reflection.development.js",
@ -501,8 +501,8 @@
"filename": "React-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react",
"size": 60700,
"gzip": 16126
"size": 60786,
"gzip": 16140
},
{
"filename": "React-prod.js",
@ -515,15 +515,15 @@
"filename": "ReactDOM-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 801709,
"gzip": 178348
"size": 809740,
"gzip": 179616
},
{
"filename": "ReactDOM-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
"size": 329235,
"gzip": 60001
"size": 329960,
"gzip": 60112
},
{
"filename": "ReactTestUtils-dev.js",
@ -550,92 +550,92 @@
"filename": "ReactDOMServer-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 126805,
"gzip": 33138
"size": 127337,
"gzip": 33235
},
{
"filename": "ReactDOMServer-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
"size": 46040,
"gzip": 10601
"size": 46343,
"gzip": 10662
},
{
"filename": "ReactART-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-art",
"size": 493866,
"gzip": 102238
"size": 501297,
"gzip": 103384
},
{
"filename": "ReactART-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-art",
"size": 199704,
"gzip": 33787
"size": 200132,
"gzip": 33817
},
{
"filename": "ReactNativeRenderer-dev.js",
"bundleType": "RN_FB_DEV",
"packageName": "react-native-renderer",
"size": 621398,
"gzip": 133323
"size": 631255,
"gzip": 134845
},
{
"filename": "ReactNativeRenderer-prod.js",
"bundleType": "RN_FB_PROD",
"packageName": "react-native-renderer",
"size": 252107,
"gzip": 44030
"size": 252735,
"gzip": 44084
},
{
"filename": "ReactNativeRenderer-dev.js",
"bundleType": "RN_OSS_DEV",
"packageName": "react-native-renderer",
"size": 621309,
"gzip": 133286
"size": 631168,
"gzip": 134810
},
{
"filename": "ReactNativeRenderer-prod.js",
"bundleType": "RN_OSS_PROD",
"packageName": "react-native-renderer",
"size": 252121,
"gzip": 44026
"size": 252749,
"gzip": 44079
},
{
"filename": "ReactFabric-dev.js",
"bundleType": "RN_FB_DEV",
"packageName": "react-native-renderer",
"size": 612032,
"gzip": 130990
"size": 621889,
"gzip": 132504
},
{
"filename": "ReactFabric-prod.js",
"bundleType": "RN_FB_PROD",
"packageName": "react-native-renderer",
"size": 244266,
"gzip": 42532
"size": 244896,
"gzip": 42571
},
{
"filename": "ReactFabric-dev.js",
"bundleType": "RN_OSS_DEV",
"packageName": "react-native-renderer",
"size": 611935,
"gzip": 130940
"size": 621794,
"gzip": 132455
},
{
"filename": "ReactFabric-prod.js",
"bundleType": "RN_OSS_PROD",
"packageName": "react-native-renderer",
"size": 244272,
"gzip": 42522
"size": 244902,
"gzip": 42561
},
{
"filename": "ReactTestRenderer-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-test-renderer",
"size": 501300,
"gzip": 103689
"size": 508573,
"gzip": 104823
},
{
"filename": "ReactShallowRenderer-dev.js",
@ -676,15 +676,15 @@
"filename": "scheduler.development.js",
"bundleType": "NODE_DEV",
"packageName": "scheduler",
"size": 23870,
"gzip": 6174
"size": 23505,
"gzip": 6019
},
{
"filename": "scheduler.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "scheduler",
"size": 5000,
"gzip": 1883
"size": 4888,
"gzip": 1819
},
{
"filename": "SimpleCacheProvider-dev.js",
@ -704,50 +704,50 @@
"filename": "react-noop-renderer-persistent.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-noop-renderer",
"size": 32977,
"gzip": 7525
"size": 28359,
"gzip": 6753
},
{
"filename": "react-noop-renderer-persistent.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-noop-renderer",
"size": 11508,
"gzip": 3772
"size": 10103,
"gzip": 3373
},
{
"filename": "react-dom.profiling.min.js",
"bundleType": "NODE_PROFILING",
"packageName": "react-dom",
"size": 111156,
"gzip": 35048
"size": 111178,
"gzip": 35023
},
{
"filename": "ReactNativeRenderer-profiling.js",
"bundleType": "RN_OSS_PROFILING",
"packageName": "react-native-renderer",
"size": 258626,
"gzip": 45614
"size": 259254,
"gzip": 45666
},
{
"filename": "ReactFabric-profiling.js",
"bundleType": "RN_OSS_PROFILING",
"packageName": "react-native-renderer",
"size": 250654,
"gzip": 44086
"size": 251284,
"gzip": 44137
},
{
"filename": "Scheduler-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "scheduler",
"size": 24123,
"gzip": 6223
"size": 23758,
"gzip": 6067
},
{
"filename": "Scheduler-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "scheduler",
"size": 14327,
"gzip": 2953
"size": 14025,
"gzip": 2841
},
{
"filename": "react.profiling.min.js",
@ -767,43 +767,43 @@
"filename": "ReactDOM-profiling.js",
"bundleType": "FB_WWW_PROFILING",
"packageName": "react-dom",
"size": 335969,
"gzip": 61519
"size": 336694,
"gzip": 61627
},
{
"filename": "ReactNativeRenderer-profiling.js",
"bundleType": "RN_FB_PROFILING",
"packageName": "react-native-renderer",
"size": 258607,
"gzip": 45621
"size": 259235,
"gzip": 45675
},
{
"filename": "ReactFabric-profiling.js",
"bundleType": "RN_FB_PROFILING",
"packageName": "react-native-renderer",
"size": 250643,
"gzip": 44092
"size": 251273,
"gzip": 44141
},
{
"filename": "react.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react",
"size": 14756,
"gzip": 5369
"size": 14773,
"gzip": 5356
},
{
"filename": "react-dom.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react-dom",
"size": 110830,
"gzip": 35633
"size": 110796,
"gzip": 35607
},
{
"filename": "scheduler-tracing.development.js",
"bundleType": "NODE_DEV",
"packageName": "scheduler",
"size": 10554,
"gzip": 2432
"size": 10737,
"gzip": 2535
},
{
"filename": "scheduler-tracing.production.min.js",
@ -823,8 +823,8 @@
"filename": "SchedulerTracing-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "scheduler",
"size": 10121,
"gzip": 2117
"size": 10207,
"gzip": 2134
},
{
"filename": "SchedulerTracing-prod.js",
@ -928,15 +928,15 @@
"filename": "eslint-plugin-react-hooks.development.js",
"bundleType": "NODE_DEV",
"packageName": "eslint-plugin-react-hooks",
"size": 26115,
"gzip": 6005
"size": 50458,
"gzip": 11887
},
{
"filename": "eslint-plugin-react-hooks.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "eslint-plugin-react-hooks",
"size": 5080,
"gzip": 1872
"size": 12598,
"gzip": 4566
},
{
"filename": "ReactDOMFizzServer-dev.js",
@ -1026,71 +1026,71 @@
"filename": "ESLintPluginReactHooks-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "eslint-plugin-react-hooks",
"size": 27788,
"gzip": 6145
"size": 54073,
"gzip": 12239
},
{
"filename": "react-dom-unstable-fire.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 784046,
"gzip": 178758
"size": 792036,
"gzip": 180102
},
{
"filename": "react-dom-unstable-fire.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 107857,
"gzip": 34738
"size": 107823,
"gzip": 34713
},
{
"filename": "react-dom-unstable-fire.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react-dom",
"size": 110845,
"gzip": 35642
"size": 110811,
"gzip": 35616
},
{
"filename": "react-dom-unstable-fire.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 778520,
"gzip": 177227
"size": 786540,
"gzip": 178557
},
{
"filename": "react-dom-unstable-fire.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 108023,
"gzip": 34220
"size": 108045,
"gzip": 34197
},
{
"filename": "react-dom-unstable-fire.profiling.min.js",
"bundleType": "NODE_PROFILING",
"packageName": "react-dom",
"size": 111170,
"gzip": 35058
"size": 111192,
"gzip": 35033
},
{
"filename": "ReactFire-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 800900,
"gzip": 178268
"size": 808931,
"gzip": 179569
},
{
"filename": "ReactFire-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
"size": 317385,
"gzip": 57621
"size": 318110,
"gzip": 57721
},
{
"filename": "ReactFire-profiling.js",
"bundleType": "FB_WWW_PROFILING",
"packageName": "react-dom",
"size": 324156,
"gzip": 59066
"size": 324881,
"gzip": 59172
},
{
"filename": "jest-mock-scheduler.development.js",
@ -1119,6 +1119,34 @@
"packageName": "jest-mock-scheduler",
"size": 1085,
"gzip": 532
},
{
"filename": "scheduler-unstable_mock.development.js",
"bundleType": "NODE_DEV",
"packageName": "scheduler",
"size": 17926,
"gzip": 4128
},
{
"filename": "scheduler-unstable_mock.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "scheduler",
"size": 4173,
"gzip": 1606
},
{
"filename": "SchedulerMock-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "scheduler",
"size": 18170,
"gzip": 4175
},
{
"filename": "SchedulerMock-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "scheduler",
"size": 12088,
"gzip": 2473
}
]
}