From 12eec85229dcbb348035877264e7b5ade043aee7 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 26 Jun 2025 14:15:50 -0400 Subject: [PATCH 1/6] Add completedDebugChunks queue --- .../react-server/src/ReactFlightServer.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 77f25f6e1b405..bdffe7156be53 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -454,6 +454,7 @@ export type Request = { // Profiling-only timeOrigin: number, // DEV-only + completedDebugChunks: Array, environmentName: () => string, filterStackFrame: (url: string, functionName: string) => boolean, didWarnForKey: null | WeakSet, @@ -567,6 +568,7 @@ function RequestInstance( this.onFatalError = onFatalError; if (__DEV__) { + this.completedDebugChunks = ([]: Array); this.environmentName = environmentName === undefined ? () => 'Server' @@ -5214,6 +5216,24 @@ function flushCompletedChunks( } } errorChunks.splice(0, i); + + // Next comes debug meta data. + // TODO: Move this first since other chunks are blocked on their debug info. I'm only testing that the client is resilient. + if (__DEV__) { + const debugChunks = request.completedDebugChunks; + i = 0; + for (; i < debugChunks.length; i++) { + request.pendingChunks--; + const chunk = debugChunks[i]; + const keepWriting: boolean = writeChunkAndReturn(destination, chunk); + if (!keepWriting) { + request.destination = null; + i++; + break; + } + } + debugChunks.splice(0, i); + } } finally { request.flushScheduled = false; completeWriting(destination); From 1510ae97318dcd8307c5c2ee56dac6e299ae272b Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 26 Jun 2025 14:59:19 -0400 Subject: [PATCH 2/6] Write debug info to the new chunks This requires some forks to avoid contanimating the main stream and to avoid the main stream having hard dependencies on the debug stream. --- .../react-server/src/ReactFlightServer.js | 287 ++++++++++++++---- 1 file changed, 229 insertions(+), 58 deletions(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index bdffe7156be53..98ddb8c71abb2 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -454,7 +454,7 @@ export type Request = { // Profiling-only timeOrigin: number, // DEV-only - completedDebugChunks: Array, + completedDebugChunks: Array, environmentName: () => string, filterStackFrame: (url: string, functionName: string) => boolean, didWarnForKey: null | WeakSet, @@ -729,7 +729,7 @@ function serializeDebugThenable( } else { // We don't log these errors since they didn't actually throw into Flight. const digest = ''; - emitErrorChunk(request, id, digest, x); + emitErrorChunk(request, id, digest, x, true); } return ref; } @@ -779,7 +779,7 @@ function serializeDebugThenable( } else { // We don't log these errors since they didn't actually throw into Flight. const digest = ''; - emitErrorChunk(request, id, digest, reason); + emitErrorChunk(request, id, digest, reason, true); } enqueueFlush(request); }, @@ -2463,7 +2463,7 @@ function serializeClientReference( resolveClientReferenceMetadata(request.bundlerConfig, clientReference); request.pendingChunks++; const importId = request.nextChunkId++; - emitImportChunk(request, importId, clientReferenceMetadata); + emitImportChunk(request, importId, clientReferenceMetadata, false); writtenClientReferences.set(clientReferenceKey, importId); if (parent[0] === REACT_ELEMENT_TYPE && parentPropertyName === '1') { // If we're encoding the "type" of an element, we can refer @@ -2478,7 +2478,56 @@ function serializeClientReference( request.pendingChunks++; const errorId = request.nextChunkId++; const digest = logRecoverableError(request, x, null); - emitErrorChunk(request, errorId, digest, x); + emitErrorChunk(request, errorId, digest, x, false); + return serializeByValueID(errorId); + } +} + +function serializeDebugClientReference( + request: Request, + parent: + | {+[propertyName: string | number]: ReactClientValue} + | $ReadOnlyArray, + parentPropertyName: string, + clientReference: ClientReference, +): string { + // Like serializeDebugClientReference but it doesn't dedupe in the regular set + // and it writes to completedDebugChunk instead of imports. + const clientReferenceKey: ClientReferenceKey = + getClientReferenceKey(clientReference); + const writtenClientReferences = request.writtenClientReferences; + const existingId = writtenClientReferences.get(clientReferenceKey); + if (existingId !== undefined) { + if (parent[0] === REACT_ELEMENT_TYPE && parentPropertyName === '1') { + // If we're encoding the "type" of an element, we can refer + // to that by a lazy reference instead of directly since React + // knows how to deal with lazy values. This lets us suspend + // on this component rather than its parent until the code has + // loaded. + return serializeLazyID(existingId); + } + return serializeByValueID(existingId); + } + try { + const clientReferenceMetadata: ClientReferenceMetadata = + resolveClientReferenceMetadata(request.bundlerConfig, clientReference); + request.pendingChunks++; + const importId = request.nextChunkId++; + emitImportChunk(request, importId, clientReferenceMetadata, true); + if (parent[0] === REACT_ELEMENT_TYPE && parentPropertyName === '1') { + // If we're encoding the "type" of an element, we can refer + // to that by a lazy reference instead of directly since React + // knows how to deal with lazy values. This lets us suspend + // on this component rather than its parent until the code has + // loaded. + return serializeLazyID(importId); + } + return serializeByValueID(importId); + } catch (x) { + request.pendingChunks++; + const errorId = request.nextChunkId++; + const digest = logRecoverableError(request, x, null); + emitErrorChunk(request, errorId, digest, x, true); return serializeByValueID(errorId); } } @@ -2573,7 +2622,14 @@ function serializeTemporaryReference( function serializeLargeTextString(request: Request, text: string): string { request.pendingChunks++; const textId = request.nextChunkId++; - emitTextChunk(request, textId, text); + emitTextChunk(request, textId, text, false); + return serializeByValueID(textId); +} + +function serializeDebugLargeTextString(request: Request, text: string): string { + request.pendingChunks++; + const textId = request.nextChunkId++; + emitTextChunk(request, textId, text, true); return serializeByValueID(textId); } @@ -2592,6 +2648,16 @@ function serializeFormData(request: Request, formData: FormData): string { return '$K' + id.toString(16); } +function serializeDebugFormData(request: Request, formData: FormData): string { + const entries = Array.from(formData.entries()); + const id = outlineDebugModel( + request, + {objectLimit: entries.length * 2 + 1}, + (entries: any), + ); + return '$K' + id.toString(16); +} + function serializeSet(request: Request, set: Set): string { const entries = Array.from(set); const id = outlineModel(request, entries); @@ -2661,10 +2727,55 @@ function serializeTypedArray( ): string { request.pendingChunks++; const bufferId = request.nextChunkId++; - emitTypedArrayChunk(request, bufferId, tag, typedArray); + emitTypedArrayChunk(request, bufferId, tag, typedArray, false); + return serializeByValueID(bufferId); +} + +function serializeDebugTypedArray( + request: Request, + tag: string, + typedArray: $ArrayBufferView, +): string { + request.pendingChunks++; + const bufferId = request.nextChunkId++; + emitTypedArrayChunk(request, bufferId, tag, typedArray, true); return serializeByValueID(bufferId); } +function serializeDebugBlob(request: Request, blob: Blob): string { + const model: Array = [blob.type]; + const reader = blob.stream().getReader(); + const id = request.nextChunkId++; + function progress( + entry: {done: false, value: Uint8Array} | {done: true, value: void}, + ): Promise | void { + if (entry.done) { + emitOutlinedDebugModelChunk( + request, + id, + {objectLimit: model.length + 2}, + model, + ); + enqueueFlush(request); + return; + } + // TODO: Emit the chunk early and refer to it later by dedupe. + model.push(entry.value); + // $FlowFixMe[incompatible-call] + return reader.read().then(progress).catch(error); + } + function error(reason: mixed) { + const digest = ''; + emitErrorChunk(request, id, digest, reason, true); + enqueueFlush(request); + // $FlowFixMe should be able to pass mixed + reader.cancel(reason).then(noop, noop); + } + // $FlowFixMe[incompatible-call] + reader.read().then(progress).catch(error); + return '$B' + id.toString(16); +} + function serializeBlob(request: Request, blob: Blob): string { const model: Array = [blob.type]; const newTask = createTask( @@ -2851,7 +2962,7 @@ function renderModel( emitPostponeChunk(request, errorId, postponeInstance); } else { const digest = logRecoverableError(request, x, task); - emitErrorChunk(request, errorId, digest, x); + emitErrorChunk(request, errorId, digest, x, false); } if (wasReactNode) { // We'll replace this element with a lazy reference that throws on the client @@ -3602,11 +3713,48 @@ function serializeErrorValue(request: Request, error: Error): string { } } +function serializeDebugErrorValue(request: Request, error: Error): string { + if (__DEV__) { + let name: string = 'Error'; + let message: string; + let stack: ReactStackTrace; + let env = (0, request.environmentName)(); + try { + name = error.name; + // eslint-disable-next-line react-internal/safe-string-coercion + message = String(error.message); + stack = filterStackTrace(request, parseStackTrace(error, 0)); + const errorEnv = (error: any).environmentName; + if (typeof errorEnv === 'string') { + // This probably came from another FlightClient as a pass through. + // Keep the environment name. + env = errorEnv; + } + } catch (x) { + message = 'An error occurred but serializing the error message failed.'; + stack = []; + } + const errorInfo: ReactErrorInfoDev = {name, message, stack, env}; + const id = outlineDebugModel( + request, + {objectLimit: stack.length + 2}, + errorInfo, + ); + return '$Z' + id.toString(16); + } else { + // In prod we don't emit any information about this Error object to avoid + // unintentional leaks. Since this doesn't actually throw on the server + // we don't go through onError and so don't register any digest neither. + return '$Z'; + } +} + function emitErrorChunk( request: Request, id: number, digest: string, error: mixed, + debug: boolean, ): void { let errorInfo: ReactErrorInfo; if (__DEV__) { @@ -3644,19 +3792,28 @@ function emitErrorChunk( } const row = serializeRowHeader('E', id) + stringify(errorInfo) + '\n'; const processedChunk = stringToChunk(row); - request.completedErrorChunks.push(processedChunk); + if (__DEV__ && debug) { + request.completedDebugChunks.push(processedChunk); + } else { + request.completedErrorChunks.push(processedChunk); + } } function emitImportChunk( request: Request, id: number, clientReferenceMetadata: ClientReferenceMetadata, + debug: boolean, ): void { // $FlowFixMe[incompatible-type] stringify can return null const json: string = stringify(clientReferenceMetadata); const row = serializeRowHeader('I', id) + json + '\n'; const processedChunk = stringToChunk(row); - request.completedImportChunks.push(processedChunk); + if (__DEV__ && debug) { + request.completedDebugChunks.push(processedChunk); + } else { + request.completedImportChunks.push(processedChunk); + } } function emitHintChunk( @@ -3694,7 +3851,7 @@ function emitDebugHaltChunk(request: Request, id: number): void { // even when the client stream is closed. We use just the lack of data to indicate this. const row = id.toString(16) + ':\n'; const processedChunk = stringToChunk(row); - request.completedRegularChunks.push(processedChunk); + request.completedDebugChunks.push(processedChunk); } function emitDebugChunk( @@ -3717,7 +3874,7 @@ function emitDebugChunk( const json: string = serializeDebugModel(request, 500, debugInfo); const row = serializeRowHeader('D', id) + json + '\n'; const processedChunk = stringToChunk(row); - request.completedRegularChunks.push(processedChunk); + request.completedDebugChunks.push(processedChunk); } function outlineComponentInfo( @@ -3841,7 +3998,7 @@ function emitIOInfoChunk( const json: string = serializeDebugModel(request, objectLimit, debugIOInfo); const row = id.toString(16) + ':J' + json + '\n'; const processedChunk = stringToChunk(row); - request.completedRegularChunks.push(processedChunk); + request.completedDebugChunks.push(processedChunk); } function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void { @@ -3951,6 +4108,7 @@ function emitTypedArrayChunk( id: number, tag: string, typedArray: $ArrayBufferView, + debug: boolean, ): void { if (enableTaint) { if (TaintRegistryByteLengths.has(typedArray.byteLength)) { @@ -3970,10 +4128,19 @@ function emitTypedArrayChunk( const binaryLength = byteLengthOfBinaryChunk(binaryChunk); const row = id.toString(16) + ':' + tag + binaryLength.toString(16) + ','; const headerChunk = stringToChunk(row); - request.completedRegularChunks.push(headerChunk, binaryChunk); + if (__DEV__ && debug) { + request.completedDebugChunks.push(headerChunk, binaryChunk); + } else { + request.completedRegularChunks.push(headerChunk, binaryChunk); + } } -function emitTextChunk(request: Request, id: number, text: string): void { +function emitTextChunk( + request: Request, + id: number, + text: string, + debug: boolean, +): void { if (byteLengthOfChunk === null) { // eslint-disable-next-line react-internal/prod-error-codes throw new Error( @@ -3985,7 +4152,11 @@ function emitTextChunk(request: Request, id: number, text: string): void { const binaryLength = byteLengthOfChunk(textChunk); const row = id.toString(16) + ':T' + binaryLength.toString(16) + ','; const headerChunk = stringToChunk(row); - request.completedRegularChunks.push(headerChunk, textChunk); + if (__DEV__ && debug) { + request.completedDebugChunks.push(headerChunk, textChunk); + } else { + request.completedRegularChunks.push(headerChunk, textChunk); + } } function serializeEval(source: string): string { @@ -4029,7 +4200,7 @@ function renderDebugModel( // This might be confusing though because on the Server it won't actually // be this value, so if you're debugging client references maybe you'd be // better with a place holder. - return serializeClientReference( + return serializeDebugClientReference( request, parent, parentPropertyName, @@ -4111,7 +4282,7 @@ function renderDebugModel( } if (counter.objectLimit <= 0 && !doNotLimit.has(value)) { - // We've reached our max number of objects to serialize across the wire so we serialize this + // We've reached our max number of objects to serializeDebug across the wire so we serializeDebug this // as a marker so that the client can error or lazy load this when accessed by the console. return serializeDeferredObject(request, value); } @@ -4193,65 +4364,65 @@ function renderDebugModel( } // TODO: FormData is not available in old Node. Remove the typeof later. if (typeof FormData === 'function' && value instanceof FormData) { - return serializeFormData(request, value); + return serializeDebugFormData(request, value); } if (value instanceof Error) { - return serializeErrorValue(request, value); + return serializeDebugErrorValue(request, value); } if (value instanceof ArrayBuffer) { - return serializeTypedArray(request, 'A', new Uint8Array(value)); + return serializeDebugTypedArray(request, 'A', new Uint8Array(value)); } if (value instanceof Int8Array) { // char - return serializeTypedArray(request, 'O', value); + return serializeDebugTypedArray(request, 'O', value); } if (value instanceof Uint8Array) { // unsigned char - return serializeTypedArray(request, 'o', value); + return serializeDebugTypedArray(request, 'o', value); } if (value instanceof Uint8ClampedArray) { // unsigned clamped char - return serializeTypedArray(request, 'U', value); + return serializeDebugTypedArray(request, 'U', value); } if (value instanceof Int16Array) { // sort - return serializeTypedArray(request, 'S', value); + return serializeDebugTypedArray(request, 'S', value); } if (value instanceof Uint16Array) { // unsigned short - return serializeTypedArray(request, 's', value); + return serializeDebugTypedArray(request, 's', value); } if (value instanceof Int32Array) { // long - return serializeTypedArray(request, 'L', value); + return serializeDebugTypedArray(request, 'L', value); } if (value instanceof Uint32Array) { // unsigned long - return serializeTypedArray(request, 'l', value); + return serializeDebugTypedArray(request, 'l', value); } if (value instanceof Float32Array) { // float - return serializeTypedArray(request, 'G', value); + return serializeDebugTypedArray(request, 'G', value); } if (value instanceof Float64Array) { // double - return serializeTypedArray(request, 'g', value); + return serializeDebugTypedArray(request, 'g', value); } if (value instanceof BigInt64Array) { // number - return serializeTypedArray(request, 'M', value); + return serializeDebugTypedArray(request, 'M', value); } if (value instanceof BigUint64Array) { // unsigned number // We use "m" instead of "n" since JSON can start with "null" - return serializeTypedArray(request, 'm', value); + return serializeDebugTypedArray(request, 'm', value); } if (value instanceof DataView) { - return serializeTypedArray(request, 'V', value); + return serializeDebugTypedArray(request, 'V', value); } // TODO: Blob is not available in old Node. Remove the typeof check later. if (typeof Blob === 'function' && value instanceof Blob) { - return serializeBlob(request, value); + return serializeDebugBlob(request, value); } const iteratorFn = getIteratorFn(value); @@ -4304,7 +4475,7 @@ function renderDebugModel( if (value.length >= 1024) { // Large strings are counted towards the object limit. if (counter.objectLimit <= 0) { - // We've reached our max number of objects to serialize across the wire so we serialize this + // We've reached our max number of objects to serializeDebug across the wire so we serializeDebug this // as a marker so that the client can error or lazy load this when accessed by the console. return serializeDeferredObject(request, value); } @@ -4312,7 +4483,7 @@ function renderDebugModel( // For large strings, we encode them outside the JSON payload so that we // don't have to double encode and double parse the strings. This can also // be more compact in case the string has a lot of escaped characters. - return serializeLargeTextString(request, value); + return serializeDebugLargeTextString(request, value); } return escapeStringValue(value); } @@ -4331,7 +4502,7 @@ function renderDebugModel( if (typeof value === 'function') { if (isClientReference(value)) { - return serializeClientReference( + return serializeDebugClientReference( request, parent, parentPropertyName, @@ -4364,7 +4535,7 @@ function renderDebugModel( request.pendingChunks++; const id = request.nextChunkId++; const processedChunk = encodeReferenceChunk(request, id, serializedValue); - request.completedRegularChunks.push(processedChunk); + request.completedDebugChunks.push(processedChunk); const reference = serializeByValueID(id); writtenDebugObjects.set(value, reference); return reference; @@ -4502,7 +4673,7 @@ function emitOutlinedDebugModelChunk( const row = id.toString(16) + ':' + json + '\n'; const processedChunk = stringToChunk(row); - request.completedRegularChunks.push(processedChunk); + request.completedDebugChunks.push(processedChunk); } function outlineDebugModel( @@ -4562,7 +4733,7 @@ function emitConsoleChunk( } const row = ':W' + json + '\n'; const processedChunk = stringToChunk(row); - request.completedRegularChunks.push(processedChunk); + request.completedDebugChunks.push(processedChunk); } function emitTimeOriginChunk(request: Request, timeOrigin: number): void { @@ -4573,7 +4744,7 @@ function emitTimeOriginChunk(request: Request, timeOrigin: number): void { const row = ':N' + timeOrigin + '\n'; const processedChunk = stringToChunk(row); // TODO: Move to its own priority queue. - request.completedRegularChunks.push(processedChunk); + request.completedDebugChunks.push(processedChunk); } function forwardDebugInfo( @@ -4785,7 +4956,7 @@ function emitTimingChunk( serializeRowHeader('D', id) + '{"time":' + relativeTimestamp + '}\n'; const processedChunk = stringToChunk(row); // TODO: Move to its own priority queue. - request.completedRegularChunks.push(processedChunk); + request.completedDebugChunks.push(processedChunk); } function advanceTaskTime( @@ -4841,71 +5012,71 @@ function emitChunk( throwTaintViolation(tainted.message); } } - emitTextChunk(request, id, value); + emitTextChunk(request, id, value, false); return; } if (value instanceof ArrayBuffer) { - emitTypedArrayChunk(request, id, 'A', new Uint8Array(value)); + emitTypedArrayChunk(request, id, 'A', new Uint8Array(value), false); return; } if (value instanceof Int8Array) { // char - emitTypedArrayChunk(request, id, 'O', value); + emitTypedArrayChunk(request, id, 'O', value, false); return; } if (value instanceof Uint8Array) { // unsigned char - emitTypedArrayChunk(request, id, 'o', value); + emitTypedArrayChunk(request, id, 'o', value, false); return; } if (value instanceof Uint8ClampedArray) { // unsigned clamped char - emitTypedArrayChunk(request, id, 'U', value); + emitTypedArrayChunk(request, id, 'U', value, false); return; } if (value instanceof Int16Array) { // sort - emitTypedArrayChunk(request, id, 'S', value); + emitTypedArrayChunk(request, id, 'S', value, false); return; } if (value instanceof Uint16Array) { // unsigned short - emitTypedArrayChunk(request, id, 's', value); + emitTypedArrayChunk(request, id, 's', value, false); return; } if (value instanceof Int32Array) { // long - emitTypedArrayChunk(request, id, 'L', value); + emitTypedArrayChunk(request, id, 'L', value, false); return; } if (value instanceof Uint32Array) { // unsigned long - emitTypedArrayChunk(request, id, 'l', value); + emitTypedArrayChunk(request, id, 'l', value, false); return; } if (value instanceof Float32Array) { // float - emitTypedArrayChunk(request, id, 'G', value); + emitTypedArrayChunk(request, id, 'G', value, false); return; } if (value instanceof Float64Array) { // double - emitTypedArrayChunk(request, id, 'g', value); + emitTypedArrayChunk(request, id, 'g', value, false); return; } if (value instanceof BigInt64Array) { // number - emitTypedArrayChunk(request, id, 'M', value); + emitTypedArrayChunk(request, id, 'M', value, false); return; } if (value instanceof BigUint64Array) { // unsigned number // We use "m" instead of "n" since JSON can start with "null" - emitTypedArrayChunk(request, id, 'm', value); + emitTypedArrayChunk(request, id, 'm', value, false); return; } if (value instanceof DataView) { - emitTypedArrayChunk(request, id, 'V', value); + emitTypedArrayChunk(request, id, 'V', value, false); return; } // For anything else we need to try to serialize it using JSON. @@ -4932,7 +5103,7 @@ function erroredTask(request: Request, task: Task, error: mixed): void { emitPostponeChunk(request, task.id, postponeInstance); } else { const digest = logRecoverableError(request, error, task); - emitErrorChunk(request, task.id, digest, error); + emitErrorChunk(request, task.id, digest, error, false); } request.abortableTasks.delete(task); callOnAllReadyIfReady(request); @@ -5379,7 +5550,7 @@ export function abort(request: Request, reason: mixed): void { const errorId = request.nextChunkId++; request.fatalError = errorId; request.pendingChunks++; - emitErrorChunk(request, errorId, digest, error); + emitErrorChunk(request, errorId, digest, error, false); abortableTasks.forEach(task => abortTask(task, request, errorId)); abortableTasks.clear(); callOnAllReadyIfReady(request); From 1091f7179932d4d9f01ef84303ca93ef3ba85297 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 26 Jun 2025 15:37:56 -0400 Subject: [PATCH 3/6] Write the component's stack to debug chunks We need to also ensure that this can be loaded out of order if any references inside the element like a future reference to the stack isn't yet resolved. We already kind of handled this for owner but since owner can also be set to the _debugRootOwner and the owner informs the stack, we need to wait until we have both values before initializing. --- .../react-client/src/ReactFlightClient.js | 149 ++++++++++-------- .../react-server/src/ReactFlightServer.js | 33 ++-- 2 files changed, 105 insertions(+), 77 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 3f0035337c073..43437005c2d39 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -764,6 +764,76 @@ function getTaskName(type: mixed): string { } } +function initializeElement(response: Response, element: any): void { + if (!__DEV__) { + return; + } + const stack = element._debugStack; + const owner = element._owner; + if (owner === null) { + element._owner = response._debugRootOwner; + } + let env = response._rootEnvironmentName; + if (owner !== null && owner.env != null) { + // Interestingly we don't actually have the environment name of where + // this JSX was created if it doesn't have an owner but if it does + // it must be the same environment as the owner. We could send it separately + // but it seems a bit unnecessary for this edge case. + env = owner.env; + } + let normalizedStackTrace: null | Error = null; + if (owner === null && response._debugRootStack != null) { + // We override the stack if we override the owner since the stack where the root JSX + // was created on the server isn't very useful but where the request was made is. + normalizedStackTrace = response._debugRootStack; + } else if (stack !== null) { + // We create a fake stack and then create an Error object inside of it. + // This means that the stack trace is now normalized into the native format + // of the browser and the stack frames will have been registered with + // source mapping information. + // This can unfortunately happen within a user space callstack which will + // remain on the stack. + normalizedStackTrace = createFakeJSXCallStackInDEV(response, stack, env); + } + element._debugStack = normalizedStackTrace; + let task: null | ConsoleTask = null; + if (supportsCreateTask && stack !== null) { + const createTaskFn = (console: any).createTask.bind( + console, + getTaskName(element.type), + ); + const callStack = buildFakeCallStack( + response, + stack, + env, + false, + createTaskFn, + ); + // This owner should ideally have already been initialized to avoid getting + // user stack frames on the stack. + const ownerTask = + owner === null ? null : initializeFakeTask(response, owner); + if (ownerTask === null) { + const rootTask = response._debugRootTask; + if (rootTask != null) { + task = rootTask.run(callStack); + } else { + task = callStack(); + } + } else { + task = ownerTask.run(callStack); + } + } + element._debugTask = task; + + // This owner should ideally have already been initialized to avoid getting + // user stack frames on the stack. + if (owner !== null) { + initializeFakeStack(response, owner); + } + Object.freeze(element.props); +} + function createElement( response: Response, type: mixed, @@ -783,7 +853,7 @@ function createElement( type, key, props, - _owner: __DEV__ && owner === null ? response._debugRootOwner : owner, + _owner: owner, }: any); Object.defineProperty(element, 'ref', { enumerable: false, @@ -821,75 +891,18 @@ function createElement( writable: true, value: null, }); - let env = response._rootEnvironmentName; - if (owner !== null && owner.env != null) { - // Interestingly we don't actually have the environment name of where - // this JSX was created if it doesn't have an owner but if it does - // it must be the same environment as the owner. We could send it separately - // but it seems a bit unnecessary for this edge case. - env = owner.env; - } - let normalizedStackTrace: null | Error = null; - if (owner === null && response._debugRootStack != null) { - // We override the stack if we override the owner since the stack where the root JSX - // was created on the server isn't very useful but where the request was made is. - normalizedStackTrace = response._debugRootStack; - } else if (stack !== null) { - // We create a fake stack and then create an Error object inside of it. - // This means that the stack trace is now normalized into the native format - // of the browser and the stack frames will have been registered with - // source mapping information. - // This can unfortunately happen within a user space callstack which will - // remain on the stack. - normalizedStackTrace = createFakeJSXCallStackInDEV(response, stack, env); - } Object.defineProperty(element, '_debugStack', { configurable: false, enumerable: false, writable: true, - value: normalizedStackTrace, + value: stack, }); - - let task: null | ConsoleTask = null; - if (supportsCreateTask && stack !== null) { - const createTaskFn = (console: any).createTask.bind( - console, - getTaskName(type), - ); - const callStack = buildFakeCallStack( - response, - stack, - env, - false, - createTaskFn, - ); - // This owner should ideally have already been initialized to avoid getting - // user stack frames on the stack. - const ownerTask = - owner === null ? null : initializeFakeTask(response, owner); - if (ownerTask === null) { - const rootTask = response._debugRootTask; - if (rootTask != null) { - task = rootTask.run(callStack); - } else { - task = callStack(); - } - } else { - task = ownerTask.run(callStack); - } - } Object.defineProperty(element, '_debugTask', { configurable: false, enumerable: false, writable: true, - value: task, + value: null, }); - - // This owner should ideally have already been initialized to avoid getting - // user stack frames on the stack. - if (owner !== null) { - initializeFakeStack(response, owner); - } } if (initializingHandler !== null) { @@ -905,6 +918,7 @@ function createElement( handler.value, ); if (__DEV__) { + initializeElement(response, element); // Conceptually the error happened inside this Element but right before // it was rendered. We don't have a client side component to render but // we can add some DebugInfo to explain that this was conceptually a @@ -933,15 +947,17 @@ function createElement( handler.value = element; handler.chunk = blockedChunk; if (__DEV__) { - const freeze = Object.freeze.bind(Object, element.props); - blockedChunk.then(freeze, freeze); + /// After we have initialized any blocked references, initialize stack etc. + const init = initializeElement.bind(null, response, element); + blockedChunk.then(init, init); } return createLazyChunkWrapper(blockedChunk); } - } else if (__DEV__) { + } + if (__DEV__) { // TODO: We should be freezing the element but currently, we might write into // _debugInfo later. We could move it into _store which remains mutable. - Object.freeze(element.props); + initializeElement(response, element); } return element; @@ -1055,6 +1071,11 @@ function waitForReference( element._owner = mappedValue; } break; + case '5': + if (__DEV__) { + element._debugStack = mappedValue; + } + break; } } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 98ddb8c71abb2..0bb23ef02e190 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1778,25 +1778,32 @@ function renderClientElement( } else if (keyPath !== null) { key = keyPath + ',' + key; } + let debugOwner = null; + let debugStack = null; if (__DEV__) { - if (task.debugOwner !== null) { + debugOwner = task.debugOwner; + if (debugOwner !== null) { // Ensure we outline this owner if it is the first time we see it. // So that we can refer to it directly. - outlineComponentInfo(request, task.debugOwner); + outlineComponentInfo(request, debugOwner); + } + if (task.debugStack !== null) { + // Outline the debug stack so that we write to the completedDebugChunks instead. + debugStack = filterStackTrace( + request, + parseStackTrace(task.debugStack, 1), + ); + const id = outlineDebugModel( + request, + {objectLimit: debugStack.length * 2 + 1}, + debugStack, + ); + // We also store this in the main dedupe set so that it can be referenced by inline React Elements. + request.writtenObjects.set(debugStack, serializeByValueID(id)); } } const element = __DEV__ - ? [ - REACT_ELEMENT_TYPE, - type, - key, - props, - task.debugOwner, - task.debugStack === null - ? null - : filterStackTrace(request, parseStackTrace(task.debugStack, 1)), - validated, - ] + ? [REACT_ELEMENT_TYPE, type, key, props, debugOwner, debugStack, validated] : [REACT_ELEMENT_TYPE, type, key, props]; if (task.implicitSlot && key !== null) { // The root Server Component had no key so it was in an implicit slot. From 2b3e8634c84f5e81ac9339b0b505afa8e43dd171 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 26 Jun 2025 15:44:30 -0400 Subject: [PATCH 4/6] Move debug data first in the stream This is unfortunate. Ideally you'd expect it to be the least priority but because it blocks the parent elements from resolving it ends up being better to have it first anyway. --- .../src/__tests__/ReactFlightDOMEdge-test.js | 2 +- .../react-server/src/ReactFlightServer.js | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index 9e01c53ea0c45..7c5d03e68a575 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -674,7 +674,7 @@ describe('ReactFlightDOMEdge', () => { const [stream2, drip] = dripStream(stream); // Allow some of the content through. - drip(5000); + drip(__DEV__ ? 7500 : 5000); const result = await ReactServerDOMClient.createFromReadableStream( stream2, diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 0bb23ef02e190..045233e388374 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -5363,6 +5363,24 @@ function flushCompletedChunks( } hintChunks.splice(0, i); + // Debug meta data comes before the model data because it will often end up blocking the model from + // completing since the JSX will reference the debug data. + if (__DEV__) { + const debugChunks = request.completedDebugChunks; + i = 0; + for (; i < debugChunks.length; i++) { + request.pendingChunks--; + const chunk = debugChunks[i]; + const keepWriting: boolean = writeChunkAndReturn(destination, chunk); + if (!keepWriting) { + request.destination = null; + i++; + break; + } + } + debugChunks.splice(0, i); + } + // Next comes model data. const regularChunks = request.completedRegularChunks; i = 0; @@ -5394,24 +5412,6 @@ function flushCompletedChunks( } } errorChunks.splice(0, i); - - // Next comes debug meta data. - // TODO: Move this first since other chunks are blocked on their debug info. I'm only testing that the client is resilient. - if (__DEV__) { - const debugChunks = request.completedDebugChunks; - i = 0; - for (; i < debugChunks.length; i++) { - request.pendingChunks--; - const chunk = debugChunks[i]; - const keepWriting: boolean = writeChunkAndReturn(destination, chunk); - if (!keepWriting) { - request.destination = null; - i++; - break; - } - } - debugChunks.splice(0, i); - } } finally { request.flushScheduled = false; completeWriting(destination); From 5eff405d9cae7dbe8a198826e881789eaa94e764 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 27 Jun 2025 08:52:26 -0400 Subject: [PATCH 5/6] Stack limit --- packages/react-server/src/ReactFlightServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 045233e388374..c3ad1ee20aff0 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -3744,7 +3744,7 @@ function serializeDebugErrorValue(request: Request, error: Error): string { const errorInfo: ReactErrorInfoDev = {name, message, stack, env}; const id = outlineDebugModel( request, - {objectLimit: stack.length + 2}, + {objectLimit: stack.length * 2 + 1}, errorInfo, ); return '$Z' + id.toString(16); From a6efdebc179df3697aad7bb26be5c92974eebf6e Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 27 Jun 2025 08:57:47 -0400 Subject: [PATCH 6/6] Comments --- packages/react-client/src/ReactFlightClient.js | 4 ++-- packages/react-server/src/ReactFlightServer.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 43437005c2d39..3eed93c732ba1 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -831,6 +831,8 @@ function initializeElement(response: Response, element: any): void { if (owner !== null) { initializeFakeStack(response, owner); } + // TODO: We should be freezing the element but currently, we might write into + // _debugInfo later. We could move it into _store which remains mutable. Object.freeze(element.props); } @@ -955,8 +957,6 @@ function createElement( } } if (__DEV__) { - // TODO: We should be freezing the element but currently, we might write into - // _debugInfo later. We could move it into _store which remains mutable. initializeElement(response, element); } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index c3ad1ee20aff0..a2c25145817a5 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -4289,7 +4289,7 @@ function renderDebugModel( } if (counter.objectLimit <= 0 && !doNotLimit.has(value)) { - // We've reached our max number of objects to serializeDebug across the wire so we serializeDebug this + // We've reached our max number of objects to serialize across the wire so we serialize this // as a marker so that the client can error or lazy load this when accessed by the console. return serializeDeferredObject(request, value); } @@ -4482,7 +4482,7 @@ function renderDebugModel( if (value.length >= 1024) { // Large strings are counted towards the object limit. if (counter.objectLimit <= 0) { - // We've reached our max number of objects to serializeDebug across the wire so we serializeDebug this + // We've reached our max number of objects to serialize across the wire so we serialize this // as a marker so that the client can error or lazy load this when accessed by the console. return serializeDeferredObject(request, value); }