Skip to content

Commit d4688df

Browse files
authored
[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.
1 parent ae75d5a commit d4688df

18 files changed

+443
-26
lines changed

packages/react-art/src/ReactFiberConfigART.js

+8
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,14 @@ export function resolveUpdatePriority(): EventPriority {
363363
return currentUpdatePriority || DefaultEventPriority;
364364
}
365365

366+
export function resolveEventType(): null | string {
367+
return null;
368+
}
369+
370+
export function resolveEventTimeStamp(): number {
371+
return -1.1;
372+
}
373+
366374
export function shouldAttemptEagerTransition() {
367375
return false;
368376
}

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+10
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,16 @@ export function shouldAttemptEagerTransition(): boolean {
606606
return false;
607607
}
608608

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+
609619
export const isPrimaryRenderer = true;
610620
export const warnsIfNotActing = true;
611621
// This initialization code may run even on server environments

packages/react-native-renderer/src/ReactFiberConfigFabric.js

+8
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,14 @@ export function resolveUpdatePriority(): EventPriority {
372372
return DefaultEventPriority;
373373
}
374374

375+
export function resolveEventType(): null | string {
376+
return null;
377+
}
378+
379+
export function resolveEventTimeStamp(): number {
380+
return -1.1;
381+
}
382+
375383
export function shouldAttemptEagerTransition(): boolean {
376384
return false;
377385
}

packages/react-native-renderer/src/ReactFiberConfigNative.js

+8
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,14 @@ export function resolveUpdatePriority(): EventPriority {
288288
return DefaultEventPriority;
289289
}
290290

291+
export function resolveEventType(): null | string {
292+
return null;
293+
}
294+
295+
export function resolveEventTimeStamp(): number {
296+
return -1.1;
297+
}
298+
291299
export function shouldAttemptEagerTransition(): boolean {
292300
return false;
293301
}

packages/react-noop-renderer/src/createReactNoop.js

+8
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
533533
return currentEventPriority;
534534
},
535535

536+
resolveEventType(): null | string {
537+
return null;
538+
},
539+
540+
resolveEventTimeStamp(): number {
541+
return -1.1;
542+
},
543+
536544
shouldAttemptEagerTransition(): boolean {
537545
return false;
538546
},

packages/react-reconciler/src/ReactFiberAsyncAction.js

+35-17
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent';
1717

1818
import {requestTransitionLane} from './ReactFiberRootScheduler';
1919
import {NoLane} from './ReactFiberLane';
20+
import {
21+
hasScheduledTransitionWork,
22+
clearAsyncTransitionTimer,
23+
} from './ReactProfilerTimer';
24+
import {
25+
enableComponentPerformanceTrack,
26+
enableProfilerTimer,
27+
} from 'shared/ReactFeatureFlags';
2028

2129
// If there are multiple, concurrent async actions, they are entangled. All
2230
// transition updates that occur while the async action is still in progress
@@ -64,24 +72,34 @@ export function entangleAsyncAction<S>(
6472
}
6573

6674
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+
}
7786
}
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+
}
85103
}
86104
}
87105
}

packages/react-reconciler/src/ReactFiberClassComponent.js

+4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import {
7272
markStateUpdateScheduled,
7373
setIsStrictModeForDevtools,
7474
} from './ReactFiberDevToolsHook';
75+
import {startUpdateTimerByLane} from './ReactProfilerTimer';
7576

7677
const fakeInternalInstance = {};
7778

@@ -194,6 +195,7 @@ const classComponentUpdater = {
194195

195196
const root = enqueueUpdate(fiber, update, lane);
196197
if (root !== null) {
198+
startUpdateTimerByLane(lane);
197199
scheduleUpdateOnFiber(root, fiber, lane);
198200
entangleTransitions(root, fiber, lane);
199201
}
@@ -228,6 +230,7 @@ const classComponentUpdater = {
228230

229231
const root = enqueueUpdate(fiber, update, lane);
230232
if (root !== null) {
233+
startUpdateTimerByLane(lane);
231234
scheduleUpdateOnFiber(root, fiber, lane);
232235
entangleTransitions(root, fiber, lane);
233236
}
@@ -262,6 +265,7 @@ const classComponentUpdater = {
262265

263266
const root = enqueueUpdate(fiber, update, lane);
264267
if (root !== null) {
268+
startUpdateTimerByLane(lane);
265269
scheduleUpdateOnFiber(root, fiber, lane);
266270
entangleTransitions(root, fiber, lane);
267271
}

packages/react-reconciler/src/ReactFiberHooks.js

+60-9
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ import {
131131
markStateUpdateScheduled,
132132
setIsStrictModeForDevtools,
133133
} from './ReactFiberDevToolsHook';
134+
import {startUpdateTimerByLane} from './ReactProfilerTimer';
134135
import {createCache} from './ReactFiberCacheComponent';
135136
import {
136137
createUpdate as createLegacyQueueUpdate,
@@ -3019,7 +3020,12 @@ function startTransition<S>(
30193020
dispatchOptimisticSetState(fiber, false, queue, pendingState);
30203021
} else {
30213022
ReactSharedInternals.T = null;
3022-
dispatchSetState(fiber, queue, pendingState);
3023+
dispatchSetStateInternal(
3024+
fiber,
3025+
queue,
3026+
pendingState,
3027+
requestUpdateLane(fiber),
3028+
);
30233029
ReactSharedInternals.T = currentTransition;
30243030
}
30253031

@@ -3062,13 +3068,28 @@ function startTransition<S>(
30623068
thenable,
30633069
finishedState,
30643070
);
3065-
dispatchSetState(fiber, queue, (thenableForFinishedState: any));
3071+
dispatchSetStateInternal(
3072+
fiber,
3073+
queue,
3074+
(thenableForFinishedState: any),
3075+
requestUpdateLane(fiber),
3076+
);
30663077
} else {
3067-
dispatchSetState(fiber, queue, finishedState);
3078+
dispatchSetStateInternal(
3079+
fiber,
3080+
queue,
3081+
finishedState,
3082+
requestUpdateLane(fiber),
3083+
);
30683084
}
30693085
} else {
30703086
// Async actions are not enabled.
3071-
dispatchSetState(fiber, queue, finishedState);
3087+
dispatchSetStateInternal(
3088+
fiber,
3089+
queue,
3090+
finishedState,
3091+
requestUpdateLane(fiber),
3092+
);
30723093
callback();
30733094
}
30743095
} catch (error) {
@@ -3081,7 +3102,12 @@ function startTransition<S>(
30813102
status: 'rejected',
30823103
reason: error,
30833104
};
3084-
dispatchSetState(fiber, queue, rejectedThenable);
3105+
dispatchSetStateInternal(
3106+
fiber,
3107+
queue,
3108+
rejectedThenable,
3109+
requestUpdateLane(fiber),
3110+
);
30853111
} else {
30863112
// The error rethrowing behavior is only enabled when the async actions
30873113
// feature is on, even for sync actions.
@@ -3253,7 +3279,12 @@ export function requestFormReset(formFiber: Fiber) {
32533279
const newResetState = {};
32543280
const resetStateHook: Hook = (stateHook.next: any);
32553281
const resetStateQueue = resetStateHook.queue;
3256-
dispatchSetState(formFiber, resetStateQueue, newResetState);
3282+
dispatchSetStateInternal(
3283+
formFiber,
3284+
resetStateQueue,
3285+
newResetState,
3286+
requestUpdateLane(formFiber),
3287+
);
32573288
}
32583289

32593290
function mountTransition(): [
@@ -3385,6 +3416,7 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T): void {
33853416
const refreshUpdate = createLegacyQueueUpdate(lane);
33863417
const root = enqueueLegacyQueueUpdate(provider, refreshUpdate, lane);
33873418
if (root !== null) {
3419+
startUpdateTimerByLane(lane);
33883420
scheduleUpdateOnFiber(root, provider, lane);
33893421
entangleLegacyQueueTransitions(root, provider, lane);
33903422
}
@@ -3450,6 +3482,7 @@ function dispatchReducerAction<S, A>(
34503482
} else {
34513483
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
34523484
if (root !== null) {
3485+
startUpdateTimerByLane(lane);
34533486
scheduleUpdateOnFiber(root, fiber, lane);
34543487
entangleTransitionUpdate(root, queue, lane);
34553488
}
@@ -3474,7 +3507,24 @@ function dispatchSetState<S, A>(
34743507
}
34753508

34763509
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+
}
34773521

3522+
function dispatchSetStateInternal<S, A>(
3523+
fiber: Fiber,
3524+
queue: UpdateQueue<S, A>,
3525+
action: A,
3526+
lane: Lane,
3527+
): boolean {
34783528
const update: Update<S, A> = {
34793529
lane,
34803530
revertLane: NoLane,
@@ -3518,7 +3568,7 @@ function dispatchSetState<S, A>(
35183568
// time the reducer has changed.
35193569
// TODO: Do we still need to entangle transitions in this case?
35203570
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
3521-
return;
3571+
return false;
35223572
}
35233573
} catch (error) {
35243574
// Suppress the error. It will throw again in the render phase.
@@ -3534,10 +3584,10 @@ function dispatchSetState<S, A>(
35343584
if (root !== null) {
35353585
scheduleUpdateOnFiber(root, fiber, lane);
35363586
entangleTransitionUpdate(root, queue, lane);
3587+
return true;
35373588
}
35383589
}
3539-
3540-
markUpdateInDevTools(fiber, lane, action);
3590+
return false;
35413591
}
35423592

35433593
function dispatchOptimisticSetState<S, A>(
@@ -3619,6 +3669,7 @@ function dispatchOptimisticSetState<S, A>(
36193669
// will never be attempted before the optimistic update. This currently
36203670
// holds because the optimistic update is always synchronous. If we ever
36213671
// change that, we'll need to account for this.
3672+
startUpdateTimerByLane(SyncLane);
36223673
scheduleUpdateOnFiber(root, fiber, SyncLane);
36233674
// Optimistic updates are always synchronous, so we don't need to call
36243675
// entangleTransitionUpdate here.

packages/react-reconciler/src/ReactFiberLane.js

+17
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,10 @@ export function includesSyncLane(lanes: Lanes): boolean {
592592
return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes;
593593
}
594594

595+
export function isSyncLane(lanes: Lanes): boolean {
596+
return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes;
597+
}
598+
595599
export function includesNonIdleWork(lanes: Lanes): boolean {
596600
return (lanes & NonIdleLanes) !== NoLanes;
597601
}
@@ -608,6 +612,10 @@ export function includesOnlyTransitions(lanes: Lanes): boolean {
608612
return (lanes & TransitionLanes) === lanes;
609613
}
610614

615+
export function includesTransitionLane(lanes: Lanes): boolean {
616+
return (lanes & TransitionLanes) !== NoLanes;
617+
}
618+
611619
export function includesBlockingLane(lanes: Lanes): boolean {
612620
const SyncDefaultLanes =
613621
InputContinuousHydrationLane |
@@ -623,6 +631,15 @@ export function includesExpiredLane(root: FiberRoot, lanes: Lanes): boolean {
623631
return (lanes & root.expiredLanes) !== NoLanes;
624632
}
625633

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+
626643
export function isTransitionLane(lane: Lane): boolean {
627644
return (lane & TransitionLanes) !== NoLanes;
628645
}

0 commit comments

Comments
 (0)