Skip to content

Commit

Permalink
Add end-of-frame scheduling for default events
Browse files Browse the repository at this point in the history
  • Loading branch information
rickhanlonii committed May 24, 2022
1 parent 6e2f38f commit 1ab4eaa
Show file tree
Hide file tree
Showing 24 changed files with 258 additions and 8 deletions.
10 changes: 10 additions & 0 deletions packages/jest-react/src/internalAct.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ export function act<T>(scope: () => Thenable<T> | T): Thenable<T> {
let didFlushWork;
do {
didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();

// Flush scheduled rAF.
if (global.flushRequestAnimationFrameQueue) {
global.flushRequestAnimationFrameQueue();
}
} while (didFlushWork);
return {
then(resolve, reject) {
Expand All @@ -126,6 +131,11 @@ function flushActWork(resolve, reject) {
enqueueTask(() => {
try {
const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();

// Flush scheduled rAF.
if (global.flushRequestAnimationFrameQueue) {
global.flushRequestAnimationFrameQueue();
}
if (didFlushWork) {
flushActWork(resolve, reject);
} else {
Expand Down
64 changes: 63 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ describe('ReactDOMFiberAsync', () => {

handleChange = e => {
const nextValue = e.target.value;
requestIdleCallback(() => {
React.startTransition(() => {
this.setState({
asyncValue: nextValue,
});
Expand Down Expand Up @@ -545,6 +545,68 @@ describe('ReactDOMFiberAsync', () => {
// Therefore the form should have been submitted.
expect(formSubmitted).toBe(true);
});

// @gate enableFameEndScheduling
it.skip('batch default updates with existing unknown updates', () => {
let setState = null;
let counterRef = null;
function Counter() {
const [count, setCount] = React.useState(0);
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
return <p ref={ref}>Count: {count}</p>;
}

const root = ReactDOMClient.createRoot(container);
act(() => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);

window.event = undefined;
setState(1);
window.event = 'test';
setState(2);

expect(Scheduler).toFlushAndYield([]);
expect(counterRef.current.textContent).toBe('Count: 0');
// global.flushRequestAnimationFrameQueue();
expect(Scheduler).toHaveYielded(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
});

// @gate enableFameEndScheduling
it.skip('do not batch unknown updates with existing default updates', () => {
let setState = null;
let counterRef = null;
function Counter() {
const [count, setCount] = React.useState(0);
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
return <p ref={ref}>Count: {count}</p>;
}

const root = ReactDOMClient.createRoot(container);
act(() => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);

window.event = 'test';
setState(1);
window.event = undefined;
setState(2);

expect(Scheduler).toFlushAndYield(['Count: 1']);
expect(counterRef.current.textContent).toBe('Count: 1');
global.flushRequestAnimationFrameQueue();
expect(Scheduler).toHaveYielded(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
});
});

it('regression test: does not drop passive effects across roots (#17066)', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1430,12 +1430,16 @@ describe('ReactDOMServerPartialHydration', () => {

// While we're part way through the hydration, we update the state.
// This will schedule an update on the children of the suspense boundary.
expect(() => updateText('Hi')).toErrorDev(
expect(() => {
act(() => {
updateText('Hi');
});
}).toErrorDev(
"Can't perform a React state update on a component that hasn't mounted yet.",
);

// This will throw it away and rerender.
expect(Scheduler).toFlushAndYield(['Child', 'Sibling']);
expect(Scheduler).toHaveYielded(['Child', 'Sibling']);

expect(container.textContent).toBe('Hello');

Expand Down
15 changes: 14 additions & 1 deletion packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,12 @@ export const cancelTimeout: any =
typeof clearTimeout === 'function' ? clearTimeout : (undefined: any);
export const noTimeout = -1;
const localPromise = typeof Promise === 'function' ? Promise : undefined;

const localRequestAnimationFrame =
typeof requestAnimationFrame === 'function'
? requestAnimationFrame
: undefined;
const localCancelAnimationFrame =
typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
// -------------------
// Microtasks
// -------------------
Expand All @@ -400,6 +405,14 @@ export const scheduleMicrotask: any =
.catch(handleErrorInNextTick)
: scheduleTimeout; // TODO: Determine the best fallback here.

// -------------------
// requestAnimationFrame
// -------------------
export const supportsAnimationFrame =
typeof localRequestAnimationFrame !== 'undefined';
export const scheduleAnimationFrame: any = localRequestAnimationFrame;
export const cancelAnimationFrame: any = localCancelAnimationFrame;

function handleErrorInNextTick(error) {
setTimeout(() => {
throw error;
Expand Down
4 changes: 4 additions & 0 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
})
: setTimeout,

supportsAnimationFrame: false,
scheduleAnimationFrame: undefined,
cancelAnimationFrame: undefined,

prepareForCommit(): null | Object {
return null;
},
Expand Down
12 changes: 12 additions & 0 deletions packages/react-reconciler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,18 @@ Set this to true to indicate that your renderer supports `scheduleMicrotask`. We

Optional. You can proxy this to `queueMicrotask` or its equivalent in your environment.

#### `supportsAnimationFrame`

TODO

#### `scheduleAnimationFrame(fn)`

TODO

#### `cancelAnimationFrame(fn)`

TODO

#### `isPrimaryRenderer`

This is a property (not a function) that should be set to `true` if your renderer is the main one on the page. For example, if you're writing a renderer for the Terminal, it makes sense to set it to `true`, but if your renderer is used *on top of* React DOM or some other existing renderer, set it to `false`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ function shim(...args: any) {
// Test selectors (when unsupported)
export const supportsMicrotasks = false;
export const scheduleMicrotask = shim;

// Test selectors (when unsupported)
export const supportsAnimationFrame = false;
export const scheduleAnimationFrame = shim;
export const cancelAnimationFrame = shim;
49 changes: 47 additions & 2 deletions packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
enableUpdaterTracking,
enableCache,
enableTransitionTracing,
enableFameEndScheduling,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import is from 'shared/objectIs';
Expand Down Expand Up @@ -79,6 +80,9 @@ import {
afterActiveInstanceBlur,
getCurrentEventPriority,
supportsMicrotasks,
supportsAnimationFrame,
scheduleAnimationFrame,
cancelAnimationFrame,
errorHydratingContainer,
scheduleMicrotask,
} from './ReactFiberHostConfig';
Expand Down Expand Up @@ -149,6 +153,7 @@ import {
movePendingFibersToMemoized,
addTransitionToLanesMap,
getTransitionsForLanes,
DefaultLane,
} from './ReactFiberLane.new';
import {
DiscreteEventPriority,
Expand Down Expand Up @@ -750,6 +755,7 @@ export function isInterleavedUpdate(fiber: Fiber, lane: Lane) {
// exiting a task.
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
const existingFrameAlignedNode = root.frameAlignedNode;

// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
Expand Down Expand Up @@ -804,13 +810,22 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
return;
}

if (existingCallbackNode != null) {
if (existingCallbackNode !== null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}

if (
enableFameEndScheduling &&
cancelAnimationFrame != null &&
existingFrameAlignedNode != null
) {
cancelAnimationFrame(existingFrameAlignedNode);
}

// Schedule a new callback.
let newCallbackNode;
let newFrameAlignedNode;
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
Expand Down Expand Up @@ -850,6 +865,31 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
}
newCallbackNode = null;
} else if (
enableFameEndScheduling &&
supportsAnimationFrame &&
newCallbackPriority === DefaultLane
) {
if (existingCallbackPriority === -1) {
// Do nothing, batch Default updates in the existing rAF.
} else if (
typeof window !== 'undefined' &&
typeof window.event === 'undefined'
) {
// Schedule both tasks, we'll race them and use the first to fire.
newFrameAlignedNode = scheduleAnimationFrame(
performConcurrentWorkOnRoot.bind(null, root),
);
newCallbackNode = scheduleCallback(
NormalSchedulerPriority,
performConcurrentWorkOnRoot.bind(null, root),
);
} else {
newCallbackNode = scheduleCallback(
NormalSchedulerPriority,
performConcurrentWorkOnRoot.bind(null, root),
);
}
} else {
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
Expand Down Expand Up @@ -877,6 +917,9 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {

root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
if (enableFameEndScheduling) {
root.frameAlignedNode = newFrameAlignedNode;
}
}

// This is the entry point for every concurrent task, i.e. anything that
Expand Down Expand Up @@ -932,7 +975,9 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
(disableSchedulerTimeoutInWorkLoop || !didTimeout) &&
enableFameEndScheduling &&
root.frameAlignedNode !== null;
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
Expand Down

0 comments on commit 1ab4eaa

Please sign in to comment.