@samitouri / QOS-React / commits / e3c9656d20

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([