From 53cb88e4d9138848c79fbfa5bebf6fbd682ae937 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 21 Jun 2025 10:20:31 -0400 Subject: [PATCH 1/8] Keep the Promise WeakRef around after the promise resolves The WeakRef keeps it from retaining itself in a cycle. However, we create a chain of dependencies between Promise instances so that the child retains the parent so that in practice the WeakRefs are kept alive as long as we need them which makes it safe to extract debugInfo from them later. --- .../src/ReactFlightAsyncSequence.js | 10 ++-- .../react-server/src/ReactFlightServer.js | 22 +++++---- .../src/ReactFlightServerConfigDebugNode.js | 47 +++++++++++++++---- 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/packages/react-server/src/ReactFlightAsyncSequence.js b/packages/react-server/src/ReactFlightAsyncSequence.js index 6c44e52a07fa6..488e8e3ad2662 100644 --- a/packages/react-server/src/ReactFlightAsyncSequence.js +++ b/packages/react-server/src/ReactFlightAsyncSequence.js @@ -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, // 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, // 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, // 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, // 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, // 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, // 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 }; diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index eb64ce9d3de6e..0a14dc46b9f20 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -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; } @@ -2112,10 +2115,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; } diff --git a/packages/react-server/src/ReactFlightServerConfigDebugNode.js b/packages/react-server/src/ReactFlightServerConfigDebugNode.js index e0982621f4786..ca7080b3d48ff 100644 --- a/packages/react-server/src/ReactFlightServerConfigDebugNode.js +++ b/packages/react-server/src/ReactFlightServerConfigDebugNode.js @@ -34,6 +34,23 @@ const getAsyncId = AsyncResource.prototype.asyncId; const pendingOperations: Map = __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> = __DEV__ && +enableAsyncDebugInfo + ? new WeakMap() + : (null: any); +const previousPromise: WeakMap, Promise> = __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)), stack: parseStackTrace(new Error(), 1), start: performance.now(), end: -1.1, // set when resolved. + promise: new WeakRef((resource: Promise)), 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)), stack: parseStackTrace(new Error(), 1), start: performance.now(), end: -1.1, // Set when we resolve. + promise: new WeakRef((resource: Promise)), 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. 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), start: performance.now(), end: -1.1, // Only set when pinged. + promise: null, awaited: null, previous: trigger, }: IONode); From 8f7118f1ce3b7e063870675552e1197f4e67d81f Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 21 Jun 2025 14:22:49 -0400 Subject: [PATCH 2/8] Pass the Promise to the client if it's still alive This lets the client inspect the value or reason it rejected. --- .../react-server/src/ReactFlightServer.js | 13 + .../ReactFlightAsyncDebugInfo-test.js | 535 ++++++++++-------- packages/shared/ReactTypes.js | 1 + 3 files changed, 325 insertions(+), 224 deletions(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 0a14dc46b9f20..ffaf9bc40cb5f 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -3732,6 +3732,7 @@ function emitIOInfoChunk( name: string, start: number, end: number, + value: ?Promise, env: ?string, owner: ?ReactComponentInfo, stack: ?ReactStackTrace, @@ -3756,6 +3757,10 @@ function emitIOInfoChunk( start: relativeStartTimestamp, end: relativeEndTimestamp, }; + if (value != null) { + // $FlowFixMe[cannot-write] + debugIOInfo.value = value; + } if (env != null) { // $FlowFixMe[cannot-write] debugIOInfo.env = env; @@ -3803,6 +3808,7 @@ function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void { ioInfo.name, ioInfo.start, ioInfo.end, + ioInfo.value, ioInfo.env, owner, debugStack, @@ -3840,6 +3846,12 @@ function serializeIONode( outlineComponentInfo(request, owner); } + let value: void | Promise = undefined; + const promiseRef = ioNode.promise; + 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)(); @@ -3852,6 +3864,7 @@ function serializeIONode( name, ioNode.start, ioNode.end, + value, env, owner, stack, diff --git a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js index 42fa56a836a2d..ca801046e666e 100644 --- a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js +++ b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js @@ -50,6 +50,23 @@ function normalizeIOInfo(ioInfo) { if (typeof ioInfo.end === 'number') { copy.end = 0; } + const promise = ioInfo.value; + if (promise) { + promise.then(); // init + if (promise.status === 'fulfilled') { + copy.value = { + value: promise.value, + }; + } else if (promise.status === 'rejected') { + copy.value = { + reason: promise.reason, + }; + } else { + copy.value = { + status: promise.status, + }; + } + } return copy; } @@ -129,6 +146,16 @@ describe('ReactFlightAsyncDebugInfo', () => { Stream = require('stream'); }); + function finishLoadingStream(readable) { + return new Promise(resolve => { + if (readable.readableEnded) { + resolve(); + } else { + readable.on('end', () => resolve()); + } + }); + } + function delay(timeout) { return new Promise(resolve => { setTimeout(resolve, timeout); @@ -183,6 +210,8 @@ describe('ReactFlightAsyncDebugInfo', () => { stream.pipe(readable); expect(await result).toBe('HI, SEB'); + + await finishLoadingStream(readable); if ( __DEV__ && gate( @@ -204,9 +233,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 175, + 202, 109, - 155, + 182, 50, ], ], @@ -228,9 +257,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 175, + 202, 109, - 155, + 182, 50, ], ], @@ -239,29 +268,32 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 157, + 184, 13, - 156, + 183, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 164, + 191, 26, - 163, + 190, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", "owner": { @@ -273,9 +305,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 175, + 202, 109, - 155, + 182, 50, ], ], @@ -284,17 +316,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 157, + 184, 13, - 156, + 183, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 164, + 191, 26, - 163, + 190, 5, ], ], @@ -319,9 +351,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 175, + 202, 109, - 155, + 182, 50, ], ], @@ -330,29 +362,32 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 158, + 185, 21, - 156, + 183, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 164, + 191, 20, - 163, + 190, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", "owner": { @@ -364,9 +399,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 175, + 202, 109, - 155, + 182, 50, ], ], @@ -375,17 +410,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 159, + 186, 21, - 156, + 183, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 164, + 191, 20, - 163, + 190, 5, ], ], @@ -405,9 +440,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 166, + 193, 60, - 163, + 190, 5, ], ], @@ -429,9 +464,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 175, + 202, 109, - 155, + 182, 50, ], ], @@ -440,21 +475,24 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 158, + 185, 21, - 156, + 183, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", "owner": { @@ -466,9 +504,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 166, + 193, 60, - 163, + 190, 5, ], ], @@ -477,9 +515,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "InnerComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 172, + 199, 35, - 169, + 196, 5, ], ], @@ -530,6 +568,8 @@ describe('ReactFlightAsyncDebugInfo', () => { stream.pipe(readable); expect(await result).toBe('HI, SEB'); + + await finishLoadingStream(readable); if ( __DEV__ && gate( @@ -551,9 +591,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 517, + 555, 40, - 498, + 536, 49, ], ], @@ -575,9 +615,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 517, + 555, 40, - 498, + 536, 49, ], ], @@ -586,29 +626,32 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 500, + 538, 13, - 499, + 537, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 505, + 543, 36, - 504, + 542, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", "owner": { @@ -620,9 +663,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 517, + 555, 40, - 498, + 536, 49, ], ], @@ -631,17 +674,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 500, + 538, 13, - 499, + 537, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 505, + 543, 36, - 504, + 542, 5, ], ], @@ -661,9 +704,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 507, + 545, 60, - 504, + 542, 5, ], ], @@ -682,9 +725,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 517, + 555, 40, - 498, + 536, 49, ], ], @@ -693,29 +736,32 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 500, + 538, 13, - 499, + 537, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 506, + 544, 22, - 504, + 542, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", "owner": { @@ -727,9 +773,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 507, + 545, 60, - 504, + 542, 5, ], ], @@ -738,9 +784,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "InnerComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 513, + 551, 40, - 510, + 548, 5, ], ], @@ -780,6 +826,8 @@ describe('ReactFlightAsyncDebugInfo', () => { stream.pipe(readable); expect(await result).toBe('hi'); + + await finishLoadingStream(readable); if ( __DEV__ && gate( @@ -801,9 +849,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 772, + 818, 109, - 759, + 805, 67, ], ], @@ -822,9 +870,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 772, + 818, 109, - 759, + 805, 67, ], ], @@ -833,9 +881,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 762, + 808, 7, - 760, + 806, 5, ], ], @@ -874,6 +922,8 @@ describe('ReactFlightAsyncDebugInfo', () => { stream.pipe(readable); expect(await result).toBe('hi'); + + await finishLoadingStream(readable); if ( __DEV__ && gate( @@ -895,9 +945,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 866, + 914, 109, - 857, + 905, 94, ], ], @@ -945,6 +995,8 @@ describe('ReactFlightAsyncDebugInfo', () => { stream.pipe(readable); expect(await result).toBe('HI'); + + await finishLoadingStream(readable); if ( __DEV__ && gate( @@ -966,9 +1018,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 937, + 987, 109, - 913, + 963, 50, ], ], @@ -1027,6 +1079,8 @@ describe('ReactFlightAsyncDebugInfo', () => { stream.pipe(readable); expect(await result).toBe('HI'); + + await finishLoadingStream(readable); if ( __DEV__ && gate( @@ -1048,9 +1102,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1019, + 1071, 109, - 1002, + 1054, 63, ], ], @@ -1067,17 +1121,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "fetchThirdParty", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 140, + 167, 40, - 138, + 165, 3, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1015, + 1067, 24, - 1014, + 1066, 5, ], ], @@ -1099,17 +1153,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "fetchThirdParty", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 140, + 167, 40, - 138, + 165, 3, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1015, + 1067, 24, - 1014, + 1066, 5, ], ], @@ -1118,29 +1172,32 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1004, + 1056, 13, - 1003, + 1055, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1010, + 1062, 24, - 1009, + 1061, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "third-party", "owner": { @@ -1152,17 +1209,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "fetchThirdParty", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 140, + 167, 40, - 138, + 165, 3, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1015, + 1067, 24, - 1014, + 1066, 5, ], ], @@ -1171,17 +1228,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1004, + 1056, 13, - 1003, + 1055, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1010, + 1062, 24, - 1009, + 1061, 5, ], ], @@ -1206,17 +1263,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "fetchThirdParty", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 140, + 167, 40, - 138, + 165, 3, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1015, + 1067, 24, - 1014, + 1066, 5, ], ], @@ -1225,29 +1282,32 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1005, + 1057, 13, - 1003, + 1055, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1010, + 1062, 18, - 1009, + 1061, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "third-party", "owner": { @@ -1259,17 +1319,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "fetchThirdParty", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 140, + 167, 40, - 138, + 165, 3, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1015, + 1067, 24, - 1014, + 1066, 5, ], ], @@ -1278,17 +1338,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1005, + 1057, 13, - 1003, + 1055, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1010, + 1062, 18, - 1009, + 1061, 5, ], ], @@ -1340,6 +1400,8 @@ describe('ReactFlightAsyncDebugInfo', () => { stream.pipe(readable); expect(await result).toBe('HI, Seb'); + + await finishLoadingStream(readable); if ( __DEV__ && gate( @@ -1361,9 +1423,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1327, + 1387, 40, - 1310, + 1370, 62, ], ], @@ -1385,9 +1447,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1327, + 1387, 40, - 1310, + 1370, 62, ], ], @@ -1396,29 +1458,32 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1312, + 1372, 13, - 1311, + 1371, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1322, + 1382, 13, - 1321, + 1381, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", "owner": { @@ -1430,9 +1495,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1327, + 1387, 40, - 1310, + 1370, 62, ], ], @@ -1441,17 +1506,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1312, + 1372, 13, - 1311, + 1371, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1322, + 1382, 13, - 1321, + 1381, 5, ], ], @@ -1471,9 +1536,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1323, + 1383, 60, - 1321, + 1381, 5, ], ], @@ -1495,9 +1560,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1327, + 1387, 40, - 1310, + 1370, 62, ], ], @@ -1506,29 +1571,32 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1312, + 1372, 13, - 1311, + 1371, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1322, + 1382, 13, - 1321, + 1381, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", "owner": { @@ -1540,9 +1608,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1323, + 1383, 60, - 1321, + 1381, 5, ], ], @@ -1551,9 +1619,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Child", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1317, + 1377, 28, - 1316, + 1376, 5, ], ], @@ -1601,6 +1669,8 @@ describe('ReactFlightAsyncDebugInfo', () => { stream.pipe(readable); expect(await result).toBe('HI'); + + await finishLoadingStream(readable); if ( __DEV__ && gate( @@ -1622,9 +1692,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1588, + 1656, 40, - 1572, + 1640, 57, ], ], @@ -1646,9 +1716,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1588, + 1656, 40, - 1572, + 1640, 57, ], ], @@ -1657,29 +1727,32 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1574, + 1642, 13, - 1573, + 1641, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1583, + 1651, 23, - 1582, + 1650, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", "owner": { @@ -1691,9 +1764,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1588, + 1656, 40, - 1572, + 1640, 57, ], ], @@ -1702,17 +1775,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1574, + 1642, 13, - 1573, + 1641, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1583, + 1651, 23, - 1582, + 1650, 5, ], ], @@ -1732,9 +1805,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1584, + 1652, 60, - 1582, + 1650, 5, ], ], @@ -1753,9 +1826,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1588, + 1656, 40, - 1572, + 1640, 57, ], ], @@ -1764,29 +1837,32 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1574, + 1642, 13, - 1573, + 1641, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1583, + 1651, 23, - 1582, + 1650, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", }, @@ -1835,6 +1911,8 @@ describe('ReactFlightAsyncDebugInfo', () => { stream.pipe(readable); expect(await result).toBe('hi'); + + await finishLoadingStream(readable); if ( __DEV__ && gate( @@ -1856,9 +1934,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1822, + 1898, 40, - 1804, + 1880, 80, ], ], @@ -1880,9 +1958,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1822, + 1898, 40, - 1804, + 1880, 80, ], ], @@ -1891,29 +1969,32 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1812, + 1888, 13, - 1810, + 1886, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1817, + 1893, 13, - 1816, + 1892, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", "owner": { @@ -1925,9 +2006,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1822, + 1898, 40, - 1804, + 1880, 80, ], ], @@ -1936,17 +2017,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1812, + 1888, 13, - 1810, + 1886, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1817, + 1893, 13, - 1816, + 1892, 5, ], ], @@ -1968,9 +2049,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1822, + 1898, 40, - 1804, + 1880, 80, ], ], @@ -1979,37 +2060,40 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1806, + 1882, 13, - 1805, + 1881, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1811, + 1887, 15, - 1810, + 1886, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1817, + 1893, 13, - 1816, + 1892, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", "owner": { @@ -2021,9 +2105,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1822, + 1898, 40, - 1804, + 1880, 80, ], ], @@ -2032,25 +2116,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1806, + 1882, 13, - 1805, + 1881, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1811, + 1887, 15, - 1810, + 1886, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1817, + 1893, 13, - 1816, + 1892, 5, ], ], @@ -2072,9 +2156,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1822, + 1898, 40, - 1804, + 1880, 80, ], ], @@ -2083,21 +2167,24 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 133, + 160, 12, - 132, + 159, 3, ], [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1807, + 1883, 13, - 1805, + 1881, 5, ], ], "start": 0, + "value": { + "value": undefined, + }, }, "env": "Server", "owner": { @@ -2109,9 +2196,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1822, + 1898, 40, - 1804, + 1880, 80, ], ], @@ -2120,9 +2207,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1807, + 1883, 13, - 1805, + 1881, 5, ], ], diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index ea9d3d1d0789a..04a30dbf8b300 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -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, // 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, From 86c9bcaf575ea9c4c5364e6380457912563db12f Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 21 Jun 2025 15:10:32 -0400 Subject: [PATCH 3/8] Log value to io entries in the performance track --- fixtures/flight/src/App.js | 13 +- .../react-client/src/ReactFlightClient.js | 33 +++- .../src/ReactFlightPerformanceTrack.js | 185 ++++++++++++++++-- .../src/ReactFlightPropertyAccess.js | 13 ++ 4 files changed, 220 insertions(+), 24 deletions(-) create mode 100644 packages/react-client/src/ReactFlightPropertyAccess.js diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js index 2f29de7abaffe..5657b040ffa2b 100644 --- a/fixtures/flight/src/App.js +++ b/fixtures/flight/src/App.js @@ -33,12 +33,22 @@ function Foo({children}) { return
{children}
; } +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 = ; const message = getServerState(); diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 42f697c94a869..5c00a19e5d9f2 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -79,6 +79,7 @@ import { logDedupedComponentRender, logComponentErrored, logIOInfo, + logIOInfoErrored, logComponentAwait, } from './ReactFlightPerformanceTrack'; @@ -96,6 +97,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 +1687,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 +2908,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 = (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( diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index faa5cf9650d9c..c7479a322efd9 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -17,14 +17,106 @@ import type { import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; +import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess'; + +import hasOwnProperty from 'shared/hasOwnProperty'; + 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 ⚛'; +function isPrimitiveArray(array: Object) { + for (let i = 0; i < array.length; i++) { + const value = array[i]; + if (typeof value === 'object' && value !== null) { + return false; + } + if (typeof value === 'function') { + return false; + } + if (typeof value === 'string' && value.length > 50) { + return false; + } + } + return true; +} + +function addObjectToProperties( + object: Object, + properties: Array<[string, string]>, + indent: number, +): void { + for (const key in object) { + if (hasOwnProperty.call(object, key)) { + 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); + const objectName = objectToString.slice(8, objectToString.length - 1); + if (objectName === 'Array' && isPrimitiveArray(value)) { + desc = JSON.stringify(value); + break; + } + 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 +225,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 && @@ -270,7 +357,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 +425,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( diff --git a/packages/react-client/src/ReactFlightPropertyAccess.js b/packages/react-client/src/ReactFlightPropertyAccess.js new file mode 100644 index 0000000000000..16f8e8cbfcade --- /dev/null +++ b/packages/react-client/src/ReactFlightPropertyAccess.js @@ -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.'; From bf48b20024bafac3caa19d5496ce289c6e4cbe52 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 21 Jun 2025 15:57:28 -0400 Subject: [PATCH 4/8] Log value to await entries in the performance track --- .../react-client/src/ReactFlightClient.js | 57 +++++++++++-- .../src/ReactFlightPerformanceTrack.js | 83 +++++++++++++++++-- 2 files changed, 124 insertions(+), 16 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 5c00a19e5d9f2..01e40cdc551ec 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -81,6 +81,7 @@ import { logIOInfo, logIOInfoErrored, logComponentAwait, + logComponentAwaitErrored, } from './ReactFlightPerformanceTrack'; import { @@ -3214,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 = (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, + ); + } } } } diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index c7479a322efd9..19a3117f6220f 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -315,12 +315,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; @@ -332,17 +388,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( From ef034dd7b57e09d973db99d45f1c8e1b5ad09fae Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 21 Jun 2025 18:06:07 -0400 Subject: [PATCH 5/8] Use the Promise from the first await in user space This is better context than the internal Promise of the third party implementation. To do this we need to find the first await in user space and not just the first await that has any call frames in user space. To do this we need to find the first frame outside of the async hooks instrumentation. We do that by hard coding how many extra frames Node currently adds (fairly stable). The problem with this approach is that it doesn't work if someone adds a third party wrapper around .then() like we do in our ReactPromises atm. It's fine if you await it but not if you call it directly. --- .../react-server/src/ReactFlightServer.js | 32 +- .../src/ReactFlightServerConfigDebugNode.js | 8 +- .../ReactFlightAsyncDebugInfo-test.js | 306 +++++++++--------- 3 files changed, 183 insertions(+), 163 deletions(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index ffaf9bc40cb5f..f07e3c6304582 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -2087,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. @@ -2106,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); } @@ -2147,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. @@ -3757,7 +3775,7 @@ function emitIOInfoChunk( start: relativeStartTimestamp, end: relativeEndTimestamp, }; - if (value != null) { + if (value !== undefined) { // $FlowFixMe[cannot-write] debugIOInfo.value = value; } @@ -3819,6 +3837,7 @@ function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void { function serializeIONode( request: Request, ioNode: IONode | PromiseNode, + promiseRef: null | WeakRef>, ): string { const existingRef = request.writtenDebugObjects.get(ioNode); if (existingRef !== undefined) { @@ -3847,7 +3866,6 @@ function serializeIONode( } let value: void | Promise = undefined; - const promiseRef = ioNode.promise; if (promiseRef !== null) { value = promiseRef.deref(); } diff --git a/packages/react-server/src/ReactFlightServerConfigDebugNode.js b/packages/react-server/src/ReactFlightServerConfigDebugNode.js index ca7080b3d48ff..d42e9b2bb0a97 100644 --- a/packages/react-server/src/ReactFlightServerConfigDebugNode.js +++ b/packages/react-server/src/ReactFlightServerConfigDebugNode.js @@ -113,7 +113,7 @@ export function initAsyncDebugInfo(): void { node = ({ tag: UNRESOLVED_AWAIT_NODE, owner: resolveOwner(), - stack: parseStackTrace(new Error(), 1), + stack: parseStackTrace(new Error(), 5), start: performance.now(), end: -1.1, // set when resolved. promise: new WeakRef((resource: Promise)), @@ -124,7 +124,7 @@ export function initAsyncDebugInfo(): void { node = ({ tag: UNRESOLVED_PROMISE_NODE, owner: resolveOwner(), - 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)), @@ -145,7 +145,7 @@ export function initAsyncDebugInfo(): void { node = ({ tag: IO_NODE, owner: resolveOwner(), - 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, @@ -160,7 +160,7 @@ export function initAsyncDebugInfo(): void { node = ({ tag: IO_NODE, owner: resolveOwner(), - stack: parseStackTrace(new Error(), 1), + stack: parseStackTrace(new Error(), 3), start: performance.now(), end: -1.1, // Only set when pinged. promise: null, diff --git a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js index ca801046e666e..7b5a284bd916d 100644 --- a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js +++ b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js @@ -386,7 +386,9 @@ describe('ReactFlightAsyncDebugInfo', () => { ], "start": 0, "value": { - "value": undefined, + "value": [ + , + ], }, }, "env": "Server", @@ -491,7 +493,7 @@ describe('ReactFlightAsyncDebugInfo', () => { ], "start": 0, "value": { - "value": undefined, + "status": "halted", }, }, "env": "Server", @@ -591,9 +593,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 555, + 557, 40, - 536, + 538, 49, ], ], @@ -615,9 +617,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 555, + 557, 40, - 536, + 538, 49, ], ], @@ -634,17 +636,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 538, + 540, 13, - 537, + 539, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 543, + 545, 36, - 542, + 544, 5, ], ], @@ -663,9 +665,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 555, + 557, 40, - 536, + 538, 49, ], ], @@ -674,17 +676,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 538, + 540, 13, - 537, + 539, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 543, + 545, 36, - 542, + 544, 5, ], ], @@ -704,9 +706,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 545, + 547, 60, - 542, + 544, 5, ], ], @@ -725,9 +727,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 555, + 557, 40, - 536, + 538, 49, ], ], @@ -744,17 +746,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 538, + 540, 13, - 537, + 539, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 544, + 546, 22, - 542, + 544, 5, ], ], @@ -773,9 +775,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 545, + 547, 60, - 542, + 544, 5, ], ], @@ -784,9 +786,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "InnerComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 551, + 553, 40, - 548, + 550, 5, ], ], @@ -849,9 +851,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 818, + 820, 109, - 805, + 807, 67, ], ], @@ -870,9 +872,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 818, + 820, 109, - 805, + 807, 67, ], ], @@ -881,9 +883,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 808, + 810, 7, - 806, + 808, 5, ], ], @@ -945,9 +947,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 914, + 916, 109, - 905, + 907, 94, ], ], @@ -1018,9 +1020,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 987, + 989, 109, - 963, + 965, 50, ], ], @@ -1102,9 +1104,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1071, + 1073, 109, - 1054, + 1056, 63, ], ], @@ -1129,9 +1131,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1067, + 1069, 24, - 1066, + 1068, 5, ], ], @@ -1161,9 +1163,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1067, + 1069, 24, - 1066, + 1068, 5, ], ], @@ -1180,17 +1182,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1056, + 1058, 13, - 1055, + 1057, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1062, + 1064, 24, - 1061, + 1063, 5, ], ], @@ -1217,9 +1219,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1067, + 1069, 24, - 1066, + 1068, 5, ], ], @@ -1228,17 +1230,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1056, + 1058, 13, - 1055, + 1057, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1062, + 1064, 24, - 1061, + 1063, 5, ], ], @@ -1271,9 +1273,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1067, + 1069, 24, - 1066, + 1068, 5, ], ], @@ -1290,17 +1292,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1057, + 1059, 13, - 1055, + 1057, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1062, + 1064, 18, - 1061, + 1063, 5, ], ], @@ -1327,9 +1329,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1067, + 1069, 24, - 1066, + 1068, 5, ], ], @@ -1338,17 +1340,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1057, + 1059, 13, - 1055, + 1057, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1062, + 1064, 18, - 1061, + 1063, 5, ], ], @@ -1423,9 +1425,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1387, + 1389, 40, - 1370, + 1372, 62, ], ], @@ -1447,9 +1449,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1387, + 1389, 40, - 1370, + 1372, 62, ], ], @@ -1466,17 +1468,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1372, + 1374, 13, - 1371, + 1373, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1382, + 1384, 13, - 1381, + 1383, 5, ], ], @@ -1495,9 +1497,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1387, + 1389, 40, - 1370, + 1372, 62, ], ], @@ -1506,17 +1508,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1372, + 1374, 13, - 1371, + 1373, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1382, + 1384, 13, - 1381, + 1383, 5, ], ], @@ -1536,9 +1538,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1383, + 1385, 60, - 1381, + 1383, 5, ], ], @@ -1560,9 +1562,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1387, + 1389, 40, - 1370, + 1372, 62, ], ], @@ -1579,17 +1581,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1372, + 1374, 13, - 1371, + 1373, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1382, + 1384, 13, - 1381, + 1383, 5, ], ], @@ -1608,9 +1610,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1383, + 1385, 60, - 1381, + 1383, 5, ], ], @@ -1619,9 +1621,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Child", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1377, + 1379, 28, - 1376, + 1378, 5, ], ], @@ -1692,9 +1694,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1656, + 1658, 40, - 1640, + 1642, 57, ], ], @@ -1716,9 +1718,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1656, + 1658, 40, - 1640, + 1642, 57, ], ], @@ -1735,17 +1737,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1642, + 1644, 13, - 1641, + 1643, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1651, + 1653, 23, - 1650, + 1652, 5, ], ], @@ -1764,9 +1766,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1656, + 1658, 40, - 1640, + 1642, 57, ], ], @@ -1775,17 +1777,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1642, + 1644, 13, - 1641, + 1643, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1651, + 1653, 23, - 1650, + 1652, 5, ], ], @@ -1805,9 +1807,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1652, + 1654, 60, - 1650, + 1652, 5, ], ], @@ -1826,9 +1828,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1656, + 1658, 40, - 1640, + 1642, 57, ], ], @@ -1845,17 +1847,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1642, + 1644, 13, - 1641, + 1643, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1651, + 1653, 23, - 1650, + 1652, 5, ], ], @@ -1934,9 +1936,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1898, + 1900, 40, - 1880, + 1882, 80, ], ], @@ -1958,9 +1960,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1898, + 1900, 40, - 1880, + 1882, 80, ], ], @@ -1977,17 +1979,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1888, + 1890, 13, - 1886, + 1888, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1893, + 1895, 13, - 1892, + 1894, 5, ], ], @@ -2006,9 +2008,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1898, + 1900, 40, - 1880, + 1882, 80, ], ], @@ -2017,17 +2019,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1888, + 1890, 13, - 1886, + 1888, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1893, + 1895, 13, - 1892, + 1894, 5, ], ], @@ -2049,9 +2051,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1898, + 1900, 40, - 1880, + 1882, 80, ], ], @@ -2068,25 +2070,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1882, + 1884, 13, - 1881, + 1883, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1887, + 1889, 15, - 1886, + 1888, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1893, + 1895, 13, - 1892, + 1894, 5, ], ], @@ -2105,9 +2107,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1898, + 1900, 40, - 1880, + 1882, 80, ], ], @@ -2116,25 +2118,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1882, + 1884, 13, - 1881, + 1883, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1887, + 1889, 15, - 1886, + 1888, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1893, + 1895, 13, - 1892, + 1894, 5, ], ], @@ -2156,9 +2158,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1898, + 1900, 40, - 1880, + 1882, 80, ], ], @@ -2175,9 +2177,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1883, + 1885, 13, - 1881, + 1883, 5, ], ], @@ -2196,9 +2198,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1898, + 1900, 40, - 1880, + 1882, 80, ], ], @@ -2207,9 +2209,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1883, + 1885, 13, - 1881, + 1883, 5, ], ], From 44e3d197ada9fe7f9ac455d9bb5f7e67b00d91eb Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 21 Jun 2025 20:18:50 -0400 Subject: [PATCH 6/8] Ignore properties prefixed with `_` by convention When printing common classes it gets noisy to show internals. --- packages/react-client/src/ReactFlightPerformanceTrack.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index 19a3117f6220f..41d6564b4774f 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -54,7 +54,7 @@ function addObjectToProperties( indent: number, ): void { for (const key in object) { - if (hasOwnProperty.call(object, key)) { + if (hasOwnProperty.call(object, key) && key[0] !== '_') { const value = object[key]; addValueToProperties(key, value, properties, indent); } From 8de9450a42f268a7612f523ee43d4996b145b4db Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 22 Jun 2025 00:50:30 -0400 Subject: [PATCH 7/8] Special case array of tuples presentation --- .../src/ReactFlightPerformanceTrack.js | 55 +++++++++++++++---- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index 41d6564b4774f..362b4fa490295 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -20,6 +20,7 @@ import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess'; import hasOwnProperty from 'shared/hasOwnProperty'; +import isArray from 'shared/isArray'; const supportsUserTiming = enableProfilerTimer && @@ -32,20 +33,39 @@ const supportsUserTiming = const IO_TRACK = 'Server Requests ⚛'; const COMPONENTS_TRACK = 'Server Components ⚛'; -function isPrimitiveArray(array: Object) { +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) { - return false; - } - if (typeof value === 'function') { - return false; - } - if (typeof value === 'string' && value.length > 50) { - return false; + 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 true; + return kind; } function addObjectToProperties( @@ -77,9 +97,20 @@ function addValueToProperties( // $FlowFixMe[method-unbinding] const objectToString = Object.prototype.toString.call(value); const objectName = objectToString.slice(8, objectToString.length - 1); - if (objectName === 'Array' && isPrimitiveArray(value)) { - desc = JSON.stringify(value); - break; + if (objectName === 'Array') { + const array: Array = (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; + } } properties.push([ '\xa0\xa0'.repeat(indent) + propertyName, From 15d22ad4046d8ab05d71e84a4684e92c7aeb605b Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 22 Jun 2025 01:11:27 -0400 Subject: [PATCH 8/8] Print constructor name --- packages/react-client/src/ReactFlightPerformanceTrack.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index 362b4fa490295..76b23a15fdcd7 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -96,7 +96,7 @@ function addValueToProperties( } else { // $FlowFixMe[method-unbinding] const objectToString = Object.prototype.toString.call(value); - const objectName = objectToString.slice(8, objectToString.length - 1); + let objectName = objectToString.slice(8, objectToString.length - 1); if (objectName === 'Array') { const array: Array = (value: any); const kind = getArrayKind(array); @@ -112,6 +112,12 @@ function addValueToProperties( 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,