diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 1c2151362d89b..8bcabfb5563bb 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -2561,6 +2561,7 @@ function ResponseInstance( findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only replayConsole: boolean, // DEV-only environmentName: void | string, // DEV-only + debugStartTime: void | number, // DEV-only debugChannel: void | DebugChannel, // DEV-only ) { const chunks: Map> = new Map(); @@ -2621,7 +2622,8 @@ function ResponseInstance( // Note: createFromFetch allows this to be marked at the start of the fetch // where as if you use createFromReadableStream from the body of the fetch // then the start time is when the headers resolved. - this._debugStartTime = performance.now(); + this._debugStartTime = + debugStartTime == null ? performance.now() : debugStartTime; this._debugIOStarted = false; // We consider everything before the first setTimeout task to be cached data // and is not considered I/O required to load the stream. @@ -2669,6 +2671,7 @@ export function createResponse( findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only replayConsole: boolean, // DEV-only environmentName: void | string, // DEV-only + debugStartTime: void | number, // DEV-only debugChannel: void | DebugChannel, // DEV-only ): WeakResponse { return getWeakResponse( @@ -2684,6 +2687,7 @@ export function createResponse( findSourceMapURL, replayConsole, environmentName, + debugStartTime, debugChannel, ), ); diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 4dd4a619cb4db..4a61fba652596 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -2862,7 +2862,10 @@ export function attach( let parentInstance = reconcilingParent; while ( parentInstance.kind === FILTERED_FIBER_INSTANCE && - parentInstance.parent !== null + parentInstance.parent !== null && + // We can't move past the parent Suspense node. + // The Suspense node holding async info must be a parent of the devtools instance (or the instance itself) + parentInstance !== parentSuspenseNode.instance ) { parentInstance = parentInstance.parent; } diff --git a/packages/react-devtools-shared/src/devtools/views/useInferredName.js b/packages/react-devtools-shared/src/devtools/views/useInferredName.js index f0822e126d701..2c93ded46910d 100644 --- a/packages/react-devtools-shared/src/devtools/views/useInferredName.js +++ b/packages/react-devtools-shared/src/devtools/views/useInferredName.js @@ -16,7 +16,7 @@ export default function useInferredName( const fetchFileWithCaching = useContext(FetchFileWithCachingContext); const name = asyncInfo.awaited.name; let inferNameFromStack = null; - if (!name || name === 'Promise') { + if (!name || name === 'Promise' || name === 'lazy') { // If all we have is a generic name, we can try to infer a better name from // the stack. We only do this if the stack has more than one frame since // otherwise it's likely to just be the name of the component which isn't better. diff --git a/packages/react-markup/src/ReactMarkupServer.js b/packages/react-markup/src/ReactMarkupServer.js index 0b35404cb80aa..6f1e35e6156eb 100644 --- a/packages/react-markup/src/ReactMarkupServer.js +++ b/packages/react-markup/src/ReactMarkupServer.js @@ -91,6 +91,9 @@ export function experimental_renderToHTML( undefined, undefined, false, + undefined, + undefined, + undefined, ); const streamState = createFlightStreamState(flightResponse, null); const flightDestination = { diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js index cfc8dcf5f1b25..371f08abc9a62 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js @@ -52,6 +52,7 @@ export type Options = { findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, + startTime?: number, }; function createDebugCallbackFromWritableStream( @@ -103,6 +104,9 @@ function createResponseFromOptions(options: void | Options) { __DEV__ && options && options.environmentName ? options.environmentName : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js index 2bf32729472d2..78dce936158eb 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js @@ -57,6 +57,7 @@ export type Options = { findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, + startTime?: number, // For the Node.js client we only support a single-direction debug channel. debugChannel?: Readable, }; @@ -112,6 +113,9 @@ function createFromNodeStream( __DEV__ && options && options.environmentName ? options.environmentName : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, debugChannel, ); diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js index 2f71ce2fa9597..0f0141e640988 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js @@ -129,6 +129,9 @@ function createResponseFromOptions(options: void | Options) { __DEV__ && options && options.environmentName ? options.environmentName : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, debugChannel, ); } @@ -205,6 +208,7 @@ export type Options = { temporaryReferences?: TemporaryReferenceSet, replayConsoleLogs?: boolean, environmentName?: string, + startTime?: number, }; export function createFromReadableStream( diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js index f9c63ccbc3370..5c8d1023b2373 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js @@ -79,6 +79,7 @@ export type Options = { temporaryReferences?: TemporaryReferenceSet, replayConsoleLogs?: boolean, environmentName?: string, + startTime?: number, // For the Edge client we only support a single-direction debug channel. debugChannel?: {readable?: ReadableStream, ...}, }; @@ -107,6 +108,9 @@ function createResponseFromOptions(options?: Options) { __DEV__ && options && options.environmentName ? options.environmentName : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js index b874bcd7d44f5..fbc633a175321 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js @@ -52,6 +52,7 @@ export type Options = { encodeFormAction?: EncodeFormActionCallback, replayConsoleLogs?: boolean, environmentName?: string, + startTime?: number, // For the Node.js client we only support a single-direction debug channel. debugChannel?: Readable, }; @@ -103,6 +104,9 @@ export function createFromNodeStream( __DEV__ && options && options.environmentName ? options.environmentName : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, debugChannel, ); diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js index b4b84f1c41b23..b3d31bd1bbba9 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js @@ -51,6 +51,7 @@ export type Options = { findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, + startTime?: number, }; function createDebugCallbackFromWritableStream( @@ -102,6 +103,9 @@ function createResponseFromOptions(options: void | Options) { __DEV__ && options && options.environmentName ? options.environmentName : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js index b994841be18b3..5517e0f73cf94 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js @@ -79,6 +79,7 @@ export type Options = { findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, + startTime?: number, // For the Edge client we only support a single-direction debug channel. debugChannel?: {readable?: ReadableStream, ...}, }; @@ -109,6 +110,9 @@ function createResponseFromOptions(options: Options) { __DEV__ && options && options.environmentName ? options.environmentName : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js index f174f10c0b88f..6d117929df007 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js @@ -60,6 +60,7 @@ export type Options = { findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, + startTime?: number, // For the Node.js client we only support a single-direction debug channel. debugChannel?: Readable, }; @@ -114,6 +115,9 @@ function createFromNodeStream( __DEV__ && options && options.environmentName ? options.environmentName : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, debugChannel, ); diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js index b4b84f1c41b23..b3d31bd1bbba9 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js @@ -51,6 +51,7 @@ export type Options = { findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, + startTime?: number, }; function createDebugCallbackFromWritableStream( @@ -102,6 +103,9 @@ function createResponseFromOptions(options: void | Options) { __DEV__ && options && options.environmentName ? options.environmentName : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js index 63dae4954550a..bc4caac767fb8 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js @@ -79,6 +79,7 @@ export type Options = { findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, + startTime?: number, // For the Edge client we only support a single-direction debug channel. debugChannel?: {readable?: ReadableStream, ...}, }; @@ -109,6 +110,9 @@ function createResponseFromOptions(options: Options) { __DEV__ && options && options.environmentName ? options.environmentName : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js index f174f10c0b88f..6d117929df007 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js @@ -60,6 +60,7 @@ export type Options = { findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, + startTime?: number, // For the Node.js client we only support a single-direction debug channel. debugChannel?: Readable, }; @@ -114,6 +115,9 @@ function createFromNodeStream( __DEV__ && options && options.environmentName ? options.environmentName : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, debugChannel, ); diff --git a/packages/react-server/src/ReactFlightServerConfigDebugNode.js b/packages/react-server/src/ReactFlightServerConfigDebugNode.js index a78297ad83f73..e79c19cc73aa6 100644 --- a/packages/react-server/src/ReactFlightServerConfigDebugNode.js +++ b/packages/react-server/src/ReactFlightServerConfigDebugNode.js @@ -142,10 +142,28 @@ export function initAsyncDebugInfo(): void { }: UnresolvedPromiseNode); } } else if ( - type !== 'Microtask' && - type !== 'TickObject' && - type !== 'Immediate' + // bound-anonymous-fn is the default name for snapshots and .bind() without a name. + // This isn't I/O by itself but likely just a continuation. If the bound function + // has a name, we might treat it as I/O but we can't tell the difference. + type === 'bound-anonymous-fn' || + // queueMicroTask, process.nextTick and setImmediate aren't considered new I/O + // for our purposes but just continuation of existing I/O. + type === 'Microtask' || + type === 'TickObject' || + type === 'Immediate' ) { + // Treat the trigger as the node to carry along the sequence. + // For "bound-anonymous-fn" this will be the callsite of the .bind() which may not + // be the best if the callsite of the .run() call is within I/O which should be + // tracked. It might be better to track the execution context of "before()" as the + // execution context for anything spawned from within the run(). Basically as if + // it wasn't an AsyncResource at all. + if (trigger === undefined) { + return; + } + node = trigger; + } else { + // New I/O if (trigger === undefined) { // We have begun a new I/O sequence. const owner = resolveOwner(); @@ -181,13 +199,6 @@ export function initAsyncDebugInfo(): void { // Otherwise, this is just a continuation of the same I/O sequence. node = trigger; } - } else { - // Ignore nextTick and microtasks as they're not considered I/O operations. - // we just treat the trigger as the node to carry along the sequence. - if (trigger === undefined) { - return; - } - node = trigger; } pendingOperations.set(asyncId, node); }, diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index 55b1690b7caea..47f37afbae220 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -20,6 +20,8 @@ import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags'; import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; +import noop from 'shared/noop'; + const Uninitialized = -1; const Pending = 0; const Resolved = 1; @@ -67,12 +69,20 @@ export type LazyComponent = { function lazyInitializer(payload: Payload): T { if (payload._status === Uninitialized) { + let resolveDebugValue: (void | T) => void = (null: any); + let rejectDebugValue: mixed => void = (null: any); if (__DEV__ && enableAsyncDebugInfo) { const ioInfo = payload._ioInfo; if (ioInfo != null) { // Mark when we first kicked off the lazy request. // $FlowFixMe[cannot-write] ioInfo.start = ioInfo.end = performance.now(); + // Stash a Promise for introspection of the value later. + // $FlowFixMe[cannot-write] + ioInfo.value = new Promise((resolve, reject) => { + resolveDebugValue = resolve; + rejectDebugValue = reject; + }); } } const ctor = payload._result; @@ -92,12 +102,20 @@ function lazyInitializer(payload: Payload): T { const resolved: ResolvedPayload = (payload: any); resolved._status = Resolved; resolved._result = moduleObject; - if (__DEV__) { + if (__DEV__ && enableAsyncDebugInfo) { const ioInfo = payload._ioInfo; if (ioInfo != null) { // Mark the end time of when we resolved. // $FlowFixMe[cannot-write] ioInfo.end = performance.now(); + // Surface the default export as the resolved "value" for debug purposes. + const debugValue = + moduleObject == null ? undefined : moduleObject.default; + resolveDebugValue(debugValue); + // $FlowFixMe + ioInfo.value.status = 'fulfilled'; + // $FlowFixMe + ioInfo.value.value = debugValue; } // Make the thenable introspectable if (thenable.status === undefined) { @@ -124,6 +142,14 @@ function lazyInitializer(payload: Payload): T { // Mark the end time of when we rejected. // $FlowFixMe[cannot-write] ioInfo.end = performance.now(); + // Hide unhandled rejections. + // $FlowFixMe + ioInfo.value.then(noop, noop); + rejectDebugValue(error); + // $FlowFixMe + ioInfo.value.status = 'rejected'; + // $FlowFixMe + ioInfo.value.reason = error; } // Make the thenable introspectable if (thenable.status === undefined) { @@ -139,9 +165,6 @@ function lazyInitializer(payload: Payload): T { if (__DEV__ && enableAsyncDebugInfo) { const ioInfo = payload._ioInfo; if (ioInfo != null) { - // Stash the thenable for introspection of the value later. - // $FlowFixMe[cannot-write] - ioInfo.value = thenable; const displayName = thenable.displayName; if (typeof displayName === 'string') { // $FlowFixMe[cannot-write] diff --git a/packages/shared/ReactIODescription.js b/packages/shared/ReactIODescription.js index e1a0fce2c4d0a..6d7bf648fc84f 100644 --- a/packages/shared/ReactIODescription.js +++ b/packages/shared/ReactIODescription.js @@ -13,6 +13,8 @@ export function getIODescription(value: mixed): string { } try { switch (typeof value) { + case 'function': + return value.name || ''; case 'object': // Test the object for a bunch of common property names that are useful identifiers. // While we only have the return value here, it should ideally be a name that