[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 = {|