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/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()} 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 { 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/__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; 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/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/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index 3e4f22b854104..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'; @@ -611,10 +612,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 +678,8 @@ export function includesLoadingIndicatorLanes(lanes: Lanes): boolean { export function includesBlockingLane(lanes: Lanes): boolean { const SyncDefaultLanes = + SyncHydrationLane | + SyncLane | InputContinuousHydrationLane | InputContinuousLane | DefaultHydrationLane | @@ -697,10 +696,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; } @@ -709,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; } @@ -1270,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..65cc7f0406688 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,111 @@ export function logBlockingStart( } } +export function logGestureStart( + 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 (eventTime > 0) { + if (eventTime > updateTime) { + eventTime = updateTime; + } + } else { + eventTime = updateTime; + } + + 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) { + debugTask.run( + console.timeStamp.bind( + console, + eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType, + eventTime, + updateTime, + currentTrack, + LANES_TRACK_GROUP, + color, + ), + ); + } else { + console.timeStamp( + eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType, + eventTime, + updateTime, + currentTrack, + LANES_TRACK_GROUP, + color, + ); + } + } + if (renderStartTime > updateTime) { + // Log the time from when we called setState until we started rendering. + const label = isPingedUpdate + ? 'Promise Resolved' + : renderStartTime - updateTime > 5 + ? 'Gesture Blocked' + : 'Gesture'; + 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 7c899f6ff899a..cd193d04e45fa 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,16 @@ import { blockingEventType, blockingEventIsRepeat, blockingSuspendedTime, + gestureClampTime, + gestureUpdateTime, + gestureUpdateTask, + gestureUpdateType, + gestureUpdateMethodName, + gestureUpdateComponentName, + gestureEventTime, + gestureEventType, + gestureEventIsRepeat, + gestureSuspendedTime, transitionClampTime, transitionStartTime, transitionUpdateTime, @@ -294,8 +305,11 @@ import { transitionEventIsRepeat, transitionSuspendedTime, clearBlockingTimers, + clearGestureTimers, + clearGestureUpdates, clearTransitionTimers, clampBlockingTimers, + clampGestureTimers, clampTransitionTimers, clampRetryTimers, clampIdleTimers, @@ -1898,7 +1912,9 @@ function resetWorkInProgressStack() { function finalizeRender(lanes: Lanes, finalizationTime: number): void { if (enableProfilerTimer && enableComponentPerformanceTrack) { - if (includesSyncLane(lanes) || includesBlockingLane(lanes)) { + if (isGestureRender(lanes)) { + clampGestureTimers(finalizationTime); + } else if (includesBlockingLane(lanes)) { clampBlockingTimers(finalizationTime); } if (includesTransitionLane(lanes)) { @@ -1963,7 +1979,53 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { const previousUpdateTask = workInProgressUpdateTask; workInProgressUpdateTask = null; - if (includesSyncLane(lanes) || includesBlockingLane(lanes)) { + if (isGestureRender(lanes)) { + workInProgressUpdateTask = gestureUpdateTask; + 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( + clampedUpdateTime, + clampedEventTime, + gestureEventType, + gestureEventIsRepeat, + gestureUpdateType === PINGED_UPDATE, + renderStartTime, + gestureUpdateTask, + gestureUpdateMethodName, + gestureUpdateComponentName, + ); + clearGestureTimers(); + } else if (includesBlockingLane(lanes)) { workInProgressUpdateTask = blockingUpdateTask; const clampedUpdateTime = blockingUpdateTime >= 0 && blockingUpdateTime < blockingClampTime @@ -1988,7 +2050,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { previousUpdateTask, ); } else if ( - includesSyncLane(animatingLanes) || + !isGestureRender(animatingLanes) && includesBlockingLane(animatingLanes) ) { // If this lane is still animating, log the time from previous render finishing to now as animating. @@ -3464,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; } @@ -3719,14 +3786,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 ( - (includesSyncLane(lanes) || includesBlockingLane(lanes)) && - !includesSyncLane(workInProgressRootRenderLanes) && - !includesBlockingLane(workInProgressRootRenderLanes) && - !includesSyncLane(pendingEffectsLanes) && - !includesBlockingLane(pendingEffectsLanes) + isGestureRender(lanes) && + !isGestureRender(workInProgressRootRenderLanes) && + !isGestureRender(pendingEffectsLanes) ) { - setCurrentTrackFromLanes(SyncLane); - logAnimatingPhase(blockingClampTime, now(), task); + setCurrentTrackFromLanes(GestureLane); + logAnimatingPhase(gestureClampTime, now(), task); } if ( includesTransitionLane(lanes) && @@ -4189,6 +4254,10 @@ function commitGestureOnRoot( } deleteScheduledGesture(root, finishedGesture); + if (enableProfilerTimer && enableComponentPerformanceTrack) { + startAnimating(pendingEffectsLanes); + } + const prevTransition = ReactSharedInternals.T; ReactSharedInternals.T = null; const previousPriority = getCurrentUpdatePriority(); @@ -4216,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), ); } @@ -4258,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; @@ -4282,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 f2cf8efdce5b2..152810f85068c 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -18,10 +18,9 @@ import type {CapturedValue} from './ReactCapturedValue'; import { isTransitionLane, isBlockingLane, - isSyncLane, + isGestureRender, includesTransitionLane, includesBlockingLane, - includesSyncLane, NoLanes, } from './ReactFiberLane'; @@ -76,6 +75,18 @@ 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 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. @@ -114,7 +125,26 @@ export function startUpdateTimerByLane( if (!enableProfilerTimer || !enableComponentPerformanceTrack) { return; } - if (isSyncLane(lane) || isBlockingLane(lane)) { + if (isGestureRender(lane)) { + if (gestureUpdateTime < 0) { + gestureUpdateTime = now(); + gestureUpdateTask = createTask(method); + gestureUpdateMethodName = method; + if (__DEV__ && fiber != null) { + gestureUpdateComponentName = getComponentNameFromFiber(fiber); + } + 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); @@ -220,7 +250,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 (includesSyncLane(lanes) || 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'); @@ -239,7 +275,9 @@ export function trackSuspendedTime(lanes: Lanes, renderEndTime: number) { if (!enableProfilerTimer || !enableComponentPerformanceTrack) { return; } - if (includesSyncLane(lanes) || includesBlockingLane(lanes)) { + if (isGestureRender(lanes)) { + gestureSuspendedTime = renderEndTime; + } else if (includesBlockingLane(lanes)) { blockingSuspendedTime = renderEndTime; } else if (includesTransitionLane(lanes)) { transitionSuspendedTime = renderEndTime; @@ -293,6 +331,28 @@ export function clearTransitionTimers(): void { transitionClampTime = now(); } +export function hasScheduledGestureTransitionWork(): boolean { + // If we have call setOptimistic on a gesture + return gestureUpdateTime > -1; +} + +export function clearGestureTimers(): void { + gestureUpdateTime = -1.1; + gestureUpdateType = 0; + gestureSuspendedTime = -1.1; + gestureEventIsRepeat = true; + 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; @@ -303,6 +363,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; 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; } 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"