Skip to content

Commit

Permalink
[DevTools] Add Map for Server Component Logs (#30905)
Browse files Browse the repository at this point in the history
Stacked on #30899.

This adds another map to store Server Components logs. When they're
replayed with an owner we can associate them with a DevToolsInstance.
The replaying should happen before they can mount in Fiber so they'll
always have all logs when they mount. There can be more than one
Instance associated with any particular ReactComponentInfo. It can also
be unmounted and restored later.

One thing that's interesting about these is that when a Server Component
tree refreshes a new set of ReactComponentInfo will update through the
tree and the VirtualInstances will update with new instances. This means
that the old errors/warnings are no longer associated with the
VirtualInstance. I.e. it's not continually appended like updates do for
Fiber backed instances. On the client we dedupe errors/warnings for the
life time of the page. On the server that doesn't work well because it
would mean that when you refresh the page, you miss out on warnings so
we dedupe them per request instead. If we just appended on refresh it
would keep adding them.

If ever add a deduping mechanism that spans longer than a request, we
might need to do more of a merge when these updates.

Nothing actually adds logs to this map yet. That will need an
integration with Flight in a follow up.
  • Loading branch information
sebmarkbage authored Sep 9, 2024
1 parent f4b3a1f commit fa3cf50
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 23 deletions.
70 changes: 47 additions & 23 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ import {
SERVER_CONTEXT_SYMBOL_STRING,
} from '../shared/ReactSymbols';
import {enableStyleXFeatures} from 'react-devtools-feature-flags';

import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponentLogs';

import is from 'shared/objectIs';
import hasOwnProperty from 'shared/hasOwnProperty';

Expand Down Expand Up @@ -995,7 +998,8 @@ export function attach(
// Note, this only clears logs for Fibers that have instances. If they're filtered
// and then mount, the logs are there. Ensuring we only clear what you've seen.
// If we wanted to clear the whole set, we'd replace fiberToComponentLogsMap with a
// new WeakMap.
// new WeakMap. It's unclear whether we should clear componentInfoToComponentLogsMap
// since it's shared by other renderers but presumably it would.

// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {
Expand All @@ -1006,7 +1010,7 @@ export function attach(
fiberToComponentLogsMap.delete(fiber.alternate);
}
} else {
// TODO: Handle VirtualInstance.
componentInfoToComponentLogsMap.delete(devtoolsInstance.data);
}
const changed = recordConsoleLogs(devtoolsInstance, undefined);
if (changed) {
Expand All @@ -1019,28 +1023,27 @@ export function attach(
function clearConsoleLogsHelper(instanceID: number, type: 'error' | 'warn') {
const devtoolsInstance = idToDevToolsInstanceMap.get(instanceID);
if (devtoolsInstance !== undefined) {
let componentLogsEntry;
if (devtoolsInstance.kind === FIBER_INSTANCE) {
const fiber = devtoolsInstance.data;
const componentLogsEntry = fiberToComponentLogsMap.get(fiber);
if (componentLogsEntry !== undefined) {
if (type === 'error') {
componentLogsEntry.errors.clear();
componentLogsEntry.errorsCount = 0;
} else {
componentLogsEntry.warnings.clear();
componentLogsEntry.warningsCount = 0;
}
const changed = recordConsoleLogs(
devtoolsInstance,
componentLogsEntry,
);
if (changed) {
flushPendingEvents();
updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
}
}
componentLogsEntry = fiberToComponentLogsMap.get(fiber);
} else {
// TODO: Handle VirtualInstance.
const componentInfo = devtoolsInstance.data;
componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo);
}
if (componentLogsEntry !== undefined) {
if (type === 'error') {
componentLogsEntry.errors.clear();
componentLogsEntry.errorsCount = 0;
} else {
componentLogsEntry.warnings.clear();
componentLogsEntry.warningsCount = 0;
}
const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry);
if (changed) {
flushPendingEvents();
updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
}
}
}
}
Expand Down Expand Up @@ -2188,6 +2191,10 @@ export function attach(
pushOperation(ownerID);
pushOperation(displayNameStringID);
pushOperation(keyStringID);

const componentLogsEntry =
componentInfoToComponentLogsMap.get(componentInfo);
recordConsoleLogs(instance, componentLogsEntry);
}

function recordUnmount(fiberInstance: FiberInstance): void {
Expand Down Expand Up @@ -2857,6 +2864,14 @@ export function attach(
) {
recordResetChildren(virtualInstance);
}
// Update the errors/warnings count. If this Instance has switched to a different
// ReactComponentInfo instance, such as when refreshing Server Components, then
// we replace all the previous logs with the ones associated with the new ones rather
// than merging. Because deduping is expected to happen at the request level.
const componentLogsEntry = componentInfoToComponentLogsMap.get(
virtualInstance.data,
);
recordConsoleLogs(virtualInstance, componentLogsEntry);
// Must be called after all children have been appended.
recordVirtualProfilingDurations(virtualInstance);
} finally {
Expand Down Expand Up @@ -4293,6 +4308,9 @@ export function attach(
stylex: null,
};

const componentLogsEntry =
componentInfoToComponentLogsMap.get(componentInfo);

return {
id: virtualInstance.id,

Expand Down Expand Up @@ -4326,8 +4344,14 @@ export function attach(
hooks: null,
props: props,
state: null,
errors: [], // TODO: Handle errors on Virtual Instances.
warnings: [], // TODO: Handle warnings on Virtual Instances.
errors:
componentLogsEntry === undefined
? []
: Array.from(componentLogsEntry.errors.entries()),
warnings:
componentLogsEntry === undefined
? []
: Array.from(componentLogsEntry.warnings.entries()),
// List of owners
owners,

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

// This keeps track of Server Component logs which may come from.
// This is in a shared module because Server Component logs don't come from a specific renderer
// but can become associated with a Virtual Instance of any renderer.

import type {ReactComponentInfo} from 'shared/ReactTypes';

type ComponentLogs = {
errors: Map<string, number>,
errorsCount: number,
warnings: Map<string, number>,
warningsCount: number,
};

// This keeps it around as long as the ComponentInfo is alive which
// lets the Fiber get reparented/remounted and still observe the previous errors/warnings.
// Unless we explicitly clear the logs from a Fiber.
export const componentInfoToComponentLogsMap: WeakMap<
ReactComponentInfo,
ComponentLogs,
> = new WeakMap();

0 comments on commit fa3cf50

Please sign in to comment.