Skip to content

Commit fa5159a

Browse files
committed
Mark await end time when they begin
promiseResolve, for already resolved promises, is invoked at the end of its execution phase. That means that any awaits that happen inside would have their start time below the end time which cuts them off.
1 parent 3d36d3f commit fa5159a

File tree

3 files changed

+188
-137
lines changed

3 files changed

+188
-137
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,10 +1933,6 @@ function visitAsyncNode(
19331933
// If this Promise was created inside only third party code, then try to use
19341934
// the inner I/O node instead. This could happen if third party calls into first
19351935
// party to perform some I/O.
1936-
if (ioNode.end < 0) {
1937-
// If we haven't defined an end time, use the resolve of the outer Promise.
1938-
ioNode.end = node.end;
1939-
}
19401936
match = ioNode;
19411937
} else {
19421938
match = node;
@@ -1951,30 +1947,17 @@ function visitAsyncNode(
19511947
}
19521948
return match;
19531949
}
1954-
case UNRESOLVED_AWAIT_NODE:
1955-
// We could be inside the .then() which is about to resolve this node.
1956-
// TODO: We could call emitAsyncSequence in a microtask to avoid this issue.
1957-
// Fallthrough to the resolved path.
1950+
case UNRESOLVED_AWAIT_NODE: {
1951+
return null;
1952+
}
19581953
case AWAIT_NODE: {
19591954
const awaited = node.awaited;
19601955
let match = null;
19611956
if (awaited !== null) {
19621957
const ioNode = visitAsyncNode(request, task, awaited, visited);
19631958
if (ioNode !== null) {
19641959
const startTime: number = node.start;
1965-
let endTime: number;
1966-
if (node.tag === UNRESOLVED_AWAIT_NODE) {
1967-
// If we haven't defined an end time, use the resolve of the inner Promise.
1968-
// This can happen because the ping gets invoked before the await gets resolved.
1969-
if (ioNode.end < node.start) {
1970-
// If we're awaiting a resolved Promise it could have finished before we started.
1971-
endTime = node.start;
1972-
} else {
1973-
endTime = ioNode.end;
1974-
}
1975-
} else {
1976-
endTime = node.end;
1977-
}
1960+
const endTime: number = node.end;
19781961
if (endTime <= request.timeOrigin) {
19791962
// This was already resolved when we started this render. It must have been either something
19801963
// that's part of a start up sequence or externally cached data. We exclude that information.
@@ -1998,10 +1981,8 @@ function visitAsyncNode(
19981981
match = ioNode;
19991982
} else {
20001983
// Outline the IO node.
2001-
if (ioNode.end < 0) {
2002-
ioNode.end = endTime;
2003-
}
20041984
serializeIONode(request, ioNode);
1985+
20051986
// We log the environment at the time when the last promise pigned ping which may
20061987
// be later than what the environment was when we actually started awaiting.
20071988
const env = (0, request.environmentName)();
@@ -2021,16 +2002,7 @@ function visitAsyncNode(
20212002
}
20222003
// We need to forward after we visit awaited nodes because what ever I/O we requested that's
20232004
// the thing that generated this node and its virtual children.
2024-
let debugInfo: null | ReactDebugInfo;
2025-
if (node.tag === UNRESOLVED_AWAIT_NODE) {
2026-
const promise = node.debugInfo.deref();
2027-
debugInfo =
2028-
promise === undefined || promise._debugInfo === undefined
2029-
? null
2030-
: promise._debugInfo;
2031-
} else {
2032-
debugInfo = node.debugInfo;
2033-
}
2005+
const debugInfo: null | ReactDebugInfo = node.debugInfo;
20342006
if (debugInfo !== null) {
20352007
forwardDebugInfo(request, task, debugInfo);
20362008
}
@@ -2052,14 +2024,6 @@ function emitAsyncSequence(
20522024
const awaitedNode = visitAsyncNode(request, task, node, visited);
20532025
if (awaitedNode !== null) {
20542026
// Nothing in user space (unfiltered stack) awaited this.
2055-
if (awaitedNode.end < 0) {
2056-
// If this was I/O directly without a Promise, then it means that some custom Thenable
2057-
// called our ping directly and not from a native .then(). We use the current ping time
2058-
// as the end time and treat it as an await with no stack.
2059-
// TODO: If this I/O is recurring then we really should have different entries for
2060-
// each occurrence. Right now we'll only track the first time it is invoked.
2061-
awaitedNode.end = performance.now();
2062-
}
20632027
serializeIONode(request, awaitedNode);
20642028
request.pendingChunks++;
20652029
// We log the environment at the time when we ping which may be later than what the

packages/react-server/src/ReactFlightServerConfigDebugNode.js

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
3030
const pendingOperations: Map<number, AsyncSequence> =
3131
__DEV__ && enableAsyncDebugInfo ? new Map() : (null: any);
3232

33+
function resolvePromiseOrAwaitNode(
34+
unresolvedNode: UnresolvedAwaitNode | UnresolvedPromiseNode,
35+
endTime: number,
36+
): AwaitNode | PromiseNode {
37+
const resolvedNode: AwaitNode | PromiseNode = (unresolvedNode: any);
38+
resolvedNode.tag = ((unresolvedNode.tag === UNRESOLVED_PROMISE_NODE
39+
? PROMISE_NODE
40+
: AWAIT_NODE): any);
41+
// The Promise can be garbage collected after this so we should extract debugInfo first.
42+
const promise = unresolvedNode.debugInfo.deref();
43+
resolvedNode.debugInfo =
44+
promise === undefined || promise._debugInfo === undefined
45+
? null
46+
: promise._debugInfo;
47+
resolvedNode.end = endTime;
48+
return resolvedNode;
49+
}
50+
3351
// Initialize the tracing of async operations.
3452
// We do this globally since the async work can potentially eagerly
3553
// start before the first request and once requests start they can interleave.
@@ -129,42 +147,50 @@ export function initAsyncDebugInfo(): void {
129147
}
130148
pendingOperations.set(asyncId, node);
131149
},
150+
before(asyncId: number): void {
151+
const node = pendingOperations.get(asyncId);
152+
if (node !== undefined) {
153+
switch (node.tag) {
154+
case IO_NODE: {
155+
// Log the end time when we resolved the I/O. This can happen
156+
// more than once if it's a recurring resource like a connection.
157+
const ioNode: IONode = (node: any);
158+
ioNode.end = performance.now();
159+
break;
160+
}
161+
case UNRESOLVED_AWAIT_NODE:
162+
case UNRESOLVED_PROMISE_NODE: {
163+
// If we begin before we resolve, that means that this is actually already resolved but
164+
// the promiseResolve hook is called at the end of the execution. This means that it was
165+
// actually already resolved when we started so we just use the start time as the end time.
166+
resolvePromiseOrAwaitNode(node, node.start);
167+
break;
168+
}
169+
}
170+
}
171+
},
132172
promiseResolve(asyncId: number): void {
133173
const node = pendingOperations.get(asyncId);
134174
if (node !== undefined) {
135175
let resolvedNode: AwaitNode | PromiseNode;
136176
switch (node.tag) {
137-
case UNRESOLVED_AWAIT_NODE: {
138-
const awaitNode: AwaitNode = (node: any);
139-
awaitNode.tag = AWAIT_NODE;
140-
resolvedNode = awaitNode;
177+
case UNRESOLVED_AWAIT_NODE:
178+
case UNRESOLVED_PROMISE_NODE: {
179+
resolvedNode = resolvePromiseOrAwaitNode(node, performance.now());
141180
break;
142181
}
143-
case UNRESOLVED_PROMISE_NODE: {
144-
const promiseNode: PromiseNode = (node: any);
145-
promiseNode.tag = PROMISE_NODE;
146-
resolvedNode = promiseNode;
182+
case AWAIT_NODE:
183+
case PROMISE_NODE: {
184+
resolvedNode = node;
185+
// We already resolved this in the begin phase.
147186
break;
148187
}
149-
case IO_NODE:
150-
// eslint-disable-next-line react-internal/prod-error-codes
151-
throw new Error(
152-
'A Promise should never be an IO_NODE. This is a bug in React.',
153-
);
154188
default:
155189
// eslint-disable-next-line react-internal/prod-error-codes
156190
throw new Error(
157-
'A Promise should never be resolved twice. This is a bug in React or Node.js.',
191+
'A Promise should never be an IO_NODE. This is a bug in React.',
158192
);
159193
}
160-
// Log the end time when we resolved the promise.
161-
resolvedNode.end = performance.now();
162-
// The Promise can be garbage collected after this so we should extract debugInfo first.
163-
const promise = node.debugInfo.deref();
164-
resolvedNode.debugInfo =
165-
promise === undefined || promise._debugInfo === undefined
166-
? null
167-
: promise._debugInfo;
168194
const currentAsyncId = executionAsyncId();
169195
if (asyncId !== currentAsyncId) {
170196
// If the promise was not resolved by itself, then that means that

0 commit comments

Comments
 (0)