@samitouri / QOS-React-1 / commits / 566b0b0f14

[Flight] Don't limit objects that are children of special types (#31160)

We can't make a special getter to mark the boundary of deep serialization (which can be used for lazy loading in the future) when the parent object is a special object that we parse with getOutlinedModel. Such as Map/Set and JSX. This marks the objects that are direct children of those as not possible to limit. I don't love this solution since ideally it would maybe be more local to the serialization of a specific object. It also means that very deep trees of only Map/Set never get cut off. Maybe we should instead override the `get()` and enumeration methods on these instead somehow. It's important to have it be a getter though because that's the mechanism that lets us lazy-load more depth in the future.

Sebastian Markbåge committed Oct 10, 2024 at 07:36 UTC 566b0b0f14356f6e1a13722cac005c2f94f4c3f9
2 files changed +140 -17
packages/react-client/src/__tests__/ReactFlight-test.js
+92
@@ -3435,4 +3435,96 @@ describe('ReactFlight', () => {
3435 );
3436 expect(caughtError.digest).toBe('digest("my-error")');
3437 });
3438 +
3439 + // @gate __DEV__
3440 + it('can render deep but cut off JSX in debug info', async () => {
3441 + function createDeepJSX(n) {
3442 + if (n <= 0) {
3443 + return null;
3444 + }
3445 + return <div>{createDeepJSX(n - 1)}</div>;
3446 + }
3447 +
3448 + function ServerComponent(props) {
3449 + return <div>not using props</div>;
3450 + }
3451 +
3452 + const transport = ReactNoopFlightServer.render({
3453 + root: (
3454 + <ServerComponent>
3455 + {createDeepJSX(100) /* deper than objectLimit */}
3456 + </ServerComponent>
3457 + ),
3458 + });
3459 +
3460 + await act(async () => {
3461 + const rootModel = await ReactNoopFlightClient.read(transport);
3462 + const root = rootModel.root;
3463 + const children = root._debugInfo[0].props.children;
3464 + expect(children.type).toBe('div');
3465 + expect(children.props.children.type).toBe('div');
3466 + ReactNoop.render(root);
3467 + });
3468 +
3469 + expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
3470 + });
3471 +
3472 + // @gate __DEV__
3473 + it('can render deep but cut off Map/Set in debug info', async () => {
3474 + function createDeepMap(n) {
3475 + if (n <= 0) {
3476 + return null;
3477 + }
3478 + const map = new Map();
3479 + map.set('key', createDeepMap(n - 1));
3480 + return map;
3481 + }
3482 +
3483 + function createDeepSet(n) {
3484 + if (n <= 0) {
3485 + return null;
3486 + }
3487 + const set = new Set();
3488 + set.add(createDeepSet(n - 1));
3489 + return set;
3490 + }
3491 +
3492 + function ServerComponent(props) {
3493 + return <div>not using props</div>;
3494 + }
3495 +
3496 + const transport = ReactNoopFlightServer.render({
3497 + set: (
3498 + <ServerComponent
3499 + set={createDeepSet(100) /* deper than objectLimit */}
3500 + />
3501 + ),
3502 + map: (
3503 + <ServerComponent
3504 + map={createDeepMap(100) /* deper than objectLimit */}
3505 + />
3506 + ),
3507 + });
3508 +
3509 + await act(async () => {
3510 + const rootModel = await ReactNoopFlightClient.read(transport);
3511 + const set = rootModel.set._debugInfo[0].props.set;
3512 + const map = rootModel.map._debugInfo[0].props.map;
3513 + expect(set instanceof Set).toBe(true);
3514 + expect(set.size).toBe(1);
3515 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
3516 + for (const entry of set) {
3517 + expect(entry instanceof Set).toBe(true);
3518 + break;
3519 + }
3520 +
3521 + expect(map instanceof Map).toBe(true);
3522 + expect(map.size).toBe(1);
3523 + expect(map.get('key') instanceof Map).toBe(true);
3524 +
3525 + ReactNoop.render(rootModel.set);
3526 + });
3527 +
3528 + expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
3529 + });
3530 });
packages/react-server/src/ReactFlightServer.js
+48 -17
@@ -137,6 +137,9 @@ import binaryToComparableString from 'shared/binaryToComparableString';
137
138 import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
139
140 +// DEV-only set containing internal objects that should not be limited and turned into getters.
141 +const doNotLimit: WeakSet<Reference> = __DEV__ ? new WeakSet() : (null: any);
142 +
143 function defaultFilterStackFrame(
144 filename: string,
145 functionName: string,
@@ -2153,6 +2156,22 @@ function serializeConsoleMap(
2156 ): string {
2157 // Like serializeMap but for renderConsoleValue.
2158 const entries = Array.from(map);
2159 + // The Map itself doesn't take up any space but the outlined object does.
2160 + counter.objectLimit++;
2161 + for (let i = 0; i < entries.length; i++) {
2162 + // Outline every object entry in case we run out of space to serialize them.
2163 + // Because we can't mark these values as limited.
2164 + const entry = entries[i];
2165 + doNotLimit.add(entry);
2166 + const key = entry[0];
2167 + const value = entry[1];
2168 + if (typeof key === 'object' && key !== null) {
2169 + doNotLimit.add(key);
2170 + }
2171 + if (typeof value === 'object' && value !== null) {
2172 + doNotLimit.add(value);
2173 + }
2174 + }
2175 const id = outlineConsoleValue(request, counter, entries);
2176 return '$Q' + id.toString(16);
2177 }
@@ -2164,6 +2183,16 @@ function serializeConsoleSet(
2183 ): string {
2184 // Like serializeMap but for renderConsoleValue.
2185 const entries = Array.from(set);
2186 + // The Set itself doesn't take up any space but the outlined object does.
2187 + counter.objectLimit++;
2188 + for (let i = 0; i < entries.length; i++) {
2189 + // Outline every object entry in case we run out of space to serialize them.
2190 + // Because we can't mark these values as limited.
2191 + const entry = entries[i];
2192 + if (typeof entry === 'object' && entry !== null) {
2193 + doNotLimit.add(entry);
2194 + }
2195 + }
2196 const id = outlineConsoleValue(request, counter, entries);
2197 return '$W' + id.toString(16);
2198 }
@@ -3376,20 +3405,15 @@ function renderConsoleValue(
3405 parentPropertyName: string,
3406 value: ReactClientValue,
3407 ): ReactJSONValue {
3379 - // Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us
3380 - // $FlowFixMe[incompatible-use]
3381 - const originalValue = parent[parentPropertyName];
3382 - if (
3383 - typeof originalValue === 'object' &&
3384 - originalValue !== value &&
3385 - !(originalValue instanceof Date)
3386 - ) {
3387 - }
3388 -
3408 if (value === null) {
3409 return null;
3410 }
3411
3412 + // Special Symbol, that's very common.
3413 + if (value === REACT_ELEMENT_TYPE) {
3414 + return '$';
3415 + }
3416 +
3417 if (typeof value === 'object') {
3418 if (isClientReference(value)) {
3419 // We actually have this value on the client so we could import it.
@@ -3421,7 +3445,7 @@ function renderConsoleValue(
3445 return existingReference;
3446 }
3447
3424 - if (counter.objectLimit <= 0) {
3448 + if (counter.objectLimit <= 0 && !doNotLimit.has(value)) {
3449 // We've reached our max number of objects to serialize across the wire so we serialize this
3450 // as a marker so that the client can error when this is accessed by the console.
3451 return serializeLimitedObject();
@@ -3441,13 +3465,12 @@ function renderConsoleValue(
3465 if (element._debugStack != null) {
3466 // Outline the debug stack so that it doesn't get cut off.
3467 debugStack = filterStackTrace(request, element._debugStack, 1);
3444 - const stackId = outlineConsoleValue(
3445 - request,
3446 - {objectLimit: debugStack.length + 2},
3447 - debugStack,
3448 - );
3449 - request.writtenObjects.set(debugStack, serializeByValueID(stackId));
3468 + doNotLimit.add(debugStack);
3469 + for (let i = 0; i < debugStack.length; i++) {
3470 + doNotLimit.add(debugStack[i]);
3471 + }
3472 }
3473 + doNotLimit.add(element.props);
3474 return [
3475 REACT_ELEMENT_TYPE,
3476 element.type,
@@ -3592,6 +3615,9 @@ function renderConsoleValue(
3615 if (typeof value === 'string') {
3616 if (value[value.length - 1] === 'Z') {
3617 // Possibly a Date, whose toJSON automatically calls toISOString
3618 + // Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us
3619 + // $FlowFixMe[incompatible-use]
3620 + const originalValue = parent[parentPropertyName];
3621 if (originalValue instanceof Date) {
3622 return serializeDateFromDateJSON(value);
3623 }
@@ -3680,6 +3706,11 @@ function outlineConsoleValue(
3706 );
3707 }
3708
3709 + if (typeof model === 'object' && model !== null) {
3710 + // We can't limit outlined values.
3711 + doNotLimit.add(model);
3712 + }
3713 +
3714 function replacer(
3715 this:
3716 | {+[key: string | number]: ReactClientValue}