Compare commits

..

2 Commits

Author SHA1 Message Date
Joe Savona
718105c603 [compiler] Improve IIFE inlining
We currently inline IIFEs by creating a temporary and a labeled block w the original code. The original return statements turn into an assignment to the temporary and  break out of the label. However, many cases of IIFEs are due to inlining of manual `useMemo()`, and these cases often have only a single return statement. Here, the output is cleaner if we avoid the temporary and label - so that's what we do in this PR.

Note that the most complex part of the change is actually around ValidatePreserveExistingMemo - we have some logic to track the IIFE temporary reassignmetns which needs to be updated to handle the simpler version of inlining.
2025-07-07 16:02:19 -07:00
Joe Savona
82b8f612de [compiler] Fix for consecutive DCE'd branches with phis
This is an optimized version of @asmjmp0's fix in https://github.com/facebook/react/pull/31940. When we merge consecutive blocks we need to take care to rewrite later phis whose operands will now be different blocks due to merging. Rather than iterate all the blocks on each merge as in #31940, we can do a single iteration over all the phis at the end to fix them up.

Note: this is a redo of #31959
2025-07-07 15:00:11 -07:00
25 changed files with 409 additions and 1331 deletions

View File

@@ -44,7 +44,6 @@ import {
PrintedCompilerPipelineValue,
} from './Output';
import {transformFromAstSync} from '@babel/core';
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
function parseInput(
input: string,
@@ -144,7 +143,6 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
@@ -212,11 +210,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
},
logger: {
debugLogIRs: logIR,
logEvent: (_filename: string | null, event: LoggerEvent) => {
if (event.kind === 'CompileError') {
otherErrors.push(new CompilerErrorDetail(event.detail));
}
},
logEvent: () => {},
},
});
transformOutput = invokeCompiler(source, language, opts);
@@ -243,10 +237,6 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
);
}
}
// Only include logger errors if there weren't other errors
if (!error.hasErrors() && otherErrors.length !== 0) {
otherErrors.forEach(e => error.push(e));
}
if (error.hasErrors()) {
return [{kind: 'err', results, error: error}, language];
}

View File

@@ -1355,83 +1355,13 @@ function lowerStatement(
return;
}
case 'WithStatement': {
builder.errors.push({
reason: `JavaScript 'with' syntax is not supported`,
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'ClassDeclaration': {
/*
* We can in theory support nested classes, similarly to functions where we track values
* captured by the class and consider mutations of the instances to mutate the class itself
*/
builder.errors.push({
reason: `Support nested class declarations`,
severity: ErrorSeverity.Todo,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'EnumDeclaration':
case 'TSEnumDeclaration': {
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'ExportAllDeclaration':
case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration':
case 'ImportDeclaration':
case 'TSExportAssignment':
case 'TSImportEqualsDeclaration': {
builder.errors.push({
reason:
'JavaScript `import` and `export` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
case 'TSNamespaceExportDeclaration': {
builder.errors.push({
reason:
'TypeScript `namespace` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
case 'TypeAlias':
case 'TSInterfaceDeclaration':
case 'TSTypeAliasDeclaration': {
// We do not preserve type annotations/syntax through transformation
return;
}
case 'ClassDeclaration':
case 'DeclareClass':
case 'DeclareExportAllDeclaration':
case 'DeclareExportDeclaration':
@@ -1442,14 +1372,31 @@ function lowerStatement(
case 'DeclareOpaqueType':
case 'DeclareTypeAlias':
case 'DeclareVariable':
case 'EnumDeclaration':
case 'ExportAllDeclaration':
case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration':
case 'ImportDeclaration':
case 'InterfaceDeclaration':
case 'OpaqueType':
case 'TSDeclareFunction':
case 'TSInterfaceDeclaration':
case 'TSEnumDeclaration':
case 'TSExportAssignment':
case 'TSImportEqualsDeclaration':
case 'TSModuleDeclaration':
case 'TSTypeAliasDeclaration':
case 'TypeAlias': {
// We do not preserve type annotations/syntax through transformation
case 'TSNamespaceExportDeclaration':
case 'WithStatement': {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${stmtPath.type} statements`,
severity: ErrorSeverity.Todo,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
default: {
@@ -3555,16 +3502,6 @@ function lowerIdentifier(
return place;
}
default: {
if (binding.kind === 'Global' && binding.name === 'eval') {
builder.errors.push({
reason: `The 'eval' function is not supported`,
description:
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
severity: ErrorSeverity.InvalidJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
}
return lowerValueToTemporary(builder, {
kind: 'LoadGlobal',
binding,

View File

@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import generate from '@babel/generator';
import {CompilerError} from '../CompilerError';
import {printReactiveScopeSummary} from '../ReactiveScopes/PrintReactiveFunction';
import DisjointSet from '../Utils/DisjointSet';
@@ -465,7 +466,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
break;
}
case 'UnsupportedNode': {
value = `UnsupportedNode ${instrValue.node.type}`;
value = `UnsupportedNode(${generate(instrValue.node).code})`;
break;
}
case 'LoadLocal': {

View File

@@ -829,14 +829,12 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
};
}
case 'UnsupportedNode': {
const lvalues = [];
if (lvalue !== null) {
lvalues.push({place: lvalue, level: MemoizationLevel.Never});
}
return {
lvalues,
rvalues: [],
};
CompilerError.invariant(false, {
reason: `Unexpected unsupported node`,
description: null,
loc: value.loc,
suggestions: null,
});
}
default: {
assertExhaustive(

View File

@@ -1,24 +0,0 @@
## Input
```javascript
function Component(props) {
eval('props.x = true');
return <div />;
}
```
## Error
```
1 | function Component(props) {
> 2 | eval('props.x = true');
| ^^^^ InvalidJS: The 'eval' function is not supported. Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler (2:2)
3 | return <div />;
4 | }
5 |
```

View File

@@ -1,4 +0,0 @@
function Component(props) {
eval('props.x = true');
return <div />;
}

View File

@@ -84,7 +84,7 @@ let moduleLocal = false;
> 3 | var x = [];
| ^^^^^^^^^^^ Todo: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration (3:3)
Todo: Support nested class declarations (5:10)
Todo: (BuildHIR::lowerStatement) Handle ClassDeclaration statements (5:10)
Todo: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement (20:22)

View File

@@ -1,60 +0,0 @@
## Input
```javascript
// @flow
function Component(props) {
enum Bool {
True = 'true',
False = 'false',
}
let bool: Bool = Bool.False;
if (props.value) {
bool = Bool.True;
}
return <div>{bool}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: true}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
enum Bool {
True = "true",
False = "false",
}
let bool = Bool.False;
if (props.value) {
bool = Bool.True;
}
let t0;
if ($[0] !== bool) {
t0 = <div>{bool}</div>;
$[0] = bool;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: true }],
};
```
### Eval output
(kind: exception) Bool is not defined

View File

@@ -1,18 +0,0 @@
// @flow
function Component(props) {
enum Bool {
True = 'true',
False = 'false',
}
let bool: Bool = Bool.False;
if (props.value) {
bool = Bool.True;
}
return <div>{bool}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: true}],
};

View File

@@ -1,59 +0,0 @@
## Input
```javascript
function Component(props) {
enum Bool {
True = 'true',
False = 'false',
}
let bool: Bool = Bool.False;
if (props.value) {
bool = Bool.True;
}
return <div>{bool}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: true}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
enum Bool {
True = "true",
False = "false",
}
let bool = Bool.False;
if (props.value) {
bool = Bool.True;
}
let t0;
if ($[0] !== bool) {
t0 = <div>{bool}</div>;
$[0] = bool;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: true }],
};
```
### Eval output
(kind: ok) <div>true</div>

View File

@@ -1,17 +0,0 @@
function Component(props) {
enum Bool {
True = 'true',
False = 'false',
}
let bool: Bool = Bool.False;
if (props.value) {
bool = Bool.True;
}
return <div>{bool}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: true}],
};

View File

@@ -53,15 +53,6 @@ const React = require('react');
const activeDebugChannels =
process.env.NODE_ENV === 'development' ? new Map() : null;
function filterStackFrame(sourceURL, functionName) {
return (
sourceURL !== '' &&
!sourceURL.startsWith('node:') &&
!sourceURL.includes('node_modules') &&
!sourceURL.endsWith('library.js')
);
}
function getDebugChannel(req) {
if (process.env.NODE_ENV !== 'development') {
return undefined;
@@ -132,7 +123,6 @@ async function renderApp(
const payload = {root, returnValue, formState};
const {pipe} = renderToPipeableStream(payload, moduleMap, {
debugChannel: await promiseForDebugChannel,
filterStackFrame,
});
pipe(res);
}
@@ -188,9 +178,7 @@ async function prerenderApp(res, returnValue, formState, noCache) {
);
// For client-invoked server actions we refresh the tree and return a return value.
const payload = {root, returnValue, formState};
const {prelude} = await prerenderToNodeStream(payload, moduleMap, {
filterStackFrame,
});
const {prelude} = await prerenderToNodeStream(payload, moduleMap);
prelude.pipe(res);
}

View File

@@ -24,7 +24,6 @@ import {GenerateImage} from './GenerateImage.js';
import {like, greet, increment} from './actions.js';
import {getServerState} from './ServerState.js';
import {sdkMethod} from './library.js';
const promisedText = new Promise(resolve =>
setTimeout(() => resolve('deferred text'), 50)
@@ -121,69 +120,9 @@ async function ServerComponent({noCache}) {
return await fetchThirdParty(noCache);
}
let veryDeepObject = [
{
bar: {
baz: {
a: {},
},
},
},
{
bar: {
baz: {
a: {},
},
},
},
{
bar: {
baz: {
a: {},
},
},
},
{
bar: {
baz: {
a: {
b: {
c: {
d: {
e: {
f: {
g: {
h: {
i: {
j: {
k: {
l: {
m: {
yay: 'You reached the end',
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
];
export default async function App({prerender, noCache}) {
const res = await fetch('http://localhost:3001/todos');
const todos = await res.json();
await sdkMethod('http://localhost:3001/todos');
console.log('Expand me:', veryDeepObject);
const dedupedChild = <ServerComponent noCache={noCache} />;
const message = getServerState();

View File

@@ -1,9 +0,0 @@
export async function sdkMethod(input, init) {
return fetch(input, init).then(async response => {
await new Promise(resolve => {
setTimeout(resolve, 10);
});
return response;
});
}

View File

@@ -1774,40 +1774,6 @@ function applyConstructor(
return undefined;
}
function defineLazyGetter<T>(
response: Response,
chunk: SomeChunk<T>,
parentObject: Object,
key: string,
): any {
// We don't immediately initialize it even if it's resolved.
// Instead, we wait for the getter to get accessed.
Object.defineProperty(parentObject, key, {
get: function () {
if (chunk.status === RESOLVED_MODEL) {
// If it was now resolved, then we initialize it. This may then discover
// a new set of lazy references that are then asked for eagerly in case
// we get that deep.
initializeModelChunk(chunk);
}
switch (chunk.status) {
case INITIALIZED: {
return chunk.value;
}
case ERRORED:
throw chunk.reason;
}
// Otherwise, we didn't have enough time to load the object before it was
// accessed or the connection closed. So we just log that it was omitted.
// TODO: We should ideally throw here to indicate a difference.
return OMITTED_PROP_ERROR;
},
enumerable: true,
configurable: false,
});
return null;
}
function extractIterator(response: Response, model: Array<any>): Iterator<any> {
// $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
return model[Symbol.iterator]();
@@ -2048,31 +2014,8 @@ function parseModelString(
if (value.length > 2) {
const debugChannel = response._debugChannel;
if (debugChannel) {
if (value[2] === '@') {
// This is a deferred Promise.
const ref = value.slice(3); // We assume this doesn't have a path just id.
const id = parseInt(ref, 16);
if (!response._chunks.has(id)) {
// We haven't seen this id before. Query the server to start sending it.
debugChannel('P:' + ref);
}
// Start waiting. This now creates a pending chunk if it doesn't already exist.
// This is the actual Promise we're waiting for.
return getChunk(response, id);
}
const ref = value.slice(2); // We assume this doesn't have a path just id.
const id = parseInt(ref, 16);
if (!response._chunks.has(id)) {
// We haven't seen this id before. Query the server to start sending it.
debugChannel('Q:' + ref);
}
// Start waiting. This now creates a pending chunk if it doesn't already exist.
const chunk = getChunk(response, id);
if (chunk.status === INITIALIZED) {
// We already loaded this before. We can just use the real value.
return chunk.value;
}
return defineLazyGetter(response, chunk, parentObject, key);
const ref = value.slice(2);
debugChannel('R:' + ref); // Release this reference immediately
}
}

View File

@@ -9544,102 +9544,6 @@ describe('ReactDOMFizzServer', () => {
);
});
it('will attempt to render the preamble inline to allow rendering before a later abort in the same task', async () => {
const promise = new Promise(() => {});
function Pending() {
React.use(promise);
}
const controller = new AbortController();
function Abort() {
controller.abort();
return <Comp />;
}
function Comp() {
return null;
}
function App() {
return (
<html>
<head>
<meta content="here" />
</head>
<body>
<main>hello</main>
<Suspense>
<Pending />
</Suspense>
<Abort />
</body>
</html>
);
}
const signal = controller.signal;
let thrownError = null;
const errors = [];
try {
await act(() => {
const {pipe, abort} = renderToPipeableStream(<App />, {
onError(e, ei) {
errors.push({
error: e,
componentStack: normalizeCodeLocInfo(ei.componentStack),
});
},
});
signal.addEventListener('abort', () => abort('boom'));
pipe(writable);
});
} catch (e) {
thrownError = e;
}
expect(thrownError).toBe('boom');
// TODO there should actually be three errors. One for the pending Suspense, one for the fallback task, and one for the task
// that does the abort itself. At the moment abort will flush queues and if there is no pending tasks will close the request before
// the task which initiated the abort can even be processed. This is a bug but not one that I am fixing with the current change
// so I am asserting the current behavior
expect(errors).toEqual([
{
error: 'boom',
componentStack: componentStack([
'Pending',
'Suspense',
'body',
'html',
'App',
]),
},
{
error: 'boom',
componentStack: componentStack([
'Suspense Fallback',
'body',
'html',
'App',
]),
// }, {
// error: 'boom',
// componentStack: componentStack(['Abort', 'body', 'html', 'App'])
},
]);
// We expect the render to throw before streaming anything so the default
// document is still loaded
expect(getVisibleChildren(document)).toEqual(
<html>
<head />
<body>
<div id="container" />
</body>
</html>,
);
});
it('Will wait to flush Document chunks until all boundaries which might contain a preamble are errored or resolved', async () => {
let rejectFirst;
const firstPromise = new Promise((_, reject) => {

View File

@@ -34,7 +34,7 @@ import {
} from 'shared/ReactComponentStackFrame';
import {formatOwnerStack} from 'shared/ReactOwnerStackFrames';
function describeFiber(fiber: Fiber, childFiber: null | Fiber): string {
function describeFiber(fiber: Fiber): string {
switch (fiber.tag) {
case HostHoistable:
case HostSingleton:
@@ -44,10 +44,6 @@ function describeFiber(fiber: Fiber, childFiber: null | Fiber): string {
// TODO: When we support Thenables as component types we should rename this.
return describeBuiltInComponentFrame('Lazy');
case SuspenseComponent:
if (fiber.child !== childFiber && childFiber !== null) {
// If we came from the second Fiber then we're in the Suspense Fallback.
return describeBuiltInComponentFrame('Suspense Fallback');
}
return describeBuiltInComponentFrame('Suspense');
case SuspenseListComponent:
return describeBuiltInComponentFrame('SuspenseList');
@@ -74,9 +70,8 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
try {
let info = '';
let node: Fiber = workInProgress;
let previous: null | Fiber = null;
do {
info += describeFiber(node, previous);
info += describeFiber(node);
if (__DEV__) {
// Add any Server Component stack frames in reverse order.
const debugInfo = node._debugInfo;
@@ -93,7 +88,6 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
}
}
}
previous = node;
// $FlowFixMe[incompatible-type] we bail out when we get a null
node = node.return;
} while (node);

View File

@@ -87,7 +87,7 @@ describe('ReactFragment', () => {
function normalizeCodeLocInfo(str) {
return (
str &&
str.replace(/\n +(?:at|in) ([^\(]+) [^\n]*/g, function (m, name) {
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
return '\n in ' + name + ' (at **)';
})
);
@@ -168,40 +168,6 @@ describe('ReactFragment', () => {
]);
});
it('includes built-in for Suspense fallbacks', async () => {
const SomethingThatSuspends = React.lazy(() => {
return new Promise(() => {});
});
ReactNoop.createRoot({
onCaughtError,
}).render(
<CatchingBoundary>
<Suspense fallback={<SomethingThatErrors />}>
<SomethingThatSuspends />
</Suspense>
</CatchingBoundary>,
);
await waitForAll([]);
expect(didCatchErrors).toEqual([
'uh oh',
componentStack([
'SomethingThatErrors',
'Suspense Fallback',
'CatchingBoundary',
]),
]);
expect(rootCaughtErrors).toEqual([
'uh oh',
componentStack([
'SomethingThatErrors',
'Suspense Fallback',
'CatchingBoundary',
]),
__DEV__ ? componentStack(['SomethingThatErrors']) : null,
]);
});
// @gate enableActivity
it('includes built-in for Activity', async () => {
ReactNoop.createRoot({

View File

@@ -2206,7 +2206,7 @@ function renderSuspenseList(
function renderPreamble(
request: Request,
task: RenderTask,
task: Task,
blockedSegment: Segment,
node: ReactNodeList,
): void {
@@ -2219,21 +2219,28 @@ function renderPreamble(
false,
);
blockedSegment.preambleChildren.push(preambleSegment);
task.blockedSegment = preambleSegment;
try {
preambleSegment.status = RENDERING;
renderNode(request, task, node, -1);
pushSegmentFinale(
preambleSegment.chunks,
request.renderState,
preambleSegment.lastPushedText,
preambleSegment.textEmbedded,
);
preambleSegment.status = COMPLETED;
finishedSegment(request, task.blockedBoundary, preambleSegment);
} finally {
task.blockedSegment = blockedSegment;
}
// @TODO we can just attempt to render in the current task rather than spawning a new one
const preambleTask = createRenderTask(
request,
null,
node,
-1,
task.blockedBoundary,
preambleSegment,
task.blockedPreamble,
task.hoistableState,
request.abortableTasks,
task.keyPath,
task.formatContext,
task.context,
task.treeContext,
task.row,
task.componentStack,
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ ? task.debugTask : null,
);
pushComponentStack(preambleTask);
request.pingedTasks.push(preambleTask);
}
function renderHostElement(
@@ -2285,8 +2292,7 @@ function renderHostElement(
props,
));
if (isPreambleContext(newContext)) {
// $FlowFixMe: Refined
renderPreamble(request, (task: RenderTask), segment, children);
renderPreamble(request, task, segment, children);
} else {
// We use the non-destructive form because if something suspends, we still
// need to pop back up and finish this subtree of HTML.

View File

@@ -26,7 +26,7 @@ type PromiseWithDebugInfo = interface extends Promise<any> {
export type IONode = {
tag: 0,
owner: null | ReactComponentInfo,
stack: null | ReactStackTrace, // callsite that spawned the I/O
stack: ReactStackTrace, // callsite that spawned the I/O
start: number, // start time when the first part of the I/O sequence started
end: number, // we typically don't use this. only when there's no promise intermediate.
promise: null, // not used on I/O
@@ -37,7 +37,7 @@ export type IONode = {
export type PromiseNode = {
tag: 1,
owner: null | ReactComponentInfo,
stack: null | ReactStackTrace, // callsite that created the Promise
stack: ReactStackTrace, // callsite that created the Promise
start: number, // start time when the Promise was created
end: number, // end time when the Promise was resolved.
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced
@@ -48,7 +48,7 @@ export type PromiseNode = {
export type AwaitNode = {
tag: 2,
owner: null | ReactComponentInfo,
stack: null | ReactStackTrace, // callsite that awaited (using await, .then(), Promise.all(), ...)
stack: ReactStackTrace, // callsite that awaited (using await, .then(), Promise.all(), ...)
start: number, // when we started blocking. This might be later than the I/O started.
end: number, // when we unblocked. This might be later than the I/O resolved if there's CPU time.
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced
@@ -59,7 +59,7 @@ export type AwaitNode = {
export type UnresolvedPromiseNode = {
tag: 3,
owner: null | ReactComponentInfo,
stack: null | ReactStackTrace, // callsite that created the Promise
stack: ReactStackTrace, // callsite that created the Promise
start: number, // start time when the Promise was created
end: -1.1, // set when we resolve.
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced
@@ -70,7 +70,7 @@ export type UnresolvedPromiseNode = {
export type UnresolvedAwaitNode = {
tag: 4,
owner: null | ReactComponentInfo,
stack: null | ReactStackTrace, // callsite that awaited (using await, .then(), Promise.all(), ...)
stack: ReactStackTrace, // callsite that awaited (using await, .then(), Promise.all(), ...)
start: number, // when we started blocking. This might be later than the I/O started.
end: -1.1, // set when we resolve.
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced

View File

@@ -94,7 +94,6 @@ import {
getCurrentAsyncSequence,
getAsyncSequenceFromPromise,
parseStackTrace,
parseStackTracePrivate,
supportsComponentStorage,
componentStorage,
unbadgeConsole,
@@ -252,54 +251,6 @@ function filterStackTrace(
return filteredStack;
}
function hasUnfilteredFrame(request: Request, stack: ReactStackTrace): boolean {
const filterStackFrame = request.filterStackFrame;
for (let i = 0; i < stack.length; i++) {
const callsite = stack[i];
const functionName = callsite[0];
const url = devirtualizeURL(callsite[1]);
const lineNumber = callsite[2];
const columnNumber = callsite[3];
// Ignore async stack frames because they're not "real". We'd expect to have at least
// one non-async frame if we're actually executing inside a first party function.
// Otherwise we might just be in the resume of a third party function that resumed
// inside a first party stack.
const isAsync = callsite[6];
if (
!isAsync &&
filterStackFrame(url, functionName, lineNumber, columnNumber)
) {
return true;
}
}
return false;
}
export function isAwaitInUserspace(
request: Request,
stack: ReactStackTrace,
): boolean {
let firstFrame = 0;
while (stack.length > firstFrame && stack[firstFrame][0] === 'Promise.then') {
// Skip Promise.then frame itself.
firstFrame++;
}
if (stack.length > firstFrame) {
// Check if the very first stack frame that awaited this Promise was in user space.
// TODO: This doesn't take into account wrapper functions such as our fake .then()
// in FlightClient which will always be considered third party awaits if you call
// .then directly.
const filterStackFrame = request.filterStackFrame;
const callsite = stack[firstFrame];
const functionName = callsite[0];
const url = devirtualizeURL(callsite[1]);
const lineNumber = callsite[2];
const columnNumber = callsite[3];
return filterStackFrame(url, functionName, lineNumber, columnNumber);
}
return false;
}
initAsyncDebugInfo();
function patchConsole(consoleInst: typeof console, methodName: string) {
@@ -325,7 +276,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
// one stack frame but keeping it simple for now and include all frames.
const stack = filterStackTrace(
request,
parseStackTracePrivate(new Error('react-stack-top-frame'), 1) || [],
parseStackTrace(new Error('react-stack-top-frame'), 1),
);
request.pendingDebugChunks++;
const owner: null | ReactComponentInfo = resolveOwner();
@@ -801,19 +752,6 @@ function serializeDebugThenable(
return ref;
}
const deferredDebugObjects = request.deferredDebugObjects;
if (deferredDebugObjects !== null) {
// For Promises that are not yet resolved, we always defer them. They are async anyway so it's
// safe to defer them. This also ensures that we don't eagerly call .then() on a Promise that
// otherwise wouldn't have initialized. It also ensures that we don't "handle" a rejection
// that otherwise would have triggered unhandled rejection.
deferredDebugObjects.retained.set(id, (thenable: any));
const deferredRef = '$Y@' + id.toString(16);
// We can now refer to the deferred object in the future.
request.writtenDebugObjects.set(thenable, deferredRef);
return deferredRef;
}
let cancelled = false;
thenable.then(
@@ -866,36 +804,6 @@ function serializeDebugThenable(
return ref;
}
function emitRequestedDebugThenable(
request: Request,
id: number,
counter: {objectLimit: number},
thenable: Thenable<any>,
): void {
thenable.then(
value => {
if (request.status === ABORTING) {
emitDebugHaltChunk(request, id);
enqueueFlush(request);
return;
}
emitOutlinedDebugModelChunk(request, id, counter, value);
enqueueFlush(request);
},
reason => {
if (request.status === ABORTING) {
emitDebugHaltChunk(request, id);
enqueueFlush(request);
return;
}
// We don't log these errors since they didn't actually throw into Flight.
const digest = '';
emitErrorChunk(request, id, digest, reason, true);
enqueueFlush(request);
},
);
}
function serializeThenable(
request: Request,
task: Task,
@@ -1079,9 +987,8 @@ function serializeReadableStream(
signal.removeEventListener('abort', abortStream);
const reason = signal.reason;
if (enableHalt && request.type === PRERENDER) {
request.abortableTasks.delete(streamTask);
haltTask(streamTask, request);
finishHaltedTask(streamTask, request);
request.abortableTasks.delete(streamTask);
} else {
// TODO: Make this use abortTask() instead.
erroredTask(request, streamTask, reason);
@@ -1209,9 +1116,8 @@ function serializeAsyncIterable(
signal.removeEventListener('abort', abortIterable);
const reason = signal.reason;
if (enableHalt && request.type === PRERENDER) {
request.abortableTasks.delete(streamTask);
haltTask(streamTask, request);
finishHaltedTask(streamTask, request);
request.abortableTasks.delete(streamTask);
} else {
// TODO: Make this use abortTask() instead.
erroredTask(request, streamTask, signal.reason);
@@ -2182,10 +2088,7 @@ function visitAsyncNode(
// If the ioNode was a Promise, then that means we found one in user space since otherwise
// we would've returned an IO node. We assume this has the best stack.
match = ioNode;
} else if (
node.stack === null ||
!hasUnfilteredFrame(request, node.stack)
) {
} else if (filterStackTrace(request, node.stack).length === 0) {
// If this Promise was created inside only third party code, then try to use
// the inner I/O node instead. This could happen if third party calls into first
// party to perform some I/O.
@@ -2198,10 +2101,7 @@ function visitAsyncNode(
// We aborted this render. If this Promise spanned the abort time it was probably the
// Promise that was aborted. This won't necessarily have I/O associated with it but
// it's a point of interest.
if (
node.stack !== null &&
hasUnfilteredFrame(request, node.stack)
) {
if (filterStackTrace(request, node.stack).length > 0) {
match = node;
}
}
@@ -2247,10 +2147,35 @@ function visitAsyncNode(
// just part of a previous component's rendering.
match = ioNode;
} else {
if (
node.stack === null ||
!isAwaitInUserspace(request, node.stack)
let isAwaitInUserspace = false;
const fullStack = node.stack;
let firstFrame = 0;
while (
fullStack.length > firstFrame &&
fullStack[firstFrame][0] === 'Promise.then'
) {
// Skip Promise.then frame itself.
firstFrame++;
}
if (fullStack.length > firstFrame) {
// Check if the very first stack frame that awaited this Promise was in user space.
// TODO: This doesn't take into account wrapper functions such as our fake .then()
// in FlightClient which will always be considered third party awaits if you call
// .then directly.
const filterStackFrame = request.filterStackFrame;
const callsite = fullStack[firstFrame];
const functionName = callsite[0];
const url = devirtualizeURL(callsite[1]);
const lineNumber = callsite[2];
const columnNumber = callsite[3];
isAwaitInUserspace = filterStackFrame(
url,
functionName,
lineNumber,
columnNumber,
);
}
if (!isAwaitInUserspace) {
// If this await was fully filtered out, then it was inside third party code
// such as in an external library. We return the I/O node and try another await.
match = ioNode;
@@ -2279,10 +2204,7 @@ function visitAsyncNode(
awaited: ((ioNode: any): ReactIOInfo), // This is deduped by this reference.
env: env,
owner: node.owner,
stack:
node.stack === null
? null
: filterStackTrace(request, node.stack),
stack: filterStackTrace(request, node.stack),
});
// Mark the end time of the await. If we're aborting then we don't emit this
// to signal that this never resolved inside this render.
@@ -2970,9 +2892,7 @@ function serializeBlob(request: Request, blob: Blob): string {
signal.removeEventListener('abort', abortBlob);
const reason = signal.reason;
if (enableHalt && request.type === PRERENDER) {
request.abortableTasks.delete(newTask);
haltTask(newTask, request);
finishHaltedTask(newTask, request);
} else {
// TODO: Make this use abortTask() instead.
erroredTask(request, newTask, reason);
@@ -4128,6 +4048,10 @@ function emitIOInfoChunk(
start: relativeStartTimestamp,
end: relativeEndTimestamp,
};
if (value !== undefined) {
// $FlowFixMe[cannot-write]
debugIOInfo.value = value;
}
if (env != null) {
// $FlowFixMe[cannot-write]
debugIOInfo.env = env;
@@ -4140,10 +4064,6 @@ function emitIOInfoChunk(
// $FlowFixMe[cannot-write]
debugIOInfo.owner = owner;
}
if (value !== undefined) {
// $FlowFixMe[cannot-write]
debugIOInfo.value = value;
}
const json: string = serializeDebugModel(request, objectLimit, debugIOInfo);
const row = id.toString(16) + ':J' + json + '\n';
const processedChunk = stringToChunk(row);
@@ -4401,12 +4321,6 @@ function renderDebugModel(
if (parentReference !== undefined) {
// If the parent has a reference, we can refer to this object indirectly
// through the property name inside that parent.
if (counter.objectLimit <= 0 && !doNotLimit.has(value)) {
// If we are going to defer this, don't dedupe it since then we'd dedupe it to be
// deferred in future reference.
return serializeDeferredObject(request, value);
}
let propertyName = parentPropertyName;
if (isArray(parent) && parent[0] === REACT_ELEMENT_TYPE) {
// For elements, we've converted it to an array but we'll have converted
@@ -4431,15 +4345,8 @@ function renderDebugModel(
} else if (debugNoOutline !== value) {
// If this isn't the root object (like meta data) and we don't have an id for it, outline
// it so that we can dedupe it by reference later.
// $FlowFixMe[method-unbinding]
if (typeof value.then === 'function') {
// If this is a Promise we're going to assign it an external ID anyway which can be deduped.
const thenable: Thenable<any> = (value: any);
return serializeDebugThenable(request, counter, thenable);
} else {
const outlinedId = outlineDebugModel(request, counter, value);
return serializeByValueID(outlinedId);
}
const outlinedId = outlineDebugModel(request, counter, value);
return serializeByValueID(outlinedId);
}
}
@@ -4889,15 +4796,10 @@ function emitConsoleChunk(
const payload = [methodName, stackTrace, owner, env];
// $FlowFixMe[method-unbinding]
payload.push.apply(payload, args);
const objectLimit = request.deferredDebugObjects === null ? 500 : 10;
let json = serializeDebugModel(
request,
objectLimit + stackTrace.length,
payload,
);
let json = serializeDebugModel(request, 500, payload);
if (json[0] !== '[') {
// This looks like an error. Try a simpler object.
json = serializeDebugModel(request, 10 + stackTrace.length, [
json = serializeDebugModel(request, 500, [
methodName,
stackTrace,
owner,
@@ -5834,32 +5736,11 @@ export function resolveDebugMessage(request: Request, message: string): void {
if (retainedValue !== undefined) {
// If we still have this object, and haven't emitted it before, emit it on the stream.
const counter = {objectLimit: 10};
deferredDebugObjects.retained.delete(id);
deferredDebugObjects.existing.delete(retainedValue);
emitOutlinedDebugModelChunk(request, id, counter, retainedValue);
enqueueFlush(request);
}
}
break;
case 80 /* "P" */:
// Query Promise IDs
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
const retainedValue = deferredDebugObjects.retained.get(id);
if (retainedValue !== undefined) {
// If we still have this Promise, and haven't emitted it before, wait for it
// and then emit it on the stream.
const counter = {objectLimit: 10};
deferredDebugObjects.retained.delete(id);
emitRequestedDebugThenable(
request,
id,
counter,
(retainedValue: any),
);
}
}
break;
default:
throw new Error(
'Unknown command. The debugChannel was not wired up properly.',

View File

@@ -7,8 +7,6 @@
* @flow
*/
import type {ReactStackTrace} from 'shared/ReactTypes';
import type {
AsyncSequence,
IONode,
@@ -26,10 +24,9 @@ import {
UNRESOLVED_AWAIT_NODE,
} from './ReactFlightAsyncSequence';
import {resolveOwner} from './flight/ReactFlightCurrentOwner';
import {resolveRequest, isAwaitInUserspace} from './ReactFlightServer';
import {createHook, executionAsyncId, AsyncResource} from 'async_hooks';
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
import {parseStackTracePrivate} from './ReactFlightServerConfig';
import {parseStackTrace} from './ReactFlightServerConfig';
// $FlowFixMe[method-unbinding]
const getAsyncId = AsyncResource.prototype.asyncId;
@@ -37,6 +34,23 @@ const getAsyncId = AsyncResource.prototype.asyncId;
const pendingOperations: Map<number, AsyncSequence> =
__DEV__ && enableAsyncDebugInfo ? new Map() : (null: any);
// This is a weird one. This map, keeps a dependent Promise alive if the child Promise is still alive.
// A PromiseNode/AwaitNode cannot hold a strong reference to its own Promise because then it'll never get
// GC:ed. We only need it if a dependent AwaitNode points to it. We could put a reference in the Node
// but that would require a GC pass between every Node that gets destroyed. I.e. the root gets destroy()
// called on it and then that release it from the pendingOperations map which allows the next one to GC
// and so on. By putting this relationship in a WeakMap this could be done as a single pass in the VM.
// We don't actually ever have to read from this map since we have WeakRef reference to these Promises
// if they're still alive. It's also optional information so we could just expose only if GC didn't run.
const awaitedPromise: WeakMap<Promise<any>, Promise<any>> = __DEV__ &&
enableAsyncDebugInfo
? new WeakMap()
: (null: any);
const previousPromise: WeakMap<Promise<any>, Promise<any>> = __DEV__ &&
enableAsyncDebugInfo
? new WeakMap()
: (null: any);
// Keep the last resolved await as a workaround for async functions missing data.
let lastRanAwait: null | AwaitNode = null;
@@ -52,8 +66,6 @@ function resolvePromiseOrAwaitNode(
return resolvedNode;
}
const emptyStack: ReactStackTrace = [];
// Initialize the tracing of async operations.
// We do this globally since the async work can potentially eagerly
// start before the first request and once requests start they can interleave.
@@ -71,6 +83,14 @@ export function initAsyncDebugInfo(): void {
const trigger = pendingOperations.get(triggerAsyncId);
let node: AsyncSequence;
if (type === 'PROMISE') {
if (trigger !== undefined && trigger.promise !== null) {
const triggerPromise = trigger.promise.deref();
if (triggerPromise !== undefined) {
// Keep the awaited Promise alive as long as the child is alive so we can
// trace its value at the end.
awaitedPromise.set(resource, triggerPromise);
}
}
const currentAsyncId = executionAsyncId();
if (currentAsyncId !== triggerAsyncId) {
// When you call .then() on a native Promise, or await/Promise.all() a thenable,
@@ -79,58 +99,32 @@ export function initAsyncDebugInfo(): void {
// We don't track awaits on things that started outside our tracked scope.
return;
}
// If the thing we're waiting on is another Await we still track that sequence
// so that we can later pick the best stack trace in user space.
let stack = null;
let promiseRef: WeakRef<Promise<any>>;
if (
trigger.stack !== null &&
(trigger.tag === AWAIT_NODE ||
trigger.tag === UNRESOLVED_AWAIT_NODE)
) {
// We already had a stack for an await. In a chain of awaits we'll only need one good stack.
// We mark it with an empty stack to signal to any await on this await that we have a stack.
stack = emptyStack;
if (resource._debugInfo !== undefined) {
// We may need to forward this debug info at the end so we need to retain this promise.
promiseRef = new WeakRef((resource: Promise<any>));
} else {
// Otherwise, we can just refer to the inner one since that's the one we'll log anyway.
promiseRef = trigger.promise;
}
} else {
promiseRef = new WeakRef((resource: Promise<any>));
const request = resolveRequest();
if (request === null) {
// We don't collect stacks for awaits that weren't in the scope of a specific render.
} else {
stack = parseStackTracePrivate(new Error(), 5);
if (stack !== null && !isAwaitInUserspace(request, stack)) {
// If this await was not done directly in user space, then clear the stack. We won't use it
// anyway. This lets future awaits on this await know that we still need to get their stacks
// until we find one in user space.
stack = null;
}
const current = pendingOperations.get(currentAsyncId);
if (current !== undefined && current.promise !== null) {
const currentPromise = current.promise.deref();
if (currentPromise !== undefined) {
// Keep the previous Promise alive as long as the child is alive so we can
// trace its value at the end.
previousPromise.set(resource, currentPromise);
}
}
const current = pendingOperations.get(currentAsyncId);
// If the thing we're waiting on is another Await we still track that sequence
// so that we can later pick the best stack trace in user space.
node = ({
tag: UNRESOLVED_AWAIT_NODE,
owner: resolveOwner(),
stack: stack,
stack: parseStackTrace(new Error(), 5),
start: performance.now(),
end: -1.1, // set when resolved.
promise: promiseRef,
promise: new WeakRef((resource: Promise<any>)),
awaited: trigger, // The thing we're awaiting on. Might get overrriden when we resolve.
previous: current === undefined ? null : current, // The path that led us here.
}: UnresolvedAwaitNode);
} else {
const owner = resolveOwner();
node = ({
tag: UNRESOLVED_PROMISE_NODE,
owner: owner,
stack:
owner === null ? null : parseStackTracePrivate(new Error(), 5),
owner: resolveOwner(),
stack: parseStackTrace(new Error(), 5),
start: performance.now(),
end: -1.1, // Set when we resolve.
promise: new WeakRef((resource: Promise<any>)),
@@ -148,12 +142,10 @@ export function initAsyncDebugInfo(): void {
) {
if (trigger === undefined) {
// We have begun a new I/O sequence.
const owner = resolveOwner();
node = ({
tag: IO_NODE,
owner: owner,
stack:
owner === null ? parseStackTracePrivate(new Error(), 3) : null,
owner: resolveOwner(),
stack: parseStackTrace(new Error(), 3), // This is only used if no native promises are used.
start: performance.now(),
end: -1.1, // Only set when pinged.
promise: null,
@@ -165,12 +157,10 @@ export function initAsyncDebugInfo(): void {
trigger.tag === UNRESOLVED_AWAIT_NODE
) {
// We have begun a new I/O sequence after the await.
const owner = resolveOwner();
node = ({
tag: IO_NODE,
owner: owner,
stack:
owner === null ? parseStackTracePrivate(new Error(), 3) : null,
owner: resolveOwner(),
stack: parseStackTrace(new Error(), 3),
start: performance.now(),
end: -1.1, // Only set when pinged.
promise: null,

View File

@@ -49,7 +49,7 @@ function getMethodCallName(callSite: CallSite): string {
return result;
}
function collectStackTracePrivate(
function collectStackTrace(
error: Error,
structuredStackTrace: CallSite[],
): string {
@@ -63,9 +63,7 @@ function collectStackTracePrivate(
// Skip everything after the bottom frame since it'll be internals.
break;
} else if (callSite.isNative()) {
// $FlowFixMe[prop-missing]
const isAsync = callSite.isAsync();
result.push([name, '', 0, 0, 0, 0, isAsync]);
result.push([name, '', 0, 0, 0, 0]);
} else {
// We encode complex function calls as if they're part of the function
// name since we cannot simulate the complex ones and they look the same
@@ -81,11 +79,11 @@ function collectStackTracePrivate(
let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
if (filename === '<anonymous>') {
filename = '';
if (callSite.isEval()) {
const origin = callSite.getEvalOrigin();
if (origin) {
filename = origin.toString() + ', <anonymous>';
}
}
if (callSite.isEval() && !filename) {
const origin = callSite.getEvalOrigin();
if (origin) {
filename = origin.toString() + ', <anonymous>';
}
}
const line = callSite.getLineNumber() || 0;
@@ -100,28 +98,9 @@ function collectStackTracePrivate(
typeof callSite.getEnclosingColumnNumber === 'function'
? (callSite: any).getEnclosingColumnNumber() || 0
: 0;
// $FlowFixMe[prop-missing]
const isAsync = callSite.isAsync();
result.push([
name,
filename,
line,
col,
enclosingLine,
enclosingCol,
isAsync,
]);
result.push([name, filename, line, col, enclosingLine, enclosingCol]);
}
}
collectedStackTrace = result;
return '';
}
function collectStackTrace(
error: Error,
structuredStackTrace: CallSite[],
): string {
collectStackTracePrivate(error, structuredStackTrace);
// At the same time we generate a string stack trace just in case someone
// else reads it. Ideally, we'd call the previous prepareStackTrace to
// ensure it's in the expected format but it's common for that to be
@@ -136,6 +115,7 @@ function collectStackTrace(
for (let i = 0; i < structuredStackTrace.length; i++) {
stack += '\n at ' + structuredStackTrace[i].toString();
}
collectedStackTrace = result;
return stack;
}
@@ -151,26 +131,6 @@ const stackTraceCache: WeakMap<Error, ReactStackTrace> = __DEV__
? new WeakMap()
: (null: any);
// This version is only used when React fully owns the Error object and there's no risk of it having
// been already initialized and no risky that anyone else will initialize it later.
export function parseStackTracePrivate(
error: Error,
skipFrames: number,
): null | ReactStackTrace {
collectedStackTrace = null;
framesToSkip = skipFrames;
const previousPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = collectStackTracePrivate;
try {
if (error.stack !== '') {
return null;
}
} finally {
Error.prepareStackTrace = previousPrepare;
}
return collectedStackTrace;
}
export function parseStackTrace(
error: Error,
skipFrames: number,
@@ -233,12 +193,8 @@ export function parseStackTrace(
continue;
}
let name = parsed[1] || '';
let isAsync = parsed[8] === 'async ';
if (name === '<anonymous>') {
name = '';
} else if (name.startsWith('async ')) {
name = name.slice(5);
isAsync = true;
}
let filename = parsed[2] || parsed[5] || '';
if (filename === '<anonymous>') {
@@ -246,7 +202,7 @@ export function parseStackTrace(
}
const line = +(parsed[3] || parsed[6]);
const col = +(parsed[4] || parsed[7]);
parsedFrames.push([name, filename, line, col, 0, 0, isAsync]);
parsedFrames.push([name, filename, line, col, 0, 0]);
}
stackTraceCache.set(error, parsedFrames);
return parsedFrames;

File diff suppressed because it is too large Load Diff

View File

@@ -188,7 +188,6 @@ export type ReactCallSite = [
number, // column number
number, // enclosing line number
number, // enclosing column number
boolean, // async resume
];
export type ReactStackTrace = Array<ReactCallSite>;