Add Gesture Track in Performance Tab (#34546)

This commit is contained in:
Sebastian Markbåge 2025-09-24 11:20:14 -04:00 committed by GitHub
parent e0c421ab71
commit 05b61f812a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 328 additions and 13 deletions

View File

@ -28,6 +28,7 @@ import {
retryLaneExpirationMs,
disableLegacyMode,
enableDefaultTransitionIndicator,
enableGestureTransition,
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
import {clz32} from './clz32';
@ -710,6 +711,9 @@ export function isTransitionLane(lane: Lane): boolean {
}
export function isGestureRender(lanes: Lanes): boolean {
if (!enableGestureTransition) {
return false;
}
// This should render only the one lane.
return lanes === GestureLane;
}
@ -1271,11 +1275,13 @@ export function getGroupNameOfHighestPriorityLane(lanes: Lanes): string {
InputContinuousHydrationLane |
InputContinuousLane |
DefaultHydrationLane |
DefaultLane |
GestureLane)
DefaultLane)
) {
return 'Blocking';
}
if (lanes & GestureLane) {
return 'Gesture';
}
if (lanes & (TransitionHydrationLane | TransitionLanes)) {
return 'Transition';
}

View File

@ -33,7 +33,10 @@ import {
addObjectDiffToProperties,
} from 'shared/ReactPerformanceTrackProperties';
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
import {
enableProfilerTimer,
enableGestureTransition,
} from 'shared/ReactFeatureFlags';
const supportsUserTiming =
enableProfilerTimer &&
@ -68,6 +71,16 @@ export function markAllLanesInOrder() {
LANES_TRACK_GROUP,
'primary-light',
);
if (enableGestureTransition) {
console.timeStamp(
'Gesture Track',
0.003,
0.003,
'Gesture',
LANES_TRACK_GROUP,
'primary-light',
);
}
console.timeStamp(
'Transition Track',
0.003,
@ -739,6 +752,145 @@ export function logBlockingStart(
}
}
export function logGestureStart(
startTime: number,
updateTime: number,
eventTime: number,
eventType: null | string,
eventIsRepeat: boolean,
isPingedUpdate: boolean,
renderStartTime: number,
debugTask: null | ConsoleTask, // DEV-only
updateMethodName: null | string,
updateComponentName: null | string,
): void {
if (supportsUserTiming) {
currentTrack = 'Gesture';
// Clamp start times
if (updateTime > 0) {
if (updateTime > renderStartTime) {
updateTime = renderStartTime;
}
} else {
updateTime = renderStartTime;
}
if (startTime > 0) {
if (startTime > updateTime) {
startTime = updateTime;
}
} else {
startTime = updateTime;
}
if (eventTime > 0) {
if (eventTime > startTime) {
eventTime = startTime;
}
} else {
eventTime = startTime;
}
if (startTime > eventTime && eventType !== null) {
// Log the time from the event timeStamp until we started a gesture.
const color = eventIsRepeat ? 'secondary-light' : 'warning';
if (__DEV__ && debugTask) {
debugTask.run(
console.timeStamp.bind(
console,
eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
eventTime,
startTime,
currentTrack,
LANES_TRACK_GROUP,
color,
),
);
} else {
console.timeStamp(
eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
eventTime,
startTime,
currentTrack,
LANES_TRACK_GROUP,
color,
);
}
}
if (updateTime > startTime) {
// Log the time from when we started a gesture until we called setState or started rendering.
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Gesture',
startTime,
updateTime,
currentTrack,
LANES_TRACK_GROUP,
'primary-dark',
),
);
} else {
console.timeStamp(
'Gesture',
startTime,
updateTime,
currentTrack,
LANES_TRACK_GROUP,
'primary-dark',
);
}
}
if (renderStartTime > updateTime) {
// Log the time from when we called setState until we started rendering.
const label = isPingedUpdate
? 'Promise Resolved'
: renderStartTime - updateTime > 5
? 'Update Blocked'
: 'Update';
if (__DEV__) {
const properties = [];
if (updateComponentName != null) {
properties.push(['Component name', updateComponentName]);
}
if (updateMethodName != null) {
properties.push(['Method name', updateMethodName]);
}
const measureOptions = {
start: updateTime,
end: renderStartTime,
detail: {
devtools: {
properties,
track: currentTrack,
trackGroup: LANES_TRACK_GROUP,
color: 'primary-light',
},
},
};
if (debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(performance, label, measureOptions),
);
} else {
performance.measure(label, measureOptions);
}
} else {
console.timeStamp(
label,
updateTime,
renderStartTime,
currentTrack,
LANES_TRACK_GROUP,
'primary-light',
);
}
}
}
}
export function logTransitionStart(
startTime: number,
updateTime: number,

View File

@ -71,6 +71,7 @@ import {
} from './Scheduler';
import {
logBlockingStart,
logGestureStart,
logTransitionStart,
logRenderPhase,
logInterruptedRenderPhase,
@ -282,6 +283,17 @@ import {
blockingEventType,
blockingEventIsRepeat,
blockingSuspendedTime,
gestureClampTime,
gestureStartTime,
gestureUpdateTime,
gestureUpdateTask,
gestureUpdateType,
gestureUpdateMethodName,
gestureUpdateComponentName,
gestureEventTime,
gestureEventType,
gestureEventIsRepeat,
gestureSuspendedTime,
transitionClampTime,
transitionStartTime,
transitionUpdateTime,
@ -294,8 +306,10 @@ import {
transitionEventIsRepeat,
transitionSuspendedTime,
clearBlockingTimers,
clearGestureTimers,
clearTransitionTimers,
clampBlockingTimers,
clampGestureTimers,
clampTransitionTimers,
clampRetryTimers,
clampIdleTimers,
@ -1898,7 +1912,9 @@ function resetWorkInProgressStack() {
function finalizeRender(lanes: Lanes, finalizationTime: number): void {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if (includesBlockingLane(lanes)) {
if (isGestureRender(lanes)) {
clampGestureTimers(finalizationTime);
} else if (includesBlockingLane(lanes)) {
clampBlockingTimers(finalizationTime);
}
if (includesTransitionLane(lanes)) {
@ -1963,7 +1979,58 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
const previousUpdateTask = workInProgressUpdateTask;
workInProgressUpdateTask = null;
if (includesBlockingLane(lanes)) {
if (isGestureRender(lanes)) {
workInProgressUpdateTask = gestureUpdateTask;
const clampedStartTime =
gestureStartTime >= 0 && gestureStartTime < gestureClampTime
? gestureClampTime
: gestureStartTime;
const clampedUpdateTime =
gestureUpdateTime >= 0 && gestureUpdateTime < gestureClampTime
? gestureClampTime
: gestureUpdateTime;
const clampedEventTime =
gestureEventTime >= 0 && gestureEventTime < gestureClampTime
? gestureClampTime
: gestureEventTime;
const clampedRenderStartTime =
// Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime;
if (gestureSuspendedTime >= 0) {
setCurrentTrackFromLanes(GestureLane);
logSuspendedWithDelayPhase(
gestureSuspendedTime,
clampedRenderStartTime,
lanes,
workInProgressUpdateTask,
);
} else if (isGestureRender(animatingLanes)) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(GestureLane);
logAnimatingPhase(
gestureClampTime,
clampedRenderStartTime,
animatingTask,
);
}
logGestureStart(
clampedStartTime,
clampedUpdateTime,
clampedEventTime,
gestureEventType,
gestureEventIsRepeat,
gestureUpdateType === PINGED_UPDATE,
renderStartTime,
gestureUpdateTask,
gestureUpdateMethodName,
gestureUpdateComponentName,
);
clearGestureTimers();
} else if (includesBlockingLane(lanes)) {
workInProgressUpdateTask = blockingUpdateTask;
const clampedUpdateTime =
blockingUpdateTime >= 0 && blockingUpdateTime < blockingClampTime
@ -3716,12 +3783,12 @@ function finishedViewTransition(lanes: Lanes): void {
// If an affected track isn't in the middle of rendering or committing, log from the previous
// finished render until the end of the animation.
if (
includesBlockingLane(lanes) &&
!includesBlockingLane(workInProgressRootRenderLanes) &&
!includesBlockingLane(pendingEffectsLanes)
isGestureRender(lanes) &&
!isGestureRender(workInProgressRootRenderLanes) &&
!isGestureRender(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(SyncLane);
logAnimatingPhase(blockingClampTime, now(), task);
setCurrentTrackFromLanes(GestureLane);
logAnimatingPhase(gestureClampTime, now(), task);
}
if (
includesTransitionLane(lanes) &&

View File

@ -18,6 +18,7 @@ import type {CapturedValue} from './ReactCapturedValue';
import {
isTransitionLane,
isBlockingLane,
isGestureRender,
includesTransitionLane,
includesBlockingLane,
NoLanes,
@ -74,6 +75,19 @@ export let blockingEventTime: number = -1.1; // Event timeStamp of the first set
export let blockingEventType: null | string = null; // Event type of the first setState.
export let blockingEventIsRepeat: boolean = false;
export let blockingSuspendedTime: number = -1.1;
export let gestureClampTime: number = -0;
export let gestureStartTime: number = -1.1; // First startGestureTransition call before setOptimistic.
export let gestureUpdateTime: number = -1.1; // First setOptimistic scheduled inside startGestureTransition.
export let gestureUpdateTask: null | ConsoleTask = null; // First sync setState's stack trace.
export let gestureUpdateType: UpdateType = 0;
export let gestureUpdateMethodName: null | string = null; // The name of the method that caused first gesture update.
export let gestureUpdateComponentName: null | string = null; // The name of the component where first gesture update happened.
export let gestureEventTime: number = -1.1; // Event timeStamp of the first setState.
export let gestureEventType: null | string = null; // Event type of the first setState.
export let gestureEventIsRepeat: boolean = false;
export let gestureSuspendedTime: number = -1.1;
// TODO: This should really be one per Transition lane.
export let transitionClampTime: number = -0;
export let transitionStartTime: number = -1.1; // First startTransition call before setState.
@ -112,7 +126,28 @@ export function startUpdateTimerByLane(
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
if (isBlockingLane(lane)) {
if (isGestureRender(lane)) {
if (gestureUpdateTime < 0) {
gestureUpdateTime = now();
gestureUpdateTask = createTask(method);
gestureUpdateMethodName = method;
if (__DEV__ && fiber != null) {
gestureUpdateComponentName = getComponentNameFromFiber(fiber);
}
if (gestureStartTime < 0) {
const newEventTime = resolveEventTimeStamp();
const newEventType = resolveEventType();
if (
newEventTime !== gestureEventTime ||
newEventType !== gestureEventType
) {
gestureEventIsRepeat = false;
}
gestureEventTime = newEventTime;
gestureEventType = newEventType;
}
}
} else if (isBlockingLane(lane)) {
if (blockingUpdateTime < 0) {
blockingUpdateTime = now();
blockingUpdateTask = createTask(method);
@ -218,7 +253,13 @@ export function startPingTimerByLanes(lanes: Lanes): void {
// Mark the update time and clamp anything before it because we don't want
// to show the event time for pings but we also don't want to clear it
// because we still need to track if this was a repeat.
if (includesBlockingLane(lanes)) {
if (isGestureRender(lanes)) {
if (gestureUpdateTime < 0) {
gestureClampTime = gestureUpdateTime = now();
gestureUpdateTask = createTask('Promise Resolved');
gestureUpdateType = PINGED_UPDATE;
}
} else if (includesBlockingLane(lanes)) {
if (blockingUpdateTime < 0) {
blockingClampTime = blockingUpdateTime = now();
blockingUpdateTask = createTask('Promise Resolved');
@ -237,7 +278,9 @@ export function trackSuspendedTime(lanes: Lanes, renderEndTime: number) {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
if (includesBlockingLane(lanes)) {
if (isGestureRender(lanes)) {
gestureSuspendedTime = renderEndTime;
} else if (includesBlockingLane(lanes)) {
blockingSuspendedTime = renderEndTime;
} else if (includesTransitionLane(lanes)) {
transitionSuspendedTime = renderEndTime;
@ -291,6 +334,43 @@ export function clearTransitionTimers(): void {
transitionClampTime = now();
}
export function startGestureTransitionTimer(): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
if (gestureStartTime < 0 && gestureUpdateTime < 0) {
gestureStartTime = now();
const newEventTime = resolveEventTimeStamp();
const newEventType = resolveEventType();
if (
newEventTime !== gestureEventTime ||
newEventType !== gestureEventType
) {
gestureEventIsRepeat = false;
}
gestureEventTime = newEventTime;
gestureEventType = newEventType;
}
}
export function hasScheduledGestureTransitionWork(): boolean {
// If we have call setOptimistic on a gesture
return gestureUpdateTime > -1;
}
export function clearGestureTransitionTimer(): void {
gestureStartTime = -1.1;
}
export function clearGestureTimers(): void {
gestureStartTime = -1.1;
gestureUpdateTime = -1.1;
gestureUpdateType = 0;
gestureSuspendedTime = -1.1;
gestureEventIsRepeat = true;
gestureClampTime = now();
}
export function clampBlockingTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
@ -301,6 +381,16 @@ export function clampBlockingTimers(finalTime: number): void {
blockingClampTime = finalTime;
}
export function clampGestureTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
// If we had new updates come in while we were still rendering or committing, we don't want
// those update times to create overlapping tracks in the performance timeline so we clamp
// them to the end of the commit phase.
gestureClampTime = finalTime;
}
export function clampTransitionTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;