@samitouri / QOS-React-2 / commits / 119736b1c2

[FB-only] Show which hooks (indices) changed when profiling (#20998)

Brian Vaughn committed Mar 17, 2021 at 12:28 UTC 119736b1c2ea0d404ad466e39138462453392008
13 files changed +172 -35
packages/react-devtools-extensions/package.json
+1 -1
@@ -6,7 +6,7 @@
6 "build": "cross-env NODE_ENV=production yarn run build:chrome && yarn run build:firefox && yarn run build:edge",
7 "build:dev": "cross-env NODE_ENV=development yarn run build:chrome:dev && yarn run build:firefox:dev && yarn run build:edge:dev",
8 "build:chrome": "cross-env NODE_ENV=production node ./chrome/build",
9 - "build:chrome:crx": "cross-env NODE_ENV=production node ./chrome/build --crx",
9 + "build:chrome:crx": "cross-env NODE_ENV=production FEATURE_FLAG_TARGET=extension-fb node ./chrome/build --crx",
10 "build:chrome:dev": "cross-env NODE_ENV=development node ./chrome/build",
11 "build:firefox": "cross-env NODE_ENV=production node ./firefox/build",
12 "build:firefox:dev": "cross-env NODE_ENV=development node ./firefox/build",
packages/react-devtools-extensions/webpack.backend.js
+4
@@ -3,6 +3,7 @@
3 const {resolve} = require('path');
4 const {DefinePlugin} = require('webpack');
5 const {GITHUB_URL, getVersionString} = require('./utils');
6 +const {resolveFeatureFlags} = require('react-devtools-shared/buildUtils');
7
8 const NODE_ENV = process.env.NODE_ENV;
9 if (!NODE_ENV) {
@@ -16,6 +17,8 @@ const __DEV__ = NODE_ENV === 'development';
17
18 const DEVTOOLS_VERSION = getVersionString();
19
20 +const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
21 +
22 module.exports = {
23 mode: __DEV__ ? 'development' : 'production',
24 devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
@@ -34,6 +37,7 @@ module.exports = {
37 alias: {
38 react: resolve(builtModulesDir, 'react'),
39 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'),
40 + 'react-devtools-feature-flags': resolveFeatureFlags(featureFlagTarget),
41 'react-dom': resolve(builtModulesDir, 'react-dom'),
42 'react-is': resolve(builtModulesDir, 'react-is'),
43 scheduler: resolve(builtModulesDir, 'scheduler'),
packages/react-devtools-extensions/webpack.config.js
+3 -1
@@ -17,6 +17,8 @@ const __DEV__ = NODE_ENV === 'development';
17
18 const DEVTOOLS_VERSION = getVersionString();
19
20 +const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
21 +
22 module.exports = {
23 mode: __DEV__ ? 'development' : 'production',
24 devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
@@ -40,7 +42,7 @@ module.exports = {
42 alias: {
43 react: resolve(builtModulesDir, 'react'),
44 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'),
43 - 'react-devtools-feature-flags': resolveFeatureFlags('extension'),
45 + 'react-devtools-feature-flags': resolveFeatureFlags(featureFlagTarget),
46 'react-dom': resolve(builtModulesDir, 'react-dom'),
47 'react-is': resolve(builtModulesDir, 'react-is'),
48 scheduler: resolve(builtModulesDir, 'scheduler'),
packages/react-devtools-shared/buildUtils.js
+5 -2
@@ -17,8 +17,11 @@ function resolveFeatureFlags(target) {
17 case 'shell':
18 flagsPath = 'DevToolsFeatureFlags.default';
19 break;
20 - case 'extension':
21 - flagsPath = 'DevToolsFeatureFlags.extension';
20 + case 'extension-oss':
21 + flagsPath = 'DevToolsFeatureFlags.extension-oss';
22 + break;
23 + case 'extension-fb':
24 + flagsPath = 'DevToolsFeatureFlags.extension-fb';
25 break;
26 default:
27 console.error(`Invalid target "${target}"`);
packages/react-devtools-shared/src/backend/renderer.js
+50 -5
@@ -79,6 +79,7 @@ import {
79 MEMO_SYMBOL_STRING,
80 } from './ReactSymbols';
81 import {format} from './utils';
82 +import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags';
83
84 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
85 import type {
@@ -978,12 +979,9 @@ export function attach(
979 state: null,
980 };
981 } else {
981 - return {
982 + const data: ChangeDescription = {
983 context: getContextChangedKeys(nextFiber),
983 - didHooksChange: didHooksChange(
984 - prevFiber.memoizedState,
985 - nextFiber.memoizedState,
986 - ),
984 + didHooksChange: false,
985 isFirstMount: false,
986 props: getChangedKeys(
987 prevFiber.memoizedProps,
@@ -994,6 +992,23 @@ export function attach(
992 nextFiber.memoizedState,
993 ),
994 };
995 +
996 + // Only traverse the hooks list once, depending on what info we're returning.
997 + if (enableProfilerChangedHookIndices) {
998 + const indices = getChangedHooksIndices(
999 + prevFiber.memoizedState,
1000 + nextFiber.memoizedState,
1001 + );
1002 + data.hooks = indices;
1003 + data.didHooksChange = indices !== null && indices.length > 0;
1004 + } else {
1005 + data.didHooksChange = didHooksChange(
1006 + prevFiber.memoizedState,
1007 + nextFiber.memoizedState,
1008 + );
1009 + }
1010 +
1011 + return data;
1012 }
1013 default:
1014 return null;
@@ -1154,6 +1169,36 @@ export function attach(
1169 return false;
1170 }
1171
1172 + function getChangedHooksIndices(prev: any, next: any): null | Array<number> {
1173 + if (enableProfilerChangedHookIndices) {
1174 + if (prev == null || next == null) {
1175 + return null;
1176 + }
1177 +
1178 + const indices = [];
1179 + let index = 0;
1180 + if (
1181 + next.hasOwnProperty('baseState') &&
1182 + next.hasOwnProperty('memoizedState') &&
1183 + next.hasOwnProperty('next') &&
1184 + next.hasOwnProperty('queue')
1185 + ) {
1186 + while (next !== null) {
1187 + if (didHookChange(prev, next)) {
1188 + indices.push(index);
1189 + }
1190 + next = next.next;
1191 + prev = prev.next;
1192 + index++;
1193 + }
1194 + }
1195 +
1196 + return indices;
1197 + }
1198 +
1199 + return null;
1200 + }
1201 +
1202 function getChangedKeys(prev: any, next: any): null | Array<string> {
1203 if (prev == null || next == null) {
1204 return null;
packages/react-devtools-shared/src/backend/types.js
+1
@@ -150,6 +150,7 @@ export type ChangeDescription = {|
150 isFirstMount: boolean,
151 props: Array<string> | null,
152 state: Array<string> | null,
153 + hooks?: Array<number> | null,
154 |};
155
156 export type CommitDataBackend = {|
packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js
+1 -1
@@ -13,4 +13,4 @@
13 * It should always be imported from "react-devtools-feature-flags".
14 ************************************************************************/
15
16 -// TODO Add feature flags here...
16 +export const enableProfilerChangedHookIndices = false;
packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js renamed
+2 -2
@@ -13,7 +13,7 @@
13 * It should always be imported from "react-devtools-feature-flags".
14 ************************************************************************/
15
16 -// TODO Add feature flags here...
16 +export const enableProfilerChangedHookIndices = true;
17
18 /************************************************************************
19 * Do not edit the code below.
@@ -21,7 +21,7 @@
21 ************************************************************************/
22
23 import typeof * as FeatureFlagsType from './DevToolsFeatureFlags.default';
24 -import typeof * as ExportsType from './DevToolsFeatureFlags.extension';
24 +import typeof * as ExportsType from './DevToolsFeatureFlags.extension-fb';
25
26 // eslint-disable-next-line no-unused-vars
27 type Check<_X, Y: _X, X: Y = _X> = null;
packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js new
+29
@@ -0,0 +1,29 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +/************************************************************************
11 + * This file is forked between different DevTools implementations.
12 + * It should never be imported directly!
13 + * It should always be imported from "react-devtools-feature-flags".
14 + ************************************************************************/
15 +
16 +export const enableProfilerChangedHookIndices = false;
17 +
18 +/************************************************************************
19 + * Do not edit the code below.
20 + * It ensures this fork exports the same types as the default flags file.
21 + ************************************************************************/
22 +
23 +import typeof * as FeatureFlagsType from './DevToolsFeatureFlags.default';
24 +import typeof * as ExportsType from './DevToolsFeatureFlags.extension-oss';
25 +
26 +// eslint-disable-next-line no-unused-vars
27 +type Check<_X, Y: _X, X: Y = _X> = null;
28 +// eslint-disable-next-line no-unused-expressions
29 +(null: Check<ExportsType, FeatureFlagsType>);
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.css
+9
@@ -69,3 +69,12 @@
69 flex: 0 0 1rem;
70 width: 1rem;
71 }
72 +
73 +.PrimitiveHookNumber {
74 + color: var(--color-component-badge-count-inverted);
75 + background-color: var(--color-component-badge-background-inverted);
76 + font-size: var(--font-size-monospace-small);
77 + margin-right: 0.25rem;
78 + border-radius: 0.125rem;
79 + padding: 0.125rem 0.25rem;
80 +}
\ No newline at end of file
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js
+14 -1
@@ -20,6 +20,7 @@ import Store from '../../store';
20 import styles from './InspectedElementHooksTree.css';
21 import useContextMenu from '../../ContextMenu/useContextMenu';
22 import {meta} from '../../../hydration';
23 +import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags';
24
25 import type {InspectedElement} from './types';
26 import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
@@ -108,7 +109,7 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
109 canEditHooksAndDeletePaths,
110 canEditHooksAndRenamePaths,
111 } = inspectedElement;
111 - const {name, id: hookID, isStateEditable, subHooks, value} = hook;
112 + const {id: hookID, isStateEditable, subHooks, value} = hook;
113
114 const isReadOnly = hookID == null || !isStateEditable;
115
@@ -162,6 +163,18 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
163
164 const isCustomHook = subHooks.length > 0;
165
166 + let name = hook.name;
167 + if (enableProfilerChangedHookIndices) {
168 + if (!isCustomHook) {
169 + name = (
170 + <>
171 + <span className={styles.PrimitiveHookNumber}>{hookID + 1}</span>
172 + {name}
173 + </>
174 + );
175 + }
176 + }
177 +
178 const type = typeof value;
179
180 let displayValue;
packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js
+52 -22
@@ -9,11 +9,30 @@
9
10 import * as React from 'react';
11 import {useContext} from 'react';
12 +import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags';
13 import {ProfilerContext} from '../Profiler/ProfilerContext';
14 import {StoreContext} from '../context';
15
16 import styles from './WhatChanged.css';
17
18 +function hookIndicesToString(indices: Array<number>): string {
19 + // This is debatable but I think 1-based might ake for a nicer UX.
20 + const numbers = indices.map(value => value + 1);
21 +
22 + switch (numbers.length) {
23 + case 0:
24 + return 'No hooks changed';
25 + case 1:
26 + return `Hook ${numbers[0]} changed`;
27 + case 2:
28 + return `Hooks ${numbers[0]} and ${numbers[1]} changed`;
29 + default:
30 + return `Hooks ${numbers.slice(0, numbers.length - 1).join(', ')} and ${
31 + numbers[numbers.length - 1]
32 + } changed`;
33 + }
34 +}
35 +
36 type Props = {|
37 fiberID: number,
38 |};
@@ -44,7 +63,16 @@ export default function WhatChanged({fiberID}: Props) {
63 return null;
64 }
65
47 - if (changeDescription.isFirstMount) {
66 + const {
67 + context,
68 + didHooksChange,
69 + hooks,
70 + isFirstMount,
71 + props,
72 + state,
73 + } = changeDescription;
74 +
75 + if (isFirstMount) {
76 return (
77 <div className={styles.Component}>
78 <label className={styles.Label}>Why did this render?</label>
@@ -57,21 +85,21 @@ export default function WhatChanged({fiberID}: Props) {
85
86 const changes = [];
87
60 - if (changeDescription.context === true) {
88 + if (context === true) {
89 changes.push(
90 <div key="context" className={styles.Item}>
91 • Context changed
92 </div>,
93 );
94 } else if (
67 - typeof changeDescription.context === 'object' &&
68 - changeDescription.context !== null &&
69 - changeDescription.context.length !== 0
95 + typeof context === 'object' &&
96 + context !== null &&
97 + context.length !== 0
98 ) {
99 changes.push(
100 <div key="context" className={styles.Item}>
101 • Context changed:
74 - {changeDescription.context.map(key => (
102 + {context.map(key => (
103 <span key={key} className={styles.Key}>
104 {key}
105 </span>
@@ -80,22 +108,27 @@ export default function WhatChanged({fiberID}: Props) {
108 );
109 }
110
83 - if (changeDescription.didHooksChange) {
84 - changes.push(
85 - <div key="hooks" className={styles.Item}>
86 - • Hooks changed
87 - </div>,
88 - );
111 + if (didHooksChange) {
112 + if (enableProfilerChangedHookIndices && Array.isArray(hooks)) {
113 + changes.push(
114 + <div key="hooks" className={styles.Item}>
115 + • {hookIndicesToString(hooks)}
116 + </div>,
117 + );
118 + } else {
119 + changes.push(
120 + <div key="hooks" className={styles.Item}>
121 + • Hooks changed
122 + </div>,
123 + );
124 + }
125 }
126
91 - if (
92 - changeDescription.props !== null &&
93 - changeDescription.props.length !== 0
94 - ) {
127 + if (props !== null && props.length !== 0) {
128 changes.push(
129 <div key="props" className={styles.Item}>
130 • Props changed:
98 - {changeDescription.props.map(key => (
131 + {props.map(key => (
132 <span key={key} className={styles.Key}>
133 {key}
134 </span>
@@ -104,14 +137,11 @@ export default function WhatChanged({fiberID}: Props) {
137 );
138 }
139
107 - if (
108 - changeDescription.state !== null &&
109 - changeDescription.state.length !== 0
110 - ) {
140 + if (state !== null && state.length !== 0) {
141 changes.push(
142 <div key="state" className={styles.Item}>
143 • State changed:
114 - {changeDescription.state.map(key => (
144 + {state.map(key => (
145 <span key={key} className={styles.Key}>
146 {key}
147 </span>
packages/react-devtools-shared/src/devtools/views/Profiler/types.js
+1
@@ -46,6 +46,7 @@ export type ChangeDescription = {|
46 isFirstMount: boolean,
47 props: Array<string> | null,
48 state: Array<string> | null,
49 + hooks?: Array<number> | null,
50 |};
51
52 export type CommitDataFrontend = {|