@samitouri / QOS-React / commits / 8a6c589be7

[Flight] Keep a separate ref count for debug chunks (#33717)

Same as #33716 but without the separate close signal. We'll need the ref count for separate debug channel anyway but I'm not sure we'll need the separate close signal.

Sebastian Markbåge committed Jul 7, 2025 at 11:42 UTC 8a6c589be74a389e62a996e74f8777ccd2a237ac
1 file changed +47 -55
packages/react-server/src/ReactFlightServer.js
+47 -55
@@ -278,7 +278,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
278 request,
279 parseStackTrace(new Error('react-stack-top-frame'), 1),
280 );
281 - request.pendingChunks++;
281 + request.pendingDebugChunks++;
282 const owner: null | ReactComponentInfo = resolveOwner();
283 const args = Array.from(arguments);
284 // Extract the env if this is a console log that was replayed from another env.
@@ -468,6 +468,7 @@ export type Request = {
468 timeOrigin: number,
469 abortTime: number,
470 // DEV-only
471 + pendingDebugChunks: number,
472 completedDebugChunks: Array<Chunk | BinaryChunk>,
473 environmentName: () => string,
474 filterStackFrame: (
@@ -587,6 +588,7 @@ function RequestInstance(
588 this.onFatalError = onFatalError;
589
590 if (__DEV__) {
591 + this.pendingDebugChunks = 0;
592 this.completedDebugChunks = ([]: Array<Chunk>);
593 this.environmentName =
594 environmentName === undefined
@@ -725,7 +727,7 @@ function serializeDebugThenable(
727 thenable: Thenable<any>,
728 ): string {
729 // Like serializeThenable but for renderDebugModel
728 - request.pendingChunks++;
730 + request.pendingDebugChunks++;
731 const id = request.nextChunkId++;
732 const ref = serializePromiseID(id);
733 request.writtenDebugObjects.set(thenable, ref);
@@ -737,20 +739,9 @@ function serializeDebugThenable(
739 }
740 case 'rejected': {
741 const x = thenable.reason;
740 - if (
741 - enablePostpone &&
742 - typeof x === 'object' &&
743 - x !== null &&
744 - (x: any).$$typeof === REACT_POSTPONE_TYPE
745 - ) {
746 - const postponeInstance: Postpone = (x: any);
747 - // We don't log this postpone.
748 - emitPostponeChunk(request, id, postponeInstance);
749 - } else {
750 - // We don't log these errors since they didn't actually throw into Flight.
751 - const digest = '';
752 - emitErrorChunk(request, id, digest, x, true);
753 - }
742 + // We don't log these errors since they didn't actually throw into Flight.
743 + const digest = '';
744 + emitErrorChunk(request, id, digest, x, true);
745 return ref;
746 }
747 }
@@ -787,20 +778,9 @@ function serializeDebugThenable(
778 enqueueFlush(request);
779 return;
780 }
790 - if (
791 - enablePostpone &&
792 - typeof reason === 'object' &&
793 - reason !== null &&
794 - (reason: any).$$typeof === REACT_POSTPONE_TYPE
795 - ) {
796 - const postponeInstance: Postpone = (reason: any);
797 - // We don't log this postpone.
798 - emitPostponeChunk(request, id, postponeInstance);
799 - } else {
800 - // We don't log these errors since they didn't actually throw into Flight.
801 - const digest = '';
802 - emitErrorChunk(request, id, digest, reason, true);
803 - }
781 + // We don't log these errors since they didn't actually throw into Flight.
782 + const digest = '';
783 + emitErrorChunk(request, id, digest, reason, true);
784 enqueueFlush(request);
785 },
786 );
@@ -1445,7 +1425,7 @@ function renderFunctionComponent<Props>(
1425 const componentName =
1426 (Component: any).displayName || Component.name || '';
1427 const componentEnv = (0, request.environmentName)();
1448 - request.pendingChunks++;
1428 + request.pendingDebugChunks++;
1429 componentDebugInfo = ({
1430 name: componentName,
1431 env: componentEnv,
@@ -2219,7 +2199,7 @@ function visitAsyncNode(
2199 const env = (0, request.environmentName)();
2200 advanceTaskTime(request, task, startTime);
2201 // Then emit a reference to us awaiting it in the current task.
2222 - request.pendingChunks++;
2202 + request.pendingDebugChunks++;
2203 emitDebugChunk(request, task.id, {
2204 awaited: ((ioNode: any): ReactIOInfo), // This is deduped by this reference.
2205 env: env,
@@ -2276,7 +2256,7 @@ function emitAsyncSequence(
2256 } else if (awaitedNode !== null) {
2257 // Nothing in user space (unfiltered stack) awaited this.
2258 serializeIONode(request, awaitedNode, awaitedNode.promise);
2279 - request.pendingChunks++;
2259 + request.pendingDebugChunks++;
2260 // We log the environment at the time when we ping which may be later than what the
2261 // environment was when we actually started awaiting.
2262 const env = (0, request.environmentName)();
@@ -2457,7 +2437,7 @@ function serializeDeferredObject(
2437 // This client supports a long lived connection. We can assign this object
2438 // an ID to be lazy loaded later.
2439 // This keeps the connection alive until we ask for it or release it.
2460 - request.pendingChunks++;
2440 + request.pendingDebugChunks++;
2441 const id = request.nextChunkId++;
2442 deferredDebugObjects.existing.set(value, id);
2443 deferredDebugObjects.retained.set(id, value);
@@ -2594,7 +2574,7 @@ function serializeDebugClientReference(
2574 try {
2575 const clientReferenceMetadata: ClientReferenceMetadata =
2576 resolveClientReferenceMetadata(request.bundlerConfig, clientReference);
2597 - request.pendingChunks++;
2577 + request.pendingDebugChunks++;
2578 const importId = request.nextChunkId++;
2579 emitImportChunk(request, importId, clientReferenceMetadata, true);
2580 if (parent[0] === REACT_ELEMENT_TYPE && parentPropertyName === '1') {
@@ -2607,7 +2587,7 @@ function serializeDebugClientReference(
2587 }
2588 return serializeByValueID(importId);
2589 } catch (x) {
2610 - request.pendingChunks++;
2590 + request.pendingDebugChunks++;
2591 const errorId = request.nextChunkId++;
2592 const digest = logRecoverableError(request, x, null);
2593 emitErrorChunk(request, errorId, digest, x, true);
@@ -2710,7 +2690,7 @@ function serializeLargeTextString(request: Request, text: string): string {
2690 }
2691
2692 function serializeDebugLargeTextString(request: Request, text: string): string {
2713 - request.pendingChunks++;
2693 + request.pendingDebugChunks++;
2694 const textId = request.nextChunkId++;
2695 emitTextChunk(request, textId, text, true);
2696 return serializeByValueID(textId);
@@ -2819,7 +2799,7 @@ function serializeDebugTypedArray(
2799 tag: string,
2800 typedArray: $ArrayBufferView,
2801 ): string {
2822 - request.pendingChunks++;
2802 + request.pendingDebugChunks++;
2803 const bufferId = request.nextChunkId++;
2804 emitTypedArrayChunk(request, bufferId, tag, typedArray, true);
2805 return serializeByValueID(bufferId);
@@ -2828,6 +2808,7 @@ function serializeDebugTypedArray(
2808 function serializeDebugBlob(request: Request, blob: Blob): string {
2809 const model: Array<string | Uint8Array> = [blob.type];
2810 const reader = blob.stream().getReader();
2811 + request.pendingDebugChunks++;
2812 const id = request.nextChunkId++;
2813 function progress(
2814 entry: {done: false, value: Uint8Array} | {done: true, value: void},
@@ -4095,7 +4076,7 @@ function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
4076 return;
4077 }
4078 // We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
4098 - request.pendingChunks++;
4079 + request.pendingDebugChunks++;
4080 const id = request.nextChunkId++;
4081 const owner = ioInfo.owner;
4082 // Ensure the owner is already outlined.
@@ -4173,7 +4154,7 @@ function serializeIONode(
4154 request.abortTime
4155 : ioNode.end;
4156
4176 - request.pendingChunks++;
4157 + request.pendingDebugChunks++;
4158 const id = request.nextChunkId++;
4159 emitIOInfoChunk(
4160 request,
@@ -4210,7 +4191,11 @@ function emitTypedArrayChunk(
4191 }
4192 }
4193 }
4213 - request.pendingChunks++; // Extra chunk for the header.
4194 + if (debug) {
4195 + request.pendingDebugChunks++;
4196 + } else {
4197 + request.pendingChunks++; // Extra chunk for the header.
4198 + }
4199 // TODO: Convert to little endian if that's not the server default.
4200 const binaryChunk = typedArrayToBinaryChunk(typedArray);
4201 const binaryLength = byteLengthOfBinaryChunk(binaryChunk);
@@ -4235,7 +4220,11 @@ function emitTextChunk(
4220 'Existence of byteLengthOfChunk should have already been checked. This is a bug in React.',
4221 );
4222 }
4238 - request.pendingChunks++; // Extra chunk for the header.
4223 + if (debug) {
4224 + request.pendingDebugChunks++;
4225 + } else {
4226 + request.pendingChunks++; // Extra chunk for the header.
4227 + }
4228 const textChunk = stringToChunk(text);
4229 const binaryLength = byteLengthOfChunk(textChunk);
4230 const row = id.toString(16) + ':T' + binaryLength.toString(16) + ',';
@@ -4620,7 +4609,7 @@ function renderDebugModel(
4609 // $FlowFixMe[method-unbinding]
4610 '(' + Function.prototype.toString.call(value) + ')',
4611 );
4623 - request.pendingChunks++;
4612 + request.pendingDebugChunks++;
4613 const id = request.nextChunkId++;
4614 const processedChunk = encodeReferenceChunk(request, id, serializedValue);
4615 request.completedDebugChunks.push(processedChunk);
@@ -4778,7 +4767,7 @@ function outlineDebugModel(
4767 }
4768
4769 const id = request.nextChunkId++;
4781 - request.pendingChunks++;
4770 + request.pendingDebugChunks++;
4771 emitOutlinedDebugModelChunk(request, id, counter, model);
4772 return id;
4773 }
@@ -4827,7 +4816,7 @@ function emitTimeOriginChunk(request: Request, timeOrigin: number): void {
4816 // We emit the time origin once. All ReactTimeInfo timestamps later in the stream
4817 // are relative to this time origin. This allows for more compact number encoding
4818 // and lower precision loss.
4830 - request.pendingChunks++;
4819 + request.pendingDebugChunks++;
4820 const row = ':N' + timeOrigin + '\n';
4821 const processedChunk = stringToChunk(row);
4822 // TODO: Move to its own priority queue.
@@ -4854,7 +4843,7 @@ function forwardDebugInfo(
4843 // being no references to this as an owner.
4844 outlineComponentInfo(request, (info: any));
4845 // Emit a reference to the outlined one.
4857 - request.pendingChunks++;
4846 + request.pendingDebugChunks++;
4847 emitDebugChunk(request, id, info);
4848 } else if (info.awaited) {
4849 const ioInfo = info.awaited;
@@ -4895,11 +4884,11 @@ function forwardDebugInfo(
4884 // $FlowFixMe[cannot-write]
4885 debugAsyncInfo.stack = debugStack;
4886 }
4898 - request.pendingChunks++;
4887 + request.pendingDebugChunks++;
4888 emitDebugChunk(request, id, debugAsyncInfo);
4889 }
4890 } else {
4902 - request.pendingChunks++;
4891 + request.pendingDebugChunks++;
4892 emitDebugChunk(request, id, info);
4893 }
4894 }
@@ -5001,7 +4990,7 @@ function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void {
4990 // complete in time before aborting.
4991 // The best we can do is try to emit the stack of where this Promise was created.
4992 serializeIONode(request, node, null);
5004 - request.pendingChunks++;
4993 + request.pendingDebugChunks++;
4994 const env = (0, request.environmentName)();
4995 const asyncInfo: ReactAsyncInfo = {
4996 awaited: ((node: any): ReactIOInfo), // This is deduped by this reference.
@@ -5030,7 +5019,7 @@ function emitTimingChunk(
5019 if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
5020 return;
5021 }
5033 - request.pendingChunks++;
5022 + request.pendingDebugChunks++;
5023 const relativeTimestamp = timestamp - request.timeOrigin;
5024 const row =
5025 serializeRowHeader('D', id) + '{"time":' + relativeTimestamp + '}\n';
@@ -5242,7 +5231,7 @@ function retryTask(request: Request, task: Task): void {
5231 if (__DEV__) {
5232 const currentEnv = (0, request.environmentName)();
5233 if (currentEnv !== task.environmentName) {
5245 - request.pendingChunks++;
5234 + request.pendingDebugChunks++;
5235 // The environment changed since we last emitted any debug information for this
5236 // task. We emit an entry that just includes the environment name change.
5237 emitDebugChunk(request, task.id, {env: currentEnv});
@@ -5462,7 +5451,7 @@ function flushCompletedChunks(
5451 const debugChunks = request.completedDebugChunks;
5452 i = 0;
5453 for (; i < debugChunks.length; i++) {
5465 - request.pendingChunks--;
5454 + request.pendingDebugChunks--;
5455 const chunk = debugChunks[i];
5456 const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
5457 if (!keepWriting) {
@@ -5510,7 +5499,10 @@ function flushCompletedChunks(
5499 completeWriting(destination);
5500 }
5501 flushBuffered(destination);
5513 - if (request.pendingChunks === 0) {
5502 + if (
5503 + request.pendingChunks === 0 &&
5504 + (!__DEV__ || request.pendingDebugChunks === 0)
5505 + ) {
5506 // We're done.
5507 if (enableTaint) {
5508 cleanupTaintQueue(request);
@@ -5729,7 +5721,7 @@ export function resolveDebugMessage(request: Request, message: string): void {
5721 const retainedValue = deferredDebugObjects.retained.get(id);
5722 if (retainedValue !== undefined) {
5723 // We're no longer blocked on this. We won't emit it.
5732 - request.pendingChunks--;
5724 + request.pendingDebugChunks--;
5725 deferredDebugObjects.retained.delete(id);
5726 deferredDebugObjects.existing.delete(retainedValue);
5727 enqueueFlush(request);
@@ -5772,7 +5764,7 @@ export function closeDebugChannel(request: Request): void {
5764 );
5765 }
5766 deferredDebugObjects.retained.forEach((value, id) => {
5775 - request.pendingChunks--;
5767 + request.pendingDebugChunks--;
5768 deferredDebugObjects.retained.delete(id);
5769 deferredDebugObjects.existing.delete(value);
5770 });