@samitouri / QOS-React / commits / f65ac7bd4a

[DevTools] Make function inspection instant (#30786)

I noticed that there is a delay due to the inspection being split into one part that gets the attribute and another eval that does the inspection. This is a bit hacky and uses temporary global names that are leaky. The timeout was presumably to ensure that the first step had fully propagated but it's slow. As we've learned, it can be throttled, and it isn't a guarantee either way. Instead, we can just consolidate these into a single operation that by-passes the bridge and goes straight to the renderer interface from the eval. I did the same for the viewElementSource helper even though that's not currently in use since #28471 but I think we probably should return to that technique when it's available since it's more reliable than the throw - at least in Chrome. I'm not sure about the status of React Native here. In Firefox, inspecting a function with source maps doesn't seem to work. It doesn't jump to original code.

Sebastian Markbåge committed Aug 26, 2024 at 11:53 UTC f65ac7bd4aac61db1ec25af5b03b72d03779a890
6 files changed +90 -64
packages/react-devtools-extensions/src/main/index.js
+3 -13
@@ -21,6 +21,8 @@ import {
21 setBrowserSelectionFromReact,
22 setReactSelectionFromBrowser,
23 } from './elementSelection';
24 +import {viewAttributeSource} from './sourceSelection';
25 +
26 import {startReactPolling} from './reactPolling';
27 import cloneStyleTags from './cloneStyleTags';
28 import fetchFileWithCaching from './fetchFileWithCaching';
@@ -113,19 +115,7 @@ function createBridgeAndStore() {
115 const viewAttributeSourceFunction = (id, path) => {
116 const rendererID = store.getRendererIDForElement(id);
117 if (rendererID != null) {
116 - // Ask the renderer interface to find the specified attribute,
117 - // and store it as a global variable on the window.
118 - bridge.send('viewAttributeSource', {id, path, rendererID});
119 -
120 - setTimeout(() => {
121 - // Ask Chrome to display the location of the attribute,
122 - // assuming the renderer found a match.
123 - chrome.devtools.inspectedWindow.eval(`
124 - if (window.$attribute != null) {
125 - inspect(window.$attribute);
126 - }
127 - `);
128 - }, 100);
118 + viewAttributeSource(rendererID, id, path);
119 }
120 };
121
packages/react-devtools-extensions/src/main/sourceSelection.js new
+59
@@ -0,0 +1,59 @@
1 +/* global chrome */
2 +
3 +export function viewAttributeSource(rendererID, elementID, path) {
4 + chrome.devtools.inspectedWindow.eval(
5 + '{' + // The outer block is important because it means we can declare local variables.
6 + 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
7 + JSON.stringify(rendererID) +
8 + ');' +
9 + 'if (renderer) {' +
10 + ' const value = renderer.getElementAttributeByPath(' +
11 + JSON.stringify(elementID) +
12 + ',' +
13 + JSON.stringify(path) +
14 + ');' +
15 + ' if (value) {' +
16 + ' inspect(value);' +
17 + ' true;' +
18 + ' } else {' +
19 + ' false;' +
20 + ' }' +
21 + '} else {' +
22 + ' false;' +
23 + '}' +
24 + '}',
25 + (didInspect, evalError) => {
26 + if (evalError) {
27 + console.error(evalError);
28 + }
29 + },
30 + );
31 +}
32 +
33 +export function viewElementSource(rendererID, elementID) {
34 + chrome.devtools.inspectedWindow.eval(
35 + '{' + // The outer block is important because it means we can declare local variables.
36 + 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
37 + JSON.stringify(rendererID) +
38 + ');' +
39 + 'if (renderer) {' +
40 + ' const value = renderer.getElementSourceFunctionById(' +
41 + JSON.stringify(elementID) +
42 + ');' +
43 + ' if (value) {' +
44 + ' inspect(value);' +
45 + ' true;' +
46 + ' } else {' +
47 + ' false;' +
48 + ' }' +
49 + '} else {' +
50 + ' false;' +
51 + '}' +
52 + '}',
53 + (didInspect, evalError) => {
54 + if (evalError) {
55 + console.error(evalError);
56 + }
57 + },
58 + );
59 +}
packages/react-devtools-shared/src/backend/agent.js
-20
@@ -220,8 +220,6 @@ export default class Agent extends EventEmitter<{
220 this.updateConsolePatchSettings,
221 );
222 bridge.addListener('updateComponentFilters', this.updateComponentFilters);
223 - bridge.addListener('viewAttributeSource', this.viewAttributeSource);
224 - bridge.addListener('viewElementSource', this.viewElementSource);
223
224 // Temporarily support older standalone front-ends sending commands to newer embedded backends.
225 // We do this because React Native embeds the React DevTools backend,
@@ -816,24 +814,6 @@ export default class Agent extends EventEmitter<{
814 }
815 };
816
819 - viewAttributeSource: CopyElementParams => void = ({id, path, rendererID}) => {
820 - const renderer = this._rendererInterfaces[rendererID];
821 - if (renderer == null) {
822 - console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
823 - } else {
824 - renderer.prepareViewAttributeSource(id, path);
825 - }
826 - };
827 -
828 - viewElementSource: ElementAndRendererID => void = ({id, rendererID}) => {
829 - const renderer = this._rendererInterfaces[rendererID];
830 - if (renderer == null) {
831 - console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
832 - } else {
833 - renderer.prepareViewElementSource(id);
834 - }
835 - };
836 -
817 onTraceUpdates: (nodes: Set<HostInstance>) => void = nodes => {
818 this.emit('traceUpdates', nodes);
819 };
packages/react-devtools-shared/src/backend/fiber/renderer.js
+15 -19
@@ -3874,27 +3874,28 @@ export function attach(
3874
3875 // END copied code
3876
3877 - function prepareViewAttributeSource(
3877 + function getElementAttributeByPath(
3878 id: number,
3879 path: Array<string | number>,
3880 - ): void {
3880 + ): mixed {
3881 if (isMostRecentlyInspectedElement(id)) {
3882 - window.$attribute = getInObject(
3882 + return getInObject(
3883 ((mostRecentlyInspectedElement: any): InspectedElement),
3884 path,
3885 );
3886 }
3887 + return undefined;
3888 }
3889
3889 - function prepareViewElementSource(id: number): void {
3890 + function getElementSourceFunctionById(id: number): null | Function {
3891 const devtoolsInstance = idToDevToolsInstanceMap.get(id);
3892 if (devtoolsInstance === undefined) {
3893 console.warn(`Could not find DevToolsInstance with id "${id}"`);
3893 - return;
3894 + return null;
3895 }
3896 if (devtoolsInstance.kind !== FIBER_INSTANCE) {
3897 // TODO: Handle VirtualInstance.
3897 - return;
3898 + return null;
3899 }
3900 const fiber = devtoolsInstance.data;
3901
@@ -3906,21 +3907,16 @@ export function attach(
3907 case IncompleteFunctionComponent:
3908 case IndeterminateComponent:
3909 case FunctionComponent:
3909 - global.$type = type;
3910 - break;
3910 + return type;
3911 case ForwardRef:
3912 - global.$type = type.render;
3913 - break;
3912 + return type.render;
3913 case MemoComponent:
3914 case SimpleMemoComponent:
3916 - global.$type =
3917 - elementType != null && elementType.type != null
3918 - ? elementType.type
3919 - : type;
3920 - break;
3915 + return elementType != null && elementType.type != null
3916 + ? elementType.type
3917 + : type;
3918 default:
3922 - global.$type = null;
3923 - break;
3919 + return null;
3920 }
3921 }
3922
@@ -5727,8 +5723,8 @@ export function attach(
5723 inspectElement,
5724 logElementToConsole,
5725 patchConsoleForStrictMode,
5730 - prepareViewAttributeSource,
5731 - prepareViewElementSource,
5726 + getElementAttributeByPath,
5727 + getElementSourceFunctionById,
5728 overrideError,
5729 overrideSuspense,
5730 overrideValueAtPath,
packages/react-devtools-shared/src/backend/legacy/renderer.js
+10 -9
@@ -907,30 +907,31 @@ export function attach(
907 }
908 }
909
910 - function prepareViewAttributeSource(
910 + function getElementAttributeByPath(
911 id: number,
912 path: Array<string | number>,
913 - ): void {
913 + ): mixed {
914 const inspectedElement = inspectElementRaw(id);
915 if (inspectedElement !== null) {
916 - window.$attribute = getInObject(inspectedElement, path);
916 + return getInObject(inspectedElement, path);
917 }
918 + return undefined;
919 }
920
920 - function prepareViewElementSource(id: number): void {
921 + function getElementSourceFunctionById(id: number): null | Function {
922 const internalInstance = idToInternalInstanceMap.get(id);
923 if (internalInstance == null) {
924 console.warn(`Could not find instance with id "${id}"`);
924 - return;
925 + return null;
926 }
927
928 const element = internalInstance._currentElement;
929 if (element == null) {
930 console.warn(`Could not find element with id "${id}"`);
930 - return;
931 + return null;
932 }
933
933 - global.$type = element.type;
934 + return element.type;
935 }
936
937 function deletePath(
@@ -1141,8 +1142,8 @@ export function attach(
1142 overrideValueAtPath,
1143 renamePath,
1144 patchConsoleForStrictMode,
1144 - prepareViewAttributeSource,
1145 - prepareViewElementSource,
1145 + getElementAttributeByPath,
1146 + getElementSourceFunctionById,
1147 renderer,
1148 setTraceUpdatesEnabled,
1149 setTrackedPath,
packages/react-devtools-shared/src/backend/types.js
+3 -3
@@ -394,11 +394,11 @@ export type RendererInterface = {
394 value: any,
395 ) => void,
396 patchConsoleForStrictMode: () => void,
397 - prepareViewAttributeSource: (
397 + getElementAttributeByPath: (
398 id: number,
399 path: Array<string | number>,
400 - ) => void,
401 - prepareViewElementSource: (id: number) => void,
400 + ) => mixed,
401 + getElementSourceFunctionById: (id: number) => null | Function,
402 renamePath: (
403 type: Type,
404 id: number,