[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
});