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

Interaction tracing works across hidden and SSR hydration boundaries #15872

Merged
13 changes: 12 additions & 1 deletion packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
debugRenderPhaseSideEffects,
debugRenderPhaseSideEffectsForStrictMode,
enableProfilerTimer,
enableSchedulerTracing,
enableSuspenseServerRenderer,
enableEventAPI,
} from 'shared/ReactFeatureFlags';
Expand Down Expand Up @@ -164,7 +165,11 @@ import {
createWorkInProgress,
isSimpleFunctionComponent,
} from './ReactFiber';
import {requestCurrentTime, retryTimedOutBoundary} from './ReactFiberWorkLoop';
import {
markDidDeprioritizeIdleSubtree,
requestCurrentTime,
retryTimedOutBoundary,
} from './ReactFiberWorkLoop';

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;

Expand Down Expand Up @@ -988,6 +993,9 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) {
renderExpirationTime !== Never &&
shouldDeprioritizeSubtree(type, nextProps)
) {
if (enableSchedulerTracing) {
markDidDeprioritizeIdleSubtree();
}
// Schedule this fiber to re-render at offscreen priority. Then bailout.
workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
return null;
Expand Down Expand Up @@ -2265,6 +2273,9 @@ function beginWork(
renderExpirationTime !== Never &&
shouldDeprioritizeSubtree(workInProgress.type, newProps)
) {
if (enableSchedulerTracing) {
markDidDeprioritizeIdleSubtree();
}
// Schedule this fiber to re-render at offscreen priority. Then bailout.
workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
return null;
Expand Down
5 changes: 5 additions & 0 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@ import {
popHydrationState,
} from './ReactFiberHydrationContext';
import {
enableSchedulerTracing,
enableSuspenseServerRenderer,
enableEventAPI,
} from 'shared/ReactFeatureFlags';
import {
markDidDeprioritizeIdleSubtree,
renderDidSuspend,
renderDidSuspendDelayIfPossible,
} from './ReactFiberWorkLoop';
Expand Down Expand Up @@ -815,6 +817,9 @@ function completeWork(
'A dehydrated suspense component was completed without a hydrated node. ' +
'This is probably a bug in React.',
);
if (enableSchedulerTracing) {
markDidDeprioritizeIdleSubtree();
}
skipPastDehydratedSuspenseInstance(workInProgress);
} else if ((workInProgress.effectTag & DidCapture) === NoEffect) {
// This boundary did not suspend so it's now hydrated.
Expand Down
69 changes: 52 additions & 17 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ let nestedPassiveUpdateCount: number = 0;

let interruptedBy: Fiber | null = null;

// Marks the need to reschedule pending interactions at Never priority during the commit phase.
// This enables them to be traced accross hidden boundaries or suspended SSR hydration.
let didDeprioritizeIdleSubtree: boolean = false;

// Expiration times are computed by adding to the current time (the start
// time). However, if two updates are scheduled within the same event, we
// should treat their start times as simultaneous, even if the actual clock
Expand Down Expand Up @@ -375,7 +379,7 @@ export function scheduleUpdateOnFiber(
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// Register pending interactions on the root to avoid losing traced interaction data.
schedulePendingInteraction(root, expirationTime);
schedulePendingInteractions(root, expirationTime);

// This is a legacy edge case. The initial mount of a ReactDOM.render-ed
// root inside of batchedUpdates should be synchronous, but layout updates
Expand Down Expand Up @@ -538,9 +542,8 @@ function scheduleCallbackForRoot(
}
}

// Add the current set of interactions to the pending set associated with
// this root.
schedulePendingInteraction(root, expirationTime);
// Associate the current interactions with this new root+priority.
schedulePendingInteractions(root, expirationTime);
}

function runRootCallback(root, callback, isSync) {
Expand Down Expand Up @@ -778,6 +781,10 @@ function prepareFreshStack(root, expirationTime) {
workInProgressRootCanSuspendUsingConfig = null;
workInProgressRootHasPendingPing = false;

if (enableSchedulerTracing) {
didDeprioritizeIdleSubtree = false;
}

if (__DEV__) {
ReactStrictModeWarnings.discardPendingWarnings();
componentsWithSuspendedDiscreteUpdates = null;
Expand Down Expand Up @@ -817,7 +824,7 @@ function renderRoot(
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
prepareFreshStack(root, expirationTime);
startWorkOnPendingInteraction(root, expirationTime);
startWorkOnPendingInteractions(root, expirationTime);
} else if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// We could've received an update at a lower priority while we yielded.
// We're suspended in a delayed state. Once we complete this render we're
Expand Down Expand Up @@ -1675,19 +1682,14 @@ function commitRootImpl(root) {

stopCommitTimer();

const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;

if (rootDoesHavePassiveEffects) {
// This commit has passive effects. Stash a reference to them. But don't
// schedule a callback until after flushing layout work.
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsExpirationTime = expirationTime;
} else {
if (enableSchedulerTracing) {
// If there are no passive effects, then we can complete the pending
// interactions. Otherwise, we'll wait until after the passive effects
// are flushed.
finishPendingInteractions(root, expirationTime);
}
}

// Check if there's remaining work on this root
Expand All @@ -1698,13 +1700,31 @@ function commitRootImpl(root) {
currentTime,
remainingExpirationTime,
);

if (enableSchedulerTracing) {
if (didDeprioritizeIdleSubtree) {
didDeprioritizeIdleSubtree = false;
scheduleInteractions(root, Never, root.memoizedInteractions);
}
}

scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime);
} else {
// If there's no remaining work, we can clear the set of already failed
// error boundaries.
legacyErrorBoundariesThatAlreadyFailed = null;
}

if (enableSchedulerTracing) {
if (!rootDidHavePassiveEffects) {
// If there are no passive effects, then we can complete the pending interactions.
// Otherwise, we'll wait until after the passive effects are flushed.
// Wait to do this until after remaining work has been scheduled,
// so that we don't prematurely signal complete for interactions when there's e.g. hidden work.
finishPendingInteractions(root, expirationTime);
}
}

onCommitRoot(finishedWork.stateNode, expirationTime);

if (remainingExpirationTime === Sync) {
Expand Down Expand Up @@ -2507,14 +2527,18 @@ function computeThreadID(root, expirationTime) {
return expirationTime * 1000 + root.interactionThreadID;
}

function schedulePendingInteraction(root, expirationTime) {
// This is called when work is scheduled on a root. It sets up a pending
// interaction, which is completed once the work commits.
export function markDidDeprioritizeIdleSubtree() {
if (!enableSchedulerTracing) {
return;
}
didDeprioritizeIdleSubtree = true;
}

function scheduleInteractions(root, expirationTime, interactions) {
if (!enableSchedulerTracing) {
return;
}

const interactions = __interactionsRef.current;
if (interactions.size > 0) {
const pendingInteractionMap = root.pendingInteractionMap;
const pendingInteractions = pendingInteractionMap.get(expirationTime);
Expand Down Expand Up @@ -2544,7 +2568,18 @@ function schedulePendingInteraction(root, expirationTime) {
}
}

function startWorkOnPendingInteraction(root, expirationTime) {
function schedulePendingInteractions(root, expirationTime) {
// This is called when work is scheduled on a root.
// It associates the current interactions with the newly-scheduled expiration.
// They will be restored when that expiration is later committed.
if (!enableSchedulerTracing) {
return;
}

scheduleInteractions(root, expirationTime, __interactionsRef.current);
}

function startWorkOnPendingInteractions(root, expirationTime) {
// This is called when new work is started on a root.
if (!enableSchedulerTracing) {
return;
Expand Down