Skip to content

Re-arrange slightly to prevent refactor hazard #16743

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Sep 11, 2019
6 changes: 6 additions & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
@@ -177,6 +177,7 @@ import {
retryDehydratedSuspenseBoundary,
scheduleWork,
renderDidSuspendDelayIfPossible,
markUnprocessedUpdateTime,
} from './ReactFiberWorkLoop';

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -2708,6 +2709,11 @@ function bailoutOnAlreadyFinishedWork(
stopProfilerTimerIfRunning(workInProgress);
}

const updateExpirationTime = workInProgress.expirationTime;
if (updateExpirationTime !== NoWork) {
markUnprocessedUpdateTime(updateExpirationTime);
}

// Check if the children have any pending work.
const childExpirationTime = workInProgress.childExpirationTime;
if (childExpirationTime < renderExpirationTime) {
3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactFiberExpirationTime.js
Original file line number Diff line number Diff line change
@@ -21,7 +21,10 @@ import {
export type ExpirationTime = number;

export const NoWork = 0;
// TODO: Think of a better name for Never.
export const Never = 1;
// TODO: Use the Idle expiration time for idle state updates
export const Idle = 2;
export const Sync = MAX_SIGNED_31_BIT_INT;
export const Batched = Sync - 1;

3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactFiberHooks.js
Original file line number Diff line number Diff line change
@@ -43,6 +43,7 @@ import {
warnIfNotCurrentlyActingUpdatesInDev,
warnIfNotScopedWithMatchingAct,
markRenderEventTimeAndConfig,
markUnprocessedUpdateTime,
} from './ReactFiberWorkLoop';

import invariant from 'shared/invariant';
@@ -531,6 +532,7 @@ export function resetHooks(): void {
// This is used to reset the state of this module when a component throws.
// It's also called inside mountIndeterminateComponent if we determine the
// component is a module-style component.

renderExpirationTime = NoWork;
currentlyRenderingFiber = null;

@@ -755,6 +757,7 @@ function updateReducer<S, I, A>(
// Update the remaining priority in the queue.
if (updateExpirationTime > remainingExpirationTime) {
remainingExpirationTime = updateExpirationTime;
markUnprocessedUpdateTime(remainingExpirationTime);
}
} else {
// This update does have sufficient priority.
132 changes: 125 additions & 7 deletions packages/react-reconciler/src/ReactFiberRoot.js
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
import type {Thenable} from './ReactFiberWorkLoop';
import type {Interaction} from 'scheduler/src/Tracing';
import type {SuspenseHydrationCallbacks} from './ReactFiberSuspenseComponent';
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';

import {noTimeout} from './ReactFiberHostConfig';
import {createHostRootFiber} from './ReactFiber';
@@ -23,6 +24,7 @@ import {
enableSuspenseCallback,
} from 'shared/ReactFeatureFlags';
import {unstable_getThreadID} from 'scheduler/tracing';
import {NoPriority} from './SchedulerWithReactIntegration';

// TODO: This should be lifted into the renderer.
export type Batch = {
@@ -69,12 +71,20 @@ type BaseFiberRootProperties = {|
callbackNode: *,
// Expiration of the callback associated with this root
callbackExpirationTime: ExpirationTime,
// Priority of the callback associated with this root
callbackPriority: ReactPriorityLevel,
// The earliest pending expiration time that exists in the tree
firstPendingTime: ExpirationTime,
// The latest pending expiration time that exists in the tree
lastPendingTime: ExpirationTime,
// The time at which a suspended component pinged the root to render again
pingTime: ExpirationTime,
// The earliest suspended expiration time that exists in the tree
firstSuspendedTime: ExpirationTime,
// The latest suspended expiration time that exists in the tree
lastSuspendedTime: ExpirationTime,
// The next known expiration time after the suspended range
nextKnownPendingLevel: ExpirationTime,
// The latest time at which a suspended component pinged the root to
// render again
lastPingedTime: ExpirationTime,
lastExpiredTime: ExpirationTime,
|};

// The following attributes are only used by interaction tracing builds.
@@ -117,10 +127,13 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.hydrate = hydrate;
this.firstBatch = null;
this.callbackNode = null;
this.callbackExpirationTime = NoWork;
this.callbackPriority = NoPriority;
this.firstPendingTime = NoWork;
this.lastPendingTime = NoWork;
this.pingTime = NoWork;
this.firstSuspendedTime = NoWork;
this.lastSuspendedTime = NoWork;
this.nextKnownPendingLevel = NoWork;
this.lastPingedTime = NoWork;
this.lastExpiredTime = NoWork;

if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
@@ -151,3 +164,108 @@ export function createFiberRoot(

return root;
}

export function isRootSuspendedAtTime(
root: FiberRoot,
expirationTime: ExpirationTime,
): boolean {
const firstSuspendedTime = root.firstSuspendedTime;
const lastSuspendedTime = root.lastSuspendedTime;
return (
firstSuspendedTime !== NoWork &&
(firstSuspendedTime >= expirationTime &&
lastSuspendedTime <= expirationTime)
);
}

export function markRootSuspendedAtTime(
root: FiberRoot,
expirationTime: ExpirationTime,
): void {
const firstSuspendedTime = root.firstSuspendedTime;
const lastSuspendedTime = root.lastSuspendedTime;
if (firstSuspendedTime < expirationTime) {
root.firstSuspendedTime = expirationTime;
}
if (lastSuspendedTime > expirationTime || firstSuspendedTime === NoWork) {
root.lastSuspendedTime = expirationTime;
}

if (expirationTime <= root.lastPingedTime) {
root.lastPingedTime = NoWork;
}

if (expirationTime <= root.lastExpiredTime) {
root.lastExpiredTime = NoWork;
}
}

export function markRootUpdatedAtTime(
root: FiberRoot,
expirationTime: ExpirationTime,
): void {
// Update the range of pending times
const firstPendingTime = root.firstPendingTime;
if (expirationTime > firstPendingTime) {
root.firstPendingTime = expirationTime;
}

// Update the range of suspended times. Treat everything lower priority or
// equal to this update as unsuspended.
const firstSuspendedTime = root.firstSuspendedTime;
if (firstSuspendedTime !== NoWork) {
if (expirationTime >= firstSuspendedTime) {
// The entire suspended range is now unsuspended.
root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork;
} else if (expirationTime >= root.lastSuspendedTime) {
root.lastSuspendedTime = expirationTime + 1;
}

// This is a pending level. Check if it's higher priority than the next
// known pending level.
if (expirationTime > root.nextKnownPendingLevel) {
root.nextKnownPendingLevel = expirationTime;
}
}
}

export function markRootFinishedAtTime(
root: FiberRoot,
finishedExpirationTime: ExpirationTime,
remainingExpirationTime: ExpirationTime,
): void {
// Update the range of pending times
root.firstPendingTime = remainingExpirationTime;

// Update the range of suspended times. Treat everything higher priority or
// equal to this update as unsuspended.
if (finishedExpirationTime <= root.lastSuspendedTime) {
// The entire suspended range is now unsuspended.
root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork;
} else if (finishedExpirationTime <= root.firstSuspendedTime) {
// Part of the suspended range is now unsuspended. Narrow the range to
// include everything between the unsuspended time (non-inclusive) and the
// last suspended time.
root.firstSuspendedTime = finishedExpirationTime - 1;
}

if (finishedExpirationTime <= root.lastPingedTime) {
// Clear the pinged time
root.lastPingedTime = NoWork;
}

if (finishedExpirationTime <= root.lastExpiredTime) {
// Clear the expired time
root.lastExpiredTime = NoWork;
}
}

export function markRootExpiredAtTime(
root: FiberRoot,
expirationTime: ExpirationTime,
): void {
const lastExpiredTime = root.lastExpiredTime;
if (lastExpiredTime === NoWork || lastExpiredTime > expirationTime) {
root.lastExpiredTime = expirationTime;
}
}
571 changes: 344 additions & 227 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion packages/react-reconciler/src/ReactUpdateQueue.js
Original file line number Diff line number Diff line change
@@ -103,7 +103,10 @@ import {
} from 'shared/ReactFeatureFlags';

import {StrictMode} from './ReactTypeOfMode';
import {markRenderEventTimeAndConfig} from './ReactFiberWorkLoop';
import {
markRenderEventTimeAndConfig,
markUnprocessedUpdateTime,
} from './ReactFiberWorkLoop';

import invariant from 'shared/invariant';
import warningWithoutStack from 'shared/warningWithoutStack';
@@ -580,6 +583,7 @@ export function processUpdateQueue<State>(
// dealt with the props. Context in components that specify
// shouldComponentUpdate is tricky; but we'll have to account for
// that regardless.
markUnprocessedUpdateTime(newExpirationTime);
workInProgress.expirationTime = newExpirationTime;
workInProgress.memoizedState = resultState;

Original file line number Diff line number Diff line change
@@ -243,11 +243,20 @@ describe('ReactIncrementalErrorHandling', () => {
// This update is in a separate batch
ReactNoop.render(<App isBroken={false} />, onCommit);

expect(Scheduler).toFlushAndYield([
expect(Scheduler).toFlushAndYieldThrough([
// The first render fails. But because there's a lower priority pending
// update, it doesn't throw.
'error',
// Now we retry at the lower priority. This time it succeeds.
]);

// React will try to recover by rendering all the pending updates in a
// single batch, synchronously. This time it succeeds.
//
// This tells Scheduler to render a single unit of work. Because the render
// to recover from the error is synchronous, this should be enough to
// finish the rest of the work.
Scheduler.unstable_flushNumberOfYields(1);
expect(Scheduler).toHaveYielded([
'success',
// Nothing commits until the second update completes.
'commit',
@@ -256,54 +265,80 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildren()).toEqual([span('Everything is fine.')]);
});

it('on error, retries at a lower priority using the expiration of higher priority', () => {
class Parent extends React.Component {
state = {hideChild: false};
componentDidUpdate() {
Scheduler.unstable_yieldValue('commit: ' + this.state.hideChild);
}
render() {
if (this.state.hideChild) {
Scheduler.unstable_yieldValue('(empty)');
return <span prop="(empty)" />;
}
return <Child isBroken={this.props.childIsBroken} />;
it('does not include offscreen work when retrying after an error', () => {
function App(props) {
if (props.isBroken) {
Scheduler.unstable_yieldValue('error');
throw new Error('Oops!');
}
Scheduler.unstable_yieldValue('success');
return (
<>
Everything is fine
<div hidden={true}>
<div>Offscreen content</div>
</div>
</>
);
}

function Child(props) {
if (props.isBroken) {
Scheduler.unstable_yieldValue('Error!');
throw new Error('Error!');
}
Scheduler.unstable_yieldValue('Child');
return <span prop="Child" />;
function onCommit() {
Scheduler.unstable_yieldValue('commit');
}

function interrupt() {
ReactNoop.flushSync(() => {
ReactNoop.renderToRootWithID(null, 'other-root');
});
}

// Initial mount
const parent = React.createRef(null);
ReactNoop.render(<Parent ref={parent} childIsBroken={false} />);
expect(Scheduler).toFlushAndYield(['Child']);
expect(ReactNoop.getChildren()).toEqual([span('Child')]);
ReactNoop.render(<App isBroken={true} />, onCommit);
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYieldThrough(['error']);
interrupt();

// Schedule a low priority update to hide the child
parent.current.setState({hideChild: true});
expect(ReactNoop).toMatchRenderedOutput(null);

// Before the low priority update is flushed, synchronously trigger an
// error in the child.
ReactNoop.flushSync(() => {
ReactNoop.render(<Parent ref={parent} childIsBroken={true} />);
});
// This update is in a separate batch
ReactNoop.render(<App isBroken={false} />, onCommit);

expect(Scheduler).toFlushAndYieldThrough([
// The first render fails. But because there's a lower priority pending
// update, it doesn't throw.
'error',
]);

// React will try to recover by rendering all the pending updates in a
// single batch, synchronously. This time it succeeds.
//
// This tells Scheduler to render a single unit of work. Because the render
// to recover from the error is synchronous, this should be enough to
// finish the rest of the work.
Scheduler.unstable_flushNumberOfYields(1);
expect(Scheduler).toHaveYielded([
// First the sync update triggers an error
'Error!',
// Because there's a pending low priority update, we restart at the
// lower priority. This hides the children, suppressing the error.
'(empty)',
// Now the tree can commit.
'commit: true',
'success',
// Nothing commits until the second update completes.
'commit',
'commit',
]);
expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
// This should not include the offscreen content
expect(ReactNoop).toMatchRenderedOutput(
<>
Everything is fine
<div hidden={true} />
</>,
);

// The offscreen content finishes in a subsequent render
expect(Scheduler).toFlushAndYield([]);
expect(ReactNoop).toMatchRenderedOutput(
<>
Everything is fine
<div hidden={true}>
<div>Offscreen content</div>
</div>
</>,
);
});

it('retries one more time before handling error', () => {
@@ -346,10 +381,6 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildren()).toEqual([]);
});

// TODO: This is currently unobservable, but will be once we lift renderRoot
// and commitRoot into the renderer.
// it("does not retry synchronously if there's an update between complete and commit");

it('calls componentDidCatch multiple times for multiple errors', () => {
let id = 0;
class BadMount extends React.Component {
268 changes: 268 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
Original file line number Diff line number Diff line change
@@ -263,6 +263,274 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('AsyncAfter SuspenseSibling');
});

it(
'interrupts current render if something already suspended with a ' +
"delay, and then subsequently there's a lower priority update",
() => {
const root = ReactTestRenderer.create(
<>
<Suspense fallback={<Text text="Loading..." />} />
<Text text="Initial" />
</>,
{
unstable_isConcurrent: true,
},
);
expect(Scheduler).toFlushAndYield(['Initial']);
expect(root).toMatchRenderedOutput('Initial');

// The update will suspend.
root.update(
<>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" ms={2000} />
</Suspense>
<Text text="After Suspense" />
<Text text="Sibling" />
</>,
);

// Yield past the Suspense boundary but don't complete the last sibling.
expect(Scheduler).toFlushAndYieldThrough([
'Suspend! [Async]',
'Loading...',
'After Suspense',
]);

// Receives a lower priority update before the current render phase
// has completed.
Scheduler.unstable_advanceTime(1000);
root.update(
<>
<Suspense fallback={<Text text="Loading..." />} />
<Text text="Updated" />
</>,
);
expect(Scheduler).toHaveYielded([]);
expect(root).toMatchRenderedOutput('Initial');

// Render the update, instead of continuing
expect(Scheduler).toFlushAndYield(['Updated']);
expect(root).toMatchRenderedOutput('Updated');
},
);

it(
'interrupts current render when something suspends with a ' +
"delay and we've already skipped over a lower priority update in " +
'a parent',
() => {
function interrupt() {
// React has a heuristic to batch all updates that occur within the same
// event. This is a trick to circumvent that heuristic.
ReactTestRenderer.create('whatever');
}

function App({shouldSuspend, step}) {
return (
<>
<Text text={`A${step}`} />
<Suspense fallback={<Text text="Loading..." />}>
{shouldSuspend ? <AsyncText text="Async" ms={2000} /> : null}
</Suspense>
<Text text={`B${step}`} />
<Text text={`C${step}`} />
</>
);
}

const root = ReactTestRenderer.create(null, {
unstable_isConcurrent: true,
});

root.update(<App shouldSuspend={false} step={0} />);
expect(Scheduler).toFlushAndYield(['A0', 'B0', 'C0']);
expect(root).toMatchRenderedOutput('A0B0C0');

// This update will suspend.
root.update(<App shouldSuspend={true} step={1} />);

// Need to move into the next async bucket.
Scheduler.unstable_advanceTime(1000);
// Do a bit of work, then interrupt to trigger a restart.
expect(Scheduler).toFlushAndYieldThrough(['A1']);
interrupt();

// Schedule another update. This will have lower priority because of
// the interrupt trick above.
root.update(<App shouldSuspend={false} step={2} />);

expect(Scheduler).toFlushAndYieldThrough([
// Should have restarted the first update, because of the interruption
'A1',
'Suspend! [Async]',
'Loading...',
'B1',
]);

// Should not have committed loading state
expect(root).toMatchRenderedOutput('A0B0C0');

// After suspending, should abort the first update and switch to the
// second update. So, C1 should not appear in the log.
// TODO: This should work even if React does not yield to the main
// thread. Should use same mechanism as selective hydration to interrupt
// the render before the end of the current slice of work.
expect(Scheduler).toFlushAndYield(['A2', 'B2', 'C2']);

expect(root).toMatchRenderedOutput('A2B2C2');
},
);

it(
'interrupts current render when something suspends with a ' +
"delay and we've already bailed out lower priority update in " +
'a parent',
async () => {
// This is similar to the previous test case, except this covers when
// React completely bails out on the parent component, without processing
// the update queue.

const {useState} = React;

function interrupt() {
// React has a heuristic to batch all updates that occur within the same
// event. This is a trick to circumvent that heuristic.
ReactTestRenderer.create('whatever');
}

let setShouldSuspend;
function Async() {
const [shouldSuspend, _setShouldSuspend] = useState(false);
setShouldSuspend = _setShouldSuspend;
return (
<>
<Text text="A" />
<Suspense fallback={<Text text="Loading..." />}>
{shouldSuspend ? <AsyncText text="Async" ms={2000} /> : null}
</Suspense>
<Text text="B" />
<Text text="C" />
</>
);
}

let setShouldHideInParent;
function App() {
const [shouldHideInParent, _setShouldHideInParent] = useState(false);
setShouldHideInParent = _setShouldHideInParent;
Scheduler.unstable_yieldValue(
'shouldHideInParent: ' + shouldHideInParent,
);
return shouldHideInParent ? <Text text="(empty)" /> : <Async />;
}

const root = ReactTestRenderer.create(null, {
unstable_isConcurrent: true,
});

await ReactTestRenderer.act(async () => {
root.update(<App />);
expect(Scheduler).toFlushAndYield([
'shouldHideInParent: false',
'A',
'B',
'C',
]);
expect(root).toMatchRenderedOutput('ABC');

// This update will suspend.
setShouldSuspend(true);

// Need to move into the next async bucket.
Scheduler.unstable_advanceTime(1000);
// Do a bit of work, then interrupt to trigger a restart.
expect(Scheduler).toFlushAndYieldThrough(['A']);
interrupt();
// Should not have committed loading state
expect(root).toMatchRenderedOutput('ABC');

// Schedule another update. This will have lower priority because of
// the interrupt trick above.
setShouldHideInParent(true);

expect(Scheduler).toFlushAndYieldThrough([
// Should have restarted the first update, because of the interruption
'A',
'Suspend! [Async]',
'Loading...',
'B',
]);

// Should not have committed loading state
expect(root).toMatchRenderedOutput('ABC');

// After suspending, should abort the first update and switch to the
// second update.
expect(Scheduler).toFlushAndYield([
'shouldHideInParent: true',
'(empty)',
]);

expect(root).toMatchRenderedOutput('(empty)');
});
},
);

it(
'interrupts current render when something suspends with a ' +
'delay, and a parent received an update after it completed',
() => {
function App({shouldSuspend, step}) {
return (
<>
<Text text={`A${step}`} />
<Suspense fallback={<Text text="Loading..." />}>
{shouldSuspend ? <AsyncText text="Async" ms={2000} /> : null}
</Suspense>
<Text text={`B${step}`} />
<Text text={`C${step}`} />
</>
);
}

const root = ReactTestRenderer.create(null, {
unstable_isConcurrent: true,
});

root.update(<App shouldSuspend={false} step={0} />);
expect(Scheduler).toFlushAndYield(['A0', 'B0', 'C0']);
expect(root).toMatchRenderedOutput('A0B0C0');

// This update will suspend.
root.update(<App shouldSuspend={true} step={1} />);
// Flush past the root, but stop before the async component.
expect(Scheduler).toFlushAndYieldThrough(['A1']);

// Schedule an update on the root, which already completed.
root.update(<App shouldSuspend={false} step={2} />);
// We'll keep working on the existing update.
expect(Scheduler).toFlushAndYieldThrough([
// Now the async component suspends
'Suspend! [Async]',
'Loading...',
'B1',
]);

// Should not have committed loading state
expect(root).toMatchRenderedOutput('A0B0C0');

// After suspending, should abort the first update and switch to the
// second update. So, C1 should not appear in the log.
// TODO: This should work even if React does not yield to the main
// thread. Should use same mechanism as selective hydration to interrupt
// the render before the end of the current slice of work.
expect(Scheduler).toFlushAndYield(['A2', 'B2', 'C2']);

expect(root).toMatchRenderedOutput('A2B2C2');
},
);

it('mounts a lazy class component in non-concurrent mode', async () => {
class Class extends React.Component {
componentDidMount() {
Original file line number Diff line number Diff line change
@@ -494,7 +494,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
return <Text text="(empty)" />;
}
return (
<Suspense>
<Suspense fallback="Loading...">
<AsyncText ms={2000} text="Async" />
</Suspense>
);
@@ -518,6 +518,71 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
});

it('tries each subsequent level after suspending', async () => {
const root = ReactNoop.createRoot();

function App({step, shouldSuspend}) {
return (
<Suspense fallback="Loading...">
<Text text="Sibling" />
{shouldSuspend ? (
<AsyncText ms={10000} text={'Step ' + step} />
) : (
<Text text={'Step ' + step} />
)}
</Suspense>
);
}

function interrupt() {
// React has a heuristic to batch all updates that occur within the same
// event. This is a trick to circumvent that heuristic.
ReactNoop.flushSync(() => {
ReactNoop.renderToRootWithID(null, 'other-root');
});
}

// Mount the Suspense boundary without suspending, so that the subsequent
// updates suspend with a delay.
await ReactNoop.act(async () => {
root.render(<App step={0} shouldSuspend={false} />);
});
await advanceTimers(1000);
expect(Scheduler).toHaveYielded(['Sibling', 'Step 0']);

// Schedule an update at several distinct expiration times
await ReactNoop.act(async () => {
root.render(<App step={1} shouldSuspend={true} />);
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
interrupt();

root.render(<App step={2} shouldSuspend={true} />);
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
interrupt();

root.render(<App step={3} shouldSuspend={true} />);
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
interrupt();

root.render(<App step={4} shouldSuspend={false} />);
});

// Should suspend at each distinct level
expect(Scheduler).toHaveYielded([
'Sibling',
'Suspend! [Step 1]',
'Sibling',
'Suspend! [Step 2]',
'Sibling',
'Suspend! [Step 3]',
'Sibling',
'Step 4',
]);
});

it('forces an expiration after an update times out', async () => {
ReactNoop.render(
<Fragment>
Original file line number Diff line number Diff line change
@@ -422,7 +422,10 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] =
`;

exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
"⚛ (Committing Changes)
"⚛ (React Tree Reconciliation: Completed Root)
⚛ Foo [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)