diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 61bfd5cf7f..6e0ebc4e98 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -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 @@ -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, + ); } } } diff --git a/packages/react-reconciler/src/ReactFiberRootScheduler.js b/packages/react-reconciler/src/ReactFiberRootScheduler.js index dcaadc5a6e..e3791f4c8d 100644 --- a/packages/react-reconciler/src/ReactFiberRootScheduler.js +++ b/packages/react-reconciler/src/ReactFiberRootScheduler.js @@ -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(); } } @@ -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(); } + processRootScheduleInMicrotask(); +} + +function processRootScheduleInMicrotask() { // This function is always called inside a microtask. It should never be // called synchronously. didScheduleMicrotask = false; @@ -558,7 +562,7 @@ 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` @@ -566,7 +570,7 @@ function scheduleImmediateTask(cb: () => mixed) { // 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; }); } @@ -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, + ); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 8e75135ff6..133c91f518 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -236,6 +236,7 @@ import { blockingEventTime, blockingEventType, blockingEventIsRepeat, + blockingSpawnedUpdate, blockingSuspendedTime, transitionClampTime, transitionStartTime, @@ -1664,11 +1665,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 { @@ -1797,6 +1795,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { clampedEventTime, blockingEventType, blockingEventIsRepeat, + blockingSpawnedUpdate, renderStartTime, lanes, ); diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index d408ba0ff7..d3bdf6f6a9 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -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'; @@ -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; @@ -78,6 +81,9 @@ 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 ( @@ -85,6 +91,11 @@ export function startUpdateTimerByLane(lane: Lane): void { 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; @@ -141,6 +152,7 @@ export function clearBlockingTimers(): void { blockingUpdateTime = -1.1; blockingSuspendedTime = -1.1; blockingEventIsRepeat = true; + blockingSpawnedUpdate = false; } export function startAsyncTransitionTimer(): void {