Repository: facebook/react
Pull request: Scheduling Profiler does not warn about long transitions

This is the purpose of the transition lane and APIs.

Resolves #22613

Changed files:
filename: packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
patch: @@ -1601,6 +1601,143 @@ describe('preprocessData', () => {
           );
         }
       });
+
+      it('should not warn about transition updates scheduled during commit phase', async () => {
+        function Component() {
+          const [value, setValue] = React.useState(0);
+          // eslint-disable-next-line no-unused-vars
+          const [isPending, startTransition] = React.useTransition();
+
+          Scheduler.unstable_yieldValue(
+            `Component rendered with value ${value}`,
+          );
+
+          // Fake a long render
+          if (value !== 0) {
+            Scheduler.unstable_yieldValue('Long render');
+            startTime += 20000;
+          }
+
+          React.useLayoutEffect(() => {
+            startTransition(() => {
+              setValue(1);
+            });
+          }, []);
+
+          return value;
+        }
+
+        if (gate(flags => flags.enableSchedulingProfiler)) {
+          const cpuProfilerSample = creactCpuProfilerSample();
+
+          const root = ReactDOM.createRoot(document.createElement('div'));
+          act(() => {
+            root.render(<Component />);
+          });
+
+          expect(Scheduler).toHaveYielded([
+            'Component rendered with value 0',
+            'Component rendered with value 0',
+            'Component rendered with value 1',
+            'Long render',
+          ]);
+
+          const testMarks = [];
+          clearedMarks.forEach(markName => {
+            if (markName === '--component-render-start-Component') {
+              // Fake a long running render
+              startTime += 20000;
+            }
+
+            testMarks.push({
+              pid: ++pid,
+              tid: ++tid,
+              ts: ++startTime,
+              args: {data: {}},
+              cat: 'blink.user_timing',
+              name: markName,
+              ph: 'R',
+            });
+          });
+
+          const data = await preprocessData([
+            cpuProfilerSample,
+            ...createBoilerplateEntries(),
+            ...testMarks,
+          ]);
+
+          data.schedulingEvents.forEach(event => {
+            expect(event.warning).toBeNull();
+          });
+        }
+      });
+
+      it('should not warn about deferred value updates scheduled during commit phase', async () => {
+        function Component() {
+          const [value, setValue] = React.useState(0);
+          const deferredValue = React.useDeferredValue(value);
+
+          Scheduler.unstable_yieldValue(
+            `Component rendered with value ${value} and deferredValue ${deferredValue}`,
+          );
+
+          // Fake a long render
+          if (deferredValue !== 0) {
+            Scheduler.unstable_yieldValue('Long render');
+            startTime += 20000;
+          }
+
+          React.useLayoutEffect(() => {
+            setValue(1);
+          }, []);
+
+          return value + deferredValue;
+        }
+
+        if (gate(flags => flags.enableSchedulingProfiler)) {
+          const cpuProfilerSample = creactCpuProfilerSample();
+
+          const root = ReactDOM.createRoot(document.createElement('div'));
+          act(() => {
+            root.render(<Component />);
+          });
+
+          expect(Scheduler).toHaveYielded([
+            'Component rendered with value 0 and deferredValue 0',
+            'Component rendered with value 1 and deferredValue 0',
+            'Component rendered with value 1 and deferredValue 1',
+            'Long render',
+          ]);
+
+          const testMarks = [];
+          clearedMarks.forEach(markName => {
+            if (markName === '--component-render-start-Component') {
+              // Fake a long running render
+              startTime += 20000;
+            }
+
+            testMarks.push({
+              pid: ++pid,
+              tid: ++tid,
+              ts: ++startTime,
+              args: {data: {}},
+              cat: 'blink.user_timing',
+              name: markName,
+              ph: 'R',
+            });
+          });
+
+          const data = await preprocessData([
+            cpuProfilerSample,
+            ...createBoilerplateEntries(),
+            ...testMarks,
+          ]);
+
+          data.schedulingEvents.forEach(event => {
+            expect(event.warning).toBeNull();
+          });
+        }
+      });
     });
 
     describe('errors thrown while rendering', () => {

filename: packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
patch: @@ -1160,7 +1160,16 @@ export default async function preprocessData(
     // See how long the subsequent batch of React work was.
     const [startTime, stopTime] = getBatchRange(batchUID, profilerData);
     if (stopTime - startTime > NESTED_UPDATE_DURATION_THRESHOLD) {
-      schedulingEvent.warning = WARNING_STRINGS.NESTED_UPDATE;
+      // Don't warn about transition updates scheduled during the commit phase.
+      // e.g. useTransition, useDeferredValue
+      // These are allowed to be long-running.
+      if (
+        !schedulingEvent.lanes.some(
+          lane => profilerData.laneToLabelMap.get(lane) === 'Transition',
+        )
+      ) {
+        schedulingEvent.warning = WARNING_STRINGS.NESTED_UPDATE;
+      }
     }
   });
   state.potentialSuspenseEventsOutsideOfTransition.forEach(