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

This commit is contained in:
Brian Vaughn 2021-03-17 12:28:21 -04:00 committed by GitHub
parent bf11788bf0
commit 119736b1c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 172 additions and 35 deletions

View File

@ -6,7 +6,7 @@
"build": "cross-env NODE_ENV=production yarn run build:chrome && yarn run build:firefox && yarn run build:edge",
"build:dev": "cross-env NODE_ENV=development yarn run build:chrome:dev && yarn run build:firefox:dev && yarn run build:edge:dev",
"build:chrome": "cross-env NODE_ENV=production node ./chrome/build",
"build:chrome:crx": "cross-env NODE_ENV=production node ./chrome/build --crx",
"build:chrome:crx": "cross-env NODE_ENV=production FEATURE_FLAG_TARGET=extension-fb node ./chrome/build --crx",
"build:chrome:dev": "cross-env NODE_ENV=development node ./chrome/build",
"build:firefox": "cross-env NODE_ENV=production node ./firefox/build",
"build:firefox:dev": "cross-env NODE_ENV=development node ./firefox/build",

View File

@ -3,6 +3,7 @@
const {resolve} = require('path');
const {DefinePlugin} = require('webpack');
const {GITHUB_URL, getVersionString} = require('./utils');
const {resolveFeatureFlags} = require('react-devtools-shared/buildUtils');
const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
@ -16,6 +17,8 @@ const __DEV__ = NODE_ENV === 'development';
const DEVTOOLS_VERSION = getVersionString();
const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
module.exports = {
mode: __DEV__ ? 'development' : 'production',
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
@ -34,6 +37,7 @@ module.exports = {
alias: {
react: resolve(builtModulesDir, 'react'),
'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'),
'react-devtools-feature-flags': resolveFeatureFlags(featureFlagTarget),
'react-dom': resolve(builtModulesDir, 'react-dom'),
'react-is': resolve(builtModulesDir, 'react-is'),
scheduler: resolve(builtModulesDir, 'scheduler'),

View File

@ -17,6 +17,8 @@ const __DEV__ = NODE_ENV === 'development';
const DEVTOOLS_VERSION = getVersionString();
const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
module.exports = {
mode: __DEV__ ? 'development' : 'production',
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
@ -40,7 +42,7 @@ module.exports = {
alias: {
react: resolve(builtModulesDir, 'react'),
'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'),
'react-devtools-feature-flags': resolveFeatureFlags('extension'),
'react-devtools-feature-flags': resolveFeatureFlags(featureFlagTarget),
'react-dom': resolve(builtModulesDir, 'react-dom'),
'react-is': resolve(builtModulesDir, 'react-is'),
scheduler: resolve(builtModulesDir, 'scheduler'),

View File

@ -17,8 +17,11 @@ function resolveFeatureFlags(target) {
case 'shell':
flagsPath = 'DevToolsFeatureFlags.default';
break;
case 'extension':
flagsPath = 'DevToolsFeatureFlags.extension';
case 'extension-oss':
flagsPath = 'DevToolsFeatureFlags.extension-oss';
break;
case 'extension-fb':
flagsPath = 'DevToolsFeatureFlags.extension-fb';
break;
default:
console.error(`Invalid target "${target}"`);

View File

@ -79,6 +79,7 @@ import {
MEMO_SYMBOL_STRING,
} from './ReactSymbols';
import {format} from './utils';
import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {
@ -978,12 +979,9 @@ export function attach(
state: null,
};
} else {
return {
const data: ChangeDescription = {
context: getContextChangedKeys(nextFiber),
didHooksChange: didHooksChange(
prevFiber.memoizedState,
nextFiber.memoizedState,
),
didHooksChange: false,
isFirstMount: false,
props: getChangedKeys(
prevFiber.memoizedProps,
@ -994,6 +992,23 @@ export function attach(
nextFiber.memoizedState,
),
};
// Only traverse the hooks list once, depending on what info we're returning.
if (enableProfilerChangedHookIndices) {
const indices = getChangedHooksIndices(
prevFiber.memoizedState,
nextFiber.memoizedState,
);
data.hooks = indices;
data.didHooksChange = indices !== null && indices.length > 0;
} else {
data.didHooksChange = didHooksChange(
prevFiber.memoizedState,
nextFiber.memoizedState,
);
}
return data;
}
default:
return null;
@ -1154,6 +1169,36 @@ export function attach(
return false;
}
function getChangedHooksIndices(prev: any, next: any): null | Array<number> {
if (enableProfilerChangedHookIndices) {
if (prev == null || next == null) {
return null;
}
const indices = [];
let index = 0;
if (
next.hasOwnProperty('baseState') &&
next.hasOwnProperty('memoizedState') &&
next.hasOwnProperty('next') &&
next.hasOwnProperty('queue')
) {
while (next !== null) {
if (didHookChange(prev, next)) {
indices.push(index);
}
next = next.next;
prev = prev.next;
index++;
}
}
return indices;
}
return null;
}
function getChangedKeys(prev: any, next: any): null | Array<string> {
if (prev == null || next == null) {
return null;

View File

@ -150,6 +150,7 @@ export type ChangeDescription = {|
isFirstMount: boolean,
props: Array<string> | null,
state: Array<string> | null,
hooks?: Array<number> | null,
|};
export type CommitDataBackend = {|

View File

@ -13,4 +13,4 @@
* It should always be imported from "react-devtools-feature-flags".
************************************************************************/
// TODO Add feature flags here...
export const enableProfilerChangedHookIndices = false;

View File

@ -13,7 +13,7 @@
* It should always be imported from "react-devtools-feature-flags".
************************************************************************/
// TODO Add feature flags here...
export const enableProfilerChangedHookIndices = true;
/************************************************************************
* Do not edit the code below.
@ -21,7 +21,7 @@
************************************************************************/
import typeof * as FeatureFlagsType from './DevToolsFeatureFlags.default';
import typeof * as ExportsType from './DevToolsFeatureFlags.extension';
import typeof * as ExportsType from './DevToolsFeatureFlags.extension-fb';
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) Facebook, Inc. and its 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 file is forked between different DevTools implementations.
* It should never be imported directly!
* It should always be imported from "react-devtools-feature-flags".
************************************************************************/
export const enableProfilerChangedHookIndices = false;
/************************************************************************
* Do not edit the code below.
* It ensures this fork exports the same types as the default flags file.
************************************************************************/
import typeof * as FeatureFlagsType from './DevToolsFeatureFlags.default';
import typeof * as ExportsType from './DevToolsFeatureFlags.extension-oss';
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;
// eslint-disable-next-line no-unused-expressions
(null: Check<ExportsType, FeatureFlagsType>);

View File

@ -69,3 +69,12 @@
flex: 0 0 1rem;
width: 1rem;
}
.PrimitiveHookNumber {
color: var(--color-component-badge-count-inverted);
background-color: var(--color-component-badge-background-inverted);
font-size: var(--font-size-monospace-small);
margin-right: 0.25rem;
border-radius: 0.125rem;
padding: 0.125rem 0.25rem;
}

View File

@ -20,6 +20,7 @@ import Store from '../../store';
import styles from './InspectedElementHooksTree.css';
import useContextMenu from '../../ContextMenu/useContextMenu';
import {meta} from '../../../hydration';
import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags';
import type {InspectedElement} from './types';
import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
@ -108,7 +109,7 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
canEditHooksAndDeletePaths,
canEditHooksAndRenamePaths,
} = inspectedElement;
const {name, id: hookID, isStateEditable, subHooks, value} = hook;
const {id: hookID, isStateEditable, subHooks, value} = hook;
const isReadOnly = hookID == null || !isStateEditable;
@ -162,6 +163,18 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
const isCustomHook = subHooks.length > 0;
let name = hook.name;
if (enableProfilerChangedHookIndices) {
if (!isCustomHook) {
name = (
<>
<span className={styles.PrimitiveHookNumber}>{hookID + 1}</span>
{name}
</>
);
}
}
const type = typeof value;
let displayValue;

View File

@ -9,11 +9,30 @@
import * as React from 'react';
import {useContext} from 'react';
import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags';
import {ProfilerContext} from '../Profiler/ProfilerContext';
import {StoreContext} from '../context';
import styles from './WhatChanged.css';
function hookIndicesToString(indices: Array<number>): string {
// This is debatable but I think 1-based might ake for a nicer UX.
const numbers = indices.map(value => value + 1);
switch (numbers.length) {
case 0:
return 'No hooks changed';
case 1:
return `Hook ${numbers[0]} changed`;
case 2:
return `Hooks ${numbers[0]} and ${numbers[1]} changed`;
default:
return `Hooks ${numbers.slice(0, numbers.length - 1).join(', ')} and ${
numbers[numbers.length - 1]
} changed`;
}
}
type Props = {|
fiberID: number,
|};
@ -44,7 +63,16 @@ export default function WhatChanged({fiberID}: Props) {
return null;
}
if (changeDescription.isFirstMount) {
const {
context,
didHooksChange,
hooks,
isFirstMount,
props,
state,
} = changeDescription;
if (isFirstMount) {
return (
<div className={styles.Component}>
<label className={styles.Label}>Why did this render?</label>
@ -57,21 +85,21 @@ export default function WhatChanged({fiberID}: Props) {
const changes = [];
if (changeDescription.context === true) {
if (context === true) {
changes.push(
<div key="context" className={styles.Item}>
Context changed
</div>,
);
} else if (
typeof changeDescription.context === 'object' &&
changeDescription.context !== null &&
changeDescription.context.length !== 0
typeof context === 'object' &&
context !== null &&
context.length !== 0
) {
changes.push(
<div key="context" className={styles.Item}>
Context changed:
{changeDescription.context.map(key => (
{context.map(key => (
<span key={key} className={styles.Key}>
{key}
</span>
@ -80,22 +108,27 @@ export default function WhatChanged({fiberID}: Props) {
);
}
if (changeDescription.didHooksChange) {
changes.push(
<div key="hooks" className={styles.Item}>
Hooks changed
</div>,
);
if (didHooksChange) {
if (enableProfilerChangedHookIndices && Array.isArray(hooks)) {
changes.push(
<div key="hooks" className={styles.Item}>
{hookIndicesToString(hooks)}
</div>,
);
} else {
changes.push(
<div key="hooks" className={styles.Item}>
Hooks changed
</div>,
);
}
}
if (
changeDescription.props !== null &&
changeDescription.props.length !== 0
) {
if (props !== null && props.length !== 0) {
changes.push(
<div key="props" className={styles.Item}>
Props changed:
{changeDescription.props.map(key => (
{props.map(key => (
<span key={key} className={styles.Key}>
{key}
</span>
@ -104,14 +137,11 @@ export default function WhatChanged({fiberID}: Props) {
);
}
if (
changeDescription.state !== null &&
changeDescription.state.length !== 0
) {
if (state !== null && state.length !== 0) {
changes.push(
<div key="state" className={styles.Item}>
State changed:
{changeDescription.state.map(key => (
{state.map(key => (
<span key={key} className={styles.Key}>
{key}
</span>

View File

@ -46,6 +46,7 @@ export type ChangeDescription = {|
isFirstMount: boolean,
props: Array<string> | null,
state: Array<string> | null,
hooks?: Array<number> | null,
|};
export type CommitDataFrontend = {|