mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[Flight] Send the awaited Promise to the client as additional debug information (#33592)
Stacked on #33588, #33589 and #33590. This lets us automatically show the resolved value in the UI. <img width="863" alt="Screenshot 2025-06-22 at 12 54 41 AM" src="https://github.com/user-attachments/assets/a66d1d5e-0513-4767-910c-5c7169fc2df4" /> We can also show rejected I/O that may or may not have been handled with the error message. <img width="838" alt="Screenshot 2025-06-22 at 12 55 06 AM" src="https://github.com/user-attachments/assets/e0a8b6ae-08ba-46d8-8cc5-efb60956a1d1" /> To get this working we need to keep the Promise around for longer so that we can access it once we want to emit an async sequence. I do this by storing the WeakRefs but to ensure that the Promise doesn't get garbage collected, I keep a WeakMap of Promise to the Promise that it depended on. This lets the VM still clean up any Promise chains that have leaves that are cleaned up. So this makes Promises live until the last Promise downstream is done. At that point we can go back up the chain to read the values out of them. Additionally, to get the best possible value we don't want to get a Promise that's used by internals of a third-party function. We want the value that the first party gets to observe. To do this I had to change the logic for which "await" to use, to be the one that is the first await that happened in user space. It's not enough that the await has any first party at all on the stack - it has to be the very first frame. This is a little sketchy because it relies on the `.then()` call or `await` call not having any third party wrappers. But it gives the best object since it hides all the internals. For example when you call `fetch()` we now log that actual `Response` object.
This commit is contained in:
parent
18ee505e77
commit
2a911f27dd
|
|
@ -33,12 +33,22 @@ function Foo({children}) {
|
|||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
async function delayedError(text, ms) {
|
||||
return new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(text)), ms)
|
||||
);
|
||||
}
|
||||
|
||||
async function delay(text, ms) {
|
||||
return new Promise(resolve => setTimeout(() => resolve(text), ms));
|
||||
}
|
||||
|
||||
async function delayTwice() {
|
||||
await delay('', 20);
|
||||
try {
|
||||
await delayedError('Delayed exception', 20);
|
||||
} catch (x) {
|
||||
// Ignored
|
||||
}
|
||||
await delay('', 10);
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +123,7 @@ async function ServerComponent({noCache}) {
|
|||
export default async function App({prerender, noCache}) {
|
||||
const res = await fetch('http://localhost:3001/todos');
|
||||
const todos = await res.json();
|
||||
console.log(res);
|
||||
|
||||
const dedupedChild = <ServerComponent noCache={noCache} />;
|
||||
const message = getServerState();
|
||||
|
|
|
|||
90
packages/react-client/src/ReactFlightClient.js
vendored
90
packages/react-client/src/ReactFlightClient.js
vendored
|
|
@ -79,7 +79,9 @@ import {
|
|||
logDedupedComponentRender,
|
||||
logComponentErrored,
|
||||
logIOInfo,
|
||||
logIOInfoErrored,
|
||||
logComponentAwait,
|
||||
logComponentAwaitErrored,
|
||||
} from './ReactFlightPerformanceTrack';
|
||||
|
||||
import {
|
||||
|
|
@ -96,6 +98,8 @@ import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack'
|
|||
|
||||
import {injectInternals} from './ReactFlightClientDevToolsHook';
|
||||
|
||||
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
|
||||
|
||||
import ReactVersion from 'shared/ReactVersion';
|
||||
|
||||
import isArray from 'shared/isArray';
|
||||
|
|
@ -1684,11 +1688,7 @@ function parseModelString(
|
|||
Object.defineProperty(parentObject, key, {
|
||||
get: function () {
|
||||
// TODO: We should ideally throw here to indicate a difference.
|
||||
return (
|
||||
'This object has been omitted by React in the console log ' +
|
||||
'to avoid sending too much data from the server. Try logging smaller ' +
|
||||
'or more specific objects.'
|
||||
);
|
||||
return OMITTED_PROP_ERROR;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
|
|
@ -2909,7 +2909,29 @@ function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
|
|||
// $FlowFixMe[cannot-write]
|
||||
ioInfo.end += response._timeOrigin;
|
||||
|
||||
logIOInfo(ioInfo, response._rootEnvironmentName);
|
||||
const env = response._rootEnvironmentName;
|
||||
const promise = ioInfo.value;
|
||||
if (promise) {
|
||||
const thenable: Thenable<mixed> = (promise: any);
|
||||
switch (thenable.status) {
|
||||
case INITIALIZED:
|
||||
logIOInfo(ioInfo, env, thenable.value);
|
||||
break;
|
||||
case ERRORED:
|
||||
logIOInfoErrored(ioInfo, env, thenable.reason);
|
||||
break;
|
||||
default:
|
||||
// If we haven't resolved the Promise yet, wait to log until have so we can include
|
||||
// its data in the log.
|
||||
promise.then(
|
||||
logIOInfo.bind(null, ioInfo, env),
|
||||
logIOInfoErrored.bind(null, ioInfo, env),
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logIOInfo(ioInfo, env, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveIOInfo(
|
||||
|
|
@ -3193,13 +3215,55 @@ function flushComponentPerformance(
|
|||
}
|
||||
// $FlowFixMe: Refined.
|
||||
const asyncInfo: ReactAsyncInfo = candidateInfo;
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
response._rootEnvironmentName,
|
||||
);
|
||||
const env = response._rootEnvironmentName;
|
||||
const promise = asyncInfo.awaited.value;
|
||||
if (promise) {
|
||||
const thenable: Thenable<mixed> = (promise: any);
|
||||
switch (thenable.status) {
|
||||
case INITIALIZED:
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
thenable.value,
|
||||
);
|
||||
break;
|
||||
case ERRORED:
|
||||
logComponentAwaitErrored(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
thenable.reason,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// We assume that we should have received the data by now since this is logged at the
|
||||
// end of the response stream. This is more sensitive to ordering so we don't wait
|
||||
// to log it.
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
undefined,
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,143 @@ import type {
|
|||
|
||||
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
|
||||
|
||||
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
import isArray from 'shared/isArray';
|
||||
|
||||
const supportsUserTiming =
|
||||
enableProfilerTimer &&
|
||||
typeof console !== 'undefined' &&
|
||||
typeof console.timeStamp === 'function';
|
||||
typeof console.timeStamp === 'function' &&
|
||||
typeof performance !== 'undefined' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof performance.measure === 'function';
|
||||
|
||||
const IO_TRACK = 'Server Requests ⚛';
|
||||
const COMPONENTS_TRACK = 'Server Components ⚛';
|
||||
|
||||
const EMPTY_ARRAY = 0;
|
||||
const COMPLEX_ARRAY = 1;
|
||||
const PRIMITIVE_ARRAY = 2; // Primitive values only
|
||||
const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
|
||||
function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
|
||||
let kind = EMPTY_ARRAY;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const value = array[i];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (
|
||||
isArray(value) &&
|
||||
value.length === 2 &&
|
||||
typeof value[0] === 'string'
|
||||
) {
|
||||
// Key value tuple
|
||||
if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
|
||||
return COMPLEX_ARRAY;
|
||||
}
|
||||
kind = ENTRIES_ARRAY;
|
||||
} else {
|
||||
return COMPLEX_ARRAY;
|
||||
}
|
||||
} else if (typeof value === 'function') {
|
||||
return COMPLEX_ARRAY;
|
||||
} else if (typeof value === 'string' && value.length > 50) {
|
||||
return COMPLEX_ARRAY;
|
||||
} else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
|
||||
return COMPLEX_ARRAY;
|
||||
} else {
|
||||
kind = PRIMITIVE_ARRAY;
|
||||
}
|
||||
}
|
||||
return kind;
|
||||
}
|
||||
|
||||
function addObjectToProperties(
|
||||
object: Object,
|
||||
properties: Array<[string, string]>,
|
||||
indent: number,
|
||||
): void {
|
||||
for (const key in object) {
|
||||
if (hasOwnProperty.call(object, key) && key[0] !== '_') {
|
||||
const value = object[key];
|
||||
addValueToProperties(key, value, properties, indent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addValueToProperties(
|
||||
propertyName: string,
|
||||
value: mixed,
|
||||
properties: Array<[string, string]>,
|
||||
indent: number,
|
||||
): void {
|
||||
let desc;
|
||||
switch (typeof value) {
|
||||
case 'object':
|
||||
if (value === null) {
|
||||
desc = 'null';
|
||||
break;
|
||||
} else {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const objectToString = Object.prototype.toString.call(value);
|
||||
let objectName = objectToString.slice(8, objectToString.length - 1);
|
||||
if (objectName === 'Array') {
|
||||
const array: Array<any> = (value: any);
|
||||
const kind = getArrayKind(array);
|
||||
if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
|
||||
desc = JSON.stringify(array);
|
||||
break;
|
||||
} else if (kind === ENTRIES_ARRAY) {
|
||||
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, '']);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const entry = array[i];
|
||||
addValueToProperties(entry[0], entry[1], properties, indent + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (objectName === 'Object') {
|
||||
const proto: any = Object.getPrototypeOf(value);
|
||||
if (proto && typeof proto.constructor === 'function') {
|
||||
objectName = proto.constructor.name;
|
||||
}
|
||||
}
|
||||
properties.push([
|
||||
'\xa0\xa0'.repeat(indent) + propertyName,
|
||||
objectName === 'Object' ? '' : objectName,
|
||||
]);
|
||||
if (indent < 3) {
|
||||
addObjectToProperties(value, properties, indent + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'function':
|
||||
if (value.name === '') {
|
||||
desc = '() => {}';
|
||||
} else {
|
||||
desc = value.name + '() {}';
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
if (value === OMITTED_PROP_ERROR) {
|
||||
desc = '...';
|
||||
} else {
|
||||
desc = JSON.stringify(value);
|
||||
}
|
||||
break;
|
||||
case 'undefined':
|
||||
desc = 'undefined';
|
||||
break;
|
||||
case 'boolean':
|
||||
desc = value ? 'true' : 'false';
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
desc = String(value);
|
||||
}
|
||||
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, desc]);
|
||||
}
|
||||
|
||||
export function markAllTracksInOrder() {
|
||||
if (supportsUserTiming) {
|
||||
// Ensure we create the Server Component track groups earlier than the Client Scheduler
|
||||
|
|
@ -133,12 +262,7 @@ export function logComponentErrored(
|
|||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
||||
if (
|
||||
__DEV__ &&
|
||||
typeof performance !== 'undefined' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof performance.measure === 'function'
|
||||
) {
|
||||
if (__DEV__) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
|
|
@ -228,12 +352,68 @@ function getIOColor(
|
|||
}
|
||||
}
|
||||
|
||||
export function logComponentAwaitErrored(
|
||||
asyncInfo: ReactAsyncInfo,
|
||||
trackIdx: number,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
rootEnv: string,
|
||||
error: mixed,
|
||||
): void {
|
||||
if (supportsUserTiming && endTime > 0) {
|
||||
const env = asyncInfo.env;
|
||||
const name = asyncInfo.awaited.name;
|
||||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
'await ' +
|
||||
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
|
||||
const debugTask = asyncInfo.debugTask;
|
||||
if (__DEV__ && debugTask) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
typeof error.message === 'string'
|
||||
? // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error.message)
|
||||
: // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error);
|
||||
const properties = [['Rejected', message]];
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: 'error',
|
||||
track: trackNames[trackIdx],
|
||||
trackGroup: COMPONENTS_TRACK,
|
||||
properties,
|
||||
tooltipText: entryName + ' Rejected',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
trackNames[trackIdx],
|
||||
COMPONENTS_TRACK,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logComponentAwait(
|
||||
asyncInfo: ReactAsyncInfo,
|
||||
trackIdx: number,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
rootEnv: string,
|
||||
value: mixed,
|
||||
): void {
|
||||
if (supportsUserTiming && endTime > 0) {
|
||||
const env = asyncInfo.env;
|
||||
|
|
@ -245,17 +425,26 @@ export function logComponentAwait(
|
|||
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
|
||||
const debugTask = asyncInfo.debugTask;
|
||||
if (__DEV__ && debugTask) {
|
||||
const properties: Array<[string, string]> = [];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
addObjectToProperties(value, properties, 0);
|
||||
} else if (value !== undefined) {
|
||||
addValueToProperties('Resolved', value, properties, 0);
|
||||
}
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
console.timeStamp.bind(
|
||||
console,
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
trackNames[trackIdx],
|
||||
COMPONENTS_TRACK,
|
||||
color,
|
||||
),
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: color,
|
||||
track: trackNames[trackIdx],
|
||||
trackGroup: COMPONENTS_TRACK,
|
||||
properties,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
|
|
@ -270,7 +459,63 @@ export function logComponentAwait(
|
|||
}
|
||||
}
|
||||
|
||||
export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void {
|
||||
export function logIOInfoErrored(
|
||||
ioInfo: ReactIOInfo,
|
||||
rootEnv: string,
|
||||
error: mixed,
|
||||
): void {
|
||||
const startTime = ioInfo.start;
|
||||
const endTime = ioInfo.end;
|
||||
if (supportsUserTiming && endTime >= 0) {
|
||||
const name = ioInfo.name;
|
||||
const env = ioInfo.env;
|
||||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
||||
const debugTask = ioInfo.debugTask;
|
||||
if (__DEV__ && debugTask) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
typeof error.message === 'string'
|
||||
? // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error.message)
|
||||
: // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error);
|
||||
const properties = [['Rejected', message]];
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: 'error',
|
||||
track: IO_TRACK,
|
||||
properties,
|
||||
tooltipText: entryName + ' Rejected',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
IO_TRACK,
|
||||
undefined,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logIOInfo(
|
||||
ioInfo: ReactIOInfo,
|
||||
rootEnv: string,
|
||||
value: mixed,
|
||||
): void {
|
||||
const startTime = ioInfo.start;
|
||||
const endTime = ioInfo.end;
|
||||
if (supportsUserTiming && endTime >= 0) {
|
||||
|
|
@ -282,17 +527,25 @@ export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void {
|
|||
const debugTask = ioInfo.debugTask;
|
||||
const color = getIOColor(name);
|
||||
if (__DEV__ && debugTask) {
|
||||
const properties: Array<[string, string]> = [];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
addObjectToProperties(value, properties, 0);
|
||||
} else if (value !== undefined) {
|
||||
addValueToProperties('Resolved', value, properties, 0);
|
||||
}
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
console.timeStamp.bind(
|
||||
console,
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
IO_TRACK,
|
||||
undefined,
|
||||
color,
|
||||
),
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: color,
|
||||
track: IO_TRACK,
|
||||
properties,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
|
|
|
|||
13
packages/react-client/src/ReactFlightPropertyAccess.js
vendored
Normal file
13
packages/react-client/src/ReactFlightPropertyAccess.js
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export const OMITTED_PROP_ERROR =
|
||||
'This object has been omitted by React in the console log ' +
|
||||
'to avoid sending too much data from the server. Try logging smaller ' +
|
||||
'or more specific objects.';
|
||||
|
|
@ -27,9 +27,9 @@ export type IONode = {
|
|||
tag: 0,
|
||||
owner: null | ReactComponentInfo,
|
||||
stack: ReactStackTrace, // callsite that spawned the I/O
|
||||
debugInfo: null, // not used on 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
|
||||
awaited: null, // I/O is only blocked on external.
|
||||
previous: null | AwaitNode | UnresolvedAwaitNode, // the preceeding await that spawned this new work
|
||||
};
|
||||
|
|
@ -37,10 +37,10 @@ export type IONode = {
|
|||
export type PromiseNode = {
|
||||
tag: 1,
|
||||
owner: null | ReactComponentInfo,
|
||||
debugInfo: null | ReactDebugInfo, // forwarded debugInfo from 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
|
||||
awaited: null | AsyncSequence, // the thing that ended up resolving this promise
|
||||
previous: null | AsyncSequence, // represents what the last return of an async function depended on before returning
|
||||
};
|
||||
|
|
@ -48,10 +48,10 @@ export type PromiseNode = {
|
|||
export type AwaitNode = {
|
||||
tag: 2,
|
||||
owner: null | ReactComponentInfo,
|
||||
debugInfo: null | ReactDebugInfo, // forwarded debugInfo from the Promise
|
||||
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
|
||||
awaited: null | AsyncSequence, // the promise we were waiting on
|
||||
previous: null | AsyncSequence, // the sequence that was blocking us from awaiting in the first place
|
||||
};
|
||||
|
|
@ -59,10 +59,10 @@ export type AwaitNode = {
|
|||
export type UnresolvedPromiseNode = {
|
||||
tag: 3,
|
||||
owner: null | ReactComponentInfo,
|
||||
debugInfo: WeakRef<PromiseWithDebugInfo>, // holds onto the Promise until we can extract debugInfo when it resolves
|
||||
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
|
||||
awaited: null | AsyncSequence, // the thing that ended up resolving this promise
|
||||
previous: null, // where we created the promise is not interesting since creating it doesn't mean waiting.
|
||||
};
|
||||
|
|
@ -70,10 +70,10 @@ export type UnresolvedPromiseNode = {
|
|||
export type UnresolvedAwaitNode = {
|
||||
tag: 4,
|
||||
owner: null | ReactComponentInfo,
|
||||
debugInfo: WeakRef<PromiseWithDebugInfo>, // holds onto the Promise until we can extract debugInfo when it resolves
|
||||
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
|
||||
awaited: null | AsyncSequence, // the promise we were waiting on
|
||||
previous: null | AsyncSequence, // the sequence that was blocking us from awaiting in the first place
|
||||
};
|
||||
|
|
|
|||
63
packages/react-server/src/ReactFlightServer.js
vendored
63
packages/react-server/src/ReactFlightServer.js
vendored
|
|
@ -2053,10 +2053,13 @@ function visitAsyncNode(
|
|||
}
|
||||
// We need to forward after we visit awaited nodes because what ever I/O we requested that's
|
||||
// the thing that generated this node and its virtual children.
|
||||
const debugInfo = node.debugInfo;
|
||||
if (debugInfo !== null && !visited.has(debugInfo)) {
|
||||
visited.add(debugInfo);
|
||||
forwardDebugInfo(request, task, debugInfo);
|
||||
const promise = node.promise.deref();
|
||||
if (promise !== undefined) {
|
||||
const debugInfo = promise._debugInfo;
|
||||
if (debugInfo != null && !visited.has(debugInfo)) {
|
||||
visited.add(debugInfo);
|
||||
forwardDebugInfo(request, task, debugInfo);
|
||||
}
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
|
@ -2084,14 +2087,32 @@ function visitAsyncNode(
|
|||
// just part of a previous component's rendering.
|
||||
match = ioNode;
|
||||
} else {
|
||||
const stack = filterStackTrace(request, node.stack);
|
||||
if (stack.length === 0) {
|
||||
let isAwaitInUserspace = false;
|
||||
const fullStack = node.stack;
|
||||
if (fullStack.length > 0) {
|
||||
// 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[0];
|
||||
const functionName = callsite[0];
|
||||
const url = devirtualizeURL(callsite[1]);
|
||||
isAwaitInUserspace = filterStackFrame(url, functionName);
|
||||
}
|
||||
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;
|
||||
} else {
|
||||
// We found a user space await.
|
||||
|
||||
// Outline the IO node.
|
||||
serializeIONode(request, ioNode);
|
||||
// The ioNode is where the I/O was initiated, but after that it could have been
|
||||
// processed through various awaits in the internals of the third party code.
|
||||
// Therefore we don't use the inner most Promise as the conceptual value but the
|
||||
// Promise that was ultimately awaited by the user space await.
|
||||
serializeIONode(request, ioNode, awaited.promise);
|
||||
|
||||
// We log the environment at the time when the last promise pigned ping which may
|
||||
// be later than what the environment was when we actually started awaiting.
|
||||
|
|
@ -2103,7 +2124,7 @@ function visitAsyncNode(
|
|||
awaited: ((ioNode: any): ReactIOInfo), // This is deduped by this reference.
|
||||
env: env,
|
||||
owner: node.owner,
|
||||
stack: stack,
|
||||
stack: filterStackTrace(request, node.stack),
|
||||
});
|
||||
markOperationEndTime(request, task, endTime);
|
||||
}
|
||||
|
|
@ -2112,10 +2133,13 @@ function visitAsyncNode(
|
|||
}
|
||||
// We need to forward after we visit awaited nodes because what ever I/O we requested that's
|
||||
// the thing that generated this node and its virtual children.
|
||||
const debugInfo = node.debugInfo;
|
||||
if (debugInfo !== null && !visited.has(debugInfo)) {
|
||||
visited.add(debugInfo);
|
||||
forwardDebugInfo(request, task, debugInfo);
|
||||
const promise = node.promise.deref();
|
||||
if (promise !== undefined) {
|
||||
const debugInfo = promise._debugInfo;
|
||||
if (debugInfo != null && !visited.has(debugInfo)) {
|
||||
visited.add(debugInfo);
|
||||
forwardDebugInfo(request, task, debugInfo);
|
||||
}
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
|
@ -2141,7 +2165,7 @@ function emitAsyncSequence(
|
|||
const awaitedNode = visitAsyncNode(request, task, node, visited, task.time);
|
||||
if (awaitedNode !== null) {
|
||||
// Nothing in user space (unfiltered stack) awaited this.
|
||||
serializeIONode(request, awaitedNode);
|
||||
serializeIONode(request, awaitedNode, awaitedNode.promise);
|
||||
request.pendingChunks++;
|
||||
// We log the environment at the time when we ping which may be later than what the
|
||||
// environment was when we actually started awaiting.
|
||||
|
|
@ -3726,6 +3750,7 @@ function emitIOInfoChunk(
|
|||
name: string,
|
||||
start: number,
|
||||
end: number,
|
||||
value: ?Promise<mixed>,
|
||||
env: ?string,
|
||||
owner: ?ReactComponentInfo,
|
||||
stack: ?ReactStackTrace,
|
||||
|
|
@ -3750,6 +3775,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;
|
||||
|
|
@ -3797,6 +3826,7 @@ function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
|
|||
ioInfo.name,
|
||||
ioInfo.start,
|
||||
ioInfo.end,
|
||||
ioInfo.value,
|
||||
ioInfo.env,
|
||||
owner,
|
||||
debugStack,
|
||||
|
|
@ -3807,6 +3837,7 @@ function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
|
|||
function serializeIONode(
|
||||
request: Request,
|
||||
ioNode: IONode | PromiseNode,
|
||||
promiseRef: null | WeakRef<Promise<mixed>>,
|
||||
): string {
|
||||
const existingRef = request.writtenDebugObjects.get(ioNode);
|
||||
if (existingRef !== undefined) {
|
||||
|
|
@ -3834,6 +3865,11 @@ function serializeIONode(
|
|||
outlineComponentInfo(request, owner);
|
||||
}
|
||||
|
||||
let value: void | Promise<mixed> = undefined;
|
||||
if (promiseRef !== null) {
|
||||
value = promiseRef.deref();
|
||||
}
|
||||
|
||||
// We log the environment at the time when we serialize the I/O node.
|
||||
// The environment name may have changed from when the I/O was actually started.
|
||||
const env = (0, request.environmentName)();
|
||||
|
|
@ -3846,6 +3882,7 @@ function serializeIONode(
|
|||
name,
|
||||
ioNode.start,
|
||||
ioNode.end,
|
||||
value,
|
||||
env,
|
||||
owner,
|
||||
stack,
|
||||
|
|
|
|||
|
|
@ -34,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;
|
||||
|
||||
|
|
@ -45,12 +62,6 @@ function resolvePromiseOrAwaitNode(
|
|||
resolvedNode.tag = ((unresolvedNode.tag === UNRESOLVED_PROMISE_NODE
|
||||
? PROMISE_NODE
|
||||
: AWAIT_NODE): any);
|
||||
// The Promise can be garbage collected after this so we should extract debugInfo first.
|
||||
const promise = unresolvedNode.debugInfo.deref();
|
||||
resolvedNode.debugInfo =
|
||||
promise === undefined || promise._debugInfo === undefined
|
||||
? null
|
||||
: promise._debugInfo;
|
||||
resolvedNode.end = endTime;
|
||||
return resolvedNode;
|
||||
}
|
||||
|
|
@ -72,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,
|
||||
|
|
@ -81,15 +100,23 @@ export function initAsyncDebugInfo(): void {
|
|||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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(),
|
||||
debugInfo: new WeakRef((resource: Promise<any>)),
|
||||
stack: parseStackTrace(new Error(), 1),
|
||||
stack: parseStackTrace(new Error(), 5),
|
||||
start: performance.now(),
|
||||
end: -1.1, // set when resolved.
|
||||
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);
|
||||
|
|
@ -97,10 +124,10 @@ export function initAsyncDebugInfo(): void {
|
|||
node = ({
|
||||
tag: UNRESOLVED_PROMISE_NODE,
|
||||
owner: resolveOwner(),
|
||||
debugInfo: new WeakRef((resource: Promise<any>)),
|
||||
stack: parseStackTrace(new Error(), 1),
|
||||
stack: parseStackTrace(new Error(), 5),
|
||||
start: performance.now(),
|
||||
end: -1.1, // Set when we resolve.
|
||||
promise: new WeakRef((resource: Promise<any>)),
|
||||
awaited:
|
||||
trigger === undefined
|
||||
? null // It might get overridden when we resolve.
|
||||
|
|
@ -118,10 +145,10 @@ export function initAsyncDebugInfo(): void {
|
|||
node = ({
|
||||
tag: IO_NODE,
|
||||
owner: resolveOwner(),
|
||||
debugInfo: null,
|
||||
stack: parseStackTrace(new Error(), 1), // This is only used if no native promises are used.
|
||||
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,
|
||||
awaited: null,
|
||||
previous: null,
|
||||
}: IONode);
|
||||
|
|
@ -133,10 +160,10 @@ export function initAsyncDebugInfo(): void {
|
|||
node = ({
|
||||
tag: IO_NODE,
|
||||
owner: resolveOwner(),
|
||||
debugInfo: null,
|
||||
stack: parseStackTrace(new Error(), 1),
|
||||
stack: parseStackTrace(new Error(), 3),
|
||||
start: performance.now(),
|
||||
end: -1.1, // Only set when pinged.
|
||||
promise: null,
|
||||
awaited: null,
|
||||
previous: trigger,
|
||||
}: IONode);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -234,6 +234,7 @@ export type ReactIOInfo = {
|
|||
+name: string, // the name of the async function being called (e.g. "fetch")
|
||||
+start: number, // the start time
|
||||
+end: number, // the end time (this might be different from the time the await was unblocked)
|
||||
+value?: null | Promise<mixed>, // the Promise that was awaited if any, may be rejected
|
||||
+env?: string, // the environment where this I/O was spawned.
|
||||
+owner?: null | ReactComponentInfo,
|
||||
+stack?: null | ReactStackTrace,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user