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

[Fiber] Mark cascading updates #31866

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
19 changes: 13 additions & 6 deletions packages/react-reconciler/src/ReactFiberPerformanceTrack.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,11 +276,15 @@ export function logBlockingStart(
eventTime: number,
eventType: null | string,
eventIsRepeat: boolean,
isSpawnedUpdate: boolean,
renderStartTime: number,
lanes: Lanes,
): void {
if (supportsUserTiming) {
reusableLaneDevToolDetails.track = 'Blocking';
// If a blocking update was spawned within render or an effect, that's considered a cascading render.
// If you have a second blocking update within the same event, that suggests multiple flushSync or
// setState in a microtask which is also considered a cascade.
if (eventTime > 0 && eventType !== null) {
// Log the time from the event timeStamp until we called setState.
reusableLaneDevToolDetails.color = eventIsRepeat
Expand All @@ -295,14 +299,17 @@ export function logBlockingStart(
}
if (updateTime > 0) {
// Log the time from when we called setState until we started rendering.
reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes(
lanes,
)
? 'tertiary-light'
: 'primary-light';
reusableLaneDevToolDetails.color = isSpawnedUpdate
? 'error'
: includesOnlyHydrationOrOffscreenLanes(lanes)
? 'tertiary-light'
: 'primary-light';
reusableLaneOptions.start = updateTime;
reusableLaneOptions.end = renderStartTime;
performance.measure('Blocked', reusableLaneOptions);
performance.measure(
isSpawnedUpdate ? 'Cascade' : 'Blocked',
reusableLaneOptions,
);
}
}
}
Expand Down
26 changes: 18 additions & 8 deletions packages/react-reconciler/src/ReactFiberRootScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,12 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
// We're inside an `act` scope.
if (!didScheduleMicrotask_act) {
didScheduleMicrotask_act = true;
scheduleImmediateTask(processRootScheduleInMicrotask);
scheduleImmediateRootScheduleTask();
}
} else {
if (!didScheduleMicrotask) {
didScheduleMicrotask = true;
scheduleImmediateTask(processRootScheduleInMicrotask);
scheduleImmediateRootScheduleTask();
}
}

Expand Down Expand Up @@ -229,13 +229,17 @@ function flushSyncWorkAcrossRoots_impl(
isFlushingWork = false;
}

function processRootScheduleInMicrotask() {
function processRootScheduleInImmediateTask() {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
// Track the currently executing event if there is one so we can ignore this
// event when logging events.
trackSchedulerEvent();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to restructure this a bit so that we have two separate callbacks depending on if we do the Scheduler_scheduleCallback pass or the microtask pass. Because we shouldn't call trackSchedulerEvent() if we're inside a microtask callback because that could mark a real event as if it was a scheduler event (and therefore hidden).

}

processRootScheduleInMicrotask();
}

function processRootScheduleInMicrotask() {
// This function is always called inside a microtask. It should never be
// called synchronously.
didScheduleMicrotask = false;
Expand Down Expand Up @@ -558,15 +562,15 @@ function cancelCallback(callbackNode: mixed) {
}
}

function scheduleImmediateTask(cb: () => mixed) {
function scheduleImmediateRootScheduleTask() {
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
// Special case: Inside an `act` scope, we push microtasks to the fake `act`
// callback queue. This is because we currently support calling `act`
// without awaiting the result. The plan is to deprecate that, and require
// that you always await the result so that the microtasks have a chance to
// run. But it hasn't happened yet.
ReactSharedInternals.actQueue.push(() => {
cb();
processRootScheduleInMicrotask();
return null;
});
}
Expand All @@ -588,14 +592,20 @@ function scheduleImmediateTask(cb: () => mixed) {
// wrong semantically but it prevents an infinite loop. The bug is
// Safari's, not ours, so we just do our best to not crash even though
// the behavior isn't completely correct.
Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
Scheduler_scheduleCallback(
ImmediateSchedulerPriority,
processRootScheduleInImmediateTask,
);
return;
}
cb();
processRootScheduleInMicrotask();
});
} else {
// If microtasks are not supported, use Scheduler.
Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
Scheduler_scheduleCallback(
ImmediateSchedulerPriority,
processRootScheduleInImmediateTask,
);
}
}

Expand Down
9 changes: 4 additions & 5 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ import {
blockingEventTime,
blockingEventType,
blockingEventIsRepeat,
blockingSpawnedUpdate,
blockingSuspendedTime,
transitionClampTime,
transitionStartTime,
Expand Down Expand Up @@ -1663,11 +1664,8 @@ export function flushSyncWork(): boolean {

export function isAlreadyRendering(): boolean {
// Used by the renderer to print a warning if certain APIs are called from
// the wrong context.
return (
__DEV__ &&
(executionContext & (RenderContext | CommitContext)) !== NoContext
);
// the wrong context, and for profiling warnings.
return (executionContext & (RenderContext | CommitContext)) !== NoContext;
}

export function isInvalidExecutionContextForEventFunction(): boolean {
Expand Down Expand Up @@ -1796,6 +1794,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
clampedEventTime,
blockingEventType,
blockingEventIsRepeat,
blockingSpawnedUpdate,
renderStartTime,
lanes,
);
Expand Down
12 changes: 12 additions & 0 deletions packages/react-reconciler/src/ReactProfilerTimer.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
enableComponentPerformanceTrack,
} from 'shared/ReactFeatureFlags';

import {isAlreadyRendering} from './ReactFiberWorkLoop';

// Intentionally not named imports because Rollup would use dynamic dispatch for
// CommonJS interop named imports.
import * as Scheduler from 'scheduler';
Expand All @@ -50,6 +52,7 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
export let blockingEventTime: number = -1.1; // Event timeStamp of the first setState.
export let blockingEventType: null | string = null; // Event type of the first setState.
export let blockingEventIsRepeat: boolean = false;
export let blockingSpawnedUpdate: boolean = false;
export let blockingSuspendedTime: number = -1.1;
// TODO: This should really be one per Transition lane.
export let transitionClampTime: number = -0;
Expand Down Expand Up @@ -78,13 +81,21 @@ export function startUpdateTimerByLane(lane: Lane): void {
if (isSyncLane(lane) || isBlockingLane(lane)) {
if (blockingUpdateTime < 0) {
blockingUpdateTime = now();
if (isAlreadyRendering()) {
blockingSpawnedUpdate = true;
}
const newEventTime = resolveEventTimeStamp();
const newEventType = resolveEventType();
if (
newEventTime !== blockingEventTime ||
newEventType !== blockingEventType
) {
blockingEventIsRepeat = false;
} else if (newEventType !== null) {
// If this is a second update in the same event, we treat it as a spawned update.
// This might be a microtask spawned from useEffect, multiple flushSync or
// a setState in a microtask spawned after the first setState. Regardless it's bad.
blockingSpawnedUpdate = true;
}
blockingEventTime = newEventTime;
blockingEventType = newEventType;
Expand Down Expand Up @@ -141,6 +152,7 @@ export function clearBlockingTimers(): void {
blockingUpdateTime = -1.1;
blockingSuspendedTime = -1.1;
blockingEventIsRepeat = true;
blockingSpawnedUpdate = false;
}

export function startAsyncTransitionTimer(): void {
Expand Down
Loading