From 2ee61475100e3d729816467c857366b37a231494 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com> Date: Wed, 24 Sep 2025 06:11:53 -0700 Subject: [PATCH 1/7] [DevTools] Switch sourcemap-codec dependency (#34569) [sourcemap-codec](https://www.npmjs.com/package/sourcemap-codec) (deprecated) -> [@jridgewell/sourcemap-codec](https://www.npmjs.com/package/@jridgewell/sourcemap-codec) Validated that symbolication still works. --- .../react-devtools-extensions/package.json | 2 +- packages/react-devtools-inline/package.json | 4 +- .../src/hooks/SourceMapConsumer.js | 4 +- .../hooks/__tests__/updateMockSourceMaps.js | 2 +- .../src/hooks/generateHookMap.js | 2 +- yarn.lock | 91 ++++++++++++++++++- 6 files changed, 93 insertions(+), 12 deletions(-) diff --git a/packages/react-devtools-extensions/package.json b/packages/react-devtools-extensions/package.json index 383f7d4211894..9dfceea840a05 100644 --- a/packages/react-devtools-extensions/package.json +++ b/packages/react-devtools-extensions/package.json @@ -28,6 +28,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.10.4", "@babel/plugin-transform-react-jsx-source": "^7.10.5", "@babel/preset-react": "^7.10.4", + "@jridgewell/sourcemap-codec": "1.5.5", "acorn-jsx": "^5.2.0", "archiver": "^3.0.0", "babel-core": "^7.0.0-bridge", @@ -60,7 +61,6 @@ "raw-loader": "^3.1.0", "rimraf": "^5.0.1", "source-map-js": "^0.6.2", - "sourcemap-codec": "^1.4.8", "style-loader": "^0.23.1", "webpack": "^5.82.1", "webpack-cli": "^5.1.1", diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index 8d3f1e71c10ff..9d207527b813d 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -22,8 +22,8 @@ "test:e2e": "playwright test --config=playwright.config.js" }, "dependencies": { - "source-map-js": "^0.6.2", - "sourcemap-codec": "^1.4.8" + "@jridgewell/sourcemap-codec": "1.5.5", + "source-map-js": "^0.6.2" }, "devDependencies": { "@babel/core": "^7.11.1", diff --git a/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js b/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js index c9521431fce1b..468905bf8716e 100644 --- a/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js +++ b/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js @@ -7,7 +7,7 @@ * @flow */ import {withSyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils'; -import {decode} from 'sourcemap-codec'; +import {decode} from '@jridgewell/sourcemap-codec'; import type { IndexSourceMap, @@ -47,7 +47,7 @@ export default function SourceMapConsumer( function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) { const decodedMappings: Mappings = withSyncPerfMeasurements( - 'Decoding source map mappings with sourcemap-codec', + 'Decoding source map mappings with @jridgewell/sourcemap-codec', () => decode(sourceMapJSON.mappings), ); diff --git a/packages/react-devtools-shared/src/hooks/__tests__/updateMockSourceMaps.js b/packages/react-devtools-shared/src/hooks/__tests__/updateMockSourceMaps.js index 2e10c7681feff..bdcfe03964bb6 100644 --- a/packages/react-devtools-shared/src/hooks/__tests__/updateMockSourceMaps.js +++ b/packages/react-devtools-shared/src/hooks/__tests__/updateMockSourceMaps.js @@ -14,7 +14,7 @@ const babel = require('@rollup/plugin-babel').babel; const commonjs = require('@rollup/plugin-commonjs'); const jsx = require('acorn-jsx'); const rollupResolve = require('@rollup/plugin-node-resolve').nodeResolve; -const {encode, decode} = require('sourcemap-codec'); +const {encode, decode} = require('@jridgewell/sourcemap-codec'); const {generateEncodedHookMap} = require('../generateHookMap'); const {parse} = require('@babel/parser'); diff --git a/packages/react-devtools-shared/src/hooks/generateHookMap.js b/packages/react-devtools-shared/src/hooks/generateHookMap.js index bd81d5e8ea146..a8a7576cbed9c 100644 --- a/packages/react-devtools-shared/src/hooks/generateHookMap.js +++ b/packages/react-devtools-shared/src/hooks/generateHookMap.js @@ -8,7 +8,7 @@ */ import {getHookNamesMappingFromAST} from './astUtils'; -import {encode, decode} from 'sourcemap-codec'; +import {encode, decode} from '@jridgewell/sourcemap-codec'; // Missing types in @babel/types type File = any; diff --git a/yarn.lock b/yarn.lock index f77d4483c8737..b24896f5c81a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3227,6 +3227,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" @@ -8269,7 +8274,7 @@ eslint-utils@^2.0.0, eslint-utils@^2.1.0: dependencies: eslint-visitor-keys "^1.1.0" -"eslint-v7@npm:eslint@^7.7.0", eslint@^7.7.0: +"eslint-v7@npm:eslint@^7.7.0": version "7.32.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== @@ -8468,6 +8473,52 @@ eslint@8.57.0: strip-ansi "^6.0.1" text-table "^0.2.0" +eslint@^7.7.0: + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== + dependencies: + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.4.3" + "@humanwhocodes/config-array" "^0.5.0" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + escape-string-regexp "^4.0.0" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.1.2" + globals "^13.6.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.9" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + espree@10.0.1, espree@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f" @@ -14269,7 +14320,7 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -"prettier-2@npm:prettier@^2", prettier@^2.5.1: +"prettier-2@npm:prettier@^2": version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== @@ -14284,6 +14335,11 @@ prettier@^1.19.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +prettier@^2.5.1: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + pretty-format@^29.4.1: version "29.4.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.1.tgz#0da99b532559097b8254298da7c75a0785b1751c" @@ -16158,7 +16214,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -16193,6 +16249,15 @@ string-width@^4.0.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -16253,7 +16318,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -16281,6 +16346,13 @@ strip-ansi@^5.1.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -17889,7 +17961,7 @@ workerize-loader@^2.0.2: dependencies: loader-utils "^2.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -17907,6 +17979,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From e0c421ab71e26c05afe506662a4574070c13d131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 24 Sep 2025 09:34:35 -0400 Subject: [PATCH 2/7] Include SyncLane in includesBlockingLane helper (#34543) This helper weirdly doesn't include the sync lane. Everywhere we use it we have to check the sync lane separately. We can simplify things by simply including the sync lane. This fixes a lack of optimization because we should not check the store consistency for a `flushSync` render. https://github.com/facebook/react/blob/d91d28c8ba6fe7c96e651f82fc47c9d5481bf5f9/packages/react-reconciler/src/ReactFiberHooks.js#L1691-L1693 --- packages/react-reconciler/src/ReactFiberLane.js | 11 ++++++----- packages/react-reconciler/src/ReactFiberWorkLoop.js | 13 ++++--------- packages/react-reconciler/src/ReactProfilerTimer.js | 8 +++----- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index 3e4f22b854104..520283a7c3cc2 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -611,10 +611,6 @@ export function includesSyncLane(lanes: Lanes): boolean { return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes; } -export function isSyncLane(lanes: Lanes): boolean { - return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes; -} - export function includesNonIdleWork(lanes: Lanes): boolean { return (lanes & NonIdleLanes) !== NoLanes; } @@ -681,6 +677,8 @@ export function includesLoadingIndicatorLanes(lanes: Lanes): boolean { export function includesBlockingLane(lanes: Lanes): boolean { const SyncDefaultLanes = + SyncHydrationLane | + SyncLane | InputContinuousHydrationLane | InputContinuousLane | DefaultHydrationLane | @@ -697,10 +695,13 @@ export function includesExpiredLane(root: FiberRoot, lanes: Lanes): boolean { export function isBlockingLane(lane: Lane): boolean { const SyncDefaultLanes = + SyncHydrationLane | + SyncLane | InputContinuousHydrationLane | InputContinuousLane | DefaultHydrationLane | - DefaultLane; + DefaultLane | + GestureLane; return (lane & SyncDefaultLanes) !== NoLanes; } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 7c899f6ff899a..4e67f62905827 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1898,7 +1898,7 @@ function resetWorkInProgressStack() { function finalizeRender(lanes: Lanes, finalizationTime: number): void { if (enableProfilerTimer && enableComponentPerformanceTrack) { - if (includesSyncLane(lanes) || includesBlockingLane(lanes)) { + if (includesBlockingLane(lanes)) { clampBlockingTimers(finalizationTime); } if (includesTransitionLane(lanes)) { @@ -1963,7 +1963,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { const previousUpdateTask = workInProgressUpdateTask; workInProgressUpdateTask = null; - if (includesSyncLane(lanes) || includesBlockingLane(lanes)) { + if (includesBlockingLane(lanes)) { workInProgressUpdateTask = blockingUpdateTask; const clampedUpdateTime = blockingUpdateTime >= 0 && blockingUpdateTime < blockingClampTime @@ -1987,10 +1987,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { lanes, previousUpdateTask, ); - } else if ( - includesSyncLane(animatingLanes) || - includesBlockingLane(animatingLanes) - ) { + } else if (includesBlockingLane(animatingLanes)) { // If this lane is still animating, log the time from previous render finishing to now as animating. setCurrentTrackFromLanes(SyncLane); logAnimatingPhase( @@ -3719,10 +3716,8 @@ 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 ( - (includesSyncLane(lanes) || includesBlockingLane(lanes)) && - !includesSyncLane(workInProgressRootRenderLanes) && + includesBlockingLane(lanes) && !includesBlockingLane(workInProgressRootRenderLanes) && - !includesSyncLane(pendingEffectsLanes) && !includesBlockingLane(pendingEffectsLanes) ) { setCurrentTrackFromLanes(SyncLane); diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index f2cf8efdce5b2..8b7e4274e16c7 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -18,10 +18,8 @@ import type {CapturedValue} from './ReactCapturedValue'; import { isTransitionLane, isBlockingLane, - isSyncLane, includesTransitionLane, includesBlockingLane, - includesSyncLane, NoLanes, } from './ReactFiberLane'; @@ -114,7 +112,7 @@ export function startUpdateTimerByLane( if (!enableProfilerTimer || !enableComponentPerformanceTrack) { return; } - if (isSyncLane(lane) || isBlockingLane(lane)) { + if (isBlockingLane(lane)) { if (blockingUpdateTime < 0) { blockingUpdateTime = now(); blockingUpdateTask = createTask(method); @@ -220,7 +218,7 @@ 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 (includesSyncLane(lanes) || includesBlockingLane(lanes)) { + if (includesBlockingLane(lanes)) { if (blockingUpdateTime < 0) { blockingClampTime = blockingUpdateTime = now(); blockingUpdateTask = createTask('Promise Resolved'); @@ -239,7 +237,7 @@ export function trackSuspendedTime(lanes: Lanes, renderEndTime: number) { if (!enableProfilerTimer || !enableComponentPerformanceTrack) { return; } - if (includesSyncLane(lanes) || includesBlockingLane(lanes)) { + if (includesBlockingLane(lanes)) { blockingSuspendedTime = renderEndTime; } else if (includesTransitionLane(lanes)) { transitionSuspendedTime = renderEndTime; From 05b61f812a2070276c5db0d2107808a6161632fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 24 Sep 2025 11:20:14 -0400 Subject: [PATCH 3/7] Add Gesture Track in Performance Tab (#34546) --- .../react-reconciler/src/ReactFiberLane.js | 10 +- .../src/ReactFiberPerformanceTrack.js | 154 +++++++++++++++++- .../src/ReactFiberWorkLoop.js | 81 ++++++++- .../src/ReactProfilerTimer.js | 96 ++++++++++- 4 files changed, 328 insertions(+), 13 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index 520283a7c3cc2..7ec53c096a56b 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -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'; } diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index c19ef704b20b2..dfc6051d957a7 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -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, diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 4e67f62905827..331708fb195d4 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -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) && diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index 8b7e4274e16c7..acaf540c6a6ca 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -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; From e2332183591ff3a5657c3322a21bcdcccae32088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 24 Sep 2025 11:26:03 -0400 Subject: [PATCH 4/7] Track "Animating" Entry for Gestures while the Gesture is Still On-going (#34548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #34546. Same as #34538 but for gestures. Includes various fixes. This shows how it ends with a Transition when you release in the committed state. Note how the Animation of the Gesture continues until the Transition is done so that the handoff is seamless. Screenshot 2025-09-20 at 7 37 29 PM --- .../src/client/ReactFiberConfigDOM.js | 13 +++++ .../src/ReactFiberConfigNative.js | 9 ++++ .../src/ReactFiberApplyGesture.js | 24 ++++++--- .../src/ReactFiberPerformanceTrack.js | 50 +++-------------- .../src/ReactFiberWorkLoop.js | 46 +++++++++++++--- .../src/ReactProfilerTimer.js | 54 +++++++------------ .../src/ReactFiberConfigTestHost.js | 5 ++ 7 files changed, 110 insertions(+), 91 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index d7ca1951e00aa..7d28dc43ca569 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -2320,6 +2320,9 @@ export function startViewTransition( mutationCallback(); layoutCallback(); // Skip afterMutationCallback(). We don't need it since we're not animating. + if (enableProfilerTimer) { + finishedAnimation(); + } spawnedWorkCallback(); // Skip passiveCallback(). Spawned work will schedule a task. return null; @@ -2509,6 +2512,7 @@ export function startGestureTransition( mutationCallback: () => void, animateCallback: () => void, errorCallback: mixed => void, + finishedAnimation: () => void, // Profiling-only ): null | RunningViewTransition { const ownerDocument: Document = rootContainer.nodeType === DOCUMENT_NODE @@ -2723,6 +2727,12 @@ export function startGestureTransition( // $FlowFixMe[prop-missing] ownerDocument.__reactViewTransition = null; } + if (enableProfilerTimer) { + // Signal that the Transition was unable to continue. We do that here + // instead of when we stop the running View Transition to ensure that + // we cover cases when something else stops it early. + finishedAnimation(); + } }); return transition; } catch (x) { @@ -2735,6 +2745,9 @@ export function startGestureTransition( // Run through the sequence to put state back into a consistent state. mutationCallback(); animateCallback(); + if (enableProfilerTimer) { + finishedAnimation(); + } return null; } } diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index 12b256e016fe2..89da4108fc9b8 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -35,6 +35,8 @@ import { } from 'react-reconciler/src/ReactEventPriorities'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; + import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import type {ReactContext} from 'shared/ReactTypes'; @@ -680,6 +682,9 @@ export function startViewTransition( layoutCallback(); // Skip afterMutationCallback(). We don't need it since we're not animating. spawnedWorkCallback(); + if (enableProfilerTimer) { + finishedAnimation(); + } // Skip passiveCallback(). Spawned work will schedule a task. return null; } @@ -696,9 +701,13 @@ export function startGestureTransition( mutationCallback: () => void, animateCallback: () => void, errorCallback: mixed => void, + finishedAnimation: () => void, // Profiling-only ): null | RunningViewTransition { mutationCallback(); animateCallback(); + if (enableProfilerTimer) { + finishedAnimation(); + } return null; } diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index fa75a1bdbd219..f7ec9aaeb2a8c 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -77,6 +77,12 @@ import { getViewTransitionClassName, } from './ReactFiberViewTransitionComponent'; +import { + enableProfilerTimer, + enableComponentPerformanceTrack, +} from 'shared/ReactFeatureFlags'; +import {trackAnimatingTask} from './ReactProfilerTimer'; + let didWarnForRootClone = false; // Used during the apply phase to track whether a parent ViewTransition component @@ -101,6 +107,7 @@ function applyViewTransitionToClones( name: string, className: ?string, clones: Array, + fiber: Fiber, ): void { // This gets called when we have found a pair, but after the clone in created. The clone is // created by the insertion side. If the insertion side if found before the deletion side @@ -117,6 +124,11 @@ function applyViewTransitionToClones( className, ); } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + if (fiber._debugTask != null) { + trackAnimatingTask(fiber._debugTask); + } + } } function trackDeletedPairViewTransitions(deletion: Fiber): void { @@ -171,7 +183,7 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void { // If we have clones that means that we've already visited this // ViewTransition boundary before and we can now apply the name // to those clones. Otherwise, we have to wait until we clone it. - applyViewTransitionToClones(name, className, clones); + applyViewTransitionToClones(name, className, clones, child); } } if (pairs.size === 0) { @@ -221,7 +233,7 @@ function trackEnterViewTransitions(deletion: Fiber): void { // If we have clones that means that we've already visited this // ViewTransition boundary before and we can now apply the name // to those clones. Otherwise, we have to wait until we clone it. - applyViewTransitionToClones(name, className, clones); + applyViewTransitionToClones(name, className, clones, deletion); } } } @@ -266,7 +278,7 @@ function applyAppearingPairViewTransition(child: Fiber): void { // If there are no clones at this point, that should mean that there are no // HostComponent children in this ViewTransition. if (clones !== null) { - applyViewTransitionToClones(name, className, clones); + applyViewTransitionToClones(name, className, clones, child); } } } @@ -296,7 +308,7 @@ function applyExitViewTransition(placement: Fiber): void { // If there are no clones at this point, that should mean that there are no // HostComponent children in this ViewTransition. if (clones !== null) { - applyViewTransitionToClones(name, className, clones); + applyViewTransitionToClones(name, className, clones, placement); } } } @@ -314,7 +326,7 @@ function applyNestedViewTransition(child: Fiber): void { // If there are no clones at this point, that should mean that there are no // HostComponent children in this ViewTransition. if (clones !== null) { - applyViewTransitionToClones(name, className, clones); + applyViewTransitionToClones(name, className, clones, child); } } } @@ -346,7 +358,7 @@ function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void { // If there are no clones at this point, that should mean that there are no // HostComponent children in this ViewTransition. if (clones !== null) { - applyViewTransitionToClones(oldName, className, clones); + applyViewTransitionToClones(oldName, className, clones, finishedWork); } } diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index dfc6051d957a7..65cc7f0406688 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -753,7 +753,6 @@ export function logBlockingStart( } export function logGestureStart( - startTime: number, updateTime: number, eventTime: number, eventType: null | string, @@ -774,22 +773,15 @@ export function logGestureStart( } else { updateTime = renderStartTime; } - if (startTime > 0) { - if (startTime > updateTime) { - startTime = updateTime; - } - } else { - startTime = updateTime; - } if (eventTime > 0) { - if (eventTime > startTime) { - eventTime = startTime; + if (eventTime > updateTime) { + eventTime = updateTime; } } else { - eventTime = startTime; + eventTime = updateTime; } - if (startTime > eventTime && eventType !== null) { + if (updateTime > eventTime && eventType !== null) { // Log the time from the event timeStamp until we started a gesture. const color = eventIsRepeat ? 'secondary-light' : 'warning'; if (__DEV__ && debugTask) { @@ -798,7 +790,7 @@ export function logGestureStart( console, eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType, eventTime, - startTime, + updateTime, currentTrack, LANES_TRACK_GROUP, color, @@ -808,36 +800,10 @@ export function logGestureStart( 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', + color, ); } } @@ -846,8 +812,8 @@ export function logGestureStart( const label = isPingedUpdate ? 'Promise Resolved' : renderStartTime - updateTime > 5 - ? 'Update Blocked' - : 'Update'; + ? 'Gesture Blocked' + : 'Gesture'; if (__DEV__) { const properties = []; if (updateComponentName != null) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 331708fb195d4..cd193d04e45fa 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -284,7 +284,6 @@ import { blockingEventIsRepeat, blockingSuspendedTime, gestureClampTime, - gestureStartTime, gestureUpdateTime, gestureUpdateTask, gestureUpdateType, @@ -307,6 +306,7 @@ import { transitionSuspendedTime, clearBlockingTimers, clearGestureTimers, + clearGestureUpdates, clearTransitionTimers, clampBlockingTimers, clampGestureTimers, @@ -1981,10 +1981,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { workInProgressUpdateTask = null; if (isGestureRender(lanes)) { workInProgressUpdateTask = gestureUpdateTask; - const clampedStartTime = - gestureStartTime >= 0 && gestureStartTime < gestureClampTime - ? gestureClampTime - : gestureStartTime; const clampedUpdateTime = gestureUpdateTime >= 0 && gestureUpdateTime < gestureClampTime ? gestureClampTime @@ -2018,7 +2014,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { ); } logGestureStart( - clampedStartTime, clampedUpdateTime, clampedEventTime, gestureEventType, @@ -2054,7 +2049,10 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { lanes, previousUpdateTask, ); - } else if (includesBlockingLane(animatingLanes)) { + } else if ( + !isGestureRender(animatingLanes) && + includesBlockingLane(animatingLanes) + ) { // If this lane is still animating, log the time from previous render finishing to now as animating. setCurrentTrackFromLanes(SyncLane); logAnimatingPhase( @@ -3528,6 +3526,11 @@ function commitRoot( // Gestures don't clear their lanes while the gesture is still active but it // might not be scheduled to do any more renders and so we shouldn't schedule // any more gesture lane work until a new gesture is scheduled. + if (enableProfilerTimer && (remainingLanes & GestureLane) !== NoLanes) { + // We need to clear any updates scheduled so that we can treat future updates + // as the cause of the render. + clearGestureUpdates(); + } remainingLanes &= ~GestureLane; } @@ -4251,6 +4254,10 @@ function commitGestureOnRoot( } deleteScheduledGesture(root, finishedGesture); + if (enableProfilerTimer && enableComponentPerformanceTrack) { + startAnimating(pendingEffectsLanes); + } + const prevTransition = ReactSharedInternals.T; ReactSharedInternals.T = null; const previousPriority = getCurrentUpdatePriority(); @@ -4278,6 +4285,10 @@ function commitGestureOnRoot( flushGestureMutations, flushGestureAnimations, reportViewTransitionError, + enableProfilerTimer + ? // This callback fires after "pendingEffects" so we need to snapshot the arguments. + finishedViewTransition.bind(null, pendingEffectsLanes) + : (null: any), ); } @@ -4320,6 +4331,23 @@ function flushGestureAnimations(): void { if (pendingEffectsStatus !== PENDING_GESTURE_ANIMATION_PHASE) { return; } + + const lanes = pendingEffectsLanes; + + if (enableProfilerTimer && enableComponentPerformanceTrack) { + // Update the new commitEndTime to when we started the animation. + recordCommitEndTime(); + logStartViewTransitionYieldPhase( + pendingEffectsRenderEndTime, + commitEndTime, + pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT, + animatingTask, + ); + if (pendingDelayedCommitReason !== ABORTED_VIEW_TRANSITION_COMMIT) { + pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT; + } + } + pendingEffectsStatus = NO_PENDING_EFFECTS; const root = pendingEffectsRoot; const finishedWork = pendingFinishedWork; @@ -4344,6 +4372,10 @@ function flushGestureAnimations(): void { ReactSharedInternals.T = prevTransition; } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + finalizeRender(lanes, commitEndTime); + } + // Now that we've rendered this lane. Start working on the next lane. ensureRootIsScheduled(root); } diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index acaf540c6a6ca..152810f85068c 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -77,7 +77,6 @@ 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; @@ -134,18 +133,16 @@ export function startUpdateTimerByLane( 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; + const newEventTime = resolveEventTimeStamp(); + const newEventType = resolveEventType(); + if ( + newEventTime !== gestureEventTime || + newEventType !== gestureEventType + ) { + gestureEventIsRepeat = false; } + gestureEventTime = newEventTime; + gestureEventType = newEventType; } } else if (isBlockingLane(lane)) { if (blockingUpdateTime < 0) { @@ -334,36 +331,12 @@ 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; @@ -371,6 +344,15 @@ export function clearGestureTimers(): void { gestureClampTime = now(); } +export function clearGestureUpdates(): void { + // Same as clearGestureTimers but doesn't reset the clamp time because we didn't + // actually emit a render. + gestureUpdateTime = -1.1; + gestureUpdateType = 0; + gestureSuspendedTime = -1.1; + gestureEventIsRepeat = true; +} + export function clampBlockingTimers(finalTime: number): void { if (!enableProfilerTimer || !enableComponentPerformanceTrack) { return; diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index b5bddad8e6156..e18523bc04e92 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -17,6 +17,7 @@ import { NoEventPriority, type EventPriority, } from 'react-reconciler/src/ReactEventPriorities'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; export {default as rendererVersion} from 'shared/ReactVersion'; // TODO: Consider exporting the react-native version. export const rendererPackageName = 'react-test-renderer'; @@ -446,9 +447,13 @@ export function startGestureTransition( mutationCallback: () => void, animateCallback: () => void, errorCallback: mixed => void, + finishedAnimation: () => void, // Profiling-only ): null | RunningViewTransition { mutationCallback(); animateCallback(); + if (enableProfilerTimer) { + finishedAnimation(); + } return null; } From 2c6d92fd80ec6917cb7387dbb771e35e82b0126d Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:18:16 -0700 Subject: [PATCH 5/7] [compiler] Name anonymous functions from inlined useCallbacks (#34586) @eps1lon flagged this case. Inlined useCallback has an extra LoadLocal indirection which caused us not to add a name. While I was there I added some extra checks to make sure we don't generate names for a given node twice (just in case). --- .../src/Transform/NameAnonymousFunctions.ts | 11 ++- .../name-anonymous-functions.expect.md | 73 ++++++++++++------- .../compiler/name-anonymous-functions.js | 9 ++- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/NameAnonymousFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/NameAnonymousFunctions.ts index cf6d443b907ad..23f9ed729f653 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Transform/NameAnonymousFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/NameAnonymousFunctions.ts @@ -19,7 +19,7 @@ export function nameAnonymousFunctions(fn: HIRFunction): void { const parentName = fn.id; const functions = nameAnonymousFunctionsImpl(fn); function visit(node: Node, prefix: string): void { - if (node.generatedName != null) { + if (node.generatedName != null && node.fn.nameHint == null) { /** * Note that we don't generate a name for functions that already had one, * so we'll only add the prefix to anonymous functions regardless of @@ -70,6 +70,10 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array { if (name != null && name.kind === 'named') { names.set(lvalue.identifier.id, name.value); } + const func = functions.get(value.place.identifier.id); + if (func != null) { + functions.set(lvalue.identifier.id, func); + } break; } case 'PropertyLoad': { @@ -106,6 +110,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array { const variableName = value.lvalue.place.identifier.name; if ( node != null && + node.generatedName == null && variableName != null && variableName.kind === 'named' ) { @@ -137,7 +142,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array { continue; } const node = functions.get(arg.identifier.id); - if (node != null) { + if (node != null && node.generatedName == null) { const generatedName = fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`; node.generatedName = generatedName; @@ -152,7 +157,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array { continue; } const node = functions.get(attr.place.identifier.id); - if (node != null) { + if (node != null && node.generatedName == null) { const elementName = value.tag.kind === 'BuiltinTag' ? value.tag.name diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.expect.md index 88e270647d3e6..6fccad7685b20 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.expect.md @@ -4,15 +4,19 @@ ```javascript // @enableNameAnonymousFunctions -import {useEffect} from 'react'; +import {useCallback, useEffect} from 'react'; import {identity, Stringify, useIdentity} from 'shared-runtime'; import * as SharedRuntime from 'shared-runtime'; function Component(props) { function named() { const inner = () => props.named; - return inner(); + const innerIdentity = identity(() => props.named); + return inner(innerIdentity()); } + const callback = useCallback(() => { + return 'ok'; + }, []); const namedVariable = function () { return props.namedVariable; }; @@ -30,6 +34,7 @@ function Component(props) { return ( <> {named()} + {callback()} {namedVariable()} {methodCall()} {call()} @@ -63,7 +68,7 @@ export const TODO_FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions -import { useEffect } from "react"; +import { useCallback, useEffect } from "react"; import { identity, Stringify, useIdentity } from "shared-runtime"; import * as SharedRuntime from "shared-runtime"; @@ -75,7 +80,12 @@ function Component(props) { const inner = { "Component[named > inner]": () => props.named }[ "Component[named > inner]" ]; - return inner(); + const innerIdentity = identity( + { "Component[named > identity()]": () => props.named }[ + "Component[named > identity()]" + ], + ); + return inner(innerIdentity()); }; $[0] = props.named; $[1] = t0; @@ -83,6 +93,8 @@ function Component(props) { t0 = $[1]; } const named = t0; + + const callback = _ComponentCallback; let t1; if ($[2] !== props.namedVariable) { t1 = { @@ -197,57 +209,62 @@ function Component(props) { } else { t9 = $[18]; } - let t10; + const t10 = callback(); + let t11; if ($[19] !== namedVariable) { - t10 = namedVariable(); + t11 = namedVariable(); $[19] = namedVariable; - $[20] = t10; + $[20] = t11; } else { - t10 = $[20]; + t11 = $[20]; } - const t11 = methodCall(); - const t12 = call(); - let t13; + const t12 = methodCall(); + const t13 = call(); + let t14; if ($[21] !== hookArgument) { - t13 = hookArgument(); + t14 = hookArgument(); $[21] = hookArgument; - $[22] = t13; + $[22] = t14; } else { - t13 = $[22]; + t14 = $[22]; } - let t14; + let t15; if ( $[23] !== builtinElementAttr || $[24] !== namedElementAttr || - $[25] !== t10 || - $[26] !== t11 || - $[27] !== t12 || - $[28] !== t13 || + $[25] !== t11 || + $[26] !== t12 || + $[27] !== t13 || + $[28] !== t14 || $[29] !== t9 ) { - t14 = ( + t15 = ( <> {t9} {t10} {t11} {t12} + {t13} {builtinElementAttr} {namedElementAttr} - {t13} + {t14} ); $[23] = builtinElementAttr; $[24] = namedElementAttr; - $[25] = t10; - $[26] = t11; - $[27] = t12; - $[28] = t13; + $[25] = t11; + $[26] = t12; + $[27] = t13; + $[28] = t14; $[29] = t9; - $[30] = t14; + $[30] = t15; } else { - t14 = $[30]; + t15 = $[30]; } - return t14; + return t15; +} +function _ComponentCallback() { + return "ok"; } export const TODO_FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.js index ff4f1f6017d98..963bee9ea6f7e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.js @@ -1,14 +1,18 @@ // @enableNameAnonymousFunctions -import {useEffect} from 'react'; +import {useCallback, useEffect} from 'react'; import {identity, Stringify, useIdentity} from 'shared-runtime'; import * as SharedRuntime from 'shared-runtime'; function Component(props) { function named() { const inner = () => props.named; - return inner(); + const innerIdentity = identity(() => props.named); + return inner(innerIdentity()); } + const callback = useCallback(() => { + return 'ok'; + }, []); const namedVariable = function () { return props.namedVariable; }; @@ -26,6 +30,7 @@ function Component(props) { return ( <> {named()} + {callback()} {namedVariable()} {methodCall()} {call()} From 58d17912e89b56067eeba08619fd7f7b6c813519 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Wed, 24 Sep 2025 19:08:13 +0200 Subject: [PATCH 6/7] Fix failing React DevTools regression tests (#34585) --- .../src/__tests__/TimelineProfiler-test.js | 190 ++++++++++-------- .../src/__tests__/preprocessData-test.js | 28 ++- 2 files changed, 126 insertions(+), 92 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js index d46cac42b703f..8d9f34371b348 100644 --- a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js +++ b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js @@ -130,24 +130,28 @@ describe('Timeline profiler', () => { // @reactVersion <= 18.2 // @reactVersion >= 18.0 it('should mark sync render without suspends or state updates', () => { + utils.act(() => store.profilerStore.startProfiling()); legacyRender(
); + utils.act(() => store.profilerStore.stopProfiling()); expect(registeredMarks).toMatchInlineSnapshot(` - [ - "--schedule-render-1", - "--render-start-1", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start- at filtered (:0:0)", - "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-1", - "--layout-effects-stop", - "--commit-stop", - ] - `); + [ + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--schedule-render-1", + "--render-start-1", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); }); // TODO(hoxyq): investigate why running this test with React 18 fails @@ -260,46 +264,50 @@ describe('Timeline profiler', () => { throw Error('Expected error'); } + utils.act(() => store.profilerStore.startProfiling()); legacyRender( , ); + utils.act(() => store.profilerStore.stopProfiling()); expect(registeredMarks).toMatchInlineSnapshot(` - [ - "--schedule-render-1", - "--render-start-1", - "--component-render-start-ErrorBoundary", - "--component-render-stop", - "--component-render-start-ExampleThatThrows", - "--component-render-start-ExampleThatThrows", - "--component-render-stop", - "--error-ExampleThatThrows-mount-Expected error", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start- at filtered (:0:0)", - "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-1", - "--schedule-state-update-1-ErrorBoundary", - "--layout-effects-stop", - "--commit-stop", - "--render-start-1", - "--component-render-start-ErrorBoundary", - "--component-render-stop", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start- at filtered (:0:0)", - "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - ] - `); + [ + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--schedule-render-1", + "--render-start-1", + "--component-render-start-ErrorBoundary", + "--component-render-stop", + "--component-render-start-ExampleThatThrows", + "--component-render-start-ExampleThatThrows", + "--component-render-stop", + "--error-ExampleThatThrows-mount-Expected error", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--schedule-state-update-1-ErrorBoundary", + "--layout-effects-stop", + "--commit-stop", + "--render-start-1", + "--component-render-start-ErrorBoundary", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + ] + `); }); }); @@ -1095,24 +1103,28 @@ describe('Timeline profiler', () => { // @reactVersion <= 18.2 // @reactVersion >= 18.0 it('regression test SyncLane', () => { + utils.act(() => store.profilerStore.startProfiling()); legacyRender(
); + utils.act(() => store.profilerStore.stopProfiling()); expect(registeredMarks).toMatchInlineSnapshot(` - [ - "--schedule-render-1", - "--render-start-1", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start- at filtered (:0:0)", - "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-1", - "--layout-effects-stop", - "--commit-stop", - ] - `); + [ + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--schedule-render-1", + "--render-start-1", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); }); }); @@ -1432,19 +1444,19 @@ describe('Timeline profiler', () => { expect(timelineData.suspenseEvents).toHaveLength(1); const suspenseEvent = timelineData.suspenseEvents[0]; expect(suspenseEvent).toMatchInlineSnapshot(` - { - "componentName": "Example", - "depth": 0, - "duration": 10, - "id": "0", - "phase": "mount", - "promiseName": "", - "resolution": "resolved", - "timestamp": 10, - "type": "suspense", - "warning": null, - } - `); + { + "componentName": "Example", + "depth": 0, + "duration": 0, + "id": "0", + "phase": "mount", + "promiseName": "", + "resolution": "unresolved", + "timestamp": 10, + "type": "suspense", + "warning": null, + } + `); // There should be two batches of renders: Suspeneded and resolved. expect(timelineData.batchUIDToMeasuresMap.size).toBe(2); @@ -1490,19 +1502,19 @@ describe('Timeline profiler', () => { expect(timelineData.suspenseEvents).toHaveLength(1); const suspenseEvent = timelineData.suspenseEvents[0]; expect(suspenseEvent).toMatchInlineSnapshot(` - { - "componentName": "Example", - "depth": 0, - "duration": 10, - "id": "0", - "phase": "mount", - "promiseName": "", - "resolution": "rejected", - "timestamp": 10, - "type": "suspense", - "warning": null, - } - `); + { + "componentName": "Example", + "depth": 0, + "duration": 0, + "id": "0", + "phase": "mount", + "promiseName": "", + "resolution": "unresolved", + "timestamp": 10, + "type": "suspense", + "warning": null, + } + `); // There should be two batches of renders: Suspeneded and resolved. expect(timelineData.batchUIDToMeasuresMap.size).toBe(2); diff --git a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js index 1de972658c232..6d60b7e63254d 100644 --- a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js +++ b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js @@ -111,9 +111,31 @@ describe('Timeline profiler', () => { ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); - const InternalTestUtils = require('internal-test-utils'); - assertLog = InternalTestUtils.assertLog; - waitFor = InternalTestUtils.waitFor; + if (typeof Scheduler.log !== 'function') { + // backwards compat for older scheduler versions + Scheduler.log = Scheduler.unstable_yieldValue; + Scheduler.unstable_clearLog = Scheduler.unstable_clearYields; + const InternalTestUtils = require('internal-test-utils'); + assertLog = InternalTestUtils.assertLog; + + // polyfill waitFor as Scheduler.toFlushAndYieldThrough + waitFor = expectedYields => { + let actualYields = Scheduler.unstable_clearYields(); + if (actualYields.length !== 0) { + throw new Error( + 'Log of yielded values is not empty. ' + + 'Call expect(Scheduler).toHaveYielded(...) first.', + ); + } + Scheduler.unstable_flushNumberOfYields(expectedYields.length); + actualYields = Scheduler.unstable_clearYields(); + expect(actualYields).toEqual(expectedYields); + }; + } else { + const InternalTestUtils = require('internal-test-utils'); + assertLog = InternalTestUtils.assertLog; + waitFor = InternalTestUtils.waitFor; + } setPerformanceMock = require('react-devtools-shared/src/backend/profilingHooks').setPerformanceMock_ONLY_FOR_TESTING; From 8ad773b1f342d20e4773c8d086028c6927445a22 Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:17:42 -0700 Subject: [PATCH 7/7] [compiler] Add support for commonjs (#34589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We previously always generated import statements for any modules that had to be required, notably the `import {c} from 'react/compiler-runtime'` for the memo cache function. However, this obviously doesn't work when the source is using commonjs. Now we check the sourceType of the module and generate require() statements if the source type is 'script'. I initially explored using https://babeljs.io/docs/babel-helper-module-imports, but the API design was unfortunately not flexible enough for our use-case. Specifically, our pipeline is as follows: * Compile individual functions. Generate candidate imports, pre-allocating the local names for those imports. * If the file is compiled successfully, actually add the imports to the program. Ie we need to pre-allocate identifier names for the imports before we add them to the program — but that isn't supported by babel-helper-module-imports. So instead we generate our own require() calls if the sourceType is script. --- .../src/Entrypoint/Imports.ts | 28 ++++++++-- .../compiler/script-source-type.expect.md | 52 +++++++++++++++++++ .../fixtures/compiler/script-source-type.js | 14 +++++ compiler/packages/snap/src/compiler.ts | 13 +++-- .../packages/snap/src/sprout/evaluator.ts | 5 +- 5 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts index c4f665720b4ca..6fc702fbf4cd8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts @@ -240,7 +240,7 @@ export function addImportsToProgram( programContext: ProgramContext, ): void { const existingImports = getExistingImports(path); - const stmts: Array = []; + const stmts: Array = []; const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) => a.localeCompare(b), ); @@ -303,9 +303,29 @@ export function addImportsToProgram( if (maybeExistingImports != null) { maybeExistingImports.pushContainer('specifiers', importSpecifiers); } else { - stmts.push( - t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)), - ); + if (path.node.sourceType === 'module') { + stmts.push( + t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)), + ); + } else { + stmts.push( + t.variableDeclaration('const', [ + t.variableDeclarator( + t.objectPattern( + sortedImport.map(specifier => { + return t.objectProperty( + t.identifier(specifier.imported), + t.identifier(specifier.name), + ); + }), + ), + t.callExpression(t.identifier('require'), [ + t.stringLiteral(moduleName), + ]), + ), + ]), + ); + } } } path.unshiftContainer('body', stmts); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.expect.md new file mode 100644 index 0000000000000..891a0fb0ddea9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +// @script +const React = require('react'); + +function Component(props) { + return
{props.name}
; +} + +// To work with snap evaluator +exports = { + FIXTURE_ENTRYPOINT: { + fn: Component, + params: [{name: 'React Compiler'}], + }, +}; + +``` + +## Code + +```javascript +const { c: _c } = require("react/compiler-runtime"); // @script +const React = require("react"); + +function Component(props) { + const $ = _c(2); + let t0; + if ($[0] !== props.name) { + t0 =
{props.name}
; + $[0] = props.name; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +// To work with snap evaluator +exports = { + FIXTURE_ENTRYPOINT: { + fn: Component, + params: [{ name: "React Compiler" }], + }, +}; + +``` + +### Eval output +(kind: ok)
React Compiler
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.js new file mode 100644 index 0000000000000..604f0d96187bc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.js @@ -0,0 +1,14 @@ +// @script +const React = require('react'); + +function Component(props) { + return
{props.name}
; +} + +// To work with snap evaluator +exports = { + FIXTURE_ENTRYPOINT: { + fn: Component, + params: [{name: 'React Compiler'}], + }, +}; diff --git a/compiler/packages/snap/src/compiler.ts b/compiler/packages/snap/src/compiler.ts index a6041bd5cc2a7..cafe8692446b9 100644 --- a/compiler/packages/snap/src/compiler.ts +++ b/compiler/packages/snap/src/compiler.ts @@ -31,10 +31,15 @@ import prettier from 'prettier'; import SproutTodoFilter from './SproutTodoFilter'; import {isExpectError} from './fixture-utils'; import {makeSharedRuntimeTypeProvider} from './sprout/shared-runtime-type-provider'; + export function parseLanguage(source: string): 'flow' | 'typescript' { return source.indexOf('@flow') !== -1 ? 'flow' : 'typescript'; } +export function parseSourceType(source: string): 'script' | 'module' { + return source.indexOf('@script') !== -1 ? 'script' : 'module'; +} + /** * Parse react compiler plugin + environment options from test fixture. Note * that although this primarily uses `Environment:parseConfigPragma`, it also @@ -98,6 +103,7 @@ export function parseInput( input: string, filename: string, language: 'flow' | 'typescript', + sourceType: 'module' | 'script', ): BabelCore.types.File { // Extract the first line to quickly check for custom test directives if (language === 'flow') { @@ -105,14 +111,14 @@ export function parseInput( babel: true, flow: 'all', sourceFilename: filename, - sourceType: 'module', + sourceType, enableExperimentalComponentSyntax: true, }); } else { return BabelParser.parse(input, { sourceFilename: filename, plugins: ['typescript', 'jsx'], - sourceType: 'module', + sourceType, }); } } @@ -221,11 +227,12 @@ export async function transformFixtureInput( const firstLine = input.substring(0, input.indexOf('\n')); const language = parseLanguage(firstLine); + const sourceType = parseSourceType(firstLine); // Preserve file extension as it determines typescript's babel transform // mode (e.g. stripping types, parsing rules for brackets) const filename = path.basename(fixturePath) + (language === 'typescript' ? '.ts' : ''); - const inputAst = parseInput(input, filename, language); + const inputAst = parseInput(input, filename, language, sourceType); // Give babel transforms an absolute path as relative paths get prefixed // with `cwd`, which is different across machines const virtualFilepath = '/' + filename; diff --git a/compiler/packages/snap/src/sprout/evaluator.ts b/compiler/packages/snap/src/sprout/evaluator.ts index 60da5dc53cec3..8af8487d01109 100644 --- a/compiler/packages/snap/src/sprout/evaluator.ts +++ b/compiler/packages/snap/src/sprout/evaluator.ts @@ -298,7 +298,10 @@ export function doEval(source: string): EvaluatorResult { return { kind: 'UnexpectedError', value: - 'Unexpected error during eval, possible syntax error?\n' + e.message, + 'Unexpected error during eval, possible syntax error?\n' + + e.message + + '\n\nsource:\n' + + source, logs, }; } finally {