mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #310 from bvaughn/track-changed-props
Add profiling option to track why a component rendered
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,7 @@ describe('ProfilerContext', () => {
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
store.recordChangeDescriptions = true;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
|
||||
@@ -14,6 +14,7 @@ describe('ProfilerStore', () => {
|
||||
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
store.recordChangeDescriptions = true;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
|
||||
@@ -5,6 +5,7 @@ import type Bridge from 'src/bridge';
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
describe('ProfilingCache', () => {
|
||||
let PropTypes;
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let Scheduler;
|
||||
@@ -21,7 +22,9 @@ describe('ProfilingCache', () => {
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
store.recordChangeDescriptions = true;
|
||||
|
||||
PropTypes = require('prop-types');
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
Scheduler = require('scheduler');
|
||||
@@ -187,6 +190,117 @@ describe('ProfilingCache', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should record changed props/state/context/hooks', () => {
|
||||
let instance = null;
|
||||
|
||||
const ModernContext = React.createContext(0);
|
||||
|
||||
class LegacyContextProvider extends React.Component<
|
||||
any,
|
||||
{| count: number |}
|
||||
> {
|
||||
static childContextTypes = {
|
||||
count: PropTypes.number,
|
||||
};
|
||||
state = { count: 0 };
|
||||
getChildContext() {
|
||||
return this.state;
|
||||
}
|
||||
render() {
|
||||
instance = this;
|
||||
return (
|
||||
<ModernContext.Provider value={this.state.count}>
|
||||
<React.Fragment>
|
||||
<ModernContextConsumer />
|
||||
<LegacyContextConsumer />
|
||||
</React.Fragment>
|
||||
</ModernContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const FunctionComponentWithHooks = ({ count }) => {
|
||||
React.useMemo(() => count, [count]);
|
||||
return null;
|
||||
};
|
||||
|
||||
class ModernContextConsumer extends React.Component<any> {
|
||||
static contextType = ModernContext;
|
||||
render() {
|
||||
return <FunctionComponentWithHooks count={this.context} />;
|
||||
}
|
||||
}
|
||||
|
||||
class LegacyContextConsumer extends React.Component<any> {
|
||||
static contextTypes = {
|
||||
count: PropTypes.number,
|
||||
};
|
||||
render() {
|
||||
return <FunctionComponentWithHooks count={this.context.count} />;
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => ReactDOM.render(<LegacyContextProvider />, container));
|
||||
expect(instance).not.toBeNull();
|
||||
utils.act(() => (instance: any).setState({ count: 1 }));
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<LegacyContextProvider foo={123} />, container)
|
||||
);
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<LegacyContextProvider bar="abc" />, container)
|
||||
);
|
||||
utils.act(() => ReactDOM.render(<LegacyContextProvider />, container));
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
const allCommitData = [];
|
||||
|
||||
function Validator({ commitIndex, previousCommitDetails, rootID }) {
|
||||
const commitData = store.profilerStore.getCommitData(rootID, commitIndex);
|
||||
if (previousCommitDetails != null) {
|
||||
expect(commitData).toEqual(previousCommitDetails);
|
||||
} else {
|
||||
allCommitData.push(commitData);
|
||||
expect(commitData).toMatchSnapshot(
|
||||
`CommitDetails commitIndex: ${commitIndex}`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootID = store.roots[0];
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 5; commitIndex++) {
|
||||
utils.act(() => {
|
||||
TestRenderer.create(
|
||||
<Validator
|
||||
commitIndex={commitIndex}
|
||||
previousCommitDetails={null}
|
||||
rootID={rootID}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
expect(allCommitData).toHaveLength(5);
|
||||
|
||||
utils.exportImportHelper(bridge, store);
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 5; commitIndex++) {
|
||||
utils.act(() => {
|
||||
TestRenderer.create(
|
||||
<Validator
|
||||
commitIndex={commitIndex}
|
||||
previousCommitDetails={allCommitData[commitIndex]}
|
||||
rootID={rootID}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should calculate a self duration based on actual children (not filtered children)', () => {
|
||||
store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ describe('profiling charts', () => {
|
||||
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
store.recordChangeDescriptions = true;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
|
||||
@@ -17,6 +17,7 @@ describe('commit tree', () => {
|
||||
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
store.recordChangeDescriptions = true;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
|
||||
@@ -21,6 +21,7 @@ describe('Store component filters', () => {
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
store.componentFilters = [];
|
||||
store.recordChangeDescriptions = true;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
|
||||
+17
-4
@@ -6,6 +6,7 @@ import throttle from 'lodash.throttle';
|
||||
import {
|
||||
SESSION_STORAGE_LAST_SELECTION_KEY,
|
||||
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
|
||||
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
|
||||
__DEBUG__,
|
||||
} from '../constants';
|
||||
import {
|
||||
@@ -69,6 +70,7 @@ type PersistedSelection = {|
|
||||
export default class Agent extends EventEmitter {
|
||||
_bridge: Bridge;
|
||||
_isProfiling: boolean = false;
|
||||
_recordChangeDescriptions: boolean = false;
|
||||
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
|
||||
_persistedSelection: PersistedSelection | null = null;
|
||||
_persistedSelectionMatch: PathMatch | null = null;
|
||||
@@ -79,8 +81,13 @@ export default class Agent extends EventEmitter {
|
||||
if (
|
||||
sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
|
||||
) {
|
||||
this._recordChangeDescriptions =
|
||||
sessionStorageGetItem(
|
||||
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY
|
||||
) === 'true';
|
||||
this._isProfiling = true;
|
||||
|
||||
sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY);
|
||||
sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY);
|
||||
}
|
||||
|
||||
@@ -250,8 +257,12 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
reloadAndProfile = () => {
|
||||
reloadAndProfile = (recordChangeDescriptions: boolean) => {
|
||||
sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
|
||||
sessionStorageSetItem(
|
||||
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
|
||||
recordChangeDescriptions ? 'true' : 'false'
|
||||
);
|
||||
|
||||
// This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
|
||||
// In that case, the shell must also listen for this specific message to know when it needs to reload the app.
|
||||
@@ -358,7 +369,7 @@ export default class Agent extends EventEmitter {
|
||||
this._rendererInterfaces[rendererID] = rendererInterface;
|
||||
|
||||
if (this._isProfiling) {
|
||||
rendererInterface.startProfiling();
|
||||
rendererInterface.startProfiling(this._recordChangeDescriptions);
|
||||
}
|
||||
|
||||
// When the renderer is attached, we need to tell it whether
|
||||
@@ -397,13 +408,14 @@ export default class Agent extends EventEmitter {
|
||||
window.addEventListener('pointerup', this._onPointerUp, true);
|
||||
};
|
||||
|
||||
startProfiling = () => {
|
||||
startProfiling = (recordChangeDescriptions: boolean) => {
|
||||
this._recordChangeDescriptions = recordChangeDescriptions;
|
||||
this._isProfiling = true;
|
||||
for (let rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
renderer.startProfiling();
|
||||
renderer.startProfiling(recordChangeDescriptions);
|
||||
}
|
||||
this._bridge.send('profilingStatus', this._isProfiling);
|
||||
};
|
||||
@@ -422,6 +434,7 @@ export default class Agent extends EventEmitter {
|
||||
|
||||
stopProfiling = () => {
|
||||
this._isProfiling = false;
|
||||
this._recordChangeDescriptions = false;
|
||||
for (let rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(rendererID: any)
|
||||
|
||||
+220
-6
@@ -30,6 +30,7 @@ import { cleanForBridge, copyWithSet, setInObject } from './utils';
|
||||
import {
|
||||
__DEBUG__,
|
||||
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
|
||||
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
|
||||
TREE_OPERATION_ADD,
|
||||
TREE_OPERATION_REMOVE,
|
||||
TREE_OPERATION_REORDER_CHILDREN,
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
import { inspectHooksOfFiber } from './ReactDebugHooks';
|
||||
|
||||
import type {
|
||||
ChangeDescription,
|
||||
CommitDataBackend,
|
||||
DevToolsHook,
|
||||
Fiber,
|
||||
@@ -350,7 +352,7 @@ export function attach(
|
||||
// The unmount operations are already significantly smaller than mount opreations though.
|
||||
// This is something to keep in mind for later.
|
||||
function updateComponentFilters(componentFilters: Array<ComponentFilter>) {
|
||||
if (this._isProfiling) {
|
||||
if (isProfiling) {
|
||||
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
|
||||
// If necessary, we could support this- but it doesn't seem like a necessary use case.
|
||||
throw Error('Cannot modify filter preferences while profiling');
|
||||
@@ -646,8 +648,185 @@ export function attach(
|
||||
return ((fiberToIDMap.get(primaryFiber): any): number);
|
||||
}
|
||||
|
||||
function getChangeDescription(
|
||||
prevFiber: Fiber | null,
|
||||
nextFiber: Fiber
|
||||
): ChangeDescription | null {
|
||||
switch (getElementTypeForFiber(nextFiber)) {
|
||||
case ElementTypeClass:
|
||||
case ElementTypeFunction:
|
||||
case ElementTypeMemo:
|
||||
case ElementTypeForwardRef:
|
||||
if (prevFiber === null) {
|
||||
return {
|
||||
context: null,
|
||||
didHooksChange: false,
|
||||
isFirstMount: true,
|
||||
props: null,
|
||||
state: null,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
context: getContextChangedKeys(nextFiber),
|
||||
didHooksChange: didHooksChange(
|
||||
prevFiber.memoizedState,
|
||||
nextFiber.memoizedState
|
||||
),
|
||||
isFirstMount: false,
|
||||
props: getChangedKeys(
|
||||
prevFiber.memoizedProps,
|
||||
nextFiber.memoizedProps
|
||||
),
|
||||
state: getChangedKeys(
|
||||
prevFiber.memoizedState,
|
||||
nextFiber.memoizedState
|
||||
),
|
||||
};
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateContextsForFiber(fiber: Fiber) {
|
||||
switch (getElementTypeForFiber(fiber)) {
|
||||
case ElementTypeClass:
|
||||
if (idToContextsMap !== null) {
|
||||
const id = getFiberID(getPrimaryFiber(fiber));
|
||||
const contexts = getContextsForFiber(fiber);
|
||||
if (contexts !== null) {
|
||||
idToContextsMap.set(id, contexts);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Differentiates between a null context value and no context.
|
||||
const NO_CONTEXT = {};
|
||||
|
||||
function getContextsForFiber(fiber: Fiber): [Object, any] | null {
|
||||
switch (getElementTypeForFiber(fiber)) {
|
||||
case ElementTypeClass:
|
||||
const instance = fiber.stateNode;
|
||||
let legacyContext = NO_CONTEXT;
|
||||
let modernContext = NO_CONTEXT;
|
||||
if (instance != null) {
|
||||
if (
|
||||
instance.constructor &&
|
||||
instance.constructor.contextType != null
|
||||
) {
|
||||
modernContext = instance.context;
|
||||
} else {
|
||||
legacyContext = instance.context;
|
||||
if (legacyContext && Object.keys(legacyContext).length === 0) {
|
||||
legacyContext = NO_CONTEXT;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [legacyContext, modernContext];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Record all contexts at the time profiling is started.
|
||||
// Fibers only store the current context value,
|
||||
// so we need to track them separatenly in order to determine changed keys.
|
||||
function crawlToInitializeContextsMap(fiber: Fiber) {
|
||||
updateContextsForFiber(fiber);
|
||||
let current = fiber.child;
|
||||
while (current !== null) {
|
||||
crawlToInitializeContextsMap(current);
|
||||
current = current.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
function getContextChangedKeys(fiber: Fiber): null | boolean | Array<string> {
|
||||
switch (getElementTypeForFiber(fiber)) {
|
||||
case ElementTypeClass:
|
||||
if (idToContextsMap !== null) {
|
||||
const id = getFiberID(getPrimaryFiber(fiber));
|
||||
const prevContexts = idToContextsMap.has(id)
|
||||
? idToContextsMap.get(id)
|
||||
: null;
|
||||
const nextContexts = getContextsForFiber(fiber);
|
||||
|
||||
if (prevContexts == null || nextContexts == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [prevLegacyContext, prevModernContext] = prevContexts;
|
||||
const [nextLegacyContext, nextModernContext] = nextContexts;
|
||||
|
||||
if (nextLegacyContext !== NO_CONTEXT) {
|
||||
return getChangedKeys(prevLegacyContext, nextLegacyContext);
|
||||
} else if (nextModernContext !== NO_CONTEXT) {
|
||||
return prevModernContext !== nextModernContext;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function didHooksChange(prev: any, next: any): boolean {
|
||||
if (next == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We can't report anything meaningful for hooks changes.
|
||||
if (
|
||||
next.hasOwnProperty('baseState') &&
|
||||
next.hasOwnProperty('memoizedState') &&
|
||||
next.hasOwnProperty('next') &&
|
||||
next.hasOwnProperty('queue')
|
||||
) {
|
||||
while (next !== null) {
|
||||
if (next.memoizedState !== prev.memoizedState) {
|
||||
return true;
|
||||
} else {
|
||||
next = next.next;
|
||||
prev = prev.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getChangedKeys(prev: any, next: any): null | Array<string> {
|
||||
if (prev == null || next == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We can't report anything meaningful for hooks changes.
|
||||
if (
|
||||
next.hasOwnProperty('baseState') &&
|
||||
next.hasOwnProperty('memoizedState') &&
|
||||
next.hasOwnProperty('next') &&
|
||||
next.hasOwnProperty('queue')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
|
||||
const changedKeys = [];
|
||||
for (let key of keys) {
|
||||
if (prev[key] !== next[key]) {
|
||||
changedKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return changedKeys;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function hasDataChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
|
||||
function didFiberRender(prevFiber: Fiber, nextFiber: Fiber): boolean {
|
||||
switch (nextFiber.tag) {
|
||||
case ClassComponent:
|
||||
case FunctionComponent:
|
||||
@@ -1024,7 +1203,7 @@ export function attach(
|
||||
pushOperation(treeBaseDuration);
|
||||
}
|
||||
|
||||
if (alternate == null || hasDataChanged(alternate, fiber)) {
|
||||
if (alternate == null || didFiberRender(alternate, fiber)) {
|
||||
if (actualDuration != null) {
|
||||
// The actual duration reported by React includes time spent working on children.
|
||||
// This is useful information, but it's also useful to be able to exclude child durations.
|
||||
@@ -1049,6 +1228,17 @@ export function attach(
|
||||
metadata.maxActualDuration,
|
||||
actualDuration
|
||||
);
|
||||
|
||||
if (recordChangeDescriptions) {
|
||||
const changeDescription = getChangeDescription(alternate, fiber);
|
||||
if (changeDescription !== null) {
|
||||
if (metadata.changeDescriptions !== null) {
|
||||
metadata.changeDescriptions.set(id, changeDescription);
|
||||
}
|
||||
}
|
||||
|
||||
updateContextsForFiber(fiber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1110,7 +1300,7 @@ export function attach(
|
||||
mostRecentlyInspectedElementID !== null &&
|
||||
mostRecentlyInspectedElementID ===
|
||||
getFiberID(getPrimaryFiber(nextFiber)) &&
|
||||
hasDataChanged(prevFiber, nextFiber)
|
||||
didFiberRender(prevFiber, nextFiber)
|
||||
) {
|
||||
// If this Fiber has updated, clear cached inspected data.
|
||||
// If it is inspected again, it may need to be re-run to obtain updated hooks values.
|
||||
@@ -1300,6 +1490,7 @@ export function attach(
|
||||
// If profiling is active, store commit time and duration, and the current interactions.
|
||||
// The frontend may request this information after profiling has stopped.
|
||||
currentCommitProfilingMetadata = {
|
||||
changeDescriptions: recordChangeDescriptions ? new Map() : null,
|
||||
durations: [],
|
||||
commitTime: performance.now() - profilingStartTime,
|
||||
interactions: Array.from(root.memoizedInteractions).map(
|
||||
@@ -1343,6 +1534,7 @@ export function attach(
|
||||
// If profiling is active, store commit time and duration, and the current interactions.
|
||||
// The frontend may request this information after profiling has stopped.
|
||||
currentCommitProfilingMetadata = {
|
||||
changeDescriptions: recordChangeDescriptions ? new Map() : null,
|
||||
durations: [],
|
||||
commitTime: performance.now() - profilingStartTime,
|
||||
interactions: Array.from(root.memoizedInteractions).map(
|
||||
@@ -2058,6 +2250,7 @@ export function attach(
|
||||
}
|
||||
|
||||
type CommitProfilingData = {|
|
||||
changeDescriptions: Map<number, ChangeDescription> | null,
|
||||
commitTime: number,
|
||||
durations: Array<number>,
|
||||
interactions: Array<Interaction>,
|
||||
@@ -2070,10 +2263,12 @@ export function attach(
|
||||
|
||||
let currentCommitProfilingMetadata: CommitProfilingData | null = null;
|
||||
let displayNamesByRootID: DisplayNamesByRootID | null = null;
|
||||
let idToContextsMap: Map<number, any> | null = null;
|
||||
let initialTreeBaseDurationsMap: Map<number, number> | null = null;
|
||||
let initialIDToRootMap: Map<number, number> | null = null;
|
||||
let isProfiling: boolean = false;
|
||||
let profilingStartTime: number = 0;
|
||||
let recordChangeDescriptions: boolean = false;
|
||||
let rootToCommitProfilingMetadataMap: CommitProfilingMetadataMap | null = null;
|
||||
|
||||
function getProfilingData(): ProfilingDataBackend {
|
||||
@@ -2111,6 +2306,7 @@ export function attach(
|
||||
|
||||
commitProfilingMetadata.forEach((commitProfilingData, commitIndex) => {
|
||||
const {
|
||||
changeDescriptions,
|
||||
durations,
|
||||
interactions,
|
||||
maxActualDuration,
|
||||
@@ -2144,6 +2340,10 @@ export function attach(
|
||||
}
|
||||
|
||||
commitData.push({
|
||||
changeDescriptions:
|
||||
changeDescriptions !== null
|
||||
? Array.from(changeDescriptions.entries())
|
||||
: null,
|
||||
duration: maxActualDuration,
|
||||
fiberActualDurations,
|
||||
fiberSelfDurations,
|
||||
@@ -2170,11 +2370,13 @@ export function attach(
|
||||
};
|
||||
}
|
||||
|
||||
function startProfiling() {
|
||||
function startProfiling(shouldRecordChangeDescriptions: boolean) {
|
||||
if (isProfiling) {
|
||||
return;
|
||||
}
|
||||
|
||||
recordChangeDescriptions = shouldRecordChangeDescriptions;
|
||||
|
||||
// Capture initial values as of the time profiling starts.
|
||||
// It's important we snapshot both the durations and the id-to-root map,
|
||||
// since either of these may change during the profiling session
|
||||
@@ -2182,6 +2384,7 @@ export function attach(
|
||||
displayNamesByRootID = new Map();
|
||||
initialTreeBaseDurationsMap = new Map(idToTreeBaseDurationMap);
|
||||
initialIDToRootMap = new Map(idToRootMap);
|
||||
idToContextsMap = new Map();
|
||||
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
const rootID = getFiberID(getPrimaryFiber(root.current));
|
||||
@@ -2189,6 +2392,13 @@ export function attach(
|
||||
rootID,
|
||||
getDisplayNameForRoot(root.current)
|
||||
);
|
||||
|
||||
if (shouldRecordChangeDescriptions) {
|
||||
// Record all contexts at the time profiling is started.
|
||||
// Fibers only store the current context value,
|
||||
// so we need to track them separatenly in order to determine changed keys.
|
||||
crawlToInitializeContextsMap(root.current);
|
||||
}
|
||||
});
|
||||
|
||||
isProfiling = true;
|
||||
@@ -2198,13 +2408,17 @@ export function attach(
|
||||
|
||||
function stopProfiling() {
|
||||
isProfiling = false;
|
||||
recordChangeDescriptions = false;
|
||||
}
|
||||
|
||||
// Automatically start profiling so that we don't miss timing info from initial "mount".
|
||||
if (
|
||||
sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
|
||||
) {
|
||||
startProfiling();
|
||||
startProfiling(
|
||||
sessionStorageGetItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
|
||||
'true'
|
||||
);
|
||||
}
|
||||
|
||||
// React will switch between these implementations depending on whether
|
||||
|
||||
+11
-1
@@ -112,7 +112,17 @@ export type ReactRenderer = {
|
||||
currentDispatcherRef?: {| current: null | Dispatcher |},
|
||||
};
|
||||
|
||||
export type ChangeDescription = {|
|
||||
context: Array<string> | boolean | null,
|
||||
didHooksChange: boolean,
|
||||
isFirstMount: boolean,
|
||||
props: Array<string> | null,
|
||||
state: Array<string> | null,
|
||||
|};
|
||||
|
||||
export type CommitDataBackend = {|
|
||||
// Tuple of fiber ID and change description
|
||||
changeDescriptions: Array<[number, ChangeDescription]> | null,
|
||||
duration: number,
|
||||
// Tuple of fiber ID and actual duration
|
||||
fiberActualDurations: Array<[number, number]>,
|
||||
@@ -226,7 +236,7 @@ export type RendererInterface = {
|
||||
setInProps: (id: number, path: Array<string | number>, value: any) => void,
|
||||
setInState: (id: number, path: Array<string | number>, value: any) => void,
|
||||
setTrackedPath: (path: Array<PathFrame> | null) => void,
|
||||
startProfiling: () => void,
|
||||
startProfiling: (recordChangeDescriptions: boolean) => void,
|
||||
stopProfiling: () => void,
|
||||
updateComponentFilters: (somponentFilters: Array<ComponentFilter>) => void,
|
||||
};
|
||||
|
||||
@@ -14,6 +14,9 @@ export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
|
||||
export const SESSION_STORAGE_LAST_SELECTION_KEY =
|
||||
'React::DevTools::lastSelection';
|
||||
|
||||
export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
|
||||
'React::DevTools::recordChangeDescriptions';
|
||||
|
||||
export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY =
|
||||
'React::DevTools::reloadAndProfile';
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ export default class ProfilerStore extends EventEmitter {
|
||||
}
|
||||
|
||||
startProfiling(): void {
|
||||
this._bridge.send('startProfiling');
|
||||
this._bridge.send('startProfiling', this._store.recordChangeDescriptions);
|
||||
|
||||
// Don't actually update the local profiling boolean yet!
|
||||
// Wait for onProfilingStatus() to confirm the status has changed.
|
||||
|
||||
@@ -38,6 +38,8 @@ const LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY =
|
||||
'React::DevTools::captureScreenshots';
|
||||
const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =
|
||||
'React::DevTools::collapseNodesByDefault';
|
||||
const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
|
||||
'React::DevTools::recordChangeDescriptions';
|
||||
|
||||
type Config = {|
|
||||
isProfiling?: boolean,
|
||||
@@ -83,6 +85,8 @@ export default class Store extends EventEmitter {
|
||||
|
||||
_profilerStore: ProfilerStore;
|
||||
|
||||
_recordChangeDescriptions: boolean = false;
|
||||
|
||||
// Incremented each time the store is mutated.
|
||||
// This enables a passive effect to detect a mutation between render and commit phase.
|
||||
_revision: number = 0;
|
||||
@@ -118,6 +122,10 @@ export default class Store extends EventEmitter {
|
||||
localStorageGetItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) !==
|
||||
'false';
|
||||
|
||||
this._recordChangeDescriptions =
|
||||
localStorageGetItem(LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
|
||||
'true';
|
||||
|
||||
this._componentFilters = getSavedComponentFilters();
|
||||
|
||||
let isProfiling = false;
|
||||
@@ -248,6 +256,20 @@ export default class Store extends EventEmitter {
|
||||
return this._profilerStore;
|
||||
}
|
||||
|
||||
get recordChangeDescriptions(): boolean {
|
||||
return this._recordChangeDescriptions;
|
||||
}
|
||||
set recordChangeDescriptions(value: boolean): void {
|
||||
this._recordChangeDescriptions = value;
|
||||
|
||||
localStorageSetItem(
|
||||
LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
|
||||
value ? 'true' : 'false'
|
||||
);
|
||||
|
||||
this.emit('recordChangeDescriptions');
|
||||
}
|
||||
|
||||
get revision(): number {
|
||||
return this._revision;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 2 1 200px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.RightColumn {
|
||||
@@ -21,6 +22,7 @@
|
||||
flex-direction: column;
|
||||
flex: 1 1 100px;
|
||||
max-width: 300px;
|
||||
overflow-x: hidden;
|
||||
border-left: 1px solid var(--color-border);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
@@ -60,7 +62,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.VRule {
|
||||
|
||||
@@ -7,27 +7,41 @@ import { BridgeContext, StoreContext } from '../context';
|
||||
import { useSubscription } from '../hooks';
|
||||
import Store from 'src/devtools/store';
|
||||
|
||||
type SubscriptionData = {|
|
||||
recordChangeDescriptions: boolean,
|
||||
supportsReloadAndProfile: boolean,
|
||||
|};
|
||||
|
||||
export default function ReloadAndProfileButton() {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const supportsReloadAndProfileSubscription = useMemo(
|
||||
const subscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () => store.supportsReloadAndProfile,
|
||||
getCurrentValue: () => ({
|
||||
recordChangeDescriptions: store.recordChangeDescriptions,
|
||||
supportsReloadAndProfile: store.supportsReloadAndProfile,
|
||||
}),
|
||||
subscribe: (callback: Function) => {
|
||||
store.addListener('recordChangeDescriptions', callback);
|
||||
store.addListener('supportsReloadAndProfile', callback);
|
||||
return () => store.removeListener('supportsReloadAndProfile', callback);
|
||||
return () => {
|
||||
store.removeListener('recordChangeDescriptions', callback);
|
||||
store.removeListener('supportsReloadAndProfile', callback);
|
||||
};
|
||||
},
|
||||
}),
|
||||
[store]
|
||||
);
|
||||
const supportsReloadAndProfile = useSubscription<boolean, Store>(
|
||||
supportsReloadAndProfileSubscription
|
||||
);
|
||||
const {
|
||||
recordChangeDescriptions,
|
||||
supportsReloadAndProfile,
|
||||
} = useSubscription<SubscriptionData, Store>(subscription);
|
||||
|
||||
const reloadAndProfile = useCallback(() => bridge.send('reloadAndProfile'), [
|
||||
bridge,
|
||||
]);
|
||||
const reloadAndProfile = useCallback(
|
||||
() => bridge.send('reloadAndProfile', recordChangeDescriptions),
|
||||
[bridge, recordChangeDescriptions]
|
||||
);
|
||||
|
||||
if (!supportsReloadAndProfile) {
|
||||
return null;
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.Content {
|
||||
padding: 0.5rem;
|
||||
user-select: none;
|
||||
border-top: 1px solid var(--color-border);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,21 @@
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.Content {
|
||||
padding: 0.5rem;
|
||||
user-select: none;
|
||||
border-top: 1px solid var(--color-border);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.Component {
|
||||
flex: 1;
|
||||
color: var(--color-component-name);
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.Component:before {
|
||||
white-space: nowrap;
|
||||
@@ -56,3 +59,22 @@
|
||||
.CurrentCommit:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.WhatChangedItem {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.WhatChangedKey {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-small);
|
||||
line-height: 1;
|
||||
}
|
||||
.WhatChangedKey:first-of-type::before {
|
||||
content: ' (';
|
||||
}
|
||||
.WhatChangedKey::after {
|
||||
content: ', ';
|
||||
}
|
||||
.WhatChangedKey:last-of-type::after {
|
||||
content: ')';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment, useContext } from 'react';
|
||||
import ProfilerStore from 'src/devtools/ProfilerStore';
|
||||
import { ProfilerContext } from './ProfilerContext';
|
||||
import { formatDuration, formatTime } from './utils';
|
||||
import { StoreContext } from '../context';
|
||||
@@ -29,7 +30,7 @@ export default function SidebarSelectedFiberInfo(_: Props) {
|
||||
});
|
||||
|
||||
const listItems = [];
|
||||
for (let i = 0; i < commitIndices.length; i += 2) {
|
||||
for (let i = 0; i < commitIndices.length; i++) {
|
||||
const commitIndex = commitIndices[i];
|
||||
|
||||
const { duration, timestamp } = profilerStore.getCommitData(
|
||||
@@ -67,9 +68,140 @@ export default function SidebarSelectedFiberInfo(_: Props) {
|
||||
<ButtonIcon type="close" />
|
||||
</Button>
|
||||
</div>
|
||||
<WhatChanged
|
||||
commitIndex={((selectedCommitIndex: any): number)}
|
||||
fiberID={((selectedFiberID: any): number)}
|
||||
profilerStore={profilerStore}
|
||||
rootID={((rootID: any): number)}
|
||||
/>
|
||||
<div className={styles.Content}>
|
||||
<label className={styles.Label}>Rendered at</label>: {listItems}
|
||||
{listItems.length > 0 && (
|
||||
<Fragment>
|
||||
<label className={styles.Label}>Rendered at</label>: {listItems}
|
||||
</Fragment>
|
||||
)}
|
||||
{listItems.length === 0 && (
|
||||
<div>Did not render during this profiling session.</div>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
type WhatChangedProps = {|
|
||||
commitIndex: number,
|
||||
fiberID: number,
|
||||
profilerStore: ProfilerStore,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
function WhatChanged({
|
||||
commitIndex,
|
||||
fiberID,
|
||||
profilerStore,
|
||||
rootID,
|
||||
}: WhatChangedProps) {
|
||||
const { changeDescriptions } = profilerStore.getCommitData(
|
||||
((rootID: any): number),
|
||||
commitIndex
|
||||
);
|
||||
if (changeDescriptions === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const changeDescription = changeDescriptions.get(fiberID);
|
||||
if (changeDescription == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (changeDescription.isFirstMount) {
|
||||
return (
|
||||
<div className={styles.Content}>
|
||||
<label className={styles.Label}>Why did this render?</label>
|
||||
<div className={styles.WhatChangedItem}>
|
||||
This is the first time the component rendered.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const changes = [];
|
||||
|
||||
if (changeDescription.context === true) {
|
||||
changes.push(
|
||||
<div key="context" className={styles.WhatChangedItem}>
|
||||
• Context changed
|
||||
</div>
|
||||
);
|
||||
} else if (
|
||||
typeof changeDescription.context === 'object' &&
|
||||
changeDescription.context !== null &&
|
||||
changeDescription.context.length !== 0
|
||||
) {
|
||||
changes.push(
|
||||
<div key="context" className={styles.WhatChangedItem}>
|
||||
• Context changed:
|
||||
{changeDescription.context.map(key => (
|
||||
<span key={key} className={styles.WhatChangedKey}>
|
||||
{key}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (changeDescription.didHooksChange) {
|
||||
changes.push(
|
||||
<div key="hooks" className={styles.WhatChangedItem}>
|
||||
• Hooks changed
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
changeDescription.props !== null &&
|
||||
changeDescription.props.length !== 0
|
||||
) {
|
||||
changes.push(
|
||||
<div key="props" className={styles.WhatChangedItem}>
|
||||
• Props changed:
|
||||
{changeDescription.props.map(key => (
|
||||
<span key={key} className={styles.WhatChangedKey}>
|
||||
{key}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
changeDescription.state !== null &&
|
||||
changeDescription.state.length !== 0
|
||||
) {
|
||||
changes.push(
|
||||
<div key="state" className={styles.WhatChangedItem}>
|
||||
• State changed:
|
||||
{changeDescription.state.map(key => (
|
||||
<span key={key} className={styles.WhatChangedKey}>
|
||||
{key}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (changes.length === 0) {
|
||||
changes.push(
|
||||
<div key="nothing" className={styles.WhatChangedItem}>
|
||||
The parent component rendered.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.Content}>
|
||||
<label className={styles.Label}>Why did this render?</label>
|
||||
{changes}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,18 @@ export type SnapshotNode = {|
|
||||
type: ElementType,
|
||||
|};
|
||||
|
||||
export type ChangeDescription = {|
|
||||
context: Array<string> | boolean | null,
|
||||
didHooksChange: boolean,
|
||||
isFirstMount: boolean,
|
||||
props: Array<string> | null,
|
||||
state: Array<string> | null,
|
||||
|};
|
||||
|
||||
export type CommitDataFrontend = {|
|
||||
// Map of Fiber (ID) to a description of what changed in this commit.
|
||||
changeDescriptions: Map<number, ChangeDescription> | null,
|
||||
|
||||
// How long was this commit?
|
||||
duration: number,
|
||||
|
||||
@@ -93,6 +104,7 @@ export type ProfilingDataFrontend = {|
|
||||
|};
|
||||
|
||||
export type CommitDataExport = {|
|
||||
changeDescriptions: Array<[number, ChangeDescription]> | null,
|
||||
duration: number,
|
||||
// Tuple of fiber ID and actual duration
|
||||
fiberActualDurations: Array<[number, number]>,
|
||||
|
||||
@@ -58,6 +58,10 @@ export function prepareProfilingDataFrontendFromBackendAndStore(
|
||||
|
||||
dataForRoots.set(rootID, {
|
||||
commitData: commitData.map((commitDataBackend, commitIndex) => ({
|
||||
changeDescriptions:
|
||||
commitDataBackend.changeDescriptions != null
|
||||
? new Map(commitDataBackend.changeDescriptions)
|
||||
: null,
|
||||
duration: commitDataBackend.duration,
|
||||
fiberActualDurations: new Map(
|
||||
commitDataBackend.fiberActualDurations
|
||||
@@ -109,6 +113,7 @@ export function prepareProfilingDataFrontendFromExport(
|
||||
dataForRoots.set(rootID, {
|
||||
commitData: commitData.map(
|
||||
({
|
||||
changeDescriptions,
|
||||
duration,
|
||||
fiberActualDurations,
|
||||
fiberSelfDurations,
|
||||
@@ -117,6 +122,8 @@ export function prepareProfilingDataFrontendFromExport(
|
||||
screenshot,
|
||||
timestamp,
|
||||
}) => ({
|
||||
changeDescriptions:
|
||||
changeDescriptions != null ? new Map(changeDescriptions) : null,
|
||||
duration,
|
||||
fiberActualDurations: new Map(fiberActualDurations),
|
||||
fiberSelfDurations: new Map(fiberSelfDurations),
|
||||
@@ -159,6 +166,7 @@ export function prepareProfilingDataExport(
|
||||
dataForRoots.push({
|
||||
commitData: commitData.map(
|
||||
({
|
||||
changeDescriptions,
|
||||
duration,
|
||||
fiberActualDurations,
|
||||
fiberSelfDurations,
|
||||
@@ -167,6 +175,10 @@ export function prepareProfilingDataExport(
|
||||
screenshot,
|
||||
timestamp,
|
||||
}) => ({
|
||||
changeDescriptions:
|
||||
changeDescriptions != null
|
||||
? Array.from(changeDescriptions.entries())
|
||||
: null,
|
||||
duration,
|
||||
fiberActualDurations: Array.from(fiberActualDurations.entries()),
|
||||
fiberSelfDurations: Array.from(fiberSelfDurations.entries()),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import React, { Fragment, useCallback, useContext, useMemo } from 'react';
|
||||
import { useSubscription } from '../hooks';
|
||||
import { StoreContext } from '../context';
|
||||
import { SettingsContext } from './SettingsContext';
|
||||
@@ -43,6 +43,20 @@ function Settings(_: {||}) {
|
||||
collapseNodesByDefaultSubscription
|
||||
);
|
||||
|
||||
const recordChangeDescriptionsSubscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () => store.recordChangeDescriptions,
|
||||
subscribe: (callback: Function) => {
|
||||
store.addListener('recordChangeDescriptions', callback);
|
||||
return () => store.removeListener('recordChangeDescriptions', callback);
|
||||
},
|
||||
}),
|
||||
[store]
|
||||
);
|
||||
const recordChangeDescriptions = useSubscription<boolean, Store>(
|
||||
recordChangeDescriptionsSubscription
|
||||
);
|
||||
|
||||
const updateDisplayDensity = useCallback(
|
||||
({ currentTarget }) => {
|
||||
setDisplayDensity(currentTarget.value);
|
||||
@@ -69,6 +83,12 @@ function Settings(_: {||}) {
|
||||
},
|
||||
[store]
|
||||
);
|
||||
const updateRecordChangeDescriptions = useCallback(
|
||||
({ currentTarget }) => {
|
||||
store.recordChangeDescriptions = currentTarget.checked;
|
||||
},
|
||||
[store]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.Settings}>
|
||||
@@ -145,25 +165,37 @@ function Settings(_: {||}) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{store.supportsCaptureScreenshots && (
|
||||
<div className={styles.Section}>
|
||||
<div className={styles.Header}>Profiler</div>
|
||||
<label className={styles.CheckboxOption}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={captureScreenshots}
|
||||
onChange={updateCaptureScreenshotsWhileProfiling}
|
||||
/>{' '}
|
||||
Capture screenshots while profiling
|
||||
</label>
|
||||
{captureScreenshots && (
|
||||
<div className={styles.ScreenshotThrottling}>
|
||||
Screenshots will be throttled in order to reduce the negative
|
||||
impact on performance.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.Section}>
|
||||
<div className={styles.Header}>Profiler</div>
|
||||
|
||||
<label className={styles.CheckboxOption}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={recordChangeDescriptions}
|
||||
onChange={updateRecordChangeDescriptions}
|
||||
/>{' '}
|
||||
Record why each component rendered while profiling.
|
||||
</label>
|
||||
|
||||
{store.supportsCaptureScreenshots && (
|
||||
<Fragment>
|
||||
<label className={styles.CheckboxOption}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={captureScreenshots}
|
||||
onChange={updateCaptureScreenshotsWhileProfiling}
|
||||
/>{' '}
|
||||
Capture screenshots while profiling
|
||||
</label>
|
||||
{captureScreenshots && (
|
||||
<div className={styles.ScreenshotThrottling}>
|
||||
Screenshots will be throttled in order to reduce the negative
|
||||
impact on performance.
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user