@samitouri / QOS-React-1 / commits / d4688dfaaf

[Fiber] Track Event Time, startTransition Time and setState Time (#31008)

This tracks the current window.event.timeStamp the first time we setState or call startTransition. For either the blocking track or transition track. We can use this to show how long we were blocked by other events or overhead from when the user interacted until we got called into React. Then we track the time we start awaiting a Promise returned from startTransition. We can use this track how long we waited on an Action to complete before setState was called. Then finally we track when setState was called so we can track how long we were blocked by other word before we could actually start rendering. For a Transition this might be blocked by Blocking React render work. We only log these once a subsequent render actually happened. If no render was actually scheduled, then we don't log these. E.g. if an isomorphic Action doesn't call startTransition there's no render so we don't log it. We only log the first event/update/transition even if multiple are batched into it later. If multiple Actions are entangled they're all treated as one until an update happens. If no update happens and all entangled actions finish, we clear the transition so that the next time a new sequence starts we can log it. We also clamp these (start the track later) if they were scheduled within a render/commit. Since we share a single track we don't want to create overlapping tracks. The purpose of this is not to show every event/action that happens but to show a prelude to how long we were blocked before a render started. So you can follow the first event to commit. <img width="674" alt="Screenshot 2024-09-20 at 1 59 58 AM" src="https://github.com/user-attachments/assets/151ba9e8-6b3c-4fa1-9f8d-e3602745eeb7"> I still need to add the rendering/suspended phases to the timeline which why this screenshot has a gap. <img width="993" alt="Screenshot 2024-09-20 at 12 50 27 AM" src="https://github.com/user-attachments/assets/155b6675-b78a-4a22-a32b-212c15051074"> In this case it's a Form Action which started a render into the form which then suspended on the action. The action then caused a refresh, which interrupts with its own update that's blocked before rendering. Suspended roots like this is interesting because we could in theory start working on a different root in the meantime which makes this timeline less linear.

Sebastian Markbåge committed Sep 20, 2024 at 14:27 UTC d4688dfaafe51a4cb6e3c51fc2330662cb4e2296
18 files changed +443 -26
packages/react-art/src/ReactFiberConfigART.js
+8
@@ -363,6 +363,14 @@ export function resolveUpdatePriority(): EventPriority {
363 return currentUpdatePriority || DefaultEventPriority;
364 }
365
366 +export function resolveEventType(): null | string {
367 + return null;
368 +}
369 +
370 +export function resolveEventTimeStamp(): number {
371 + return -1.1;
372 +}
373 +
374 export function shouldAttemptEagerTransition() {
375 return false;
376 }
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+10
@@ -606,6 +606,16 @@ export function shouldAttemptEagerTransition(): boolean {
606 return false;
607 }
608
609 +export function resolveEventType(): null | string {
610 + const event = window.event;
611 + return event ? event.type : null;
612 +}
613 +
614 +export function resolveEventTimeStamp(): number {
615 + const event = window.event;
616 + return event ? event.timeStamp : -1.1;
617 +}
618 +
619 export const isPrimaryRenderer = true;
620 export const warnsIfNotActing = true;
621 // This initialization code may run even on server environments
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+8
@@ -372,6 +372,14 @@ export function resolveUpdatePriority(): EventPriority {
372 return DefaultEventPriority;
373 }
374
375 +export function resolveEventType(): null | string {
376 + return null;
377 +}
378 +
379 +export function resolveEventTimeStamp(): number {
380 + return -1.1;
381 +}
382 +
383 export function shouldAttemptEagerTransition(): boolean {
384 return false;
385 }
packages/react-native-renderer/src/ReactFiberConfigNative.js
+8
@@ -288,6 +288,14 @@ export function resolveUpdatePriority(): EventPriority {
288 return DefaultEventPriority;
289 }
290
291 +export function resolveEventType(): null | string {
292 + return null;
293 +}
294 +
295 +export function resolveEventTimeStamp(): number {
296 + return -1.1;
297 +}
298 +
299 export function shouldAttemptEagerTransition(): boolean {
300 return false;
301 }
packages/react-noop-renderer/src/createReactNoop.js
+8
@@ -533,6 +533,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
533 return currentEventPriority;
534 },
535
536 + resolveEventType(): null | string {
537 + return null;
538 + },
539 +
540 + resolveEventTimeStamp(): number {
541 + return -1.1;
542 + },
543 +
544 shouldAttemptEagerTransition(): boolean {
545 return false;
546 },
packages/react-reconciler/src/ReactFiberAsyncAction.js
+35 -17
@@ -17,6 +17,14 @@ import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent';
17
18 import {requestTransitionLane} from './ReactFiberRootScheduler';
19 import {NoLane} from './ReactFiberLane';
20 +import {
21 + hasScheduledTransitionWork,
22 + clearAsyncTransitionTimer,
23 +} from './ReactProfilerTimer';
24 +import {
25 + enableComponentPerformanceTrack,
26 + enableProfilerTimer,
27 +} from 'shared/ReactFeatureFlags';
28
29 // If there are multiple, concurrent async actions, they are entangled. All
30 // transition updates that occur while the async action is still in progress
@@ -64,24 +72,34 @@ export function entangleAsyncAction<S>(
72 }
73
74 function pingEngtangledActionScope() {
67 - if (
68 - currentEntangledListeners !== null &&
69 - --currentEntangledPendingCount === 0
70 - ) {
71 - // All the actions have finished. Close the entangled async action scope
72 - // and notify all the listeners.
73 - if (currentEntangledActionThenable !== null) {
74 - const fulfilledThenable: FulfilledThenable<void> =
75 - (currentEntangledActionThenable: any);
76 - fulfilledThenable.status = 'fulfilled';
75 + if (--currentEntangledPendingCount === 0) {
76 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
77 + if (!hasScheduledTransitionWork()) {
78 + // If we have received no updates since we started the entangled Actions
79 + // that means it didn't lead to a Transition being rendered. We need to
80 + // clear the timer so that if we start another entangled sequence we use
81 + // the next start timer instead of appearing like we were blocked the
82 + // whole time. We currently don't log a track for Actions that don't
83 + // render a Transition.
84 + clearAsyncTransitionTimer();
85 + }
86 }
78 - const listeners = currentEntangledListeners;
79 - currentEntangledListeners = null;
80 - currentEntangledLane = NoLane;
81 - currentEntangledActionThenable = null;
82 - for (let i = 0; i < listeners.length; i++) {
83 - const listener = listeners[i];
84 - listener();
87 + if (currentEntangledListeners !== null) {
88 + // All the actions have finished. Close the entangled async action scope
89 + // and notify all the listeners.
90 + if (currentEntangledActionThenable !== null) {
91 + const fulfilledThenable: FulfilledThenable<void> =
92 + (currentEntangledActionThenable: any);
93 + fulfilledThenable.status = 'fulfilled';
94 + }
95 + const listeners = currentEntangledListeners;
96 + currentEntangledListeners = null;
97 + currentEntangledLane = NoLane;
98 + currentEntangledActionThenable = null;
99 + for (let i = 0; i < listeners.length; i++) {
100 + const listener = listeners[i];
101 + listener();
102 + }
103 }
104 }
105 }
packages/react-reconciler/src/ReactFiberClassComponent.js
+4
@@ -72,6 +72,7 @@ import {
72 markStateUpdateScheduled,
73 setIsStrictModeForDevtools,
74 } from './ReactFiberDevToolsHook';
75 +import {startUpdateTimerByLane} from './ReactProfilerTimer';
76
77 const fakeInternalInstance = {};
78
@@ -194,6 +195,7 @@ const classComponentUpdater = {
195
196 const root = enqueueUpdate(fiber, update, lane);
197 if (root !== null) {
198 + startUpdateTimerByLane(lane);
199 scheduleUpdateOnFiber(root, fiber, lane);
200 entangleTransitions(root, fiber, lane);
201 }
@@ -228,6 +230,7 @@ const classComponentUpdater = {
230
231 const root = enqueueUpdate(fiber, update, lane);
232 if (root !== null) {
233 + startUpdateTimerByLane(lane);
234 scheduleUpdateOnFiber(root, fiber, lane);
235 entangleTransitions(root, fiber, lane);
236 }
@@ -262,6 +265,7 @@ const classComponentUpdater = {
265
266 const root = enqueueUpdate(fiber, update, lane);
267 if (root !== null) {
268 + startUpdateTimerByLane(lane);
269 scheduleUpdateOnFiber(root, fiber, lane);
270 entangleTransitions(root, fiber, lane);
271 }
packages/react-reconciler/src/ReactFiberHooks.js
+60 -9
@@ -131,6 +131,7 @@ import {
131 markStateUpdateScheduled,
132 setIsStrictModeForDevtools,
133 } from './ReactFiberDevToolsHook';
134 +import {startUpdateTimerByLane} from './ReactProfilerTimer';
135 import {createCache} from './ReactFiberCacheComponent';
136 import {
137 createUpdate as createLegacyQueueUpdate,
@@ -3019,7 +3020,12 @@ function startTransition<S>(
3020 dispatchOptimisticSetState(fiber, false, queue, pendingState);
3021 } else {
3022 ReactSharedInternals.T = null;
3022 - dispatchSetState(fiber, queue, pendingState);
3023 + dispatchSetStateInternal(
3024 + fiber,
3025 + queue,
3026 + pendingState,
3027 + requestUpdateLane(fiber),
3028 + );
3029 ReactSharedInternals.T = currentTransition;
3030 }
3031
@@ -3062,13 +3068,28 @@ function startTransition<S>(
3068 thenable,
3069 finishedState,
3070 );
3065 - dispatchSetState(fiber, queue, (thenableForFinishedState: any));
3071 + dispatchSetStateInternal(
3072 + fiber,
3073 + queue,
3074 + (thenableForFinishedState: any),
3075 + requestUpdateLane(fiber),
3076 + );
3077 } else {
3067 - dispatchSetState(fiber, queue, finishedState);
3078 + dispatchSetStateInternal(
3079 + fiber,
3080 + queue,
3081 + finishedState,
3082 + requestUpdateLane(fiber),
3083 + );
3084 }
3085 } else {
3086 // Async actions are not enabled.
3071 - dispatchSetState(fiber, queue, finishedState);
3087 + dispatchSetStateInternal(
3088 + fiber,
3089 + queue,
3090 + finishedState,
3091 + requestUpdateLane(fiber),
3092 + );
3093 callback();
3094 }
3095 } catch (error) {
@@ -3081,7 +3102,12 @@ function startTransition<S>(
3102 status: 'rejected',
3103 reason: error,
3104 };
3084 - dispatchSetState(fiber, queue, rejectedThenable);
3105 + dispatchSetStateInternal(
3106 + fiber,
3107 + queue,
3108 + rejectedThenable,
3109 + requestUpdateLane(fiber),
3110 + );
3111 } else {
3112 // The error rethrowing behavior is only enabled when the async actions
3113 // feature is on, even for sync actions.
@@ -3253,7 +3279,12 @@ export function requestFormReset(formFiber: Fiber) {
3279 const newResetState = {};
3280 const resetStateHook: Hook = (stateHook.next: any);
3281 const resetStateQueue = resetStateHook.queue;
3256 - dispatchSetState(formFiber, resetStateQueue, newResetState);
3282 + dispatchSetStateInternal(
3283 + formFiber,
3284 + resetStateQueue,
3285 + newResetState,
3286 + requestUpdateLane(formFiber),
3287 + );
3288 }
3289
3290 function mountTransition(): [
@@ -3385,6 +3416,7 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T): void {
3416 const refreshUpdate = createLegacyQueueUpdate(lane);
3417 const root = enqueueLegacyQueueUpdate(provider, refreshUpdate, lane);
3418 if (root !== null) {
3419 + startUpdateTimerByLane(lane);
3420 scheduleUpdateOnFiber(root, provider, lane);
3421 entangleLegacyQueueTransitions(root, provider, lane);
3422 }
@@ -3450,6 +3482,7 @@ function dispatchReducerAction<S, A>(
3482 } else {
3483 const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
3484 if (root !== null) {
3485 + startUpdateTimerByLane(lane);
3486 scheduleUpdateOnFiber(root, fiber, lane);
3487 entangleTransitionUpdate(root, queue, lane);
3488 }
@@ -3474,7 +3507,24 @@ function dispatchSetState<S, A>(
3507 }
3508
3509 const lane = requestUpdateLane(fiber);
3510 + const didScheduleUpdate = dispatchSetStateInternal(
3511 + fiber,
3512 + queue,
3513 + action,
3514 + lane,
3515 + );
3516 + if (didScheduleUpdate) {
3517 + startUpdateTimerByLane(lane);
3518 + }
3519 + markUpdateInDevTools(fiber, lane, action);
3520 +}
3521
3522 +function dispatchSetStateInternal<S, A>(
3523 + fiber: Fiber,
3524 + queue: UpdateQueue<S, A>,
3525 + action: A,
3526 + lane: Lane,
3527 +): boolean {
3528 const update: Update<S, A> = {
3529 lane,
3530 revertLane: NoLane,
@@ -3518,7 +3568,7 @@ function dispatchSetState<S, A>(
3568 // time the reducer has changed.
3569 // TODO: Do we still need to entangle transitions in this case?
3570 enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
3521 - return;
3571 + return false;
3572 }
3573 } catch (error) {
3574 // Suppress the error. It will throw again in the render phase.
@@ -3534,10 +3584,10 @@ function dispatchSetState<S, A>(
3584 if (root !== null) {
3585 scheduleUpdateOnFiber(root, fiber, lane);
3586 entangleTransitionUpdate(root, queue, lane);
3587 + return true;
3588 }
3589 }
3539 -
3540 - markUpdateInDevTools(fiber, lane, action);
3590 + return false;
3591 }
3592
3593 function dispatchOptimisticSetState<S, A>(
@@ -3619,6 +3669,7 @@ function dispatchOptimisticSetState<S, A>(
3669 // will never be attempted before the optimistic update. This currently
3670 // holds because the optimistic update is always synchronous. If we ever
3671 // change that, we'll need to account for this.
3672 + startUpdateTimerByLane(SyncLane);
3673 scheduleUpdateOnFiber(root, fiber, SyncLane);
3674 // Optimistic updates are always synchronous, so we don't need to call
3675 // entangleTransitionUpdate here.
packages/react-reconciler/src/ReactFiberLane.js
+17
@@ -592,6 +592,10 @@ export function includesSyncLane(lanes: Lanes): boolean {
592 return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes;
593 }
594
595 +export function isSyncLane(lanes: Lanes): boolean {
596 + return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes;
597 +}
598 +
599 export function includesNonIdleWork(lanes: Lanes): boolean {
600 return (lanes & NonIdleLanes) !== NoLanes;
601 }
@@ -608,6 +612,10 @@ export function includesOnlyTransitions(lanes: Lanes): boolean {
612 return (lanes & TransitionLanes) === lanes;
613 }
614
615 +export function includesTransitionLane(lanes: Lanes): boolean {
616 + return (lanes & TransitionLanes) !== NoLanes;
617 +}
618 +
619 export function includesBlockingLane(lanes: Lanes): boolean {
620 const SyncDefaultLanes =
621 InputContinuousHydrationLane |
@@ -623,6 +631,15 @@ export function includesExpiredLane(root: FiberRoot, lanes: Lanes): boolean {
631 return (lanes & root.expiredLanes) !== NoLanes;
632 }
633
634 +export function isBlockingLane(lane: Lane): boolean {
635 + const SyncDefaultLanes =
636 + InputContinuousHydrationLane |
637 + InputContinuousLane |
638 + DefaultHydrationLane |
639 + DefaultLane;
640 + return (lane & SyncDefaultLanes) !== NoLanes;
641 +}
642 +
643 export function isTransitionLane(lane: Lane): boolean {
644 return (lane & TransitionLanes) !== NoLanes;
645 }
packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+65
@@ -99,3 +99,68 @@ export function logComponentEffect(
99 performance.measure(name, reusableComponentOptions);
100 }
101 }
102 +
103 +export function logBlockingStart(
104 + updateTime: number,
105 + eventTime: number,
106 + eventType: null | string,
107 + renderStartTime: number,
108 +): void {
109 + if (supportsUserTiming) {
110 + reusableComponentDevToolDetails.track = 'Blocking';
111 + if (eventTime > 0 && eventType !== null) {
112 + // Log the time from the event timeStamp until we called setState.
113 + reusableComponentDevToolDetails.color = 'secondary-dark';
114 + reusableComponentOptions.start = eventTime;
115 + reusableComponentOptions.end =
116 + updateTime > 0 ? updateTime : renderStartTime;
117 + performance.measure(eventType, reusableComponentOptions);
118 + }
119 + if (updateTime > 0) {
120 + // Log the time from when we called setState until we started rendering.
121 + reusableComponentDevToolDetails.color = 'primary-light';
122 + reusableComponentOptions.start = updateTime;
123 + reusableComponentOptions.end = renderStartTime;
124 + performance.measure('Blocked', reusableComponentOptions);
125 + }
126 + }
127 +}
128 +
129 +export function logTransitionStart(
130 + startTime: number,
131 + updateTime: number,
132 + eventTime: number,
133 + eventType: null | string,
134 + renderStartTime: number,
135 +): void {
136 + if (supportsUserTiming) {
137 + reusableComponentDevToolDetails.track = 'Transition';
138 + if (eventTime > 0 && eventType !== null) {
139 + // Log the time from the event timeStamp until we started a transition.
140 + reusableComponentDevToolDetails.color = 'secondary-dark';
141 + reusableComponentOptions.start = eventTime;
142 + reusableComponentOptions.end =
143 + startTime > 0
144 + ? startTime
145 + : updateTime > 0
146 + ? updateTime
147 + : renderStartTime;
148 + performance.measure(eventType, reusableComponentOptions);
149 + }
150 + if (startTime > 0) {
151 + // Log the time from when we started an async transition until we called setState or started rendering.
152 + reusableComponentDevToolDetails.color = 'primary-dark';
153 + reusableComponentOptions.start = startTime;
154 + reusableComponentOptions.end =
155 + updateTime > 0 ? updateTime : renderStartTime;
156 + performance.measure('Action', reusableComponentOptions);
157 + }
158 + if (updateTime > 0) {
159 + // Log the time from when we called setState until we started rendering.
160 + reusableComponentDevToolDetails.color = 'primary-light';
161 + reusableComponentOptions.start = updateTime;
162 + reusableComponentOptions.end = renderStartTime;
163 + performance.measure('Blocked', reusableComponentOptions);
164 + }
165 + }
166 +}
packages/react-reconciler/src/ReactFiberReconciler.js
+2
@@ -61,6 +61,7 @@ import {
61 onScheduleRoot,
62 injectProfilingHooks,
63 } from './ReactFiberDevToolsHook';
64 +import {startUpdateTimerByLane} from './ReactProfilerTimer';
65 import {
66 requestUpdateLane,
67 scheduleUpdateOnFiber,
@@ -433,6 +434,7 @@ function updateContainerImpl(
434
435 const root = enqueueUpdate(rootFiber, update, lane);
436 if (root !== null) {
437 + startUpdateTimerByLane(lane);
438 scheduleUpdateOnFiber(root, rootFiber, lane);
439 entangleTransitions(root, rootFiber, lane);
440 }
packages/react-reconciler/src/ReactFiberTransition.js
+8
@@ -35,6 +35,7 @@ import {
35
36 import ReactSharedInternals from 'shared/ReactSharedInternals';
37 import {entangleAsyncAction} from './ReactFiberAsyncAction';
38 +import {startAsyncTransitionTimer} from './ReactProfilerTimer';
39
40 export const NoTransition = null;
41
@@ -69,6 +70,13 @@ ReactSharedInternals.S = function onStartTransitionFinishForReconciler(
70 returnValue !== null &&
71 typeof returnValue.then === 'function'
72 ) {
73 + // If we're going to wait on some async work before scheduling an update.
74 + // We mark the time so we can later log how long we were blocked on the Action.
75 + // Ideally, we'd include the sync part of the action too but since that starts
76 + // in isomorphic code it currently leads to tricky layering. We'd have to pass
77 + // in performance.now() to this callback but we sometimes use a polyfill.
78 + startAsyncTransitionTimer();
79 +
80 // This is an async action
81 const thenable: Thenable<mixed> = (returnValue: any);
82 entangleAsyncAction(transition, thenable);
packages/react-reconciler/src/ReactFiberWorkLoop.js
+70
@@ -68,6 +68,10 @@ import {
68 logRenderStarted,
69 logRenderStopped,
70 } from './DebugTracing';
71 +import {
72 + logBlockingStart,
73 + logTransitionStart,
74 +} from './ReactFiberPerformanceTrack';
75
76 import {
77 resetAfterCommit,
@@ -145,6 +149,7 @@ import {
149 includesOnlyRetries,
150 includesOnlyTransitions,
151 includesBlockingLane,
152 + includesTransitionLane,
153 includesExpiredLane,
154 getNextLanes,
155 getEntangledLanes,
@@ -221,7 +226,20 @@ import {
226 } from './ReactFiberConcurrentUpdates';
227
228 import {
229 + blockingUpdateTime,
230 + blockingEventTime,
231 + blockingEventType,
232 + transitionStartTime,
233 + transitionUpdateTime,
234 + transitionEventTime,
235 + transitionEventType,
236 + clearBlockingTimers,
237 + clearTransitionTimers,
238 + clampBlockingTimers,
239 + clampTransitionTimers,
240 markNestedUpdateScheduled,
241 + renderStartTime,
242 + recordRenderTime,
243 recordCompleteTime,
244 recordCommitTime,
245 resetNestedUpdateFlag,
@@ -1698,7 +1716,48 @@ function resetWorkInProgressStack() {
1716 workInProgress = null;
1717 }
1718
1719 +function finalizeRender(lanes: Lanes, finalizationTime: number): void {
1720 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
1721 + if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
1722 + clampBlockingTimers(finalizationTime);
1723 + }
1724 + if (includesTransitionLane(lanes)) {
1725 + clampTransitionTimers(finalizationTime);
1726 + }
1727 + }
1728 +}
1729 +
1730 function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1731 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
1732 + // Starting a new render. Log the end of any previous renders and the
1733 + // blocked time before the render started.
1734 + recordRenderTime();
1735 + // If this was a restart, e.g. due to an interrupting update, then there's no space
1736 + // in the track to log the cause since we'll have rendered all the way up until the
1737 + // restart so we need to clamp that.
1738 + finalizeRender(workInProgressRootRenderLanes, renderStartTime);
1739 +
1740 + if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
1741 + logBlockingStart(
1742 + blockingUpdateTime,
1743 + blockingEventTime,
1744 + blockingEventType,
1745 + renderStartTime,
1746 + );
1747 + clearBlockingTimers();
1748 + }
1749 + if (includesTransitionLane(lanes)) {
1750 + logTransitionStart(
1751 + transitionStartTime,
1752 + transitionUpdateTime,
1753 + transitionEventTime,
1754 + transitionEventType,
1755 + renderStartTime,
1756 + );
1757 + clearTransitionTimers();
1758 + }
1759 + }
1760 +
1761 root.finishedWork = null;
1762 root.finishedLanes = NoLanes;
1763
@@ -2240,6 +2299,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2299 }
2300
2301 workInProgressTransitions = getTransitionsForLanes(root, lanes);
2302 +
2303 resetRenderTimer();
2304 prepareFreshStack(root, lanes);
2305 } else {
@@ -3358,6 +3418,12 @@ function commitRootImpl(
3418 nestedUpdateCount = 0;
3419 }
3420
3421 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
3422 + if (!rootDidHavePassiveEffects) {
3423 + finalizeRender(lanes, now());
3424 + }
3425 + }
3426 +
3427 // If layout work was scheduled, flush it now.
3428 flushSyncWorkOnAllRoots();
3429
@@ -3539,6 +3605,10 @@ function flushPassiveEffectsImpl() {
3605
3606 executionContext = prevExecutionContext;
3607
3608 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
3609 + finalizeRender(lanes, now());
3610 + }
3611 +
3612 flushSyncWorkOnAllRoots();
3613
3614 if (enableTransitionTracing) {
packages/react-reconciler/src/ReactProfilerTimer.js
+119
@@ -9,10 +9,16 @@
9
10 import type {Fiber} from './ReactInternalTypes';
11
12 +import type {Lane} from './ReactFiberLane';
13 +import {isTransitionLane, isBlockingLane, isSyncLane} from './ReactFiberLane';
14 +
15 +import {resolveEventType, resolveEventTimeStamp} from './ReactFiberConfig';
16 +
17 import {
18 enableProfilerCommitHooks,
19 enableProfilerNestedUpdatePhase,
20 enableProfilerTimer,
21 + enableComponentPerformanceTrack,
22 } from 'shared/ReactFeatureFlags';
23
24 // Intentionally not named imports because Rollup would use dynamic dispatch for
@@ -21,6 +27,7 @@ import * as Scheduler from 'scheduler';
27
28 const {unstable_now: now} = Scheduler;
29
30 +export let renderStartTime: number = -0;
31 export let completeTime: number = -0;
32 export let commitTime: number = -0;
33 export let profilerStartTime: number = -1.1;
@@ -29,6 +36,111 @@ export let componentEffectDuration: number = -0;
36 export let componentEffectStartTime: number = -1.1;
37 export let componentEffectEndTime: number = -1.1;
38
39 +export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
40 +export let blockingEventTime: number = -1.1; // Event timeStamp of the first setState.
41 +export let blockingEventType: null | string = null; // Event type of the first setState.
42 +// TODO: This should really be one per Transition lane.
43 +export let transitionStartTime: number = -1.1; // First startTransition call before setState.
44 +export let transitionUpdateTime: number = -1.1; // First transition setState scheduled.
45 +export let transitionEventTime: number = -1.1; // Event timeStamp of the first transition.
46 +export let transitionEventType: null | string = null; // Event type of the first transition.
47 +
48 +export function startUpdateTimerByLane(lane: Lane): void {
49 + if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
50 + return;
51 + }
52 + if (isSyncLane(lane) || isBlockingLane(lane)) {
53 + if (blockingUpdateTime < 0) {
54 + blockingUpdateTime = now();
55 + blockingEventTime = resolveEventTimeStamp();
56 + blockingEventType = resolveEventType();
57 + }
58 + } else if (isTransitionLane(lane)) {
59 + if (transitionUpdateTime < 0) {
60 + transitionUpdateTime = now();
61 + if (transitionStartTime < 0) {
62 + transitionEventTime = resolveEventTimeStamp();
63 + transitionEventType = resolveEventType();
64 + }
65 + }
66 + }
67 +}
68 +
69 +export function clearBlockingTimers(): void {
70 + blockingUpdateTime = -1.1;
71 +}
72 +
73 +export function startAsyncTransitionTimer(): void {
74 + if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
75 + return;
76 + }
77 + if (transitionStartTime < 0 && transitionUpdateTime < 0) {
78 + transitionStartTime = now();
79 + transitionEventTime = resolveEventTimeStamp();
80 + transitionEventType = resolveEventType();
81 + }
82 +}
83 +
84 +export function hasScheduledTransitionWork(): boolean {
85 + // If we have setState on a transition or scheduled useActionState update.
86 + return transitionUpdateTime > -1;
87 +}
88 +
89 +// We use this marker to indicate that we have scheduled a render to be performed
90 +// but it's not an explicit state update.
91 +const ACTION_STATE_MARKER = -0.5;
92 +
93 +export function startActionStateUpdate(): void {
94 + if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
95 + return;
96 + }
97 + if (transitionUpdateTime < 0) {
98 + transitionUpdateTime = ACTION_STATE_MARKER;
99 + }
100 +}
101 +
102 +export function clearAsyncTransitionTimer(): void {
103 + transitionStartTime = -1.1;
104 +}
105 +
106 +export function clearTransitionTimers(): void {
107 + transitionStartTime = -1.1;
108 + transitionUpdateTime = -1.1;
109 +}
110 +
111 +export function clampBlockingTimers(finalTime: number): void {
112 + if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
113 + return;
114 + }
115 + // If we had new updates come in while we were still rendering or committing, we don't want
116 + // those update times to create overlapping tracks in the performance timeline so we clamp
117 + // them to the end of the commit phase.
118 + if (blockingUpdateTime >= 0 && blockingUpdateTime < finalTime) {
119 + blockingUpdateTime = finalTime;
120 + }
121 + if (blockingEventTime >= 0 && blockingEventTime < finalTime) {
122 + blockingEventTime = finalTime;
123 + }
124 +}
125 +
126 +export function clampTransitionTimers(finalTime: number): void {
127 + if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
128 + return;
129 + }
130 + // If we had new updates come in while we were still rendering or committing, we don't want
131 + // those update times to create overlapping tracks in the performance timeline so we clamp
132 + // them to the end of the commit phase.
133 + if (transitionStartTime >= 0 && transitionStartTime < finalTime) {
134 + transitionStartTime = finalTime;
135 + }
136 + if (transitionUpdateTime >= 0 && transitionUpdateTime < finalTime) {
137 + transitionUpdateTime = finalTime;
138 + }
139 + if (transitionEventTime >= 0 && transitionEventTime < finalTime) {
140 + transitionEventTime = finalTime;
141 + }
142 +}
143 +
144 export function pushNestedEffectDurations(): number {
145 if (!enableProfilerTimer || !enableProfilerCommitHooks) {
146 return 0;
@@ -136,6 +248,13 @@ export function syncNestedUpdateFlag(): void {
248 }
249 }
250
251 +export function recordRenderTime(): void {
252 + if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
253 + return;
254 + }
255 + renderStartTime = now();
256 +}
257 +
258 export function recordCompleteTime(): void {
259 if (!enableProfilerTimer) {
260 return;
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+6
@@ -83,6 +83,12 @@ describe('ReactFiberHostContext', () => {
83 }
84 return DefaultEventPriority;
85 },
86 + resolveEventType: function () {
87 + return null;
88 + },
89 + resolveEventTimeStamp: function () {
90 + return -1.1;
91 + },
92 shouldAttemptEagerTransition() {
93 return false;
94 },
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+2
@@ -73,6 +73,8 @@ export const getInstanceFromScope = $$$config.getInstanceFromScope;
73 export const setCurrentUpdatePriority = $$$config.setCurrentUpdatePriority;
74 export const getCurrentUpdatePriority = $$$config.getCurrentUpdatePriority;
75 export const resolveUpdatePriority = $$$config.resolveUpdatePriority;
76 +export const resolveEventType = $$$config.resolveEventType;
77 +export const resolveEventTimeStamp = $$$config.resolveEventTimeStamp;
78 export const shouldAttemptEagerTransition =
79 $$$config.shouldAttemptEagerTransition;
80 export const detachDeletedInstance = $$$config.detachDeletedInstance;
packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+7
@@ -224,6 +224,13 @@ export function resolveUpdatePriority(): EventPriority {
224 }
225 return DefaultEventPriority;
226 }
227 +export function resolveEventType(): null | string {
228 + return null;
229 +}
230 +
231 +export function resolveEventTimeStamp(): number {
232 + return -1.1;
233 +}
234 export function shouldAttemptEagerTransition(): boolean {
235 return false;
236 }
packages/react/src/__tests__/ReactProfiler-test.internal.js
+6
@@ -178,6 +178,9 @@ describe(`onRender`, () => {
178 'read current time',
179 'read current time',
180 'read current time',
181 + 'read current time',
182 + 'read current time',
183 + 'read current time',
184 ]);
185 } else {
186 assertLog([
@@ -212,6 +215,9 @@ describe(`onRender`, () => {
215 'read current time',
216 'read current time',
217 'read current time',
218 + 'read current time',
219 + 'read current time',
220 + 'read current time',
221 ]);
222 } else {
223 assertLog([