Skip to content

Commit 3fb17d1

Browse files
authored
[Flight] Encode ReactIOInfo as its own row type (facebook#33390)
Stacked on facebook#33388. This encodes the I/O entries as their own row type (`"J"`). This makes it possible to parse them directly without first parsing the debug info for each component. E.g. if you're just interested in logging the I/O without all the places it was awaited. This is not strictly necessary since the debug info is also readily available without parsing the actual trees. (That's how the Server Components Performance Track works.) However, we might want to exclude this information in profiling builds while retaining some limited form of I/O tracking. It also allows for logging side-effects that are not awaited if we wanted to.
1 parent acee65d commit 3fb17d1

File tree

2 files changed

+145
-64
lines changed

2 files changed

+145
-64
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
ReactComponentInfo,
1414
ReactEnvironmentInfo,
1515
ReactAsyncInfo,
16+
ReactIOInfo,
1617
ReactTimeInfo,
1718
ReactStackTrace,
1819
ReactFunctionLocation,
@@ -47,6 +48,7 @@ import {
4748
enablePostpone,
4849
enableProfilerTimer,
4950
enableComponentPerformanceTrack,
51+
enableAsyncDebugInfo,
5052
} from 'shared/ReactFeatureFlags';
5153

5254
import {
@@ -672,6 +674,14 @@ function nullRefGetter() {
672674
}
673675
}
674676

677+
function getIOInfoTaskName(ioInfo: ReactIOInfo): string {
678+
return ''; // TODO
679+
}
680+
681+
function getAsyncInfoTaskName(asyncInfo: ReactAsyncInfo): string {
682+
return 'await'; // We could be smarter about this and give it a name like `then` or `Promise.all`.
683+
}
684+
675685
function getServerComponentTaskName(componentInfo: ReactComponentInfo): string {
676686
return '<' + (componentInfo.name || '...') + '>';
677687
}
@@ -2447,30 +2457,27 @@ function getRootTask(
24472457

24482458
function initializeFakeTask(
24492459
response: Response,
2450-
debugInfo: ReactComponentInfo | ReactAsyncInfo,
2460+
debugInfo: ReactComponentInfo | ReactAsyncInfo | ReactIOInfo,
24512461
childEnvironmentName: string,
24522462
): null | ConsoleTask {
24532463
if (!supportsCreateTask) {
24542464
return null;
24552465
}
2456-
const componentInfo: ReactComponentInfo = (debugInfo: any); // Refined
24572466
if (debugInfo.stack == null) {
24582467
// If this is an error, we should've really already initialized the task.
24592468
// If it's null, we can't initialize a task.
24602469
return null;
24612470
}
24622471
const stack = debugInfo.stack;
24632472
const env: string =
2464-
componentInfo.env == null
2465-
? response._rootEnvironmentName
2466-
: componentInfo.env;
2473+
debugInfo.env == null ? response._rootEnvironmentName : debugInfo.env;
24672474
if (env !== childEnvironmentName) {
24682475
// This is the boundary between two environments so we'll annotate the task name.
24692476
// That is unusual so we don't cache it.
24702477
const ownerTask =
2471-
componentInfo.owner == null
2478+
debugInfo.owner == null
24722479
? null
2473-
: initializeFakeTask(response, componentInfo.owner, env);
2480+
: initializeFakeTask(response, debugInfo.owner, env);
24742481
return buildFakeTask(
24752482
response,
24762483
ownerTask,
@@ -2479,20 +2486,27 @@ function initializeFakeTask(
24792486
env,
24802487
);
24812488
} else {
2482-
const cachedEntry = componentInfo.debugTask;
2489+
const cachedEntry = debugInfo.debugTask;
24832490
if (cachedEntry !== undefined) {
24842491
return cachedEntry;
24852492
}
24862493
const ownerTask =
2487-
componentInfo.owner == null
2494+
debugInfo.owner == null
24882495
? null
2489-
: initializeFakeTask(response, componentInfo.owner, env);
2496+
: initializeFakeTask(response, debugInfo.owner, env);
2497+
// Some unfortunate pattern matching to refine the type.
2498+
const taskName =
2499+
debugInfo.key !== undefined
2500+
? getServerComponentTaskName(((debugInfo: any): ReactComponentInfo))
2501+
: debugInfo.name !== undefined
2502+
? getIOInfoTaskName(((debugInfo: any): ReactIOInfo))
2503+
: getAsyncInfoTaskName(((debugInfo: any): ReactAsyncInfo));
24902504
// $FlowFixMe[cannot-write]: We consider this part of initialization.
2491-
return (componentInfo.debugTask = buildFakeTask(
2505+
return (debugInfo.debugTask = buildFakeTask(
24922506
response,
24932507
ownerTask,
24942508
stack,
2495-
getServerComponentTaskName(componentInfo),
2509+
taskName,
24962510
env,
24972511
));
24982512
}
@@ -2555,7 +2569,7 @@ function fakeJSXCallSite() {
25552569

25562570
function initializeFakeStack(
25572571
response: Response,
2558-
debugInfo: ReactComponentInfo | ReactAsyncInfo,
2572+
debugInfo: ReactComponentInfo | ReactAsyncInfo | ReactIOInfo,
25592573
): void {
25602574
const cachedEntry = debugInfo.debugStack;
25612575
if (cachedEntry !== undefined) {
@@ -2740,6 +2754,54 @@ function resolveConsoleEntry(
27402754
);
27412755
}
27422756

2757+
function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
2758+
const env =
2759+
// TODO: Pass env through I/O info.
2760+
// ioInfo.env !== undefined ? ioInfo.env :
2761+
response._rootEnvironmentName;
2762+
if (ioInfo.stack !== undefined) {
2763+
initializeFakeTask(response, ioInfo, env);
2764+
initializeFakeStack(response, ioInfo);
2765+
}
2766+
// TODO: Initialize owner.
2767+
// Adjust the time to the current environment's time space.
2768+
// $FlowFixMe[cannot-write]
2769+
ioInfo.start += response._timeOrigin;
2770+
// $FlowFixMe[cannot-write]
2771+
ioInfo.end += response._timeOrigin;
2772+
}
2773+
2774+
function resolveIOInfo(
2775+
response: Response,
2776+
id: number,
2777+
model: UninitializedModel,
2778+
): void {
2779+
const chunks = response._chunks;
2780+
let chunk = chunks.get(id);
2781+
if (!chunk) {
2782+
chunk = createResolvedModelChunk(response, model);
2783+
chunks.set(id, chunk);
2784+
initializeModelChunk(chunk);
2785+
} else {
2786+
resolveModelChunk(chunk, model);
2787+
if (chunk.status === RESOLVED_MODEL) {
2788+
initializeModelChunk(chunk);
2789+
}
2790+
}
2791+
if (chunk.status === INITIALIZED) {
2792+
initializeIOInfo(response, chunk.value);
2793+
} else {
2794+
chunk.then(
2795+
v => {
2796+
initializeIOInfo(response, v);
2797+
},
2798+
e => {
2799+
// Ignore debug info errors for now. Unnecessary noise.
2800+
},
2801+
);
2802+
}
2803+
}
2804+
27432805
function mergeBuffer(
27442806
buffer: Array<Uint8Array>,
27452807
lastChunk: Uint8Array,
@@ -2844,7 +2906,7 @@ function flushComponentPerformance(
28442906

28452907
// First find the start time of the first component to know if it was running
28462908
// in parallel with the previous.
2847-
const debugInfo = root._debugInfo;
2909+
const debugInfo = __DEV__ && root._debugInfo;
28482910
if (debugInfo) {
28492911
for (let i = 1; i < debugInfo.length; i++) {
28502912
const info = debugInfo[i];
@@ -3101,6 +3163,17 @@ function processFullStringRow(
31013163
}
31023164
// Fallthrough to share the error with Console entries.
31033165
}
3166+
case 74 /* "J" */: {
3167+
if (
3168+
enableProfilerTimer &&
3169+
enableComponentPerformanceTrack &&
3170+
enableAsyncDebugInfo
3171+
) {
3172+
resolveIOInfo(response, id, row);
3173+
return;
3174+
}
3175+
// Fallthrough to share the error with Console entries.
3176+
}
31043177
case 87 /* "W" */: {
31053178
if (__DEV__) {
31063179
resolveConsoleEntry(response, row);

packages/react-server/src/ReactFlightServer.js

Lines changed: 58 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1905,7 +1905,7 @@ function visitAsyncNode(
19051905
return ioNode;
19061906
}
19071907
// Outline the IO node.
1908-
emitIOChunk(request, ioNode);
1908+
serializeIONode(request, ioNode);
19091909
// Then emit a reference to us awaiting it in the current task.
19101910
request.pendingChunks++;
19111911
emitDebugChunk(request, task.id, {
@@ -1942,7 +1942,7 @@ function emitAsyncSequence(
19421942
// each occurrence. Right now we'll only track the first time it is invoked.
19431943
awaitedNode.end = performance.now();
19441944
}
1945-
emitIOChunk(request, awaitedNode);
1945+
serializeIONode(request, awaitedNode);
19461946
request.pendingChunks++;
19471947
emitDebugChunk(request, task.id, {
19481948
awaited: ((awaitedNode: any): ReactIOInfo), // This is deduped by this reference.
@@ -3493,80 +3493,88 @@ function outlineComponentInfo(
34933493
request.writtenObjects.set(componentInfo, serializeByValueID(id));
34943494
}
34953495

3496-
function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
3496+
function emitIOInfoChunk(
3497+
request: Request,
3498+
id: number,
3499+
start: number,
3500+
end: number,
3501+
stack: ?ReactStackTrace,
3502+
): void {
34973503
if (!__DEV__) {
34983504
// These errors should never make it into a build so we don't need to encode them in codes.json
34993505
// eslint-disable-next-line react-internal/prod-error-codes
35003506
throw new Error(
3501-
'outlineIOInfo should never be called in production mode. This is a bug in React.',
3507+
'emitIOInfoChunk should never be called in production mode. This is a bug in React.',
35023508
);
35033509
}
35043510

3505-
if (request.writtenObjects.has(ioInfo)) {
3506-
// Already written
3507-
return;
3508-
}
3509-
3510-
// Limit the number of objects we write to prevent emitting giant props objects.
35113511
let objectLimit = 10;
3512-
if (ioInfo.stack != null) {
3513-
// Ensure we have enough object limit to encode the stack trace.
3514-
objectLimit += ioInfo.stack.length;
3512+
if (stack) {
3513+
objectLimit += stack.length;
35153514
}
3516-
3517-
// We use the console encoding so that we can dedupe objects but don't necessarily
3518-
// use the full serialization that requires a task.
35193515
const counter = {objectLimit};
3516+
function replacer(
3517+
this:
3518+
| {+[key: string | number]: ReactClientValue}
3519+
| $ReadOnlyArray<ReactClientValue>,
3520+
parentPropertyName: string,
3521+
value: ReactClientValue,
3522+
): ReactJSONValue {
3523+
return renderConsoleValue(
3524+
request,
3525+
counter,
3526+
this,
3527+
parentPropertyName,
3528+
value,
3529+
);
3530+
}
35203531

3521-
// We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
3522-
const relativeStartTimestamp = ioInfo.start - request.timeOrigin;
3523-
const relativeEndTimestamp = ioInfo.end - request.timeOrigin;
3532+
const relativeStartTimestamp = start - request.timeOrigin;
3533+
const relativeEndTimestamp = end - request.timeOrigin;
35243534
const debugIOInfo: Omit<ReactIOInfo, 'debugTask' | 'debugStack'> = {
35253535
start: relativeStartTimestamp,
35263536
end: relativeEndTimestamp,
3527-
stack: ioInfo.stack,
3537+
stack: stack,
35283538
};
3529-
const id = outlineConsoleValue(request, counter, debugIOInfo);
3530-
request.writtenObjects.set(ioInfo, serializeByValueID(id));
3539+
// $FlowFixMe[incompatible-type] stringify can return null
3540+
const json: string = stringify(debugIOInfo, replacer);
3541+
const row = id.toString(16) + ':J' + json + '\n';
3542+
const processedChunk = stringToChunk(row);
3543+
request.completedRegularChunks.push(processedChunk);
35313544
}
35323545

3533-
function emitIOChunk(request: Request, ioNode: IONode | PromiseNode): void {
3534-
if (!__DEV__) {
3535-
// These errors should never make it into a build so we don't need to encode them in codes.json
3536-
// eslint-disable-next-line react-internal/prod-error-codes
3537-
throw new Error(
3538-
'outlineIOInfo should never be called in production mode. This is a bug in React.',
3539-
);
3546+
function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
3547+
if (request.writtenObjects.has(ioInfo)) {
3548+
// Already written
3549+
return;
35403550
}
3551+
// We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
3552+
request.pendingChunks++;
3553+
const id = request.nextChunkId++;
3554+
emitIOInfoChunk(request, id, ioInfo.start, ioInfo.end, ioInfo.stack);
3555+
request.writtenObjects.set(ioInfo, serializeByValueID(id));
3556+
}
35413557

3542-
if (request.writtenObjects.has(ioNode)) {
3558+
function serializeIONode(
3559+
request: Request,
3560+
ioNode: IONode | PromiseNode,
3561+
): string {
3562+
const existingRef = request.writtenObjects.get(ioNode);
3563+
if (existingRef !== undefined) {
35433564
// Already written
3544-
return;
3565+
return existingRef;
35453566
}
35463567

3547-
// Limit the number of objects we write to prevent emitting giant props objects.
3548-
let objectLimit = 10;
35493568
let stack = null;
35503569
if (ioNode.stack !== null) {
35513570
stack = filterStackTrace(request, ioNode.stack, 1);
3552-
// Ensure we have enough object limit to encode the stack trace.
3553-
objectLimit += stack.length;
35543571
}
3555-
3556-
// We use the console encoding so that we can dedupe objects but don't necessarily
3557-
// use the full serialization that requires a task.
3558-
const counter = {objectLimit};
3559-
3560-
// We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
3561-
const relativeStartTimestamp = ioNode.start - request.timeOrigin;
3562-
const relativeEndTimestamp = ioNode.end - request.timeOrigin;
3563-
const debugIOInfo: Omit<ReactIOInfo, 'debugTask' | 'debugStack'> = {
3564-
start: relativeStartTimestamp,
3565-
end: relativeEndTimestamp,
3566-
stack: stack,
3567-
};
3568-
const id = outlineConsoleValue(request, counter, debugIOInfo);
3569-
request.writtenObjects.set(ioNode, serializeByValueID(id));
3572+
request.pendingChunks++;
3573+
const id = request.nextChunkId++;
3574+
emitIOInfoChunk(request, id, ioNode.start, ioNode.end, stack);
3575+
const ref = serializeByValueID(id);
3576+
request.writtenObjects.set(ioNode, ref);
3577+
return ref;
35703578
}
35713579

35723580
function emitTypedArrayChunk(

0 commit comments

Comments
 (0)