@samitouri / QOS-React / commits / 05b61f812a

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;