Compare commits

..

7 Commits

Author SHA1 Message Date
Lauren Tan
d545586fca [compiler] Fix codegen for nested method calls with memoized properties
When processing nested method calls like `Math.floor(diff.bar())`, the compiler would trigger an invariant that `MethodCall::property must be a MemberExpression but got an Identifier`.

The issue occurred when the property (e.g., Math.floor) was memoized in a reactive scope and promoted to a named identifier. Later during codegen, retrieving this memoized temporary would return just an Identifier instead of the expected MemberExpression.

The fix handles this case by checking if the property has been memoized as an Identifier and using it directly for the call expression, rather than requiring it to be a MemberExpression.

This fixes two test cases that were previously failing.
2025-08-07 11:45:25 -04:00
Sebastian Markbåge
3958d5d84b [Flight] Copy the name field of a serialized function debug value (#34085)
This ensures that if the name is set manually after the declaration,
then we get that name when we log the value. For example Node.js
`Response` is declared as `_Response` and then later assigned a new
name.

We should probably really serialize all static enumerable properties but
"name" is non-enumerable so it's still a special case.
2025-08-07 10:55:01 -04:00
Sebastian Markbåge
738aebdbac [DevTools] Add Badge to Owners and sometimes stack traces (#34106)
Stacked on #34101.

This adds a badge to owners if they are different from the currently
selected component's environment.

<img width="590" height="566" alt="Screenshot 2025-08-04 at 5 15 02 PM"
src="https://github.com/user-attachments/assets/e898254f-1b4c-498e-8713-978d90545340"
/>

We also add one to the end of stack traces if the stack trace has a
different environment than the owner which can happen when you call a
function (without rendering a component) into a third party environment
but the owner component was in the first party.

One awkward thing is that Suspense boundaries are always in the client
environment so their Server Components are always badged.
2025-08-07 10:39:08 -04:00
Sebastian Markbåge
4c9c109cea [Fiber] Try to give a stack trace to every entry in the Scheduler Performance Track (#34123)
For "render" and "commit" phases we don't give any specific stack atm.
This tries to always provide something useful to say the cause of the
render.

For normal renders this will now show the same thing as the "Event" and
"Update" entries already showed. We stash the task that was used for
those and use them throughout the render and commit phases.

For Suspense (Retry lane) and Idle (Offscreen lane), we don't have any
updates. Instead for those there's a component that left work behind in
previous passes. For those I use the debugTask of the `<Suspense>` or
`<Activity>` boundary to indicate that this was the root of the render.

Similarly when an Action is invoked on a `<form action={...}>` component
using the built-in submit handler, there's no actionable stack in user
space that called it. So we use the stack of the JSX for the form
instead.
2025-08-07 10:26:30 -04:00
Ruslan Lesiutin
552a5dadcf [DevTools] fix: handle store mutations synchronously in TreeContext (#34119)
If there is a commit that removes the currently inspected (selected)
elements in the Components tree, we are going to kick off the transition
to re-render the Tree. The elements will be re-rendered with the
previous inspectedElementID, which was just removed and all consecutive
calls to store object with this id would produce errors, since this
element was just removed.

We should handle store mutations synchronously. Doesn't make sense to
start a transition in this case, because Elements depend on the
TreeState and could make calls to store in render function.

Before:
<img width="2286" height="1734" alt="Screenshot 2025-08-06 at 17 41 14"
src="https://github.com/user-attachments/assets/97d92220-3488-47b2-aa6b-70fa39345f6b"
/>


After:


https://github.com/user-attachments/assets/3da36aff-6987-4b76-b741-ca59f829f8e6
2025-08-07 14:05:56 +01:00
Joseph Savona
f468d37739 [compiler] remove use of inspect module (#34124) 2025-08-06 23:59:55 -07:00
Joseph Savona
c403a7c548 [compiler] Upstream experimental flow integration (#34121)
all credit on the Flood/ code goes to @mvitousek and @jbrown215, i'm
just the one upstreaming it
2025-08-06 15:58:07 -07:00
35 changed files with 1072 additions and 297 deletions

View File

@@ -9,6 +9,7 @@ import {
import * as t from '@babel/types';
import * as TypeErrors from './TypeErrors';
import {assertExhaustive} from '../Utils/utils';
import {FlowType} from './FlowTypes';
export const DEBUG = false;
@@ -196,8 +197,6 @@ export function makeVariableId(id: number): VariableId {
return id as VariableId;
}
import {inspect} from 'util';
import {FlowType} from './FlowTypes';
export function printConcrete<T>(
type: ConcreteType<T>,
printType: (_: T) => string,
@@ -241,7 +240,7 @@ export function printConcrete<T>(
case 'Generic':
return `T${type.id}`;
case 'Object': {
const name = `Object ${inspect([...type.members.keys()])}`;
const name = `Object [${[...type.members.keys()].map(key => JSON.stringify(key)).join(', ')}]`;
return `${name}`;
}
case 'Tuple': {

View File

@@ -1810,41 +1810,70 @@ function codegenInstructionValue(
case 'MethodCall': {
const isHook =
getHookKind(cx.env, instrValue.property.identifier) != null;
const memberExpr = codegenPlaceToExpression(cx, instrValue.property);
CompilerError.invariant(
t.isMemberExpression(memberExpr) ||
t.isOptionalMemberExpression(memberExpr),
{
reason:
'[Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. ' +
`Got a \`${memberExpr.type}\``,
description: null,
loc: memberExpr.loc ?? null,
suggestions: null,
},
);
CompilerError.invariant(
t.isNodesEquivalent(
memberExpr.object,
codegenPlaceToExpression(cx, instrValue.receiver),
),
{
reason:
'[Codegen] Internal error: Forget should always generate MethodCall::property ' +
'as a MemberExpression of MethodCall::receiver',
description: null,
loc: memberExpr.loc ?? null,
suggestions: null,
},
);
const args = instrValue.args.map(arg => codegenArgument(cx, arg));
value = createCallExpression(
cx.env,
memberExpr,
args,
instrValue.loc,
isHook,
);
/**
* We need to check if the property was memoized. If it has, we should reconstruct the
* MemberExpression.
*/
let memberExpr: t.Expression;
const tmp = cx.temp.get(instrValue.property.identifier.declarationId);
if (tmp != null && tmp.type === 'Identifier') {
/**
* We can't reconstruct the MemberExpression from just the identifier, so we work around
* this by allowing an Identifier here.
*/
memberExpr = tmp;
} else if (tmp != null) {
memberExpr = convertValueToExpression(tmp);
} else {
memberExpr = codegenPlaceToExpression(cx, instrValue.property);
}
// Reconstruct the MemberExpression if we previously saw an Identifier.
if (memberExpr.type === 'Identifier') {
const args = instrValue.args.map(arg => codegenArgument(cx, arg));
value = createCallExpression(
cx.env,
memberExpr,
args,
instrValue.loc,
isHook,
);
} else {
CompilerError.invariant(
t.isMemberExpression(memberExpr) ||
t.isOptionalMemberExpression(memberExpr),
{
reason:
'[Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. ' +
`Got a \`${memberExpr.type}\``,
description: null,
loc: memberExpr.loc ?? null,
suggestions: null,
},
);
CompilerError.invariant(
t.isNodesEquivalent(
memberExpr.object,
codegenPlaceToExpression(cx, instrValue.receiver),
),
{
reason:
'[Codegen] Internal error: Forget should always generate MethodCall::property ' +
'as a MemberExpression of MethodCall::receiver',
description: null,
loc: memberExpr.loc ?? null,
suggestions: null,
},
);
const args = instrValue.args.map(arg => codegenArgument(cx, arg));
value = createCallExpression(
cx.env,
memberExpr,
args,
instrValue.loc,
isHook,
);
}
break;
}
case 'NewExpression': {

View File

@@ -0,0 +1,69 @@
## Input
```javascript
import {PrimitiveBox} from 'shared-runtime';
function Component({value, realmax}) {
const box = new PrimitiveBox(value);
const maxValue = Math.max(box.get(), realmax);
// ^^^^^^^^^ should not be separated into static call
return <div>{maxValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42, realmax: 100}],
isComponent: true,
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { PrimitiveBox } from "shared-runtime";
function Component(t0) {
const $ = _c(6);
const { value, realmax } = t0;
let t1;
let t2;
let t3;
if ($[0] !== value) {
const box = new PrimitiveBox(value);
t1 = Math;
t2 = t1.max;
t3 = box.get();
$[0] = value;
$[1] = t1;
$[2] = t2;
$[3] = t3;
} else {
t1 = $[1];
t2 = $[2];
t3 = $[3];
}
const maxValue = t2(t3, realmax);
let t4;
if ($[4] !== maxValue) {
t4 = <div>{maxValue}</div>;
$[4] = maxValue;
$[5] = t4;
} else {
t4 = $[5];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42, realmax: 100 }],
isComponent: true,
};
```
### Eval output
(kind: ok) <div>100</div>

View File

@@ -0,0 +1,14 @@
import {PrimitiveBox} from 'shared-runtime';
function Component({value, realmax}) {
const box = new PrimitiveBox(value);
const maxValue = Math.max(box.get(), realmax);
// ^^^^^^^^^ should not be separated into static call
return <div>{maxValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42, realmax: 100}],
isComponent: true,
};

View File

@@ -0,0 +1,85 @@
## Input
```javascript
function foo() {
return {
bar() {
return 3.14;
},
};
}
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};
export const FIXTURE_ENTRYPOINT = {
fn: YearsAndMonthsSince,
params: [],
isComponent: true,
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function foo() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {
bar() {
return 3.14;
},
};
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
const YearsAndMonthsSince = () => {
const $ = _c(4);
let t0;
let t1;
let t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const diff = foo();
t0 = Math;
t1 = t0.floor;
t2 = diff.bar();
$[0] = t0;
$[1] = t1;
$[2] = t2;
} else {
t0 = $[0];
t1 = $[1];
t2 = $[2];
}
const months = t1(t2);
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <>{months}</>;
$[3] = t3;
} else {
t3 = $[3];
}
return t3;
};
export const FIXTURE_ENTRYPOINT = {
fn: YearsAndMonthsSince,
params: [],
isComponent: true,
};
```
### Eval output
(kind: ok) 3

View File

@@ -0,0 +1,19 @@
function foo() {
return {
bar() {
return 3.14;
},
};
}
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};
export const FIXTURE_ENTRYPOINT = {
fn: YearsAndMonthsSince,
params: [],
isComponent: true,
};

View File

@@ -1,31 +0,0 @@
## Input
```javascript
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};
```
## Error
```
Found 1 error:
Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
error.bug-invariant-codegen-methodcall.ts:3:17
1 | const YearsAndMonthsSince = () => {
2 | const diff = foo();
> 3 | const months = Math.floor(diff.bar());
| ^^^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
4 | return <>{months}</>;
5 | };
6 |
```

View File

@@ -1,5 +0,0 @@
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};

View File

@@ -1,39 +0,0 @@
## Input
```javascript
import {makeArray} from 'shared-runtime';
const other = [0, 1];
function Component({}) {
const items = makeArray(0, 1, 2, null, 4, false, 6);
const max = Math.max(2, items.push(5), ...other);
return max;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Error
```
Found 1 error:
Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
error.todo-nested-method-calls-lower-property-load-into-temporary.ts:6:14
4 | function Component({}) {
5 | const items = makeArray(0, 1, 2, null, 4, false, 6);
> 6 | const max = Math.max(2, items.push(5), ...other);
| ^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
7 | return max;
8 | }
9 |
```

View File

@@ -0,0 +1,62 @@
## Input
```javascript
import {makeArray} from 'shared-runtime';
const other = [0, 1];
function Component({}) {
const items = makeArray(0, 1, 2, null, 4, false, 6);
const max = Math.max(2, items.push(5), ...other);
return max;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { makeArray } from "shared-runtime";
const other = [0, 1];
function Component(t0) {
const $ = _c(4);
let t1;
let t2;
let t3;
let t4;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const items = makeArray(0, 1, 2, null, 4, false, 6);
t1 = Math;
t2 = t1.max;
t3 = 2;
t4 = items.push(5);
$[0] = t1;
$[1] = t2;
$[2] = t3;
$[3] = t4;
} else {
t1 = $[0];
t2 = $[1];
t3 = $[2];
t4 = $[3];
}
const max = t2(t3, t4, ...other);
return max;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) 8

View File

@@ -196,6 +196,29 @@ export function makeSharedRuntimeTypeProvider({
],
},
},
PrimitiveBox: {
kind: 'object',
properties: {
get: {
kind: 'function',
positionalParams: [],
restParam: null,
calleeEffect: EffectEnum.Read,
returnType: {kind: 'type', name: 'Primitive'},
returnValueKind: ValueKindEnum.Primitive,
aliasing: {
receiver: '@receiver',
params: ['@value'],
rest: null,
returns: '@return',
temporaries: [],
effects: [
{kind: 'CreateFrom', from: '@value', into: '@return'},
],
},
},
},
},
},
};
} else if (moduleName === 'ReactCompilerTest') {

View File

@@ -421,4 +421,22 @@ export function typedMutate(x: any, v: any = null): void {
x.property = v;
}
type PrimitiveValue =
| number
| string
| boolean
| symbol
| null
| undefined
| bigint;
export class PrimitiveBox<T extends PrimitiveValue> {
value: T;
constructor(value: T) {
this.value = value;
}
get(): T {
return this.value;
}
}
export default typedLog;

View File

@@ -1969,6 +1969,44 @@ function createModel(response: Response, model: any): any {
return model;
}
const mightHaveStaticConstructor = /\bclass\b.*\bstatic\b/;
function getInferredFunctionApproximate(code: string): () => void {
let slicedCode;
if (code.startsWith('Object.defineProperty(')) {
slicedCode = code.slice('Object.defineProperty('.length);
} else if (code.startsWith('(')) {
slicedCode = code.slice(1);
} else {
slicedCode = code;
}
if (slicedCode.startsWith('async function')) {
const idx = slicedCode.indexOf('(', 14);
if (idx !== -1) {
const name = slicedCode.slice(14, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)('({' + JSON.stringify(name) + ':async function(){}})')[
name
];
}
} else if (slicedCode.startsWith('function')) {
const idx = slicedCode.indexOf('(', 8);
if (idx !== -1) {
const name = slicedCode.slice(8, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)('({' + JSON.stringify(name) + ':function(){}})')[name];
}
} else if (slicedCode.startsWith('class')) {
const idx = slicedCode.indexOf('{', 5);
if (idx !== -1) {
const name = slicedCode.slice(5, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[name];
}
}
return function () {};
}
function parseModelString(
response: Response,
parentObject: Object,
@@ -2158,41 +2196,37 @@ function parseModelString(
// This should not compile to eval() because then it has local scope access.
const code = value.slice(2);
try {
// eslint-disable-next-line no-eval
return (0, eval)(code);
// If this might be a class constructor with a static initializer or
// static constructor then don't eval it. It might cause unexpected
// side-effects. Instead, fallback to parsing out the function type
// and name.
if (!mightHaveStaticConstructor.test(code)) {
// eslint-disable-next-line no-eval
return (0, eval)(code);
}
} catch (x) {
// We currently use this to express functions so we fail parsing it,
// let's just return a blank function as a place holder.
if (code.startsWith('(async function')) {
const idx = code.indexOf('(', 15);
// Fallthrough to fallback case.
}
// We currently use this to express functions so we fail parsing it,
// let's just return a blank function as a place holder.
let fn;
try {
fn = getInferredFunctionApproximate(code);
if (code.startsWith('Object.defineProperty(')) {
const DESCRIPTOR = ',"name",{value:"';
const idx = code.lastIndexOf(DESCRIPTOR);
if (idx !== -1) {
const name = code.slice(15, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)(
'({' + JSON.stringify(name) + ':async function(){}})',
)[name];
}
} else if (code.startsWith('(function')) {
const idx = code.indexOf('(', 9);
if (idx !== -1) {
const name = code.slice(9, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)(
'({' + JSON.stringify(name) + ':function(){}})',
)[name];
}
} else if (code.startsWith('(class')) {
const idx = code.indexOf('{', 6);
if (idx !== -1) {
const name = code.slice(6, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[
name
];
const name = JSON.parse(
code.slice(idx + DESCRIPTOR.length - 1, code.length - 2),
);
// $FlowFixMe[cannot-write]
Object.defineProperty(fn, 'name', {value: name});
}
}
return function () {};
} catch (_) {
fn = function () {};
}
return fn;
}
// Fallthrough
}

View File

@@ -3239,6 +3239,8 @@ describe('ReactFlight', () => {
}
Object.defineProperty(MyClass.prototype, 'y', {enumerable: true});
Object.defineProperty(MyClass, 'name', {value: 'MyClassName'});
function ServerComponent() {
console.log('hi', {
prop: 123,
@@ -3341,6 +3343,7 @@ describe('ReactFlight', () => {
const instance = mockConsoleLog.mock.calls[0][1].instance;
expect(typeof Class).toBe('function');
expect(Class.prototype.constructor).toBe(Class);
expect(Class.name).toBe('MyClassName');
expect(instance instanceof Class).toBe(true);
expect(Object.getPrototypeOf(instance)).toBe(Class.prototype);
expect(instance.x).toBe(1);

View File

@@ -862,6 +862,7 @@ describe('ProfilingCache', () => {
{
"compiledWithForget": false,
"displayName": "render()",
"env": null,
"hocDisplayNames": null,
"id": 1,
"key": null,
@@ -903,6 +904,7 @@ describe('ProfilingCache', () => {
{
"compiledWithForget": false,
"displayName": "createRoot()",
"env": null,
"hocDisplayNames": null,
"id": 1,
"key": null,
@@ -943,6 +945,7 @@ describe('ProfilingCache', () => {
{
"compiledWithForget": false,
"displayName": "createRoot()",
"env": null,
"hocDisplayNames": null,
"id": 1,
"key": null,

View File

@@ -4818,6 +4818,7 @@ export function attach(
displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
id: instance.id,
key: fiber.key,
env: null,
type: getElementTypeForFiber(fiber),
};
} else {
@@ -4826,6 +4827,7 @@ export function attach(
displayName: componentInfo.name || 'Anonymous',
id: instance.id,
key: componentInfo.key == null ? null : componentInfo.key,
env: componentInfo.env == null ? null : componentInfo.env,
type: ElementTypeVirtual,
};
}
@@ -5451,6 +5453,8 @@ export function attach(
// List of owners
owners,
env: null,
rootType,
rendererPackageName: renderer.rendererPackageName,
rendererVersion: renderer.version,
@@ -5554,6 +5558,8 @@ export function attach(
// List of owners
owners,
env: componentInfo.env == null ? null : componentInfo.env,
rootType,
rendererPackageName: renderer.rendererPackageName,
rendererVersion: renderer.version,

View File

@@ -795,6 +795,7 @@ export function attach(
displayName: getData(owner).displayName || 'Unknown',
id: getID(owner),
key: element.key,
env: null,
type: getElementType(owner),
});
if (owner._currentElement) {
@@ -857,6 +858,8 @@ export function attach(
// List of owners
owners,
env: null,
rootType: null,
rendererPackageName: null,
rendererVersion: null,

View File

@@ -256,6 +256,7 @@ export type SerializedElement = {
displayName: string | null,
id: number,
key: number | string | null,
env: null | string,
type: ElementType,
};
@@ -301,6 +302,10 @@ export type InspectedElement = {
// List of owners
owners: Array<SerializedElement> | null,
// Environment name that this component executed in or null for the client
env: string | null,
source: ReactFunctionLocation | null,
type: ElementType,

View File

@@ -255,6 +255,7 @@ export function convertInspectedElementBackendToFrontend(
id,
type,
owners,
env,
source,
context,
hooks,
@@ -299,6 +300,7 @@ export function convertInspectedElementBackendToFrontend(
owners === null
? null
: owners.map(backendToFrontendSerializedElementMapper),
env,
context: hydrateHelper(context),
hooks: hydrateHelper(hooks),
props: hydrateHelper(props),

View File

@@ -16,18 +16,21 @@ import styles from './ElementBadges.css';
type Props = {
hocDisplayNames: Array<string> | null,
environmentName: string | null,
compiledWithForget: boolean,
className?: string,
};
export default function ElementBadges({
compiledWithForget,
environmentName,
hocDisplayNames,
className = '',
}: Props): React.Node {
if (
!compiledWithForget &&
(hocDisplayNames == null || hocDisplayNames.length === 0)
(hocDisplayNames == null || hocDisplayNames.length === 0) &&
environmentName == null
) {
return null;
}
@@ -36,6 +39,8 @@ export default function ElementBadges({
<div className={`${styles.Root} ${className}`}>
{compiledWithForget && <ForgetBadge indexable={false} />}
{environmentName != null ? <Badge>{environmentName}</Badge> : null}
{hocDisplayNames != null && hocDisplayNames.length > 0 && (
<Badge>{hocDisplayNames[0]}</Badge>
)}

View File

@@ -150,13 +150,28 @@ function SuspendedByRow({
</Button>
{isOpen && (
<div className={styles.CollapsableContent}>
{showIOStack && <StackTraceView stack={ioInfo.stack} />}
{showIOStack && (
<StackTraceView
stack={ioInfo.stack}
environmentName={
ioOwner !== null && ioOwner.env === ioInfo.env
? null
: ioInfo.env
}
/>
)}
{(showIOStack || !showAwaitStack) &&
ioOwner !== null &&
ioOwner.id !== inspectedElement.id ? (
<OwnerView
key={ioOwner.id}
displayName={ioOwner.displayName || 'Anonymous'}
environmentName={
ioOwner.env === inspectedElement.env &&
ioOwner.env === ioInfo.env
? null
: ioOwner.env
}
hocDisplayNames={ioOwner.hocDisplayNames}
compiledWithForget={ioOwner.compiledWithForget}
id={ioOwner.id}
@@ -168,12 +183,25 @@ function SuspendedByRow({
<>
<div className={styles.SmallHeader}>awaited at:</div>
{asyncInfo.stack !== null && asyncInfo.stack.length > 0 && (
<StackTraceView stack={asyncInfo.stack} />
<StackTraceView
stack={asyncInfo.stack}
environmentName={
asyncOwner !== null && asyncOwner.env === asyncInfo.env
? null
: asyncInfo.env
}
/>
)}
{asyncOwner !== null && asyncOwner.id !== inspectedElement.id ? (
<OwnerView
key={asyncOwner.id}
displayName={asyncOwner.displayName || 'Anonymous'}
environmentName={
asyncOwner.env === inspectedElement.env &&
asyncOwner.env === asyncInfo.env
? null
: asyncOwner.env
}
hocDisplayNames={asyncOwner.hocDisplayNames}
compiledWithForget={asyncOwner.compiledWithForget}
id={asyncOwner.id}

View File

@@ -174,6 +174,9 @@ export default function InspectedElementView({
key={owner.id}
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
environmentName={
inspectedElement.env === owner.env ? null : owner.env
}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}

View File

@@ -20,6 +20,7 @@ import styles from './OwnerView.css';
type OwnerViewProps = {
displayName: string,
hocDisplayNames: Array<string> | null,
environmentName: string | null,
compiledWithForget: boolean,
id: number,
isInStore: boolean,
@@ -27,6 +28,7 @@ type OwnerViewProps = {
export default function OwnerView({
displayName,
environmentName,
hocDisplayNames,
compiledWithForget,
id,
@@ -65,6 +67,7 @@ export default function OwnerView({
<ElementBadges
hocDisplayNames={hocDisplayNames}
compiledWithForget={compiledWithForget}
environmentName={environmentName}
/>
</span>
</Button>

View File

@@ -220,6 +220,7 @@ function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) {
<ElementBadges
hocDisplayNames={owner.hocDisplayNames}
environmentName={owner.env}
compiledWithForget={owner.compiledWithForget}
className={styles.BadgesBlock}
/>
@@ -268,6 +269,7 @@ function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
<ElementBadges
hocDisplayNames={hocDisplayNames}
environmentName={owner.env}
compiledWithForget={compiledWithForget}
className={styles.BadgesBlock}
/>

View File

@@ -12,6 +12,8 @@ import {use, useContext} from 'react';
import useOpenResource from '../useOpenResource';
import ElementBadges from './ElementBadges';
import styles from './StackTraceView.css';
import type {
@@ -28,9 +30,13 @@ import formatLocationForDisplay from './formatLocationForDisplay';
type CallSiteViewProps = {
callSite: ReactCallSite,
environmentName: null | string,
};
export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
export function CallSiteView({
callSite,
environmentName,
}: CallSiteViewProps): React.Node {
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
const [virtualFunctionName, virtualURL, virtualLine, virtualColumn] =
@@ -64,19 +70,33 @@ export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
title={url + ':' + line}>
{formatLocationForDisplay(url, line, column)}
</span>
<ElementBadges environmentName={environmentName} />
</div>
);
}
type Props = {
stack: ReactStackTrace,
environmentName: null | string,
};
export default function StackTraceView({stack}: Props): React.Node {
export default function StackTraceView({
stack,
environmentName,
}: Props): React.Node {
return (
<div className={styles.StackTraceView}>
{stack.map((callSite, index) => (
<CallSiteView key={index} callSite={callSite} />
<CallSiteView
key={index}
callSite={callSite}
environmentName={
// Badge last row
// TODO: If we start ignore listing the last row, we should badge the last
// non-ignored row.
index === stack.length - 1 ? environmentName : null
}
/>
))}
</div>
);

View File

@@ -954,7 +954,7 @@ function TreeContextController({
Array<number>,
Map<number, number>,
]) => {
transitionDispatch({
dispatch({
type: 'HANDLE_STORE_MUTATION',
payload: [addedElementIDs, removedElementIDs],
});
@@ -965,7 +965,7 @@ function TreeContextController({
// At the moment, we can treat this as a mutation.
// We don't know which Elements were newly added/removed, but that should be okay in this case.
// It would only impact the search state, which is unlikely to exist yet at this point.
transitionDispatch({
dispatch({
type: 'HANDLE_STORE_MUTATION',
payload: [[], new Map()],
});

View File

@@ -208,6 +208,7 @@ export type SerializedElement = {
displayName: string | null,
id: number,
key: number | string | null,
env: null | string,
hocDisplayNames: Array<string> | null,
compiledWithForget: boolean,
type: ElementType,
@@ -265,6 +266,9 @@ export type InspectedElement = {
// List of owners
owners: Array<SerializedElement> | null,
// Environment name that this component executed in or null for the client
env: string | null,
// Location of component in source code.
source: ReactFunctionLocation | null,

View File

@@ -158,6 +158,7 @@ import {
DefaultHydrationLane,
SomeRetryLane,
includesSomeLane,
includesOnlyRetries,
laneToLanes,
removeLanes,
mergeLanes,
@@ -269,6 +270,7 @@ import {
scheduleUpdateOnFiber,
renderDidSuspendDelayIfPossible,
markSkippedUpdateLanes,
markRenderDerivedCause,
getWorkInProgressRoot,
peekDeferredLane,
} from './ReactFiberWorkLoop';
@@ -946,6 +948,13 @@ function updateDehydratedActivityComponent(
// but after we've already committed once.
warnIfHydrating();
if (includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
// If we're rendering Offscreen and we're entering the activity then it's possible
// that the only reason we rendered was because this boundary left work. Provide
// it as a cause if another one doesn't already exist.
markRenderDerivedCause(workInProgress);
}
if (
// TODO: Factoring is a little weird, since we check this right below, too.
!didReceiveUpdate
@@ -1132,6 +1141,16 @@ function updateActivityComponent(
children: nextChildren,
};
if (
includesSomeLane(renderLanes, (OffscreenLane: Lane)) &&
includesSomeLane(renderLanes, current.lanes)
) {
// If we're rendering Offscreen and we're entering the activity then it's possible
// that the only reason we rendered was because this boundary left work. Provide
// it as a cause if another one doesn't already exist.
markRenderDerivedCause(workInProgress);
}
const primaryChildFragment = updateWorkInProgressOffscreenFiber(
currentChild,
offscreenChildProps,
@@ -2515,6 +2534,17 @@ function updateSuspenseComponent(
workInProgress.memoizedState = SUSPENDED_MARKER;
return fallbackChildFragment;
} else {
if (
prevState !== null &&
includesOnlyRetries(renderLanes) &&
includesSomeLane(renderLanes, current.lanes)
) {
// If we're rendering Retry lanes and we're entering the primary content then it's possible
// that the only reason we rendered was because we left this boundary to be warmed up but
// nothing else scheduled an update. If so, use it as the cause of the render.
markRenderDerivedCause(workInProgress);
}
pushPrimaryTreeSuspenseHandler(workInProgress);
const nextPrimaryChildren = nextProps.children;
@@ -2873,6 +2903,13 @@ function updateDehydratedSuspenseComponent(
// but after we've already committed once.
warnIfHydrating();
if (includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
// If we're rendering Offscreen and we're entering the activity then it's possible
// that the only reason we rendered was because this boundary left work. Provide
// it as a cause if another one doesn't already exist.
markRenderDerivedCause(workInProgress);
}
if (isSuspenseInstanceFallback(suspenseInstance)) {
// This boundary is in a permanent fallback state. In this case, we'll never
// get an update and we'll never be able to hydrate the final content. Let's just try the

View File

@@ -122,7 +122,10 @@ import {
markStateUpdateScheduled,
setIsStrictModeForDevtools,
} from './ReactFiberDevToolsHook';
import {startUpdateTimerByLane} from './ReactProfilerTimer';
import {
startUpdateTimerByLane,
startHostActionTimer,
} from './ReactProfilerTimer';
import {createCache} from './ReactFiberCacheComponent';
import {
createUpdate as createLegacyQueueUpdate,
@@ -3239,6 +3242,8 @@ export function startHostTransition<F>(
BasicStateAction<Thenable<TransitionStatus> | TransitionStatus>,
> = stateHook.queue;
startHostActionTimer(formFiber);
startTransition(
formFiber,
queue,

View File

@@ -609,6 +609,7 @@ export function logBlockingStart(
eventType: null | string,
eventIsRepeat: boolean,
isSpawnedUpdate: boolean,
isPingedUpdate: boolean,
renderStartTime: number,
lanes: Lanes,
debugTask: null | ConsoleTask, // DEV-only
@@ -658,11 +659,13 @@ export function logBlockingStart(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
isSpawnedUpdate
? 'Cascading Update'
: renderStartTime - updateTime > 5
? 'Update Blocked'
: 'Update',
isPingedUpdate
? 'Promise Resolved'
: isSpawnedUpdate
? 'Cascading Update'
: renderStartTime - updateTime > 5
? 'Update Blocked'
: 'Update',
updateTime,
renderStartTime,
currentTrack,
@@ -672,11 +675,13 @@ export function logBlockingStart(
);
} else {
console.timeStamp(
isSpawnedUpdate
? 'Cascading Update'
: renderStartTime - updateTime > 5
? 'Update Blocked'
: 'Update',
isPingedUpdate
? 'Promise Resolved'
: isSpawnedUpdate
? 'Cascading Update'
: renderStartTime - updateTime > 5
? 'Update Blocked'
: 'Update',
updateTime,
renderStartTime,
currentTrack,
@@ -694,6 +699,7 @@ export function logTransitionStart(
eventTime: number,
eventType: null | string,
eventIsRepeat: boolean,
isPingedUpdate: boolean,
renderStartTime: number,
debugTask: null | ConsoleTask, // DEV-only
): void {
@@ -763,7 +769,11 @@ export function logTransitionStart(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
renderStartTime - updateTime > 5 ? 'Update Blocked' : 'Update',
isPingedUpdate
? 'Promise Resolved'
: renderStartTime - updateTime > 5
? 'Update Blocked'
: 'Update',
updateTime,
renderStartTime,
currentTrack,
@@ -773,7 +783,11 @@ export function logTransitionStart(
);
} else {
console.timeStamp(
renderStartTime - updateTime > 5 ? 'Update Blocked' : 'Update',
isPingedUpdate
? 'Promise Resolved'
: renderStartTime - updateTime > 5
? 'Update Blocked'
: 'Update',
updateTime,
renderStartTime,
currentTrack,
@@ -789,23 +803,43 @@ export function logRenderPhase(
startTime: number,
endTime: number,
lanes: Lanes,
debugTask: null | ConsoleTask,
): void {
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
const color = includesOnlyHydrationOrOffscreenLanes(lanes)
? 'tertiary-dark'
: 'primary-dark';
console.timeStamp(
includesOnlyOffscreenLanes(lanes)
? 'Prepared'
: includesOnlyHydrationLanes(lanes)
? 'Hydrated'
: 'Render',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
);
const label = includesOnlyOffscreenLanes(lanes)
? 'Prepared'
: includesOnlyHydrationLanes(lanes)
? 'Hydrated'
: 'Render';
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
label,
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
),
);
} else {
console.timeStamp(
label,
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
);
}
}
}
@@ -813,23 +847,43 @@ export function logInterruptedRenderPhase(
startTime: number,
endTime: number,
lanes: Lanes,
debugTask: null | ConsoleTask,
): void {
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
const color = includesOnlyHydrationOrOffscreenLanes(lanes)
? 'tertiary-dark'
: 'primary-dark';
console.timeStamp(
includesOnlyOffscreenLanes(lanes)
? 'Prewarm'
: includesOnlyHydrationLanes(lanes)
? 'Interrupted Hydration'
: 'Interrupted Render',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
);
const label = includesOnlyOffscreenLanes(lanes)
? 'Prewarm'
: includesOnlyHydrationLanes(lanes)
? 'Interrupted Hydration'
: 'Interrupted Render';
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
label,
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
),
);
} else {
console.timeStamp(
label,
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
);
}
}
}
@@ -837,19 +891,38 @@ export function logSuspendedRenderPhase(
startTime: number,
endTime: number,
lanes: Lanes,
debugTask: null | ConsoleTask,
): void {
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
const color = includesOnlyHydrationOrOffscreenLanes(lanes)
? 'tertiary-dark'
: 'primary-dark';
console.timeStamp(
'Prewarm',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
);
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Prewarm',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
),
);
} else {
console.timeStamp(
'Prewarm',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
);
}
}
}
@@ -857,20 +930,39 @@ export function logSuspendedWithDelayPhase(
startTime: number,
endTime: number,
lanes: Lanes,
debugTask: null | ConsoleTask,
): void {
// This means the render was suspended and cannot commit until it gets unblocked.
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
const color = includesOnlyHydrationOrOffscreenLanes(lanes)
? 'tertiary-dark'
: 'primary-dark';
console.timeStamp(
'Suspended',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
);
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Suspended',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
),
);
} else {
console.timeStamp(
'Suspended',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
color,
);
}
}
}
@@ -880,8 +972,12 @@ export function logRecoveredRenderPhase(
lanes: Lanes,
recoverableErrors: Array<CapturedValue<mixed>>,
hydrationFailed: boolean,
debugTask: null | ConsoleTask,
): void {
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
if (__DEV__) {
const properties: Array<[string, string]> = [];
for (let i = 0; i < recoverableErrors.length; i++) {
@@ -897,7 +993,7 @@ export function logRecoveredRenderPhase(
String(error);
properties.push(['Recoverable Error', message]);
}
performance.measure('Recovered', {
const options = {
start: startTime,
end: endTime,
detail: {
@@ -911,7 +1007,15 @@ export function logRecoveredRenderPhase(
properties,
},
},
});
};
if (debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(performance, 'Recovered', options),
);
} else {
performance.measure('Recovered', options);
}
} else {
console.timeStamp(
'Recovered',
@@ -929,68 +1033,144 @@ export function logErroredRenderPhase(
startTime: number,
endTime: number,
lanes: Lanes,
debugTask: null | ConsoleTask,
): void {
if (supportsUserTiming) {
console.timeStamp(
'Errored',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'error',
);
if (endTime <= startTime) {
return;
}
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Errored',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'error',
),
);
} else {
console.timeStamp(
'Errored',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'error',
);
}
}
}
export function logInconsistentRender(
startTime: number,
endTime: number,
debugTask: null | ConsoleTask,
): void {
if (supportsUserTiming) {
console.timeStamp(
'Teared Render',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'error',
);
if (endTime <= startTime) {
return;
}
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Teared Render',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'error',
),
);
} else {
console.timeStamp(
'Teared Render',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'error',
);
}
}
}
export function logSuspenseThrottlePhase(
startTime: number,
endTime: number,
debugTask: null | ConsoleTask,
): void {
// This was inside a throttled Suspense boundary commit.
if (supportsUserTiming) {
console.timeStamp(
'Throttled',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
);
if (endTime <= startTime) {
return;
}
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Throttled',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
),
);
} else {
console.timeStamp(
'Throttled',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
);
}
}
}
export function logSuspendedCommitPhase(
startTime: number,
endTime: number,
debugTask: null | ConsoleTask,
): void {
// This means the commit was suspended on CSS or images.
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
// TODO: Include the exact reason and URLs of what resources suspended.
// TODO: This might also be Suspended while waiting on a View Transition.
console.timeStamp(
'Suspended on CSS or Images',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
);
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Suspended on CSS or Images',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
),
);
} else {
console.timeStamp(
'Suspended on CSS or Images',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
);
}
}
}
@@ -999,8 +1179,12 @@ export function logCommitErrored(
endTime: number,
errors: Array<CapturedValue<mixed>>,
passive: boolean,
debugTask: null | ConsoleTask,
): void {
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
if (__DEV__) {
const properties: Array<[string, string]> = [];
for (let i = 0; i < errors.length; i++) {
@@ -1016,7 +1200,7 @@ export function logCommitErrored(
String(error);
properties.push(['Error', message]);
}
performance.measure('Errored', {
const options = {
start: startTime,
end: endTime,
detail: {
@@ -1030,7 +1214,15 @@ export function logCommitErrored(
properties,
},
},
});
};
if (debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(performance, 'Errored', options),
);
} else {
performance.measure('Errored', options);
}
} else {
console.timeStamp(
'Errored',
@@ -1048,20 +1240,39 @@ export function logCommitPhase(
startTime: number,
endTime: number,
errors: null | Array<CapturedValue<mixed>>,
debugTask: null | ConsoleTask,
): void {
if (errors !== null) {
logCommitErrored(startTime, endTime, errors, false);
logCommitErrored(startTime, endTime, errors, false, debugTask);
return;
}
if (supportsUserTiming) {
console.timeStamp(
'Commit',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-dark',
);
if (endTime <= startTime) {
return;
}
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Commit',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-dark',
),
);
} else {
console.timeStamp(
'Commit',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-dark',
);
}
}
}
@@ -1069,16 +1280,35 @@ export function logPaintYieldPhase(
startTime: number,
endTime: number,
delayedUntilPaint: boolean,
debugTask: null | ConsoleTask,
): void {
if (supportsUserTiming) {
console.timeStamp(
delayedUntilPaint ? 'Waiting for Paint' : '',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
);
if (endTime <= startTime) {
return;
}
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
delayedUntilPaint ? 'Waiting for Paint' : '',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
),
);
} else {
console.timeStamp(
delayedUntilPaint ? 'Waiting for Paint' : '',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
);
}
}
}
@@ -1086,19 +1316,38 @@ export function logPassiveCommitPhase(
startTime: number,
endTime: number,
errors: null | Array<CapturedValue<mixed>>,
debugTask: null | ConsoleTask,
): void {
if (errors !== null) {
logCommitErrored(startTime, endTime, errors, true);
logCommitErrored(startTime, endTime, errors, true, debugTask);
return;
}
if (supportsUserTiming) {
console.timeStamp(
'Remaining Effects',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-dark',
);
if (endTime <= startTime) {
return;
}
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Remaining Effects',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-dark',
),
);
} else {
console.timeStamp(
'Remaining Effects',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-dark',
);
}
}
}

View File

@@ -346,6 +346,7 @@ export function createHydrationContainer(
update.callback =
callback !== undefined && callback !== null ? callback : null;
enqueueUpdate(current, update, lane);
startUpdateTimerByLane(lane, 'hydrateRoot()');
scheduleInitialHydrationOnRoot(root, lane);
return root;

View File

@@ -266,15 +266,16 @@ import {
blockingClampTime,
blockingUpdateTime,
blockingUpdateTask,
blockingUpdateType,
blockingEventTime,
blockingEventType,
blockingEventIsRepeat,
blockingSpawnedUpdate,
blockingSuspendedTime,
transitionClampTime,
transitionStartTime,
transitionUpdateTime,
transitionUpdateTask,
transitionUpdateType,
transitionEventTime,
transitionEventType,
transitionEventIsRepeat,
@@ -301,6 +302,8 @@ import {
startPingTimerByLanes,
recordEffectError,
resetCommitErrors,
PINGED_UPDATE,
SPAWNED_UPDATE,
} from './ReactProfilerTimer';
// DEV stuff
@@ -482,6 +485,9 @@ export function getWorkInProgressTransitions(): null | Array<Transition> {
return workInProgressTransitions;
}
// The first setState call that eventually caused the current render.
let workInProgressUpdateTask: null | ConsoleTask = null;
let currentPendingTransitionCallbacks: PendingTransitionCallbacks | null = null;
let currentEndTime: number | null = null;
@@ -1104,7 +1110,11 @@ export function performWorkOnRoot(
) {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
setCurrentTrackFromLanes(lanes);
logInconsistentRender(renderStartTime, renderEndTime);
logInconsistentRender(
renderStartTime,
renderEndTime,
workInProgressUpdateTask,
);
finalizeRender(lanes, renderEndTime);
}
// A store was mutated in an interleaved event. Render again,
@@ -1130,7 +1140,12 @@ export function performWorkOnRoot(
if (errorRetryLanes !== NoLanes) {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
setCurrentTrackFromLanes(lanes);
logErroredRenderPhase(renderStartTime, renderEndTime, lanes);
logErroredRenderPhase(
renderStartTime,
renderEndTime,
lanes,
workInProgressUpdateTask,
);
finalizeRender(lanes, renderEndTime);
}
lanes = errorRetryLanes;
@@ -1161,7 +1176,12 @@ export function performWorkOnRoot(
if (exitStatus === RootFatalErrored) {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
setCurrentTrackFromLanes(lanes);
logErroredRenderPhase(renderStartTime, renderEndTime, lanes);
logErroredRenderPhase(
renderStartTime,
renderEndTime,
lanes,
workInProgressUpdateTask,
);
finalizeRender(lanes, renderEndTime);
}
prepareFreshStack(root, NoLanes);
@@ -1302,7 +1322,12 @@ function finishConcurrentRender(
// until we receive more data.
if (enableProfilerTimer && enableComponentPerformanceTrack) {
setCurrentTrackFromLanes(lanes);
logSuspendedRenderPhase(renderStartTime, renderEndTime, lanes);
logSuspendedRenderPhase(
renderStartTime,
renderEndTime,
lanes,
workInProgressUpdateTask,
);
finalizeRender(lanes, renderEndTime);
trackSuspendedTime(lanes, renderEndTime);
}
@@ -1867,18 +1892,22 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
previousRenderStartTime,
renderStartTime,
lanes,
workInProgressUpdateTask,
);
} else {
logInterruptedRenderPhase(
previousRenderStartTime,
renderStartTime,
lanes,
workInProgressUpdateTask,
);
}
finalizeRender(workInProgressRootRenderLanes, renderStartTime);
}
workInProgressUpdateTask = null;
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
workInProgressUpdateTask = blockingUpdateTask;
const clampedUpdateTime =
blockingUpdateTime >= 0 && blockingUpdateTime < blockingClampTime
? blockingClampTime
@@ -1898,6 +1927,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
? clampedUpdateTime
: renderStartTime,
lanes,
workInProgressUpdateTask,
);
}
logBlockingStart(
@@ -1905,7 +1935,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
clampedEventTime,
blockingEventType,
blockingEventIsRepeat,
blockingSpawnedUpdate,
blockingUpdateType === SPAWNED_UPDATE,
blockingUpdateType === PINGED_UPDATE,
renderStartTime,
lanes,
blockingUpdateTask,
@@ -1913,6 +1944,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
clearBlockingTimers();
}
if (includesTransitionLane(lanes)) {
workInProgressUpdateTask = transitionUpdateTask;
const clampedStartTime =
transitionStartTime >= 0 && transitionStartTime < transitionClampTime
? transitionClampTime
@@ -1936,6 +1968,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
? clampedUpdateTime
: renderStartTime,
lanes,
workInProgressUpdateTask,
);
}
logTransitionStart(
@@ -1944,6 +1977,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
clampedEventTime,
transitionEventType,
transitionEventIsRepeat,
transitionUpdateType === PINGED_UPDATE,
renderStartTime,
transitionUpdateTask,
);
@@ -2227,6 +2261,21 @@ function popAsyncDispatcher(prevAsyncDispatcher: any) {
ReactSharedInternals.A = prevAsyncDispatcher;
}
export function markRenderDerivedCause(fiber: Fiber): void {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if (__DEV__) {
if (workInProgressUpdateTask === null) {
// If we don't have a cause associated with this render, it's likely because some
// other render left work behind on this Fiber. The real cause is this Fiber itself.
// We use its debugTask as the cause for this render. This might not be the only
// one when multiple siblings are rendered but they ideally shouldn't be.
workInProgressUpdateTask =
fiber._debugTask == null ? null : fiber._debugTask;
}
}
}
}
export function markCommitTimeOfFallback() {
globalMostRecentFallbackTime = now();
}
@@ -3239,6 +3288,7 @@ function commitRoot(
completedRenderStartTime,
completedRenderEndTime,
lanes,
workInProgressUpdateTask,
);
} else if (recoverableErrors !== null) {
const hydrationFailed =
@@ -3252,9 +3302,15 @@ function commitRoot(
lanes,
recoverableErrors,
hydrationFailed,
workInProgressUpdateTask,
);
} else {
logRenderPhase(completedRenderStartTime, completedRenderEndTime, lanes);
logRenderPhase(
completedRenderStartTime,
completedRenderEndTime,
lanes,
workInProgressUpdateTask,
);
}
}
@@ -3425,9 +3481,17 @@ function commitRoot(
recordCommitTime();
if (enableComponentPerformanceTrack) {
if (suspendedCommitReason === SUSPENDED_COMMIT) {
logSuspendedCommitPhase(completedRenderEndTime, commitStartTime);
logSuspendedCommitPhase(
completedRenderEndTime,
commitStartTime,
workInProgressUpdateTask,
);
} else if (suspendedCommitReason === THROTTLED_COMMIT) {
logSuspenseThrottlePhase(completedRenderEndTime, commitStartTime);
logSuspenseThrottlePhase(
completedRenderEndTime,
commitStartTime,
workInProgressUpdateTask,
);
}
}
}
@@ -3672,6 +3736,7 @@ function flushSpawnedWork(): void {
: commitStartTime,
commitEndTime,
commitErrors,
workInProgressUpdateTask,
);
}
@@ -4147,6 +4212,7 @@ function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) {
commitEndTime,
passiveEffectStartTime,
!!wasDelayedCommit,
workInProgressUpdateTask,
);
}
@@ -4182,6 +4248,7 @@ function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) {
passiveEffectStartTime,
passiveEffectsEndTime,
commitErrors,
workInProgressUpdateTask,
);
finalizeRender(lanes, passiveEffectsEndTime);
}

View File

@@ -48,6 +48,11 @@ const createTask =
console.createTask
: (name: string) => null;
export const REGULAR_UPDATE: UpdateType = 0;
export const SPAWNED_UPDATE: UpdateType = 1;
export const PINGED_UPDATE: UpdateType = 2;
export opaque type UpdateType = 0 | 1 | 2;
export let renderStartTime: number = -0;
export let commitStartTime: number = -0;
export let commitEndTime: number = -0;
@@ -62,15 +67,16 @@ export let componentEffectErrors: null | Array<CapturedValue<mixed>> = null;
export let blockingClampTime: number = -0;
export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
export let blockingUpdateTask: null | ConsoleTask = null; // First sync setState's stack trace.
export let blockingUpdateType: UpdateType = 0;
export let blockingEventTime: number = -1.1; // Event timeStamp of the first setState.
export let blockingEventType: null | string = null; // Event type of the first setState.
export let blockingEventIsRepeat: boolean = false;
export let blockingSpawnedUpdate: boolean = false;
export let blockingSuspendedTime: 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.
export let transitionUpdateTime: number = -1.1; // First transition setState scheduled.
export let transitionUpdateType: UpdateType = 0;
export let transitionUpdateTask: null | ConsoleTask = null; // First transition setState's stack trace.
export let transitionEventTime: number = -1.1; // Event timeStamp of the first transition.
export let transitionEventType: null | string = null; // Event type of the first transition.
@@ -97,7 +103,7 @@ export function startUpdateTimerByLane(lane: Lane, method: string): void {
blockingUpdateTime = now();
blockingUpdateTask = createTask(method);
if (isAlreadyRendering()) {
blockingSpawnedUpdate = true;
blockingUpdateType = SPAWNED_UPDATE;
}
const newEventTime = resolveEventTimeStamp();
const newEventType = resolveEventType();
@@ -110,7 +116,7 @@ export function startUpdateTimerByLane(lane: Lane, method: string): void {
// If this is a second update in the same event, we treat it as a spawned update.
// This might be a microtask spawned from useEffect, multiple flushSync or
// a setState in a microtask spawned after the first setState. Regardless it's bad.
blockingSpawnedUpdate = true;
blockingUpdateType = SPAWNED_UPDATE;
}
blockingEventTime = newEventTime;
blockingEventType = newEventType;
@@ -135,6 +141,54 @@ export function startUpdateTimerByLane(lane: Lane, method: string): void {
}
}
export function startHostActionTimer(fiber: Fiber): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
// This schedules an update on both the blocking lane for the pending state and on the
// transition lane for the action update. Using the debug task from the host fiber.
if (blockingUpdateTime < 0) {
blockingUpdateTime = now();
blockingUpdateTask =
__DEV__ && fiber._debugTask != null ? fiber._debugTask : null;
if (isAlreadyRendering()) {
blockingUpdateType = SPAWNED_UPDATE;
}
const newEventTime = resolveEventTimeStamp();
const newEventType = resolveEventType();
if (
newEventTime !== blockingEventTime ||
newEventType !== blockingEventType
) {
blockingEventIsRepeat = false;
} else if (newEventType !== null) {
// If this is a second update in the same event, we treat it as a spawned update.
// This might be a microtask spawned from useEffect, multiple flushSync or
// a setState in a microtask spawned after the first setState. Regardless it's bad.
blockingUpdateType = SPAWNED_UPDATE;
}
blockingEventTime = newEventTime;
blockingEventType = newEventType;
}
if (transitionUpdateTime < 0) {
transitionUpdateTime = now();
transitionUpdateTask =
__DEV__ && fiber._debugTask != null ? fiber._debugTask : null;
if (transitionStartTime < 0) {
const newEventTime = resolveEventTimeStamp();
const newEventType = resolveEventType();
if (
newEventTime !== transitionEventTime ||
newEventType !== transitionEventType
) {
transitionEventIsRepeat = false;
}
transitionEventTime = newEventTime;
transitionEventType = newEventType;
}
}
}
export function startPingTimerByLanes(lanes: Lanes): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
@@ -145,10 +199,14 @@ export function startPingTimerByLanes(lanes: Lanes): void {
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
if (blockingUpdateTime < 0) {
blockingClampTime = blockingUpdateTime = now();
blockingUpdateTask = createTask('Promise Resolved');
blockingUpdateType = PINGED_UPDATE;
}
} else if (includesTransitionLane(lanes)) {
if (transitionUpdateTime < 0) {
transitionClampTime = transitionUpdateTime = now();
transitionUpdateTask = createTask('Promise Resolved');
transitionUpdateType = PINGED_UPDATE;
}
}
}
@@ -166,10 +224,9 @@ export function trackSuspendedTime(lanes: Lanes, renderEndTime: number) {
export function clearBlockingTimers(): void {
blockingUpdateTime = -1.1;
blockingUpdateTask = null;
blockingUpdateType = 0;
blockingSuspendedTime = -1.1;
blockingEventIsRepeat = true;
blockingSpawnedUpdate = false;
}
export function startAsyncTransitionTimer(): void {
@@ -196,20 +253,6 @@ export function hasScheduledTransitionWork(): boolean {
return transitionUpdateTime > -1;
}
// We use this marker to indicate that we have scheduled a render to be performed
// but it's not an explicit state update.
const ACTION_STATE_MARKER = -0.5;
export function startActionStateUpdate(): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
if (transitionUpdateTime < 0) {
transitionUpdateTime = ACTION_STATE_MARKER;
transitionUpdateTask = null;
}
}
export function clearAsyncTransitionTimer(): void {
transitionStartTime = -1.1;
}
@@ -217,7 +260,7 @@ export function clearAsyncTransitionTimer(): void {
export function clearTransitionTimers(): void {
transitionStartTime = -1.1;
transitionUpdateTime = -1.1;
transitionUpdateTask = null;
transitionUpdateType = 0;
transitionSuspendedTime = -1.1;
transitionEventIsRepeat = true;
}

View File

@@ -4848,9 +4848,18 @@ function renderDebugModel(
return existingReference;
}
// $FlowFixMe[method-unbinding]
const functionBody: string = Function.prototype.toString.call(value);
const name = value.name;
const serializedValue = serializeEval(
// $FlowFixMe[method-unbinding]
'(' + Function.prototype.toString.call(value) + ')',
typeof name === 'string'
? 'Object.defineProperty(' +
functionBody +
',"name",{value:' +
JSON.stringify(name) +
'})'
: '(' + functionBody + ')',
);
request.pendingDebugChunks++;
const id = request.nextChunkId++;