Ensure Performance Track are Clamped and Don't overlap (#34509)
This simplifies the logic for clamping the start times of various phases. Instead of checking in multiple places I ensure we compute a value for each phase that is then clamped to the next phase so they don't overlap. If they're zero they're not printed. I also added a name for all the anonymous labels. Those are mainly fillers for sync work that should be quick but it helps debugging if we can name them. Finally the real fix is to update the clamp time which previously could lead to overlapping entries for consecutive updates when a previous update never finalized before the next update.
Sebastian Markbåge committed
Sep 17, 2025 at 10:52 UTC
e3c9656d20618ed321aea85cb3d844cbd1dce078
3 files changed
+58
-21
packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+55
-21
@@ -636,11 +636,25 @@ export function logBlockingStart(
636
): void {
637
if (supportsUserTiming) {
638
currentTrack = 'Blocking';
639
+ // Clamp start times
640
+ if (updateTime > 0) {
641
+ if (updateTime > renderStartTime) {
642
+ updateTime = renderStartTime;
643
+ }
644
+ } else {
645
+ updateTime = renderStartTime;
646
+ }
647
+ if (eventTime > 0) {
648
+ if (eventTime > updateTime) {
649
+ eventTime = updateTime;
650
+ }
651
+ } else {
652
+ eventTime = updateTime;
653
+ }
654
// If a blocking update was spawned within render or an effect, that's considered a cascading render.
655
// If you have a second blocking update within the same event, that suggests multiple flushSync or
656
// setState in a microtask which is also considered a cascade.
642
- const eventEndTime = updateTime > 0 ? updateTime : renderStartTime;
643
- if (eventTime > 0 && eventType !== null && eventEndTime > eventTime) {
657
+ if (eventType !== null && updateTime > eventTime) {
658
// Log the time from the event timeStamp until we called setState.
659
const color = eventIsRepeat ? 'secondary-light' : 'warning';
660
if (__DEV__ && debugTask) {
@@ -648,9 +662,9 @@ export function logBlockingStart(
662
// $FlowFixMe[method-unbinding]
663
console.timeStamp.bind(
664
console,
651
- eventIsRepeat ? '' : 'Event: ' + eventType,
665
+ eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
666
eventTime,
653
- eventEndTime,
667
+ updateTime,
668
currentTrack,
669
LANES_TRACK_GROUP,
670
color,
@@ -658,16 +672,16 @@ export function logBlockingStart(
672
);
673
} else {
674
console.timeStamp(
661
- eventIsRepeat ? '' : 'Event: ' + eventType,
675
+ eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
676
eventTime,
663
- eventEndTime,
677
+ updateTime,
678
currentTrack,
679
LANES_TRACK_GROUP,
680
color,
681
);
682
}
683
}
670
- if (updateTime > 0 && renderStartTime > updateTime) {
684
+ if (renderStartTime > updateTime) {
685
// Log the time from when we called setState until we started rendering.
686
const color = isSpawnedUpdate
687
? 'error'
@@ -739,18 +753,39 @@ export function logTransitionStart(
753
): void {
754
if (supportsUserTiming) {
755
currentTrack = 'Transition';
742
- const eventEndTime =
743
- startTime > 0 ? startTime : updateTime > 0 ? updateTime : renderStartTime;
744
- if (eventTime > 0 && eventEndTime > eventTime && eventType !== null) {
756
+ // Clamp start times
757
+ if (updateTime > 0) {
758
+ if (updateTime > renderStartTime) {
759
+ updateTime = renderStartTime;
760
+ }
761
+ } else {
762
+ updateTime = renderStartTime;
763
+ }
764
+ if (startTime > 0) {
765
+ if (startTime > updateTime) {
766
+ startTime = updateTime;
767
+ }
768
+ } else {
769
+ startTime = updateTime;
770
+ }
771
+ if (eventTime > 0) {
772
+ if (eventTime > startTime) {
773
+ eventTime = startTime;
774
+ }
775
+ } else {
776
+ eventTime = startTime;
777
+ }
778
+
779
+ if (startTime > eventTime && eventType !== null) {
780
// Log the time from the event timeStamp until we started a transition.
781
const color = eventIsRepeat ? 'secondary-light' : 'warning';
782
if (__DEV__ && debugTask) {
783
debugTask.run(
784
console.timeStamp.bind(
785
console,
751
- eventIsRepeat ? '' : 'Event: ' + eventType,
786
+ eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
787
eventTime,
753
- eventEndTime,
788
+ startTime,
789
currentTrack,
790
LANES_TRACK_GROUP,
791
color,
@@ -758,17 +793,16 @@ export function logTransitionStart(
793
);
794
} else {
795
console.timeStamp(
761
- eventIsRepeat ? '' : 'Event: ' + eventType,
796
+ eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
797
eventTime,
763
- eventEndTime,
798
+ startTime,
799
currentTrack,
800
LANES_TRACK_GROUP,
801
color,
802
);
803
}
804
}
770
- const startEndTime = updateTime > 0 ? updateTime : renderStartTime;
771
- if (startTime > 0 && startEndTime > startTime) {
805
+ if (updateTime > startTime) {
806
// Log the time from when we started an async transition until we called setState or started rendering.
807
// TODO: Ideally this would use the debugTask of the startTransition call perhaps.
808
if (__DEV__ && debugTask) {
@@ -778,7 +812,7 @@ export function logTransitionStart(
812
console,
813
'Action',
814
startTime,
781
- startEndTime,
815
+ updateTime,
816
currentTrack,
817
LANES_TRACK_GROUP,
818
'primary-dark',
@@ -788,14 +822,14 @@ export function logTransitionStart(
822
console.timeStamp(
823
'Action',
824
startTime,
791
- startEndTime,
825
+ updateTime,
826
currentTrack,
827
LANES_TRACK_GROUP,
828
'primary-dark',
829
);
830
}
831
}
798
- if (updateTime > 0 && renderStartTime > updateTime) {
832
+ if (renderStartTime > updateTime) {
833
// Log the time from when we called setState until we started rendering.
834
const label = isPingedUpdate
835
? 'Promise Resolved'
@@ -1337,7 +1371,7 @@ export function logPaintYieldPhase(
1371
// $FlowFixMe[method-unbinding]
1372
console.timeStamp.bind(
1373
console,
1340
- delayedUntilPaint ? 'Waiting for Paint' : '',
1374
+ delayedUntilPaint ? 'Waiting for Paint' : 'Waiting',
1375
startTime,
1376
endTime,
1377
currentTrack,
@@ -1347,7 +1381,7 @@ export function logPaintYieldPhase(
1381
);
1382
} else {
1383
console.timeStamp(
1350
- delayedUntilPaint ? 'Waiting for Paint' : '',
1384
+ delayedUntilPaint ? 'Waiting for Paint' : 'Waiting',
1385
startTime,
1386
endTime,
1387
currentTrack,
packages/react-reconciler/src/ReactProfilerTimer.js
+2
@@ -246,6 +246,7 @@ export function clearBlockingTimers(): void {
246
blockingUpdateComponentName = null;
247
blockingSuspendedTime = -1.1;
248
blockingEventIsRepeat = true;
249
+ blockingClampTime = now();
250
}
251
252
export function startAsyncTransitionTimer(): void {
@@ -282,6 +283,7 @@ export function clearTransitionTimers(): void {
283
transitionUpdateType = 0;
284
transitionSuspendedTime = -1.1;
285
transitionEventIsRepeat = true;
286
+ transitionClampTime = now();
287
}
288
289
export function clampBlockingTimers(finalTime: number): void {
packages/react/src/__tests__/ReactProfiler-test.internal.js
+1
@@ -179,6 +179,7 @@ describe(`onRender`, () => {
179
'read current time',
180
'read current time',
181
'read current time',
182
+ 'read current time',
183
]);
184
} else {
185
assertLog([