Skip to content
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

Revert yieldy behavior for non-use Suspense #25537

Merged
merged 2 commits into from
Oct 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/react-reconciler/src/ReactFiberHooks.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ import {now} from './Scheduler';
import {
trackUsedThenable,
getPreviouslyUsedThenableAtIndex,
} from './ReactFiberWakeable.new';
} from './ReactFiberThenable.new';

const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;

Expand Down Expand Up @@ -783,6 +783,7 @@ function use<T>(usable: Usable<T>): T {
const index = thenableIndexCounter;
thenableIndexCounter += 1;

// TODO: Unify this switch statement with the one in trackUsedThenable.
switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
Expand Down
3 changes: 2 additions & 1 deletion packages/react-reconciler/src/ReactFiberHooks.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ import {now} from './Scheduler';
import {
trackUsedThenable,
getPreviouslyUsedThenableAtIndex,
} from './ReactFiberWakeable.old';
} from './ReactFiberThenable.old';

const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;

Expand Down Expand Up @@ -783,6 +783,7 @@ function use<T>(usable: Usable<T>): T {
const index = thenableIndexCounter;
thenableIndexCounter += 1;

// TODO: Unify this switch statement with the one in trackUsedThenable.
switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import type {
Wakeable,
Thenable,
PendingThenable,
FulfilledThenable,
Expand All @@ -18,14 +17,8 @@ import type {
import ReactSharedInternals from 'shared/ReactSharedInternals';
const {ReactCurrentActQueue} = ReactSharedInternals;

let suspendedThenable: Thenable<mixed> | null = null;
let adHocSuspendCount: number = 0;

// TODO: Sparse arrays are bad for performance.
let suspendedThenable: Thenable<any> | null = null;
let usedThenables: Array<Thenable<any> | void> | null = null;
let lastUsedThenable: Thenable<any> | null = null;

const MAX_AD_HOC_SUSPEND_COUNT = 50;

export function isTrackingSuspendedThenable(): boolean {
return suspendedThenable !== null;
Expand All @@ -39,22 +32,17 @@ export function suspendedThenableDidResolve(): boolean {
return false;
}

export function trackSuspendedWakeable(wakeable: Wakeable) {
// If this wakeable isn't already a thenable, turn it into one now. Then,
// when we resume the work loop, we can check if its status is
// still pending.
// TODO: Get rid of the Wakeable type? It's superseded by UntrackedThenable.
const thenable: Thenable<mixed> = (wakeable: any);
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
if (__DEV__ && ReactCurrentActQueue.current !== null) {
ReactCurrentActQueue.didUsePromise = true;
}

if (thenable !== lastUsedThenable) {
// If this wakeable was not just `use`-d, it must be an ad hoc wakeable
// that was thrown by an older Suspense implementation. Keep a count of
// these so that we can detect an infinite ping loop.
// TODO: Once `use` throws an opaque signal instead of the actual thenable,
// a better way to count ad hoc suspends is whether an actual thenable
// is caught by the work loop.
adHocSuspendCount++;
if (usedThenables === null) {
usedThenables = [thenable];
} else {
usedThenables[index] = thenable;
}

suspendedThenable = thenable;

// We use an expando to track the status and result of a thenable so that we
Expand Down Expand Up @@ -105,34 +93,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {

export function resetWakeableStateAfterEachAttempt() {
suspendedThenable = null;
adHocSuspendCount = 0;
lastUsedThenable = null;
}

export function resetThenableStateOnCompletion() {
usedThenables = null;
}

export function throwIfInfinitePingLoopDetected() {
if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
// TODO: Guard against an infinite loop by throwing an error if the same
// component suspends too many times in a row. This should be thrown from
// the render phase so that it gets the component stack.
}
}

export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
if (usedThenables === null) {
usedThenables = [];
}
usedThenables[index] = thenable;
lastUsedThenable = thenable;

if (__DEV__ && ReactCurrentActQueue.current !== null) {
ReactCurrentActQueue.didUsePromise = true;
}
}

export function getPreviouslyUsedThenableAtIndex<T>(
index: number,
): Thenable<T> | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import type {
Wakeable,
Thenable,
PendingThenable,
FulfilledThenable,
Expand All @@ -18,14 +17,8 @@ import type {
import ReactSharedInternals from 'shared/ReactSharedInternals';
const {ReactCurrentActQueue} = ReactSharedInternals;

let suspendedThenable: Thenable<mixed> | null = null;
let adHocSuspendCount: number = 0;

// TODO: Sparse arrays are bad for performance.
let suspendedThenable: Thenable<any> | null = null;
let usedThenables: Array<Thenable<any> | void> | null = null;
let lastUsedThenable: Thenable<any> | null = null;

const MAX_AD_HOC_SUSPEND_COUNT = 50;

export function isTrackingSuspendedThenable(): boolean {
return suspendedThenable !== null;
Expand All @@ -39,22 +32,17 @@ export function suspendedThenableDidResolve(): boolean {
return false;
}

export function trackSuspendedWakeable(wakeable: Wakeable) {
// If this wakeable isn't already a thenable, turn it into one now. Then,
// when we resume the work loop, we can check if its status is
// still pending.
// TODO: Get rid of the Wakeable type? It's superseded by UntrackedThenable.
const thenable: Thenable<mixed> = (wakeable: any);
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
if (__DEV__ && ReactCurrentActQueue.current !== null) {
ReactCurrentActQueue.didUsePromise = true;
}

if (thenable !== lastUsedThenable) {
// If this wakeable was not just `use`-d, it must be an ad hoc wakeable
// that was thrown by an older Suspense implementation. Keep a count of
// these so that we can detect an infinite ping loop.
// TODO: Once `use` throws an opaque signal instead of the actual thenable,
// a better way to count ad hoc suspends is whether an actual thenable
// is caught by the work loop.
adHocSuspendCount++;
if (usedThenables === null) {
usedThenables = [thenable];
} else {
usedThenables[index] = thenable;
}

suspendedThenable = thenable;

// We use an expando to track the status and result of a thenable so that we
Expand Down Expand Up @@ -105,34 +93,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {

export function resetWakeableStateAfterEachAttempt() {
suspendedThenable = null;
adHocSuspendCount = 0;
lastUsedThenable = null;
}

export function resetThenableStateOnCompletion() {
usedThenables = null;
}

export function throwIfInfinitePingLoopDetected() {
if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
// TODO: Guard against an infinite loop by throwing an error if the same
// component suspends too many times in a row. This should be thrown from
// the render phase so that it gets the component stack.
}
}

export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
if (usedThenables === null) {
usedThenables = [];
}
usedThenables[index] = thenable;
lastUsedThenable = thenable;

if (__DEV__ && ReactCurrentActQueue.current !== null) {
ReactCurrentActQueue.didUsePromise = true;
}
}

export function getPreviouslyUsedThenableAtIndex<T>(
index: number,
): Thenable<T> | null {
Expand Down
20 changes: 6 additions & 14 deletions packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,9 @@ import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new
import {
resetWakeableStateAfterEachAttempt,
resetThenableStateOnCompletion,
trackSuspendedWakeable,
suspendedThenableDidResolve,
isTrackingSuspendedThenable,
} from './ReactFiberWakeable.new';
} from './ReactFiberThenable.new';
import {schedulePostPaintCallback} from './ReactPostPaintCallback';

const ceil = Math.ceil;
Expand Down Expand Up @@ -1739,11 +1738,6 @@ function handleThrow(root, thrownValue): void {
return;
}

const isWakeable =
thrownValue !== null &&
typeof thrownValue === 'object' &&
typeof thrownValue.then === 'function';

if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
// Record the time spent rendering before an error was thrown. This
// avoids inaccurate Profiler durations in the case of a
Expand All @@ -1753,7 +1747,11 @@ function handleThrow(root, thrownValue): void {

if (enableSchedulingProfiler) {
markComponentRenderStopped();
if (isWakeable) {
if (
thrownValue !== null &&
typeof thrownValue === 'object' &&
typeof thrownValue.then === 'function'
) {
const wakeable: Wakeable = (thrownValue: any);
markComponentSuspended(
erroredWork,
Expand All @@ -1768,12 +1766,6 @@ function handleThrow(root, thrownValue): void {
);
}
}

if (isWakeable) {
const wakeable: Wakeable = (thrownValue: any);

trackSuspendedWakeable(wakeable);
}
}

function pushDispatcher(container) {
Expand Down
20 changes: 6 additions & 14 deletions packages/react-reconciler/src/ReactFiberWorkLoop.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,9 @@ import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old
import {
resetWakeableStateAfterEachAttempt,
resetThenableStateOnCompletion,
trackSuspendedWakeable,
suspendedThenableDidResolve,
isTrackingSuspendedThenable,
} from './ReactFiberWakeable.old';
} from './ReactFiberThenable.old';
import {schedulePostPaintCallback} from './ReactPostPaintCallback';

const ceil = Math.ceil;
Expand Down Expand Up @@ -1739,11 +1738,6 @@ function handleThrow(root, thrownValue): void {
return;
}

const isWakeable =
thrownValue !== null &&
typeof thrownValue === 'object' &&
typeof thrownValue.then === 'function';

if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
// Record the time spent rendering before an error was thrown. This
// avoids inaccurate Profiler durations in the case of a
Expand All @@ -1753,7 +1747,11 @@ function handleThrow(root, thrownValue): void {

if (enableSchedulingProfiler) {
markComponentRenderStopped();
if (isWakeable) {
if (
thrownValue !== null &&
typeof thrownValue === 'object' &&
typeof thrownValue.then === 'function'
) {
const wakeable: Wakeable = (thrownValue: any);
markComponentSuspended(
erroredWork,
Expand All @@ -1768,12 +1766,6 @@ function handleThrow(root, thrownValue): void {
);
}
}

if (isWakeable) {
const wakeable: Wakeable = (thrownValue: any);

trackSuspendedWakeable(wakeable);
}
}

function pushDispatcher(container) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,32 +485,22 @@ describe('ReactOffscreen', () => {
// In the same render, also hide the offscreen tree.
root.render(<App show={false} />);

if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(Scheduler).toFlushUntilNextPaint([
// The outer update will commit, but the inner update is deferred until
// a later render.
'Outer: 1',

// Something suspended. This means we won't commit immediately; there
// will be an async gap between render and commit. In this test, we will
// use this property to schedule a concurrent update. The fact that
// we're using Suspense to schedule a concurrent update is not directly
// relevant to the test — we could also use time slicing, but I've
// chosen to use Suspense the because implementation details of time
// slicing are more volatile.
'Suspend! [Async: 1]',

'Loading...',
]);
} else {
// When default updates are time sliced, React yields before preparing
// the fallback.
expect(Scheduler).toFlushUntilNextPaint([
'Outer: 1',
'Suspend! [Async: 1]',
]);
expect(Scheduler).toFlushUntilNextPaint(['Loading...']);
}
expect(Scheduler).toFlushUntilNextPaint([
// The outer update will commit, but the inner update is deferred until
// a later render.
'Outer: 1',

// Something suspended. This means we won't commit immediately; there
// will be an async gap between render and commit. In this test, we will
// use this property to schedule a concurrent update. The fact that
// we're using Suspense to schedule a concurrent update is not directly
// relevant to the test — we could also use time slicing, but I've
// chosen to use Suspense the because implementation details of time
// slicing are more volatile.
'Suspend! [Async: 1]',

'Loading...',
]);

// Assert that we haven't committed quite yet
expect(root).toMatchRenderedOutput(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3874,6 +3874,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Suspend! [A2]',
'Loading...',
'Suspend! [B2]',
'Loading...',
]);
expect(root).toMatchRenderedOutput(
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ describe('ReactWakeable', () => {
return props.text;
}

// This behavior was intentionally disabled to derisk the rollout of `use`.
// It changes the behavior of old, pre-`use` Suspense implementations. We may
// add this back; however, the plan is to migrate all existing Suspense code
// to `use`, so the extra code probably isn't worth it.
// @gate TODO
test('if suspended fiber is pinged in a microtask, retry immediately without unwinding the stack', async () => {
let resolved = false;
function Async() {
Expand Down
Loading