mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
[Flight] Optimize Async Stack Collection (#33727)
We need to optimize the collection of debug info for dev mode. This is an incredibly hot path since it instruments all I/O and Promises in the app. These optimizations focus primarily on the collection of stack traces. They are expensive to collect because we need to eagerly collect the stacks since they can otherwise cause memory leaks. We also need to do some of the processing of them up front. We also end up only using a few of them in the end but we don't know which ones we'll use. The first compromise here is that I now only collect the stacks of "awaits" if they were in a specific request's render. In some cases it's useful to collect them even outside of this if they're part of a sequence that started early. I still collect stacks for the created Promises outside of this though which can still provide some context. The other optimization to awaits, is that since we'll only use the inner most one that had an await directly in userspace, we can stop collecting stacks on a chain of awaits after we find one. This requires a quick filter on a single callsite to determine. Since we now only collect stacks from awaits that belongs to a specific Request we can use that request's specific filter option. Technically this might not be quite correct if that same thing ends up deduped across Requests but that's an edge case. Additionally, I now stop collecting stack for I/O nodes. They're almost always superseded by the Promise that wraps them anyway. Even if you write mostly Promise free code, you'll likely end up with a Promise at the root of the component eventually anyway and then you end up using its stack anyway. You have to really contort the code to end up with zero Promises at which point it's not very useful anyway. At best it's maybe mostly useful for giving a name to the I/O when the rest is just stuff like `new Promise`. However, a possible alternative optimization could be to *only* collect the stack of spawned I/O and not the stack of Promises. The issue with Promises (not awaits) is that we never know what will end up resolving them in the end when they're created so we have to always eagerly collect stacks. This could be an issue when you have a lot of abstractions that end up not actually be related to I/O at all. The issue with collecting stacks only for I/O is that the actual I/O can be pooled or batched so you end up not having the stack when the conceptual start of each operation within the batch started. Which is why I decided to keep the Promise stack.
This commit is contained in:
parent
b44a99bf58
commit
f1ecf82bfb
|
|
@ -26,7 +26,7 @@ type PromiseWithDebugInfo = interface extends Promise<any> {
|
|||
export type IONode = {
|
||||
tag: 0,
|
||||
owner: null | ReactComponentInfo,
|
||||
stack: ReactStackTrace, // callsite that spawned the I/O
|
||||
stack: null, // 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: ReactStackTrace, // callsite that created the Promise
|
||||
stack: null | 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: ReactStackTrace, // callsite that awaited (using await, .then(), Promise.all(), ...)
|
||||
stack: null | 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: ReactStackTrace, // callsite that created the Promise
|
||||
stack: null | 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: ReactStackTrace, // callsite that awaited (using await, .then(), Promise.all(), ...)
|
||||
stack: null | 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
|
||||
|
|
|
|||
86
packages/react-server/src/ReactFlightServer.js
vendored
86
packages/react-server/src/ReactFlightServer.js
vendored
|
|
@ -251,6 +251,46 @@ 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];
|
||||
if (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) {
|
||||
|
|
@ -2088,7 +2128,10 @@ 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 (filterStackTrace(request, node.stack).length === 0) {
|
||||
} else if (
|
||||
node.stack === null ||
|
||||
!hasUnfilteredFrame(request, node.stack)
|
||||
) {
|
||||
// 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.
|
||||
|
|
@ -2101,7 +2144,10 @@ 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 (filterStackTrace(request, node.stack).length > 0) {
|
||||
if (
|
||||
node.stack !== null &&
|
||||
hasUnfilteredFrame(request, node.stack)
|
||||
) {
|
||||
match = node;
|
||||
}
|
||||
}
|
||||
|
|
@ -2147,35 +2193,10 @@ function visitAsyncNode(
|
|||
// just part of a previous component's rendering.
|
||||
match = ioNode;
|
||||
} else {
|
||||
let isAwaitInUserspace = false;
|
||||
const fullStack = node.stack;
|
||||
let firstFrame = 0;
|
||||
while (
|
||||
fullStack.length > firstFrame &&
|
||||
fullStack[firstFrame][0] === 'Promise.then'
|
||||
if (
|
||||
node.stack === null ||
|
||||
!isAwaitInUserspace(request, node.stack)
|
||||
) {
|
||||
// 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;
|
||||
|
|
@ -2204,7 +2225,10 @@ function visitAsyncNode(
|
|||
awaited: ((ioNode: any): ReactIOInfo), // This is deduped by this reference.
|
||||
env: env,
|
||||
owner: node.owner,
|
||||
stack: filterStackTrace(request, node.stack),
|
||||
stack:
|
||||
node.stack === null
|
||||
? null
|
||||
: 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.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactStackTrace} from 'shared/ReactTypes';
|
||||
|
||||
import type {
|
||||
AsyncSequence,
|
||||
IONode,
|
||||
|
|
@ -24,6 +26,7 @@ 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 {parseStackTrace} from './ReactFlightServerConfig';
|
||||
|
|
@ -66,6 +69,8 @@ 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.
|
||||
|
|
@ -110,10 +115,33 @@ export function initAsyncDebugInfo(): void {
|
|||
}
|
||||
// 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;
|
||||
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;
|
||||
} else {
|
||||
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 = parseStackTrace(new Error(), 5);
|
||||
if (!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;
|
||||
}
|
||||
}
|
||||
}
|
||||
node = ({
|
||||
tag: UNRESOLVED_AWAIT_NODE,
|
||||
owner: resolveOwner(),
|
||||
stack: parseStackTrace(new Error(), 5),
|
||||
stack: stack,
|
||||
start: performance.now(),
|
||||
end: -1.1, // set when resolved.
|
||||
promise: new WeakRef((resource: Promise<any>)),
|
||||
|
|
@ -145,7 +173,7 @@ export function initAsyncDebugInfo(): void {
|
|||
node = ({
|
||||
tag: IO_NODE,
|
||||
owner: resolveOwner(),
|
||||
stack: parseStackTrace(new Error(), 3), // This is only used if no native promises are used.
|
||||
stack: null,
|
||||
start: performance.now(),
|
||||
end: -1.1, // Only set when pinged.
|
||||
promise: null,
|
||||
|
|
@ -160,7 +188,7 @@ export function initAsyncDebugInfo(): void {
|
|||
node = ({
|
||||
tag: IO_NODE,
|
||||
owner: resolveOwner(),
|
||||
stack: parseStackTrace(new Error(), 3),
|
||||
stack: null,
|
||||
start: performance.now(),
|
||||
end: -1.1, // Only set when pinged.
|
||||
promise: null,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user