Add Gesture Track in Performance Tab (#34546)
Sebastian Markbåge committed
Sep 24, 2025 at 11:20 UTC
05b61f812a2070276c5db0d2107808a6161632fc
4 files changed
+328
-13
packages/react-reconciler/src/ReactFiberLane.js
+8
-2
@@ -28,6 +28,7 @@ import {
28
retryLaneExpirationMs,
29
disableLegacyMode,
30
enableDefaultTransitionIndicator,
31
+ enableGestureTransition,
32
} from 'shared/ReactFeatureFlags';
33
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
34
import {clz32} from './clz32';
@@ -710,6 +711,9 @@ export function isTransitionLane(lane: Lane): boolean {
711
}
712
713
export function isGestureRender(lanes: Lanes): boolean {
714
+ if (!enableGestureTransition) {
715
+ return false;
716
+ }
717
// This should render only the one lane.
718
return lanes === GestureLane;
719
}
@@ -1271,11 +1275,13 @@ export function getGroupNameOfHighestPriorityLane(lanes: Lanes): string {
1275
InputContinuousHydrationLane |
1276
InputContinuousLane |
1277
DefaultHydrationLane |
1274
- DefaultLane |
1275
- GestureLane)
1278
+ DefaultLane)
1279
) {
1280
return 'Blocking';
1281
}
1282
+ if (lanes & GestureLane) {
1283
+ return 'Gesture';
1284
+ }
1285
if (lanes & (TransitionHydrationLane | TransitionLanes)) {
1286
return 'Transition';
1287
}
packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+153
-1
@@ -33,7 +33,10 @@ import {
33
addObjectDiffToProperties,
34
} from 'shared/ReactPerformanceTrackProperties';
35
36
-import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
36
+import {
37
+ enableProfilerTimer,
38
+ enableGestureTransition,
39
+} from 'shared/ReactFeatureFlags';
40
41
const supportsUserTiming =
42
enableProfilerTimer &&
@@ -68,6 +71,16 @@ export function markAllLanesInOrder() {
71
LANES_TRACK_GROUP,
72
'primary-light',
73
);
74
+ if (enableGestureTransition) {
75
+ console.timeStamp(
76
+ 'Gesture Track',
77
+ 0.003,
78
+ 0.003,
79
+ 'Gesture',
80
+ LANES_TRACK_GROUP,
81
+ 'primary-light',
82
+ );
83
+ }
84
console.timeStamp(
85
'Transition Track',
86
0.003,
@@ -739,6 +752,145 @@ export function logBlockingStart(
752
}
753
}
754
755
+export function logGestureStart(
756
+ startTime: number,
757
+ updateTime: number,
758
+ eventTime: number,
759
+ eventType: null | string,
760
+ eventIsRepeat: boolean,
761
+ isPingedUpdate: boolean,
762
+ renderStartTime: number,
763
+ debugTask: null | ConsoleTask, // DEV-only
764
+ updateMethodName: null | string,
765
+ updateComponentName: null | string,
766
+): void {
767
+ if (supportsUserTiming) {
768
+ currentTrack = 'Gesture';
769
+ // Clamp start times
770
+ if (updateTime > 0) {
771
+ if (updateTime > renderStartTime) {
772
+ updateTime = renderStartTime;
773
+ }
774
+ } else {
775
+ updateTime = renderStartTime;
776
+ }
777
+ if (startTime > 0) {
778
+ if (startTime > updateTime) {
779
+ startTime = updateTime;
780
+ }
781
+ } else {
782
+ startTime = updateTime;
783
+ }
784
+ if (eventTime > 0) {
785
+ if (eventTime > startTime) {
786
+ eventTime = startTime;
787
+ }
788
+ } else {
789
+ eventTime = startTime;
790
+ }
791
+
792
+ if (startTime > eventTime && eventType !== null) {
793
+ // Log the time from the event timeStamp until we started a gesture.
794
+ const color = eventIsRepeat ? 'secondary-light' : 'warning';
795
+ if (__DEV__ && debugTask) {
796
+ debugTask.run(
797
+ console.timeStamp.bind(
798
+ console,
799
+ eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
800
+ eventTime,
801
+ startTime,
802
+ currentTrack,
803
+ LANES_TRACK_GROUP,
804
+ color,
805
+ ),
806
+ );
807
+ } else {
808
+ console.timeStamp(
809
+ eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
810
+ eventTime,
811
+ startTime,
812
+ currentTrack,
813
+ LANES_TRACK_GROUP,
814
+ color,
815
+ );
816
+ }
817
+ }
818
+ if (updateTime > startTime) {
819
+ // Log the time from when we started a gesture until we called setState or started rendering.
820
+ if (__DEV__ && debugTask) {
821
+ debugTask.run(
822
+ // $FlowFixMe[method-unbinding]
823
+ console.timeStamp.bind(
824
+ console,
825
+ 'Gesture',
826
+ startTime,
827
+ updateTime,
828
+ currentTrack,
829
+ LANES_TRACK_GROUP,
830
+ 'primary-dark',
831
+ ),
832
+ );
833
+ } else {
834
+ console.timeStamp(
835
+ 'Gesture',
836
+ startTime,
837
+ updateTime,
838
+ currentTrack,
839
+ LANES_TRACK_GROUP,
840
+ 'primary-dark',
841
+ );
842
+ }
843
+ }
844
+ if (renderStartTime > updateTime) {
845
+ // Log the time from when we called setState until we started rendering.
846
+ const label = isPingedUpdate
847
+ ? 'Promise Resolved'
848
+ : renderStartTime - updateTime > 5
849
+ ? 'Update Blocked'
850
+ : 'Update';
851
+ if (__DEV__) {
852
+ const properties = [];
853
+ if (updateComponentName != null) {
854
+ properties.push(['Component name', updateComponentName]);
855
+ }
856
+ if (updateMethodName != null) {
857
+ properties.push(['Method name', updateMethodName]);
858
+ }
859
+ const measureOptions = {
860
+ start: updateTime,
861
+ end: renderStartTime,
862
+ detail: {
863
+ devtools: {
864
+ properties,
865
+ track: currentTrack,
866
+ trackGroup: LANES_TRACK_GROUP,
867
+ color: 'primary-light',
868
+ },
869
+ },
870
+ };
871
+
872
+ if (debugTask) {
873
+ debugTask.run(
874
+ // $FlowFixMe[method-unbinding]
875
+ performance.measure.bind(performance, label, measureOptions),
876
+ );
877
+ } else {
878
+ performance.measure(label, measureOptions);
879
+ }
880
+ } else {
881
+ console.timeStamp(
882
+ label,
883
+ updateTime,
884
+ renderStartTime,
885
+ currentTrack,
886
+ LANES_TRACK_GROUP,
887
+ 'primary-light',
888
+ );
889
+ }
890
+ }
891
+ }
892
+}
893
+
894
export function logTransitionStart(
895
startTime: number,
896
updateTime: number,
packages/react-reconciler/src/ReactFiberWorkLoop.js
+74
-7
@@ -71,6 +71,7 @@ import {
71
} from './Scheduler';
72
import {
73
logBlockingStart,
74
+ logGestureStart,
75
logTransitionStart,
76
logRenderPhase,
77
logInterruptedRenderPhase,
@@ -282,6 +283,17 @@ import {
283
blockingEventType,
284
blockingEventIsRepeat,
285
blockingSuspendedTime,
286
+ gestureClampTime,
287
+ gestureStartTime,
288
+ gestureUpdateTime,
289
+ gestureUpdateTask,
290
+ gestureUpdateType,
291
+ gestureUpdateMethodName,
292
+ gestureUpdateComponentName,
293
+ gestureEventTime,
294
+ gestureEventType,
295
+ gestureEventIsRepeat,
296
+ gestureSuspendedTime,
297
transitionClampTime,
298
transitionStartTime,
299
transitionUpdateTime,
@@ -294,8 +306,10 @@ import {
306
transitionEventIsRepeat,
307
transitionSuspendedTime,
308
clearBlockingTimers,
309
+ clearGestureTimers,
310
clearTransitionTimers,
311
clampBlockingTimers,
312
+ clampGestureTimers,
313
clampTransitionTimers,
314
clampRetryTimers,
315
clampIdleTimers,
@@ -1898,7 +1912,9 @@ function resetWorkInProgressStack() {
1912
1913
function finalizeRender(lanes: Lanes, finalizationTime: number): void {
1914
if (enableProfilerTimer && enableComponentPerformanceTrack) {
1901
- if (includesBlockingLane(lanes)) {
1915
+ if (isGestureRender(lanes)) {
1916
+ clampGestureTimers(finalizationTime);
1917
+ } else if (includesBlockingLane(lanes)) {
1918
clampBlockingTimers(finalizationTime);
1919
}
1920
if (includesTransitionLane(lanes)) {
@@ -1963,7 +1979,58 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1979
const previousUpdateTask = workInProgressUpdateTask;
1980
1981
workInProgressUpdateTask = null;
1966
- if (includesBlockingLane(lanes)) {
1982
+ if (isGestureRender(lanes)) {
1983
+ workInProgressUpdateTask = gestureUpdateTask;
1984
+ const clampedStartTime =
1985
+ gestureStartTime >= 0 && gestureStartTime < gestureClampTime
1986
+ ? gestureClampTime
1987
+ : gestureStartTime;
1988
+ const clampedUpdateTime =
1989
+ gestureUpdateTime >= 0 && gestureUpdateTime < gestureClampTime
1990
+ ? gestureClampTime
1991
+ : gestureUpdateTime;
1992
+ const clampedEventTime =
1993
+ gestureEventTime >= 0 && gestureEventTime < gestureClampTime
1994
+ ? gestureClampTime
1995
+ : gestureEventTime;
1996
+ const clampedRenderStartTime =
1997
+ // Clamp the suspended time to the first event/update.
1998
+ clampedEventTime >= 0
1999
+ ? clampedEventTime
2000
+ : clampedUpdateTime >= 0
2001
+ ? clampedUpdateTime
2002
+ : renderStartTime;
2003
+ if (gestureSuspendedTime >= 0) {
2004
+ setCurrentTrackFromLanes(GestureLane);
2005
+ logSuspendedWithDelayPhase(
2006
+ gestureSuspendedTime,
2007
+ clampedRenderStartTime,
2008
+ lanes,
2009
+ workInProgressUpdateTask,
2010
+ );
2011
+ } else if (isGestureRender(animatingLanes)) {
2012
+ // If this lane is still animating, log the time from previous render finishing to now as animating.
2013
+ setCurrentTrackFromLanes(GestureLane);
2014
+ logAnimatingPhase(
2015
+ gestureClampTime,
2016
+ clampedRenderStartTime,
2017
+ animatingTask,
2018
+ );
2019
+ }
2020
+ logGestureStart(
2021
+ clampedStartTime,
2022
+ clampedUpdateTime,
2023
+ clampedEventTime,
2024
+ gestureEventType,
2025
+ gestureEventIsRepeat,
2026
+ gestureUpdateType === PINGED_UPDATE,
2027
+ renderStartTime,
2028
+ gestureUpdateTask,
2029
+ gestureUpdateMethodName,
2030
+ gestureUpdateComponentName,
2031
+ );
2032
+ clearGestureTimers();
2033
+ } else if (includesBlockingLane(lanes)) {
2034
workInProgressUpdateTask = blockingUpdateTask;
2035
const clampedUpdateTime =
2036
blockingUpdateTime >= 0 && blockingUpdateTime < blockingClampTime
@@ -3716,12 +3783,12 @@ function finishedViewTransition(lanes: Lanes): void {
3783
// If an affected track isn't in the middle of rendering or committing, log from the previous
3784
// finished render until the end of the animation.
3785
if (
3719
- includesBlockingLane(lanes) &&
3720
- !includesBlockingLane(workInProgressRootRenderLanes) &&
3721
- !includesBlockingLane(pendingEffectsLanes)
3786
+ isGestureRender(lanes) &&
3787
+ !isGestureRender(workInProgressRootRenderLanes) &&
3788
+ !isGestureRender(pendingEffectsLanes)
3789
) {
3723
- setCurrentTrackFromLanes(SyncLane);
3724
- logAnimatingPhase(blockingClampTime, now(), task);
3790
+ setCurrentTrackFromLanes(GestureLane);
3791
+ logAnimatingPhase(gestureClampTime, now(), task);
3792
}
3793
if (
3794
includesTransitionLane(lanes) &&
packages/react-reconciler/src/ReactProfilerTimer.js
+93
-3
@@ -18,6 +18,7 @@ import type {CapturedValue} from './ReactCapturedValue';
18
import {
19
isTransitionLane,
20
isBlockingLane,
21
+ isGestureRender,
22
includesTransitionLane,
23
includesBlockingLane,
24
NoLanes,
@@ -74,6 +75,19 @@ export let blockingEventTime: number = -1.1; // Event timeStamp of the first set
75
export let blockingEventType: null | string = null; // Event type of the first setState.
76
export let blockingEventIsRepeat: boolean = false;
77
export let blockingSuspendedTime: number = -1.1;
78
+
79
+export let gestureClampTime: number = -0;
80
+export let gestureStartTime: number = -1.1; // First startGestureTransition call before setOptimistic.
81
+export let gestureUpdateTime: number = -1.1; // First setOptimistic scheduled inside startGestureTransition.
82
+export let gestureUpdateTask: null | ConsoleTask = null; // First sync setState's stack trace.
83
+export let gestureUpdateType: UpdateType = 0;
84
+export let gestureUpdateMethodName: null | string = null; // The name of the method that caused first gesture update.
85
+export let gestureUpdateComponentName: null | string = null; // The name of the component where first gesture update happened.
86
+export let gestureEventTime: number = -1.1; // Event timeStamp of the first setState.
87
+export let gestureEventType: null | string = null; // Event type of the first setState.
88
+export let gestureEventIsRepeat: boolean = false;
89
+export let gestureSuspendedTime: number = -1.1;
90
+
91
// TODO: This should really be one per Transition lane.
92
export let transitionClampTime: number = -0;
93
export let transitionStartTime: number = -1.1; // First startTransition call before setState.
@@ -112,7 +126,28 @@ export function startUpdateTimerByLane(
126
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
127
return;
128
}
115
- if (isBlockingLane(lane)) {
129
+ if (isGestureRender(lane)) {
130
+ if (gestureUpdateTime < 0) {
131
+ gestureUpdateTime = now();
132
+ gestureUpdateTask = createTask(method);
133
+ gestureUpdateMethodName = method;
134
+ if (__DEV__ && fiber != null) {
135
+ gestureUpdateComponentName = getComponentNameFromFiber(fiber);
136
+ }
137
+ if (gestureStartTime < 0) {
138
+ const newEventTime = resolveEventTimeStamp();
139
+ const newEventType = resolveEventType();
140
+ if (
141
+ newEventTime !== gestureEventTime ||
142
+ newEventType !== gestureEventType
143
+ ) {
144
+ gestureEventIsRepeat = false;
145
+ }
146
+ gestureEventTime = newEventTime;
147
+ gestureEventType = newEventType;
148
+ }
149
+ }
150
+ } else if (isBlockingLane(lane)) {
151
if (blockingUpdateTime < 0) {
152
blockingUpdateTime = now();
153
blockingUpdateTask = createTask(method);
@@ -218,7 +253,13 @@ export function startPingTimerByLanes(lanes: Lanes): void {
253
// Mark the update time and clamp anything before it because we don't want
254
// to show the event time for pings but we also don't want to clear it
255
// because we still need to track if this was a repeat.
221
- if (includesBlockingLane(lanes)) {
256
+ if (isGestureRender(lanes)) {
257
+ if (gestureUpdateTime < 0) {
258
+ gestureClampTime = gestureUpdateTime = now();
259
+ gestureUpdateTask = createTask('Promise Resolved');
260
+ gestureUpdateType = PINGED_UPDATE;
261
+ }
262
+ } else if (includesBlockingLane(lanes)) {
263
if (blockingUpdateTime < 0) {
264
blockingClampTime = blockingUpdateTime = now();
265
blockingUpdateTask = createTask('Promise Resolved');
@@ -237,7 +278,9 @@ export function trackSuspendedTime(lanes: Lanes, renderEndTime: number) {
278
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
279
return;
280
}
240
- if (includesBlockingLane(lanes)) {
281
+ if (isGestureRender(lanes)) {
282
+ gestureSuspendedTime = renderEndTime;
283
+ } else if (includesBlockingLane(lanes)) {
284
blockingSuspendedTime = renderEndTime;
285
} else if (includesTransitionLane(lanes)) {
286
transitionSuspendedTime = renderEndTime;
@@ -291,6 +334,43 @@ export function clearTransitionTimers(): void {
334
transitionClampTime = now();
335
}
336
337
+export function startGestureTransitionTimer(): void {
338
+ if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
339
+ return;
340
+ }
341
+ if (gestureStartTime < 0 && gestureUpdateTime < 0) {
342
+ gestureStartTime = now();
343
+ const newEventTime = resolveEventTimeStamp();
344
+ const newEventType = resolveEventType();
345
+ if (
346
+ newEventTime !== gestureEventTime ||
347
+ newEventType !== gestureEventType
348
+ ) {
349
+ gestureEventIsRepeat = false;
350
+ }
351
+ gestureEventTime = newEventTime;
352
+ gestureEventType = newEventType;
353
+ }
354
+}
355
+
356
+export function hasScheduledGestureTransitionWork(): boolean {
357
+ // If we have call setOptimistic on a gesture
358
+ return gestureUpdateTime > -1;
359
+}
360
+
361
+export function clearGestureTransitionTimer(): void {
362
+ gestureStartTime = -1.1;
363
+}
364
+
365
+export function clearGestureTimers(): void {
366
+ gestureStartTime = -1.1;
367
+ gestureUpdateTime = -1.1;
368
+ gestureUpdateType = 0;
369
+ gestureSuspendedTime = -1.1;
370
+ gestureEventIsRepeat = true;
371
+ gestureClampTime = now();
372
+}
373
+
374
export function clampBlockingTimers(finalTime: number): void {
375
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
376
return;
@@ -301,6 +381,16 @@ export function clampBlockingTimers(finalTime: number): void {
381
blockingClampTime = finalTime;
382
}
383
384
+export function clampGestureTimers(finalTime: number): void {
385
+ if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
386
+ return;
387
+ }
388
+ // If we had new updates come in while we were still rendering or committing, we don't want
389
+ // those update times to create overlapping tracks in the performance timeline so we clamp
390
+ // them to the end of the commit phase.
391
+ gestureClampTime = finalTime;
392
+}
393
+
394
export function clampTransitionTimers(finalTime: number): void {
395
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
396
return;