From d6e433899f387be42a3cec2115b4607f32910a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 20 Aug 2020 17:39:29 -0400 Subject: [PATCH] Use Global Render Timeout for CPU Suspense (#19643) * Use Retry lane for resuming CPU suspended work * Use a global render timeout for CPU suspense heuristics * Fix profiler test since we're now reading time more often * Sync to new reconciler * Test synchronously rerendering should not render more rows --- .../src/ReactFiberBeginWork.new.js | 2 - .../src/ReactFiberBeginWork.old.js | 2 - .../src/ReactFiberCompleteWork.new.js | 49 +++--- .../src/ReactFiberCompleteWork.old.js | 60 +++++--- .../react-reconciler/src/ReactFiberLane.js | 2 + .../src/ReactFiberSuspenseComponent.new.js | 4 +- .../src/ReactFiberSuspenseComponent.old.js | 4 +- .../src/ReactFiberWorkLoop.new.js | 25 ++++ .../src/ReactFiberWorkLoop.old.js | 25 ++++ .../src/__tests__/ReactSuspenseList-test.js | 141 ++++++++++++++++++ .../__tests__/ReactProfiler-test.internal.js | 1 + 11 files changed, 265 insertions(+), 50 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 5dbb49e3a7c0..dd24521b1c6c 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -2644,7 +2644,6 @@ function initSuspenseListRenderState( renderingStartTime: 0, last: lastContentRow, tail: tail, - tailExpiration: 0, tailMode: tailMode, lastEffect: lastEffectBeforeRendering, }: SuspenseListRenderState); @@ -2655,7 +2654,6 @@ function initSuspenseListRenderState( renderState.renderingStartTime = 0; renderState.last = lastContentRow; renderState.tail = tail; - renderState.tailExpiration = 0; renderState.tailMode = tailMode; renderState.lastEffect = lastEffectBeforeRendering; } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 163aedb4d66b..1888c03c910e 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -2635,7 +2635,6 @@ function initSuspenseListRenderState( renderingStartTime: 0, last: lastContentRow, tail: tail, - tailExpiration: 0, tailMode: tailMode, lastEffect: lastEffectBeforeRendering, }: SuspenseListRenderState); @@ -2646,7 +2645,6 @@ function initSuspenseListRenderState( renderState.renderingStartTime = 0; renderState.last = lastContentRow; renderState.tail = tail; - renderState.tailExpiration = 0; renderState.tailMode = tailMode; renderState.lastEffect = lastEffectBeforeRendering; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index 4f833fa7c6c3..2e7f0b72593b 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -137,9 +137,10 @@ import { renderDidSuspendDelayIfPossible, renderHasNotSuspendedYet, popRenderLanes, + getRenderTargetTime, } from './ReactFiberWorkLoop.new'; import {createFundamentalStateInstance} from './ReactFiberFundamental.new'; -import {OffscreenLane} from './ReactFiberLane'; +import {OffscreenLane, SomeRetryLane} from './ReactFiberLane'; import {resetChildFibers} from './ReactChildFiber.new'; import {createScopeInstance} from './ReactFiberScope.new'; import {transferActualDuration} from './ReactProfilerTimer.new'; @@ -1076,6 +1077,29 @@ function completeWork( row = row.sibling; } } + + if (renderState.tail !== null && now() > getRenderTargetTime()) { + // We have already passed our CPU deadline but we still have rows + // left in the tail. We'll just give up further attempts to render + // the main content and only render fallbacks. + workInProgress.effectTag |= DidCapture; + didSuspendAlready = true; + + cutOffTailIfNeeded(renderState, false); + + // Since nothing actually suspended, there will nothing to ping this + // to get it started back up to attempt the next item. While in terms + // of priority this work has the same priority as this current render, + // it's not part of the same transition once the transition has + // committed. If it's sync, we still want to yield so that it can be + // painted. Conceptually, this is really the same as pinging. + // We can use any RetryLane even if it's the one currently rendering + // since we're leaving it behind on this node. + workInProgress.lanes = SomeRetryLane; + if (enableSchedulerTracing) { + markSpawnedWork(SomeRetryLane); + } + } } else { cutOffTailIfNeeded(renderState, false); } @@ -1117,10 +1141,11 @@ function completeWork( return null; } } else if ( - // The time it took to render last row is greater than time until - // the expiration. + // The time it took to render last row is greater than the remaining + // time we have to render. So rendering one more row would likely + // exceed it. now() * 2 - renderState.renderingStartTime > - renderState.tailExpiration && + getRenderTargetTime() && renderLanes !== OffscreenLane ) { // We have now passed our CPU deadline and we'll just give up further @@ -1136,9 +1161,9 @@ function completeWork( // them, then they really have the same priority as this render. // So we'll pick it back up the very next render pass once we've had // an opportunity to yield for paint. - workInProgress.lanes = renderLanes; + workInProgress.lanes = SomeRetryLane; if (enableSchedulerTracing) { - markSpawnedWork(renderLanes); + markSpawnedWork(SomeRetryLane); } } } @@ -1163,18 +1188,6 @@ function completeWork( if (renderState.tail !== null) { // We still have tail rows to render. - if (renderState.tailExpiration === 0) { - // Heuristic for how long we're willing to spend rendering rows - // until we just give up and show what we have so far. - const TAIL_EXPIRATION_TIMEOUT_MS = 500; - renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS; - // TODO: This is meant to mimic the train model or JND but this - // is a per component value. It should really be since the start - // of the total render or last commit. Consider using something like - // globalMostRecentFallbackTime. That doesn't account for being - // suspended for part of the time or when it's a new render. - // It should probably use a global start time value instead. - } // Pop a row. const next = renderState.tail; renderState.rendering = next; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index 82bc29f7495e..b85395337eed 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -135,9 +135,10 @@ import { renderDidSuspendDelayIfPossible, renderHasNotSuspendedYet, popRenderLanes, + getRenderTargetTime, } from './ReactFiberWorkLoop.old'; import {createFundamentalStateInstance} from './ReactFiberFundamental.old'; -import {OffscreenLane} from './ReactFiberLane'; +import {OffscreenLane, SomeRetryLane} from './ReactFiberLane'; import {resetChildFibers} from './ReactChildFiber.old'; import {createScopeInstance} from './ReactFiberScope.old'; import {transferActualDuration} from './ReactProfilerTimer.old'; @@ -1049,6 +1050,29 @@ function completeWork( row = row.sibling; } } + + if (renderState.tail !== null && now() > getRenderTargetTime()) { + // We have already passed our CPU deadline but we still have rows + // left in the tail. We'll just give up further attempts to render + // the main content and only render fallbacks. + workInProgress.effectTag |= DidCapture; + didSuspendAlready = true; + + cutOffTailIfNeeded(renderState, false); + + // Since nothing actually suspended, there will nothing to ping this + // to get it started back up to attempt the next item. While in terms + // of priority this work has the same priority as this current render, + // it's not part of the same transition once the transition has + // committed. If it's sync, we still want to yield so that it can be + // painted. Conceptually, this is really the same as pinging. + // We can use any RetryLane even if it's the one currently rendering + // since we're leaving it behind on this node. + workInProgress.lanes = SomeRetryLane; + if (enableSchedulerTracing) { + markSpawnedWork(SomeRetryLane); + } + } } else { cutOffTailIfNeeded(renderState, false); } @@ -1090,10 +1114,11 @@ function completeWork( return null; } } else if ( - // The time it took to render last row is greater than time until - // the expiration. + // The time it took to render last row is greater than the remaining + // time we have to render. So rendering one more row would likely + // exceed it. now() * 2 - renderState.renderingStartTime > - renderState.tailExpiration && + getRenderTargetTime() && renderLanes !== OffscreenLane ) { // We have now passed our CPU deadline and we'll just give up further @@ -1105,13 +1130,16 @@ function completeWork( cutOffTailIfNeeded(renderState, false); // Since nothing actually suspended, there will nothing to ping this - // to get it started back up to attempt the next item. If we can show - // them, then they really have the same priority as this render. - // So we'll pick it back up the very next render pass once we've had - // an opportunity to yield for paint. - workInProgress.lanes = renderLanes; + // to get it started back up to attempt the next item. While in terms + // of priority this work has the same priority as this current render, + // it's not part of the same transition once the transition has + // committed. If it's sync, we still want to yield so that it can be + // painted. Conceptually, this is really the same as pinging. + // We can use any RetryLane even if it's the one currently rendering + // since we're leaving it behind on this node. + workInProgress.lanes = SomeRetryLane; if (enableSchedulerTracing) { - markSpawnedWork(renderLanes); + markSpawnedWork(SomeRetryLane); } } } @@ -1136,18 +1164,6 @@ function completeWork( if (renderState.tail !== null) { // We still have tail rows to render. - if (renderState.tailExpiration === 0) { - // Heuristic for how long we're willing to spend rendering rows - // until we just give up and show what we have so far. - const TAIL_EXPIRATION_TIMEOUT_MS = 500; - renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS; - // TODO: This is meant to mimic the train model or JND but this - // is a per component value. It should really be since the start - // of the total render or last commit. Consider using something like - // globalMostRecentFallbackTime. That doesn't account for being - // suspended for part of the time or when it's a new render. - // It should probably use a global start time value instead. - } // Pop a row. const next = renderState.tail; renderState.rendering = next; diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index d6d49ff042b5..d5712167714f 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -97,6 +97,8 @@ const TransitionLongLanes: Lanes = /* */ 0b0000000001111000000 const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000; +export const SomeRetryLane: Lanes = /* */ 0b0000010000000000000000000000000; + export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000; const NonIdleLanes = /* */ 0b0000111111111111111111111111111; diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js index e151ab0be008..49e586c23232 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js @@ -41,14 +41,12 @@ export type SuspenseListRenderState = {| isBackwards: boolean, // The currently rendering tail row. rendering: null | Fiber, - // The absolute time when we started rendering the tail row. + // The absolute time when we started rendering the most recent tail row. renderingStartTime: number, // The last of the already rendered children. last: null | Fiber, // Remaining rows on the tail of the list. tail: null | Fiber, - // The absolute time in ms that we'll expire the tail rendering. - tailExpiration: number, // Tail insertions setting. tailMode: SuspenseListTailMode, // Last Effect before we rendered the "rendering" item. diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js index e151ab0be008..49e586c23232 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js @@ -41,14 +41,12 @@ export type SuspenseListRenderState = {| isBackwards: boolean, // The currently rendering tail row. rendering: null | Fiber, - // The absolute time when we started rendering the tail row. + // The absolute time when we started rendering the most recent tail row. renderingStartTime: number, // The last of the already rendered children. last: null | Fiber, // Remaining rows on the tail of the list. tail: null | Fiber, - // The absolute time in ms that we'll expire the tail rendering. - tailExpiration: number, // Tail insertions setting. tailMode: SuspenseListTailMode, // Last Effect before we rendered the "rendering" item. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index e1fcf914f707..a1937b8ed48f 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -328,6 +328,21 @@ let globalMostRecentFallbackTime: number = 0; const FALLBACK_THROTTLE_MS: number = 500; const DEFAULT_TIMEOUT_MS: number = 5000; +// The absolute time for when we should start giving up on rendering +// more and prefer CPU suspense heuristics instead. +let workInProgressRootRenderTargetTime: number = Infinity; +// How long a render is supposed to take before we start following CPU +// suspense heuristics and opt out of rendering more content. +const RENDER_TIMEOUT_MS = 500; + +function resetRenderTimer() { + workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS; +} + +export function getRenderTargetTime(): number { + return workInProgressRootRenderTargetTime; +} + let hasUncaughtError = false; let firstUncaughtError = null; let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; @@ -603,6 +618,7 @@ export function scheduleUpdateOnFiber( // scheduleCallbackForFiber to preserve the ability to schedule a callback // without immediately flushing it. We only do this for user-initiated // updates, to preserve historical behavior of legacy mode. + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1111,6 +1127,7 @@ export function flushRoot(root: FiberRoot, lanes: Lanes) { markRootExpired(root, lanes); ensureRootIsScheduled(root, now()); if ((executionContext & (RenderContext | CommitContext)) === NoContext) { + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1185,6 +1202,7 @@ export function batchedUpdates(fn: A => R, a: A): R { executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1199,6 +1217,7 @@ export function batchedEventUpdates(fn: A => R, a: A): R { executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1227,6 +1246,7 @@ export function discreteUpdates( executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1240,6 +1260,7 @@ export function discreteUpdates( executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1256,6 +1277,7 @@ export function unbatchedUpdates(fn: (a: A) => R, a: A): R { executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1323,6 +1345,7 @@ export function flushControlled(fn: () => mixed): void { executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1333,6 +1356,7 @@ export function flushControlled(fn: () => mixed): void { executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1651,6 +1675,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // If the root or lanes have changed, throw out the existing stack // and prepare a fresh one. Otherwise we'll continue where we left off. if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) { + resetRenderTimer(); prepareFreshStack(root, lanes); startWorkOnPendingInteractions(root, lanes); } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index f2b8f3f99ec5..59cdb9890550 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -312,6 +312,21 @@ let globalMostRecentFallbackTime: number = 0; const FALLBACK_THROTTLE_MS: number = 500; const DEFAULT_TIMEOUT_MS: number = 5000; +// The absolute time for when we should start giving up on rendering +// more and prefer CPU suspense heuristics instead. +let workInProgressRootRenderTargetTime: number = Infinity; +// How long a render is supposed to take before we start following CPU +// suspense heuristics and opt out of rendering more content. +const RENDER_TIMEOUT_MS = 500; + +function resetRenderTimer() { + workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS; +} + +export function getRenderTargetTime(): number { + return workInProgressRootRenderTargetTime; +} + let nextEffect: Fiber | null = null; let hasUncaughtError = false; let firstUncaughtError = null; @@ -590,6 +605,7 @@ export function scheduleUpdateOnFiber( // scheduleCallbackForFiber to preserve the ability to schedule a callback // without immediately flushing it. We only do this for user-initiated // updates, to preserve historical behavior of legacy mode. + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1098,6 +1114,7 @@ export function flushRoot(root: FiberRoot, lanes: Lanes) { markRootExpired(root, lanes); ensureRootIsScheduled(root, now()); if ((executionContext & (RenderContext | CommitContext)) === NoContext) { + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1172,6 +1189,7 @@ export function batchedUpdates(fn: A => R, a: A): R { executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1186,6 +1204,7 @@ export function batchedEventUpdates(fn: A => R, a: A): R { executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1214,6 +1233,7 @@ export function discreteUpdates( executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1227,6 +1247,7 @@ export function discreteUpdates( executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1243,6 +1264,7 @@ export function unbatchedUpdates(fn: (a: A) => R, a: A): R { executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1310,6 +1332,7 @@ export function flushControlled(fn: () => mixed): void { executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1320,6 +1343,7 @@ export function flushControlled(fn: () => mixed): void { executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch + resetRenderTimer(); flushSyncCallbackQueue(); } } @@ -1638,6 +1662,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // If the root or lanes have changed, throw out the existing stack // and prepare a fresh one. Otherwise we'll continue where we left off. if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) { + resetRenderTimer(); prepareFreshStack(root, lanes); startWorkOnPendingInteractions(root, lanes); } diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js index 5039e8a820e8..c45a10c99d6d 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js @@ -2556,6 +2556,147 @@ describe('ReactSuspenseList', () => { ); }); + // @gate experimental + it('should be able to progressively show CPU expensive rows with two pass rendering', async () => { + function TwoPass({text}) { + const [pass, setPass] = React.useState(0); + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Mount ' + text); + setPass(1); + }, []); + return ; + } + + function Sleep({time, children}) { + Scheduler.unstable_advanceTime(time); + return children; + } + + function App() { + Scheduler.unstable_yieldValue('App'); + return ( + + }> + + + + + }> + + + + + + + + + ); + } + + ReactNoop.render(); + + expect(Scheduler).toFlushAndYieldThrough([ + 'App', + 'First Pass A', + 'Mount A', + 'A', + ]); + expect(ReactNoop).toMatchRenderedOutput(A); + + expect(Scheduler).toFlushAndYieldThrough(['First Pass B', 'Mount B', 'B']); + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + , + ); + + expect(Scheduler).toFlushAndYield(['C']); + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + C + , + ); + }); + + // @gate experimental + it('should be able to progressively show rows with two pass rendering and visible', async () => { + function TwoPass({text}) { + const [pass, setPass] = React.useState(0); + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Mount ' + text); + setPass(1); + }, []); + return ; + } + + function Sleep({time, children}) { + Scheduler.unstable_advanceTime(time); + return children; + } + + function App() { + Scheduler.unstable_yieldValue('App'); + return ( + + }> + + + + + }> + + + + + }> + + + + + + ); + } + + ReactNoop.render(); + + expect(Scheduler).toFlushAndYieldThrough([ + 'App', + 'First Pass A', + 'Loading B', + 'Loading C', + 'Mount A', + 'A', + ]); + expect(ReactNoop).toMatchRenderedOutput( + <> + A + Loading B + Loading C + , + ); + + expect(Scheduler).toFlushAndYieldThrough(['First Pass B', 'Mount B', 'B']); + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + Loading C + , + ); + + expect(Scheduler).toFlushAndYield(['C']); + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + C + , + ); + }); + // @gate experimental && enableProfilerTimer it('counts the actual duration when profiling a SuspenseList', async () => { // Order of parameters: id, phase, actualDuration, treeBaseDuration diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index c3d1dbb3cea7..193f3d1ac7f1 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -294,6 +294,7 @@ describe('Profiler', () => { 'read current time', 'read current time', 'read current time', + 'read current time', ]); // Restore original mock