Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add profiling option to track why a component rendered #310

Merged
merged 10 commits into from
Jun 9, 2019
1,925 changes: 1,862 additions & 63 deletions src/__tests__/__snapshots__/profilingCache-test.js.snap

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/__tests__/profilerContext-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/profilerStore-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('ProfilerStore', () => {

store = global.store;
store.collapseNodesByDefault = false;
store.recordChangeDescriptions = true;

React = require('react');
ReactDOM = require('react-dom');
Expand Down
114 changes: 114 additions & 0 deletions src/__tests__/profilingCache-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
Expand Down Expand Up @@ -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$')];

Expand Down
1 change: 1 addition & 0 deletions src/__tests__/profilingCharts-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('profiling charts', () => {

store = global.store;
store.collapseNodesByDefault = false;
store.recordChangeDescriptions = true;

React = require('react');
ReactDOM = require('react-dom');
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/profilingCommitTreeBuilder-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('commit tree', () => {

store = global.store;
store.collapseNodesByDefault = false;
store.recordChangeDescriptions = true;

React = require('react');
ReactDOM = require('react-dom');
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/storeComponentFilters-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
21 changes: 17 additions & 4 deletions src/backend/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
};
Expand All @@ -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)
Expand Down
Loading