@samitouri / QOS-React / commits / e44685e4f1

[DevTools] Use Owner Stacks to Implement View Source of a Server Component (#30798)

We don't have the source location of Server Components on the client because we don't want to eagerly do the throw trick for all Server Components just in case. Unfortunately Node.js doesn't expose V8's API to get a source location of a function. We do have the owner stacks of the JSX that created it though and at some point we'll also show that location in DevTools. However, the realization is that if a Server Component is the owner of any child. The owner stack of that child will have the owner component's source location as its bottom stack frame. The technique I'm implementing here is to track whenever a child mounts we already have its owner. We track the first discovered owned child's stack on the owner. Then when we ask for a Source location of the owner do we parse that stack and extract the location of the bottom frame. This doesn't give us a location necessarily in the top of the function but somewhere in the function. In this case the first owned child is the Container: <img width="1107" alt="Screenshot 2024-08-22 at 10 24 42 PM" src="https://github.com/user-attachments/assets/95f32850-24a5-4151-8ce6-b7b89db68aee"> <img width="648" alt="Screenshot 2024-08-22 at 10 24 20 PM" src="https://github.com/user-attachments/assets/4bcba033-866f-4684-9beb-de09d189deff"> We can even use this technique for Fibers too. Currently I use this as a fallback in case the error technique didn't work. This covers a case where nothing errors but you still render a child. This case is actually quite common: ``` function Foo() { return <Bar />; } ``` However, for Fibers we could really just use the `inspect(function)` technique which works for all cases. At least in Chrome. Unfortunately, this technique doesn't work if a Component doesn't create any new JSX but just renders its children. It also currently doesn't work if the child is filtered since I only look up the owner if an instance is not filtered. This means that the container in the fixture can't view source by default since the host component is filtered: ``` export default function Container({children}) { return <div>{children}</div>; } ``` <img width="1107" alt="Screenshot 2024-08-22 at 10 24 35 PM" src="https://github.com/user-attachments/assets/c3f8f9c5-5add-4d35-9290-3a5079e82adc">

Sebastian Markbåge committed Aug 26, 2024 at 20:50 UTC e44685e4f196f9e19c3729ab2b3772a40428ac1e
2 files changed +104 -36
packages/react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack.js
+17
@@ -108,6 +108,23 @@ export function getStackByFiberInDevAndProd(
108 }
109 }
110
111 +export function getSourceLocationByFiber(
112 + workTagMap: WorkTagMap,
113 + fiber: Fiber,
114 + currentDispatcherRef: CurrentDispatcherRef,
115 +): null | string {
116 + // This is like getStackByFiberInDevAndProd but just the first stack frame.
117 + try {
118 + const info = describeFiber(workTagMap, fiber, currentDispatcherRef);
119 + if (info !== '') {
120 + return info.slice(1); // skip the leading newline
121 + }
122 + } catch (x) {
123 + console.error(x);
124 + }
125 + return null;
126 +}
127 +
128 export function supportsConsoleTasks(fiber: Fiber): boolean {
129 // If this Fiber supports native console.createTask then we are already running
130 // inside a native async stack trace if it's active - meaning the DevTools is open.
packages/react-devtools-shared/src/backend/fiber/renderer.js
+87 -36
@@ -101,6 +101,14 @@ import {
101 import {enableStyleXFeatures} from 'react-devtools-feature-flags';
102 import is from 'shared/objectIs';
103 import hasOwnProperty from 'shared/hasOwnProperty';
104 +
105 +// $FlowFixMe[method-unbinding]
106 +const toString = Object.prototype.toString;
107 +
108 +function isError(object: mixed) {
109 + return toString.call(object) === '[object Error]';
110 +}
111 +
112 import {getStyleXData} from '../StyleX/utils';
113 import {createProfilingHooks} from '../profilingHooks';
114
@@ -131,7 +139,8 @@ import type {
139 Plugins,
140 } from 'react-devtools-shared/src/frontend/types';
141 import type {Source} from 'react-devtools-shared/src/shared/types';
134 -import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack';
142 +import {getSourceLocationByFiber} from './DevToolsFiberComponentStack';
143 +import {formatOwnerStack} from '../shared/DevToolsOwnerStack';
144
145 // Kinds
146 const FIBER_INSTANCE = 0;
@@ -152,7 +161,7 @@ type FiberInstance = {
161 previousSibling: null | DevToolsInstance, // filtered next sibling, including virtual
162 nextSibling: null | DevToolsInstance, // filtered next sibling, including virtual
163 flags: number, // Force Error/Suspense
155 - componentStack: null | string,
164 + source: null | string | Error | Source, // source location of this component function, or owned child stack
165 errors: null | Map<string, number>, // error messages and count
166 warnings: null | Map<string, number>, // warning messages and count
167 data: Fiber, // one of a Fiber pair
@@ -167,7 +176,7 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
176 previousSibling: null,
177 nextSibling: null,
178 flags: 0,
170 - componentStack: null,
179 + source: null,
180 errors: null,
181 warnings: null,
182 data: fiber,
@@ -187,7 +196,7 @@ type VirtualInstance = {
196 previousSibling: null | DevToolsInstance, // filtered next sibling, including virtual
197 nextSibling: null | DevToolsInstance, // filtered next sibling, including virtual
198 flags: number,
190 - componentStack: null | string,
199 + source: null | string | Error | Source, // source location of this server component, or owned child stack
200 // Errors and Warnings happen per ReactComponentInfo which can appear in
201 // multiple places but we track them per stateful VirtualInstance so
202 // that old errors/warnings don't disappear when the instance is refreshed.
@@ -209,7 +218,7 @@ function createVirtualInstance(
218 previousSibling: null,
219 nextSibling: null,
220 flags: 0,
212 - componentStack: null,
221 + source: null,
222 errors: null,
223 warnings: null,
224 data: debugEntry,
@@ -2154,6 +2163,16 @@ export function attach(
2163 parentInstance,
2164 debugOwner,
2165 );
2166 + if (
2167 + ownerInstance !== null &&
2168 + debugOwner === fiber._debugOwner &&
2169 + fiber._debugStack != null &&
2170 + ownerInstance.source === null
2171 + ) {
2172 + // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on
2173 + // the debugStack will be a stack frame inside the ownerInstance's source.
2174 + ownerInstance.source = fiber._debugStack;
2175 + }
2176 const ownerID = ownerInstance === null ? 0 : ownerInstance.id;
2177 const parentID = parentInstance ? parentInstance.id : 0;
2178
@@ -2228,6 +2247,16 @@ export function attach(
2247 // away so maybe it's not so bad.
2248 const debugOwner = getUnfilteredOwner(componentInfo);
2249 const ownerInstance = findNearestOwnerInstance(parentInstance, debugOwner);
2250 + if (
2251 + ownerInstance !== null &&
2252 + debugOwner === componentInfo.owner &&
2253 + componentInfo.debugStack != null &&
2254 + ownerInstance.source === null
2255 + ) {
2256 + // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on
2257 + // the debugStack will be a stack frame inside the ownerInstance's source.
2258 + ownerInstance.source = componentInfo.debugStack;
2259 + }
2260 const ownerID = ownerInstance === null ? 0 : ownerInstance.id;
2261 const parentID = parentInstance ? parentInstance.id : 0;
2262
@@ -4324,7 +4353,7 @@ export function attach(
4353
4354 let source = null;
4355 if (canViewSource) {
4327 - source = getSourceForFiber(fiber);
4356 + source = getSourceForFiberInstance(fiberInstance);
4357 }
4358
4359 return {
@@ -4398,7 +4427,8 @@ export function attach(
4427 function inspectVirtualInstanceRaw(
4428 virtualInstance: VirtualInstance,
4429 ): InspectedElement | null {
4401 - const canViewSource = false;
4430 + const canViewSource = true;
4431 + const source = getSourceForInstance(virtualInstance);
4432
4433 const componentInfo = virtualInstance.data;
4434 const key =
@@ -4438,9 +4468,6 @@ export function attach(
4468 stylex: null,
4469 };
4470
4441 - // TODO: Support getting the source location from the owner stack.
4442 - const source = null;
4443 -
4471 return {
4472 id: virtualInstance.id,
4473
@@ -5664,39 +5691,63 @@ export function attach(
5691 return idToDevToolsInstanceMap.has(id);
5692 }
5693
5667 - function getComponentStackForFiber(fiber: Fiber): string | null {
5668 - // TODO: This should really just take an DevToolsInstance directly.
5669 - let fiberInstance = fiberToFiberInstanceMap.get(fiber);
5670 - if (fiberInstance === undefined && fiber.alternate !== null) {
5671 - fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
5672 - }
5673 - if (fiberInstance === undefined) {
5674 - // We're no longer tracking this instance.
5675 - return null;
5676 - }
5677 - if (fiberInstance.componentStack !== null) {
5678 - // Cached entry.
5679 - return fiberInstance.componentStack;
5694 + function getSourceForFiberInstance(
5695 + fiberInstance: FiberInstance,
5696 + ): Source | null {
5697 + const unresolvedSource = fiberInstance.source;
5698 + if (
5699 + unresolvedSource !== null &&
5700 + typeof unresolvedSource === 'object' &&
5701 + !isError(unresolvedSource)
5702 + ) {
5703 + // $FlowFixMe: isError should have refined it.
5704 + return unresolvedSource;
5705 }
5706 const dispatcherRef = getDispatcherRef(renderer);
5682 - if (dispatcherRef == null) {
5707 + const stackFrame =
5708 + dispatcherRef == null
5709 + ? null
5710 + : getSourceLocationByFiber(
5711 + ReactTypeOfWork,
5712 + fiberInstance.data,
5713 + dispatcherRef,
5714 + );
5715 + if (stackFrame === null) {
5716 + // If we don't find a source location by throwing, try to get one
5717 + // from an owned child if possible. This is the same branch as
5718 + // for virtual instances.
5719 + return getSourceForInstance(fiberInstance);
5720 + }
5721 + const source = parseSourceFromComponentStack(stackFrame);
5722 + fiberInstance.source = source;
5723 + return source;
5724 + }
5725 +
5726 + function getSourceForInstance(instance: DevToolsInstance): Source | null {
5727 + let unresolvedSource = instance.source;
5728 + if (unresolvedSource === null) {
5729 + // We don't have any source yet. We can try again later in case an owned child mounts later.
5730 + // TODO: We won't have any information here if the child is filtered.
5731 return null;
5732 }
5733
5686 - return (fiberInstance.componentStack = getStackByFiberInDevAndProd(
5687 - ReactTypeOfWork,
5688 - fiber,
5689 - dispatcherRef,
5690 - ));
5691 - }
5692 -
5693 - function getSourceForFiber(fiber: Fiber): Source | null {
5694 - const componentStack = getComponentStackForFiber(fiber);
5695 - if (componentStack == null) {
5696 - return null;
5734 + // If we have the debug stack (the creation stack of the JSX) for any owned child of this
5735 + // component, then at the bottom of that stack will be a stack frame that is somewhere within
5736 + // the component's function body. Typically it would be the callsite of the JSX unless there's
5737 + // any intermediate utility functions. This won't point to the top of the component function
5738 + // but it's at least somewhere within it.
5739 + if (isError(unresolvedSource)) {
5740 + unresolvedSource = formatOwnerStack((unresolvedSource: any));
5741 + }
5742 + if (typeof unresolvedSource === 'string') {
5743 + const idx = unresolvedSource.lastIndexOf('\n');
5744 + const lastLine =
5745 + idx === -1 ? unresolvedSource : unresolvedSource.slice(idx + 1);
5746 + return (instance.source = parseSourceFromComponentStack(lastLine));
5747 }
5748
5699 - return parseSourceFromComponentStack(componentStack);
5749 + // $FlowFixMe: refined.
5750 + return unresolvedSource;
5751 }
5752
5753 return {