diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index eb354aba58753..49968b33590d9 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -2991,6 +2991,64 @@ describe('ReactFlight', () => { ); }); + // @gate !__DEV__ || enableComponentPerformanceTrack + it('preserves debug info for server-to-server through use()', async () => { + function ThirdPartyComponent() { + return 'hi'; + } + + function ServerComponent({transport}) { + // This is a Server Component that receives other Server Components from a third party. + const text = ReactServer.use(ReactNoopFlightClient.read(transport)); + return
{text.toUpperCase()}
; + } + + const thirdPartyTransport = ReactNoopFlightServer.render( + , + { + environmentName: 'third-party', + }, + ); + + const transport = ReactNoopFlightServer.render( + , + ); + + await act(async () => { + const promise = ReactNoopFlightClient.read(transport); + expect(getDebugInfo(promise)).toEqual( + __DEV__ + ? [ + {time: 16}, + { + name: 'ServerComponent', + env: 'Server', + key: null, + stack: ' in Object. (at **)', + props: { + transport: expect.arrayContaining([]), + }, + }, + {time: 16}, + { + name: 'ThirdPartyComponent', + env: 'third-party', + key: null, + stack: ' in Object. (at **)', + props: {}, + }, + {time: 16}, + {time: 17}, + ] + : undefined, + ); + const result = await promise; + ReactNoop.render(result); + }); + + expect(ReactNoop).toMatchRenderedOutput(
HI
); + }); + it('preserves error stacks passed through server-to-server with source maps', async () => { async function ServerComponent({transport}) { // This is a Server Component that receives other Server Components from a third party. diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js index 9d00b39efad25..baa297cf33d41 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js @@ -161,6 +161,9 @@ const deepProxyHandlers = { // reference. case 'defaultProps': return undefined; + // React looks for debugInfo on thenables. + case '_debugInfo': + return undefined; // Avoid this attempting to be serialized. case 'toJSON': return undefined; @@ -210,6 +213,9 @@ function getReference(target: Function, name: string | symbol): $FlowFixMe { // reference. case 'defaultProps': return undefined; + // React looks for debugInfo on thenables. + case '_debugInfo': + return undefined; // Avoid this attempting to be serialized. case 'toJSON': return undefined; diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js index 60fe34b1c08df..c06e52a578ddb 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js @@ -162,6 +162,9 @@ const deepProxyHandlers = { // reference. case 'defaultProps': return undefined; + // React looks for debugInfo on thenables. + case '_debugInfo': + return undefined; // Avoid this attempting to be serialized. case 'toJSON': return undefined; @@ -211,6 +214,9 @@ function getReference(target: Function, name: string | symbol): $FlowFixMe { // reference. case 'defaultProps': return undefined; + // React looks for debugInfo on thenables. + case '_debugInfo': + return undefined; // Avoid this attempting to be serialized. case 'toJSON': return undefined; diff --git a/packages/react-server/src/ReactFlightHooks.js b/packages/react-server/src/ReactFlightHooks.js index c5ffc5dd70342..ed369be0e9b18 100644 --- a/packages/react-server/src/ReactFlightHooks.js +++ b/packages/react-server/src/ReactFlightHooks.js @@ -58,6 +58,12 @@ export function getThenableStateAfterSuspending(): ThenableState { return state; } +export function getTrackedThenablesAfterRendering(): null | Array< + Thenable, +> { + return thenableState; +} + export const HooksDispatcher: Dispatcher = { readContext: (unsupportedContext: any), diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 3a56148f7101d..e51197c5b2b19 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -91,6 +91,7 @@ import { initAsyncDebugInfo, markAsyncSequenceRootTask, getCurrentAsyncSequence, + getAsyncSequenceFromPromise, parseStackTrace, supportsComponentStorage, componentStorage, @@ -106,6 +107,7 @@ import { prepareToUseHooksForRequest, prepareToUseHooksForComponent, getThenableStateAfterSuspending, + getTrackedThenablesAfterRendering, resetHooksForRequest, } from './ReactFlightHooks'; import {DefaultAsyncDispatcher} from './flight/ReactFlightAsyncDispatcher'; @@ -690,26 +692,14 @@ function serializeThenable( switch (thenable.status) { case 'fulfilled': { - if (__DEV__) { - // If this came from Flight, forward any debug info into this new row. - const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo; - if (debugInfo) { - forwardDebugInfo(request, newTask, debugInfo); - } - } + forwardDebugInfoFromThenable(request, newTask, thenable, null, null); // We have the resolved value, we can go ahead and schedule it for serialization. newTask.model = thenable.value; pingTask(request, newTask); return newTask.id; } case 'rejected': { - if (__DEV__) { - // If this came from Flight, forward any debug info into this new row. - const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo; - if (debugInfo) { - forwardDebugInfo(request, newTask, debugInfo); - } - } + forwardDebugInfoFromThenable(request, newTask, thenable, null, null); const x = thenable.reason; erroredTask(request, newTask, x); return newTask.id; @@ -758,24 +748,11 @@ function serializeThenable( thenable.then( value => { - if (__DEV__) { - // If this came from Flight, forward any debug info into this new row. - const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo; - if (debugInfo) { - forwardDebugInfo(request, newTask, debugInfo); - } - } + forwardDebugInfoFromCurrentContext(request, newTask, thenable); newTask.model = value; pingTask(request, newTask); }, reason => { - if (__DEV__) { - // If this came from Flight, forward any debug info into this new row. - const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo; - if (debugInfo) { - forwardDebugInfo(request, newTask, debugInfo); - } - } if (newTask.status === PENDING) { if (enableProfilerTimer && enableComponentPerformanceTrack) { // If this is async we need to time when this task finishes. @@ -1055,13 +1032,21 @@ function readThenable(thenable: Thenable): T { throw thenable; } -function createLazyWrapperAroundWakeable(wakeable: Wakeable) { +function createLazyWrapperAroundWakeable( + request: Request, + task: Task, + wakeable: Wakeable, +) { // This is a temporary fork of the `use` implementation until we accept // promises everywhere. const thenable: Thenable = (wakeable: any); switch (thenable.status) { - case 'fulfilled': + case 'fulfilled': { + forwardDebugInfoFromThenable(request, task, thenable, null, null); + return thenable.value; + } case 'rejected': + forwardDebugInfoFromThenable(request, task, thenable, null, null); break; default: { if (typeof thenable.status === 'string') { @@ -1074,6 +1059,7 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) { pendingThenable.status = 'pending'; pendingThenable.then( fulfilledValue => { + forwardDebugInfoFromCurrentContext(request, task, thenable); if (thenable.status === 'pending') { const fulfilledThenable: FulfilledThenable = (thenable: any); fulfilledThenable.status = 'fulfilled'; @@ -1081,6 +1067,7 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) { } }, (error: mixed) => { + forwardDebugInfoFromCurrentContext(request, task, thenable); if (thenable.status === 'pending') { const rejectedThenable: RejectedThenable = (thenable: any); rejectedThenable.status = 'rejected'; @@ -1096,10 +1083,6 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) { _payload: thenable, _init: readThenable, }; - if (__DEV__) { - // If this came from React, transfer the debug info. - lazyType._debugInfo = (thenable: any)._debugInfo || []; - } return lazyType; } @@ -1178,12 +1161,9 @@ function processServerComponentReturnValue( } }, voidHandler); } - if (thenable.status === 'fulfilled') { - return thenable.value; - } // TODO: Once we accept Promises as children on the client, we can just return // the thenable here. - return createLazyWrapperAroundWakeable(result); + return createLazyWrapperAroundWakeable(request, task, result); } if (__DEV__) { @@ -1386,6 +1366,7 @@ function renderFunctionComponent( } } } else { + componentDebugInfo = (null: any); prepareToUseHooksForComponent(prevThenableState, null); // The secondArg is always undefined in Server Components since refs error early. const secondArg = undefined; @@ -1408,6 +1389,34 @@ function renderFunctionComponent( throw null; } + if ( + __DEV__ || + (enableProfilerTimer && + enableComponentPerformanceTrack && + enableAsyncDebugInfo) + ) { + // Forward any debug information for any Promises that we use():ed during the render. + // We do this at the end so that we don't keep doing this for each retry. + const trackedThenables = getTrackedThenablesAfterRendering(); + if (trackedThenables !== null) { + const stacks: Array = + __DEV__ && enableAsyncDebugInfo + ? (trackedThenables: any)._stacks || + ((trackedThenables: any)._stacks = []) + : (null: any); + for (let i = 0; i < trackedThenables.length; i++) { + const stack = __DEV__ && enableAsyncDebugInfo ? stacks[i] : null; + forwardDebugInfoFromThenable( + request, + task, + trackedThenables[i], + __DEV__ ? componentDebugInfo : null, + stack, + ); + } + } + } + // Apply special cases. result = processServerComponentReturnValue(request, task, Component, result); @@ -1884,7 +1893,7 @@ function visitAsyncNode( request: Request, task: Task, node: AsyncSequence, - visited: Set, + visited: Set, cutOff: number, ): null | PromiseNode | IONode { if (visited.has(node)) { @@ -1943,7 +1952,8 @@ 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) { + if (debugInfo !== null && !visited.has(debugInfo)) { + visited.add(debugInfo); forwardDebugInfo(request, task, debugInfo); } return match; @@ -2003,8 +2013,9 @@ 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: null | ReactDebugInfo = node.debugInfo; - if (debugInfo !== null) { + const debugInfo = node.debugInfo; + if (debugInfo !== null && !visited.has(debugInfo)) { + visited.add(debugInfo); forwardDebugInfo(request, task, debugInfo); } return match; @@ -2020,8 +2031,14 @@ function emitAsyncSequence( request: Request, task: Task, node: AsyncSequence, + alreadyForwardedDebugInfo: ?ReactDebugInfo, + owner: null | ReactComponentInfo, + stack: null | Error, ): void { - const visited: Set = new Set(); + const visited: Set = new Set(); + if (__DEV__ && alreadyForwardedDebugInfo) { + visited.add(alreadyForwardedDebugInfo); + } const awaitedNode = visitAsyncNode(request, task, node, visited, task.time); if (awaitedNode !== null) { // Nothing in user space (unfiltered stack) awaited this. @@ -2032,10 +2049,21 @@ function emitAsyncSequence( const env = (0, request.environmentName)(); // If we don't have any thing awaited, the time we started awaiting was internal // when we yielded after rendering. The current task time is basically that. - emitDebugChunk(request, task.id, { + const debugInfo: ReactAsyncInfo = { awaited: ((awaitedNode: any): ReactIOInfo), // This is deduped by this reference. env: env, - }); + }; + if (__DEV__) { + if (owner != null) { + // $FlowFixMe[cannot-write] + debugInfo.owner = owner; + } + if (stack != null) { + // $FlowFixMe[cannot-write] + debugInfo.stack = filterStackTrace(request, parseStackTrace(stack, 1)); + } + } + emitDebugChunk(request, task.id, debugInfo); markOperationEndTime(request, task, awaitedNode.end); } } @@ -2044,12 +2072,6 @@ function pingTask(request: Request, task: Task): void { if (enableProfilerTimer && enableComponentPerformanceTrack) { // If this was async we need to emit the time when it completes. task.timed = true; - if (enableAsyncDebugInfo) { - const sequence = getCurrentAsyncSequence(); - if (sequence !== null) { - emitAsyncSequence(request, task, sequence); - } - } } const pingedTasks = request.pingedTasks; pingedTasks.push(task); @@ -4316,6 +4338,58 @@ function forwardDebugInfo( } } +function forwardDebugInfoFromThenable( + request: Request, + task: Task, + thenable: Thenable, + owner: null | ReactComponentInfo, // DEV-only + stack: null | Error, // DEV-only +): void { + let debugInfo: ?ReactDebugInfo; + if (__DEV__) { + // If this came from Flight, forward any debug info into this new row. + debugInfo = thenable._debugInfo; + if (debugInfo) { + forwardDebugInfo(request, task, debugInfo); + } + } + if ( + enableProfilerTimer && + enableComponentPerformanceTrack && + enableAsyncDebugInfo + ) { + const sequence = getAsyncSequenceFromPromise(thenable); + if (sequence !== null) { + emitAsyncSequence(request, task, sequence, debugInfo, owner, stack); + } + } +} + +function forwardDebugInfoFromCurrentContext( + request: Request, + task: Task, + thenable: Thenable, +): void { + let debugInfo: ?ReactDebugInfo; + if (__DEV__) { + // If this came from Flight, forward any debug info into this new row. + debugInfo = thenable._debugInfo; + if (debugInfo) { + forwardDebugInfo(request, task, debugInfo); + } + } + if ( + enableProfilerTimer && + enableComponentPerformanceTrack && + enableAsyncDebugInfo + ) { + const sequence = getCurrentAsyncSequence(); + if (sequence !== null) { + emitAsyncSequence(request, task, sequence, debugInfo, null, null); + } + } +} + function emitTimingChunk( request: Request, id: number, diff --git a/packages/react-server/src/ReactFlightServerConfigDebugNode.js b/packages/react-server/src/ReactFlightServerConfigDebugNode.js index ddcf256525b6d..46a29fb3cb2ef 100644 --- a/packages/react-server/src/ReactFlightServerConfigDebugNode.js +++ b/packages/react-server/src/ReactFlightServerConfigDebugNode.js @@ -24,9 +24,12 @@ import { UNRESOLVED_AWAIT_NODE, } from './ReactFlightAsyncSequence'; import {resolveOwner} from './flight/ReactFlightCurrentOwner'; -import {createHook, executionAsyncId} from 'async_hooks'; +import {createHook, executionAsyncId, AsyncResource} from 'async_hooks'; import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags'; +// $FlowFixMe[method-unbinding] +const getAsyncId = AsyncResource.prototype.asyncId; + const pendingOperations: Map = __DEV__ && enableAsyncDebugInfo ? new Map() : (null: any); @@ -260,3 +263,29 @@ export function getCurrentAsyncSequence(): null | AsyncSequence { } return currentNode; } + +export function getAsyncSequenceFromPromise( + promise: any, +): null | AsyncSequence { + if (!__DEV__ || !enableAsyncDebugInfo) { + return null; + } + // A Promise is conceptually an AsyncResource but doesn't have its own methods. + // We use this hack to extract the internal asyncId off the Promise. + let asyncId: void | number; + try { + asyncId = getAsyncId.call(promise); + } catch (x) { + // Ignore errors extracting the ID. We treat it as missing. + // This could happen if our hack stops working or in the case where this is + // a Proxy that throws such as our own ClientReference proxies. + } + if (asyncId === undefined) { + return null; + } + const node = pendingOperations.get(asyncId); + if (node === undefined) { + return null; + } + return node; +} diff --git a/packages/react-server/src/ReactFlightServerConfigDebugNoop.js b/packages/react-server/src/ReactFlightServerConfigDebugNoop.js index 7418aaef18310..e435929114b57 100644 --- a/packages/react-server/src/ReactFlightServerConfigDebugNoop.js +++ b/packages/react-server/src/ReactFlightServerConfigDebugNoop.js @@ -15,3 +15,8 @@ export function markAsyncSequenceRootTask(): void {} export function getCurrentAsyncSequence(): null | AsyncSequence { return null; } +export function getAsyncSequenceFromPromise( + promise: any, +): null | AsyncSequence { + return null; +} diff --git a/packages/react-server/src/ReactFlightServerTemporaryReferences.js b/packages/react-server/src/ReactFlightServerTemporaryReferences.js index 1f6b9f8ee3611..e368b2e800795 100644 --- a/packages/react-server/src/ReactFlightServerTemporaryReferences.js +++ b/packages/react-server/src/ReactFlightServerTemporaryReferences.js @@ -52,6 +52,9 @@ const proxyHandlers = { // reference. case 'defaultProps': return undefined; + // React looks for debugInfo on thenables. + case '_debugInfo': + return undefined; // Avoid this attempting to be serialized. case 'toJSON': return undefined; diff --git a/packages/react-server/src/ReactFlightThenable.js b/packages/react-server/src/ReactFlightThenable.js index 47e7b914da176..99ddb36fa5995 100644 --- a/packages/react-server/src/ReactFlightThenable.js +++ b/packages/react-server/src/ReactFlightThenable.js @@ -20,9 +20,11 @@ import type { RejectedThenable, } from 'shared/ReactTypes'; +import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags'; + import noop from 'shared/noop'; -export opaque type ThenableState = Array>; +export type ThenableState = Array>; // An error that is thrown (e.g. by `use`) to trigger Suspense. If we // detect this is caught by userspace, we'll log a warning in development. @@ -50,6 +52,11 @@ export function trackUsedThenable( const previous = thenableState[index]; if (previous === undefined) { thenableState.push(thenable); + if (__DEV__ && enableAsyncDebugInfo) { + const stacks: Array = + (thenableState: any)._stacks || ((thenableState: any)._stacks = []); + stacks.push(new Error()); + } } else { if (previous !== thenable) { // Reuse the previous thenable, and drop the new one. We can assume diff --git a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js index 05f31ff7def9d..42fa56a836a2d 100644 --- a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js +++ b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js @@ -495,6 +495,267 @@ describe('ReactFlightAsyncDebugInfo', () => { } }); + it('can track async information when use()d', async () => { + async function getData(text) { + await delay(1); + return text.toUpperCase(); + } + + function Component() { + const result = ReactServer.use(getData('hi')); + const moreData = getData('seb'); + return ; + } + + function InnerComponent({text, promise}) { + // This async function depends on the I/O in parent components but it should not + // include that I/O as part of its own meta data. + return text + ', ' + ReactServer.use(promise); + } + + const stream = ReactServerDOMServer.renderToPipeableStream( + , + {}, + { + filterStackFrame, + }, + ); + + const readable = new Stream.PassThrough(streamOptions); + + const result = ReactServerDOMClient.createFromNodeStream(readable, { + moduleMap: {}, + moduleLoading: {}, + }); + stream.pipe(readable); + + expect(await result).toBe('HI, SEB'); + if ( + __DEV__ && + gate( + flags => + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo, + ) + ) { + expect(getDebugInfo(result)).toMatchInlineSnapshot(` + [ + { + "time": 0, + }, + { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "Object.", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 517, + 40, + 498, + 49, + ], + ], + }, + { + "time": 0, + }, + { + "awaited": { + "end": 0, + "env": "Server", + "name": "delay", + "owner": { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "Object.", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 517, + 40, + 498, + 49, + ], + ], + }, + "stack": [ + [ + "delay", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 133, + 12, + 132, + 3, + ], + [ + "getData", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 500, + 13, + 499, + 5, + ], + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 505, + 36, + 504, + 5, + ], + ], + "start": 0, + }, + "env": "Server", + "owner": { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "Object.", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 517, + 40, + 498, + 49, + ], + ], + }, + "stack": [ + [ + "getData", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 500, + 13, + 499, + 5, + ], + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 505, + 36, + 504, + 5, + ], + ], + }, + { + "time": 0, + }, + { + "time": 0, + }, + { + "env": "Server", + "key": null, + "name": "InnerComponent", + "props": {}, + "stack": [ + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 507, + 60, + 504, + 5, + ], + ], + }, + { + "awaited": { + "end": 0, + "env": "Server", + "name": "delay", + "owner": { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "Object.", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 517, + 40, + 498, + 49, + ], + ], + }, + "stack": [ + [ + "delay", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 133, + 12, + 132, + 3, + ], + [ + "getData", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 500, + 13, + 499, + 5, + ], + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 506, + 22, + 504, + 5, + ], + ], + "start": 0, + }, + "env": "Server", + "owner": { + "env": "Server", + "key": null, + "name": "InnerComponent", + "props": {}, + "stack": [ + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 507, + 60, + 504, + 5, + ], + ], + }, + "stack": [ + [ + "InnerComponent", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 513, + 40, + 510, + 5, + ], + ], + }, + { + "time": 0, + }, + { + "time": 0, + }, + ] + `); + } + }); + it('can track the start of I/O when no native promise is used', async () => { function Component() { const callbacks = []; @@ -540,9 +801,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 511, + 772, 109, - 498, + 759, 67, ], ], @@ -561,9 +822,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 511, + 772, 109, - 498, + 759, 67, ], ], @@ -572,9 +833,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 501, + 762, 7, - 499, + 760, 5, ], ], @@ -634,9 +895,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 605, + 866, 109, - 596, + 857, 94, ], ], @@ -705,9 +966,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 676, + 937, 109, - 652, + 913, 50, ], ], @@ -787,9 +1048,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 758, + 1019, 109, - 741, + 1002, 63, ], ], @@ -814,9 +1075,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 754, + 1015, 24, - 753, + 1014, 5, ], ], @@ -846,9 +1107,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 754, + 1015, 24, - 753, + 1014, 5, ], ], @@ -865,17 +1126,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 743, + 1004, 13, - 742, + 1003, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 749, + 1010, 24, - 748, + 1009, 5, ], ], @@ -899,9 +1160,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 754, + 1015, 24, - 753, + 1014, 5, ], ], @@ -910,17 +1171,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 743, + 1004, 13, - 742, + 1003, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 749, + 1010, 24, - 748, + 1009, 5, ], ], @@ -953,9 +1214,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 754, + 1015, 24, - 753, + 1014, 5, ], ], @@ -972,17 +1233,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 744, + 1005, 13, - 742, + 1003, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 749, + 1010, 18, - 748, + 1009, 5, ], ], @@ -1006,9 +1267,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 754, + 1015, 24, - 753, + 1014, 5, ], ], @@ -1017,17 +1278,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 744, + 1005, 13, - 742, + 1003, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 749, + 1010, 18, - 748, + 1009, 5, ], ], @@ -1047,12 +1308,7 @@ describe('ReactFlightAsyncDebugInfo', () => { }); it('can track cached entries awaited in later components', async () => { - let cacheKey; - let cacheValue; const getData = cache(async function getData(text) { - if (cacheKey === text) { - return cacheValue; - } await delay(1); return text.toUpperCase(); }); @@ -1105,9 +1361,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1071, + 1327, 40, - 1049, + 1310, 62, ], ], @@ -1129,9 +1385,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1071, + 1327, 40, - 1049, + 1310, 62, ], ], @@ -1148,17 +1404,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1056, + 1312, 13, - 1052, + 1311, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1066, + 1322, 13, - 1065, + 1321, 5, ], ], @@ -1174,9 +1430,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1071, + 1327, 40, - 1049, + 1310, 62, ], ], @@ -1185,17 +1441,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1056, + 1312, 13, - 1052, + 1311, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1066, + 1322, 13, - 1065, + 1321, 5, ], ], @@ -1215,9 +1471,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1067, + 1323, 60, - 1065, + 1321, 5, ], ], @@ -1239,9 +1495,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1071, + 1327, 40, - 1049, + 1310, 62, ], ], @@ -1258,17 +1514,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1056, + 1312, 13, - 1052, + 1311, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1066, + 1322, 13, - 1065, + 1321, 5, ], ], @@ -1284,9 +1540,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1067, + 1323, 60, - 1065, + 1321, 5, ], ], @@ -1295,9 +1551,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Child", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1061, + 1317, 28, - 1060, + 1316, 5, ], ], @@ -1313,6 +1569,238 @@ describe('ReactFlightAsyncDebugInfo', () => { } }); + it('can track cached entries used in child position', async () => { + const getData = cache(async function getData(text) { + await delay(1); + return text.toUpperCase(); + }); + + function Child() { + return getData('hi'); + } + + function Component() { + ReactServer.use(getData('hi')); + return ; + } + + const stream = ReactServerDOMServer.renderToPipeableStream( + , + {}, + { + filterStackFrame, + }, + ); + + const readable = new Stream.PassThrough(streamOptions); + + const result = ReactServerDOMClient.createFromNodeStream(readable, { + moduleMap: {}, + moduleLoading: {}, + }); + stream.pipe(readable); + + expect(await result).toBe('HI'); + if ( + __DEV__ && + gate( + flags => + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo, + ) + ) { + expect(getDebugInfo(result)).toMatchInlineSnapshot(` + [ + { + "time": 0, + }, + { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "Object.", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 1588, + 40, + 1572, + 57, + ], + ], + }, + { + "time": 0, + }, + { + "awaited": { + "end": 0, + "env": "Server", + "name": "delay", + "owner": { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "Object.", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 1588, + 40, + 1572, + 57, + ], + ], + }, + "stack": [ + [ + "delay", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 133, + 12, + 132, + 3, + ], + [ + "getData", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 1574, + 13, + 1573, + 25, + ], + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 1583, + 23, + 1582, + 5, + ], + ], + "start": 0, + }, + "env": "Server", + "owner": { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "Object.", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 1588, + 40, + 1572, + 57, + ], + ], + }, + "stack": [ + [ + "getData", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 1574, + 13, + 1573, + 25, + ], + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 1583, + 23, + 1582, + 5, + ], + ], + }, + { + "time": 0, + }, + { + "time": 0, + }, + { + "env": "Server", + "key": null, + "name": "Child", + "props": {}, + "stack": [ + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 1584, + 60, + 1582, + 5, + ], + ], + }, + { + "awaited": { + "end": 0, + "env": "Server", + "name": "delay", + "owner": { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "Object.", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 1588, + 40, + 1572, + 57, + ], + ], + }, + "stack": [ + [ + "delay", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 133, + 12, + 132, + 3, + ], + [ + "getData", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 1574, + 13, + 1573, + 25, + ], + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 1583, + 23, + 1582, + 5, + ], + ], + "start": 0, + }, + "env": "Server", + }, + { + "time": 0, + }, + { + "time": 0, + }, + ] + `); + } + }); + it('can track implicit returned promises that are blocked by previous data', async () => { async function delayTwice() { await delay('', 20); @@ -1368,9 +1856,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1334, + 1822, 40, - 1316, + 1804, 80, ], ], @@ -1392,9 +1880,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1334, + 1822, 40, - 1316, + 1804, 80, ], ], @@ -1411,17 +1899,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1324, + 1812, 13, - 1322, + 1810, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1329, + 1817, 13, - 1328, + 1816, 5, ], ], @@ -1437,9 +1925,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1334, + 1822, 40, - 1316, + 1804, 80, ], ], @@ -1448,17 +1936,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1324, + 1812, 13, - 1322, + 1810, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1329, + 1817, 13, - 1328, + 1816, 5, ], ], @@ -1480,9 +1968,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1334, + 1822, 40, - 1316, + 1804, 80, ], ], @@ -1499,25 +1987,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1318, + 1806, 13, - 1317, + 1805, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1323, + 1811, 15, - 1322, + 1810, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1329, + 1817, 13, - 1328, + 1816, 5, ], ], @@ -1533,9 +2021,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1334, + 1822, 40, - 1316, + 1804, 80, ], ], @@ -1544,25 +2032,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1318, + 1806, 13, - 1317, + 1805, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1323, + 1811, 15, - 1322, + 1810, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1329, + 1817, 13, - 1328, + 1816, 5, ], ], @@ -1584,9 +2072,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1334, + 1822, 40, - 1316, + 1804, 80, ], ], @@ -1603,9 +2091,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1319, + 1807, 13, - 1317, + 1805, 5, ], ], @@ -1621,9 +2109,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1334, + 1822, 40, - 1316, + 1804, 80, ], ], @@ -1632,9 +2120,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1319, + 1807, 13, - 1317, + 1805, 5, ], ], diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index d66ef65d9d318..39c792b449277 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -356,7 +356,9 @@ declare module 'async_hooks' { run(store: T, callback: (...args: any[]) => R, ...args: any[]): R; enterWith(store: T): void; } - declare interface AsyncResource {} + declare class AsyncResource { + asyncId(): number; + } declare function executionAsyncId(): number; declare function executionAsyncResource(): AsyncResource; declare function triggerAsyncId(): number;