Merge pull request #279 from bvaughn/profiling-architecture-eager-data-push

Refactor Profiler architecture
This commit is contained in:
Brian Vaughn
2019-05-22 10:00:52 -07:00
committed by GitHub
48 changed files with 3900 additions and 3666 deletions
-1
View File
@@ -44,7 +44,6 @@
"permissions": [
"<all_urls>",
"background",
"downloads",
"tabs",
"file:///*",
"http://*/*",
-1
View File
@@ -46,7 +46,6 @@
"permissions": [
"<all_urls>",
"downloads",
"activeTab",
"tabs",
"file:///*",
-11
View File
@@ -110,17 +110,6 @@ chrome.runtime.onMessage.addListener((request, sender) => {
setIconAndPopup(reactBuildType, sender.tab.id);
}
if (request.exportFile) {
let { contents, filename } = request;
if (!Array.isArray(contents)) {
contents = [contents];
}
const blob = new Blob(contents, { type: 'text/plain' });
const url = URL.createObjectURL(blob);
chrome.downloads.download({ filename, saveAs: true, url });
}
if (request.captureScreenshot) {
const { commitIndex, rootID } = request;
try {
-8
View File
@@ -85,13 +85,6 @@ function createPanelIfReactLoaded() {
localStorage.setItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
chrome.devtools.inspectedWindow.eval('window.location.reload();');
});
bridge.addListener('exportFile', ({ contents, filename }) => {
chrome.runtime.sendMessage({
exportFile: true,
contents,
filename,
});
});
bridge.addListener('captureScreenshot', ({ commitIndex, rootID }) => {
chrome.runtime.sendMessage(
{
@@ -124,7 +117,6 @@ function createPanelIfReactLoaded() {
store = new Store(bridge, {
isProfiling,
supportsCaptureScreenshots: true,
supportsFileDownloads: browserName === 'Chrome',
supportsReloadAndProfile: true,
supportsProfiling,
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -247,6 +247,20 @@ Object {
exports[`profiling charts interactions should contain valid data: Interactions 1`] = `
Object {
"interactions": Array [
Object {
"__count": 1,
"id": 0,
"name": "mount",
"timestamp": 0,
},
Object {
"__count": 0,
"id": 1,
"name": "update",
"timestamp": 15,
},
],
"lastInteractionTime": 25,
"maxCommitDuration": 15,
}
@@ -254,6 +268,20 @@ Object {
exports[`profiling charts interactions should contain valid data: Interactions 2`] = `
Object {
"interactions": Array [
Object {
"__count": 1,
"id": 0,
"name": "mount",
"timestamp": 0,
},
Object {
"__count": 0,
"id": 1,
"name": "update",
"timestamp": 15,
},
],
"lastInteractionTime": 25,
"maxCommitDuration": 15,
}
+57
View File
@@ -0,0 +1,57 @@
// @flow
import type Store from 'src/devtools/store';
describe('ProfilerStore', () => {
let React;
let ReactDOM;
let store: Store;
let utils;
beforeEach(() => {
utils = require('./utils');
utils.beforeEachProfiling();
store = global.store;
store.collapseNodesByDefault = false;
React = require('react');
ReactDOM = require('react-dom');
});
it('should not remove profiling data when roots are unmounted', async () => {
const Parent = ({ count }) =>
new Array(count)
.fill(true)
.map((_, index) => <Child key={index} duration={index} />);
const Child = () => <div>Hi!</div>;
const containerA = document.createElement('div');
const containerB = document.createElement('div');
utils.act(() => {
ReactDOM.render(<Parent key="A" count={3} />, containerA);
ReactDOM.render(<Parent key="B" count={2} />, containerB);
});
utils.act(() => store.profilerStore.startProfiling());
utils.act(() => {
ReactDOM.render(<Parent key="A" count={4} />, containerA);
ReactDOM.render(<Parent key="B" count={1} />, containerB);
});
utils.act(() => store.profilerStore.stopProfiling());
const rootA = store.roots[0];
const rootB = store.roots[1];
utils.act(() => ReactDOM.unmountComponentAtNode(containerB));
expect(store.profilerStore.getDataForRoot(rootA)).not.toBeNull();
utils.act(() => ReactDOM.unmountComponentAtNode(containerA));
expect(store.profilerStore.getDataForRoot(rootB)).not.toBeNull();
});
});
-574
View File
@@ -1,574 +0,0 @@
// @flow
import typeof ReactTestRenderer from 'react-test-renderer';
import type Bridge from 'src/bridge';
import type Store from 'src/devtools/store';
describe('profiling', () => {
let React;
let ReactDOM;
let Scheduler;
let SchedulerTracing;
let TestRenderer: ReactTestRenderer;
let bridge: Bridge;
let store: Store;
let utils;
beforeEach(() => {
utils = require('./utils');
utils.beforeEachProfiling();
bridge = global.bridge;
store = global.store;
store.collapseNodesByDefault = false;
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
SchedulerTracing = require('scheduler/tracing');
TestRenderer = utils.requireTestRenderer();
});
it('should throw if importing older/unsupported data', () => {
const {
prepareImportedProfilingData,
} = require('src/devtools/views/Profiler/utils');
expect(() =>
prepareImportedProfilingData(
JSON.stringify({
version: 0,
})
)
).toThrow('Unsupported profiler export version "0"');
});
describe('ProfilingSummary', () => {
it('should be collected for each commit', async done => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
const children = new Array(count)
.fill(true)
.map((_, index) => <Child key={index} duration={index} />);
return (
<React.Fragment>
{children}
<MemoizedChild duration={1} />
</React.Fragment>
);
};
const Child = ({ duration }) => {
Scheduler.advanceTime(duration);
return null;
};
const MemoizedChild = React.memo(Child);
const container = document.createElement('div');
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
utils.act(() => store.startProfiling());
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
utils.act(() => ReactDOM.render(<Parent count={0} />, container));
utils.act(() => store.stopProfiling());
let profilingSummary = null;
function Suspender({ previousPofilingSummary, rendererID, rootID }) {
profilingSummary = store.profilingCache.ProfilingSummary.read({
rendererID,
rootID,
});
if (previousPofilingSummary != null) {
expect(profilingSummary).toEqual(previousPofilingSummary);
} else {
expect(profilingSummary).toMatchSnapshot('ProfilingSummary');
}
return null;
}
const rendererID = utils.getRendererID();
const rootID = store.roots[0];
await utils.actAsync(() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
previousPofilingSummary={null}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
)
);
expect(profilingSummary).not.toBeNull();
utils.exportImportHelper(bridge, store, rendererID, rootID);
await utils.actAsync(() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
previousPofilingSummary={profilingSummary}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
)
);
done();
});
});
describe('CommitDetails', () => {
it('should be collected for each commit', async done => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
const children = new Array(count)
.fill(true)
.map((_, index) => <Child key={index} duration={index} />);
return (
<React.Fragment>
{children}
<MemoizedChild duration={1} />
</React.Fragment>
);
};
const Child = ({ duration }) => {
Scheduler.advanceTime(duration);
return null;
};
const MemoizedChild = React.memo(Child);
const container = document.createElement('div');
utils.act(() => store.startProfiling());
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
utils.act(() => ReactDOM.render(<Parent count={0} />, container));
utils.act(() => store.stopProfiling());
const allCommitDetails = [];
function Suspender({
commitIndex,
previousCommitDetails,
rendererID,
rootID,
}) {
const commitDetails = store.profilingCache.CommitDetails.read({
commitIndex,
rendererID,
rootID,
});
if (previousCommitDetails != null) {
expect(commitDetails).toEqual(previousCommitDetails);
} else {
allCommitDetails.push(commitDetails);
expect(commitDetails).toMatchSnapshot(
`CommitDetails commitIndex: ${commitIndex}`
);
}
return null;
}
const rendererID = utils.getRendererID();
const rootID = store.roots[0];
for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
await utils.actAsync(() => {
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
previousCommitDetails={null}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
);
});
}
expect(allCommitDetails).toHaveLength(4);
utils.exportImportHelper(bridge, store, rendererID, rootID);
for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
await utils.actAsync(() => {
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
previousCommitDetails={allCommitDetails[commitIndex]}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
);
});
}
done();
});
it('should calculate a self duration based on actual children (not filtered children)', async done => {
store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
const Grandparent = () => {
Scheduler.advanceTime(10);
return (
<React.Fragment>
<Parent key="one" />
<Parent key="two" />
</React.Fragment>
);
};
const Parent = () => {
Scheduler.advanceTime(2);
return <Child />;
};
const Child = () => {
Scheduler.advanceTime(1);
return null;
};
utils.act(() => store.startProfiling());
utils.act(() =>
ReactDOM.render(<Grandparent />, document.createElement('div'))
);
utils.act(() => store.stopProfiling());
let commitDetails = null;
function Suspender({ commitIndex, rendererID, rootID }) {
commitDetails = store.profilingCache.CommitDetails.read({
commitIndex,
rendererID,
rootID,
});
expect(commitDetails).toMatchSnapshot(
`CommitDetails with filtered self durations`
);
return null;
}
const rendererID = utils.getRendererID();
const rootID = store.roots[0];
await utils.actAsync(() => {
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
commitIndex={0}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
);
});
expect(commitDetails).not.toBeNull();
done();
});
it('should calculate self duration correctly for suspended views', async done => {
let data;
const getData = () => {
if (data) {
return data;
} else {
throw new Promise(resolve => {
data = 'abc';
resolve(data);
});
}
};
const Parent = () => {
Scheduler.advanceTime(10);
return (
<React.Suspense fallback={<Fallback />}>
<Async />
</React.Suspense>
);
};
const Fallback = () => {
Scheduler.advanceTime(2);
return 'Fallback...';
};
const Async = () => {
Scheduler.advanceTime(3);
const data = getData();
return data;
};
utils.act(() => store.startProfiling());
await utils.actAsync(() =>
ReactDOM.render(<Parent />, document.createElement('div'))
);
utils.act(() => store.stopProfiling());
const allCommitDetails = [];
function Suspender({ commitIndex, rendererID, rootID }) {
const commitDetails = store.profilingCache.CommitDetails.read({
commitIndex,
rendererID,
rootID,
});
allCommitDetails.push(commitDetails);
expect(commitDetails).toMatchSnapshot(
`CommitDetails with filtered self durations`
);
return null;
}
const rendererID = utils.getRendererID();
const rootID = store.roots[0];
for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
await utils.actAsync(() => {
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
);
});
}
expect(allCommitDetails).toHaveLength(2);
done();
});
});
describe('FiberCommits', () => {
it('should be collected for each rendered fiber', async done => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
const children = new Array(count)
.fill(true)
.map((_, index) => <Child key={index} duration={index} />);
return (
<React.Fragment>
{children}
<MemoizedChild duration={1} />
</React.Fragment>
);
};
const Child = ({ duration }) => {
Scheduler.advanceTime(duration);
return null;
};
const MemoizedChild = React.memo(Child);
const container = document.createElement('div');
utils.act(() => store.startProfiling());
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
utils.act(() => store.stopProfiling());
const allFiberCommits = [];
function Suspender({
fiberID,
previousFiberCommits,
rendererID,
rootID,
}) {
const fiberCommits = store.profilingCache.FiberCommits.read({
fiberID,
rendererID,
rootID,
});
if (previousFiberCommits != null) {
expect(fiberCommits).toEqual(previousFiberCommits);
} else {
allFiberCommits.push(fiberCommits);
expect(fiberCommits).toMatchSnapshot(
`FiberCommits: element ${fiberID}`
);
}
return null;
}
const rendererID = utils.getRendererID();
const rootID = store.roots[0];
for (let index = 0; index < store.numElements; index++) {
await utils.actAsync(() => {
const fiberID = store.getElementIDAtIndex(index);
if (fiberID == null) {
throw Error(`Unexpected null ID for element at index ${index}`);
}
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
fiberID={fiberID}
previousFiberCommits={null}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
);
});
}
expect(allFiberCommits).toHaveLength(store.numElements);
utils.exportImportHelper(bridge, store, rendererID, rootID);
for (let index = 0; index < store.numElements; index++) {
await utils.actAsync(() => {
const fiberID = store.getElementIDAtIndex(index);
if (fiberID == null) {
throw Error(`Unexpected null ID for element at index ${index}`);
}
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
fiberID={fiberID}
previousFiberCommits={allFiberCommits[index]}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
);
});
}
done();
});
});
describe('Interactions', () => {
it('should be collected for every traced interaction', async done => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
const children = new Array(count)
.fill(true)
.map((_, index) => <Child key={index} duration={index} />);
return (
<React.Fragment>
{children}
<MemoizedChild duration={1} />
</React.Fragment>
);
};
const Child = ({ duration }) => {
Scheduler.advanceTime(duration);
return null;
};
const MemoizedChild = React.memo(Child);
const container = document.createElement('div');
utils.act(() => store.startProfiling());
utils.act(() =>
SchedulerTracing.unstable_trace(
'mount: one child',
Scheduler.unstable_now(),
() => ReactDOM.render(<Parent count={1} />, container)
)
);
utils.act(() =>
SchedulerTracing.unstable_trace(
'update: two children',
Scheduler.unstable_now(),
() => ReactDOM.render(<Parent count={2} />, container)
)
);
utils.act(() => store.stopProfiling());
let interactions = null;
function Suspender({ previousInteractions, rendererID, rootID }) {
interactions = store.profilingCache.Interactions.read({
rendererID,
rootID,
});
if (previousInteractions != null) {
expect(interactions).toEqual(previousInteractions);
} else {
expect(interactions).toMatchSnapshot('Interactions');
}
return null;
}
const rendererID = utils.getRendererID();
const rootID = store.roots[0];
await utils.actAsync(() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
previousInteractions={null}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
)
);
expect(interactions).not.toBeNull();
utils.exportImportHelper(bridge, store, rendererID, rootID);
await utils.actAsync(() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
previousInteractions={interactions}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
)
);
done();
});
});
it('should remove profiling data when roots are unmounted', async () => {
const Parent = ({ count }) =>
new Array(count)
.fill(true)
.map((_, index) => <Child key={index} duration={index} />);
const Child = () => <div>Hi!</div>;
const containerA = document.createElement('div');
const containerB = document.createElement('div');
utils.act(() => {
ReactDOM.render(<Parent key="A" count={3} />, containerA);
ReactDOM.render(<Parent key="B" count={2} />, containerB);
});
utils.act(() => store.startProfiling());
utils.act(() => {
ReactDOM.render(<Parent key="A" count={4} />, containerA);
ReactDOM.render(<Parent key="B" count={1} />, containerB);
});
utils.act(() => ReactDOM.unmountComponentAtNode(containerB));
utils.act(() => ReactDOM.unmountComponentAtNode(containerA));
utils.act(() => store.stopProfiling());
// Assert all maps are empty
store.assertExpectedRootMapSizes();
});
});
+455
View File
@@ -0,0 +1,455 @@
// @flow
import typeof ReactTestRenderer from 'react-test-renderer';
import type Bridge from 'src/bridge';
import type Store from 'src/devtools/store';
describe('ProfilingCache', () => {
let React;
let ReactDOM;
let Scheduler;
let SchedulerTracing;
let TestRenderer: ReactTestRenderer;
let bridge: Bridge;
let store: Store;
let utils;
beforeEach(() => {
utils = require('./utils');
utils.beforeEachProfiling();
bridge = global.bridge;
store = global.store;
store.collapseNodesByDefault = false;
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
SchedulerTracing = require('scheduler/tracing');
TestRenderer = utils.requireTestRenderer();
});
it('should collect data for each root (including ones added or mounted after profiling started)', () => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
const children = new Array(count)
.fill(true)
.map((_, index) => <Child key={index} duration={index} />);
return (
<React.Fragment>
{children}
<MemoizedChild duration={1} />
</React.Fragment>
);
};
const Child = ({ duration }) => {
Scheduler.advanceTime(duration);
return null;
};
const MemoizedChild = React.memo(Child);
const containerA = document.createElement('div');
const containerB = document.createElement('div');
const containerC = document.createElement('div');
utils.act(() => ReactDOM.render(<Parent count={2} />, containerA));
utils.act(() => ReactDOM.render(<Parent count={1} />, containerB));
utils.act(() => store.profilerStore.startProfiling());
utils.act(() => ReactDOM.render(<Parent count={3} />, containerA));
utils.act(() => ReactDOM.render(<Parent count={1} />, containerC));
utils.act(() => ReactDOM.render(<Parent count={1} />, containerA));
utils.act(() => ReactDOM.unmountComponentAtNode(containerB));
utils.act(() => ReactDOM.render(<Parent count={0} />, containerA));
utils.act(() => store.profilerStore.stopProfiling());
let allProfilingDataForRoots = [];
function Validator({ previousProfilingDataForRoot, rootID }) {
const profilingDataForRoot = store.profilerStore.getDataForRoot(rootID);
if (previousProfilingDataForRoot != null) {
expect(profilingDataForRoot).toEqual(previousProfilingDataForRoot);
} else {
expect(profilingDataForRoot).toMatchSnapshot(
`Data for root ${profilingDataForRoot.displayName}`
);
}
allProfilingDataForRoots.push(profilingDataForRoot);
return null;
}
const dataForRoots =
store.profilerStore.profilingData !== null
? store.profilerStore.profilingData.dataForRoots
: null;
expect(dataForRoots).not.toBeNull();
if (dataForRoots !== null) {
dataForRoots.forEach(dataForRoot => {
utils.act(() =>
TestRenderer.create(
<Validator
previousProfilingDataForRoot={null}
rootID={dataForRoot.rootID}
/>
)
);
});
}
expect(allProfilingDataForRoots).toHaveLength(3);
utils.exportImportHelper(bridge, store);
allProfilingDataForRoots.forEach(profilingDataForRoot => {
utils.act(() =>
TestRenderer.create(
<Validator
previousProfilingDataForRoot={profilingDataForRoot}
rootID={profilingDataForRoot.rootID}
/>
)
);
});
});
it('should collect data for each commit', () => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
const children = new Array(count)
.fill(true)
.map((_, index) => <Child key={index} duration={index} />);
return (
<React.Fragment>
{children}
<MemoizedChild duration={1} />
</React.Fragment>
);
};
const Child = ({ duration }) => {
Scheduler.advanceTime(duration);
return null;
};
const MemoizedChild = React.memo(Child);
const container = document.createElement('div');
utils.act(() => store.profilerStore.startProfiling());
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
utils.act(() => ReactDOM.render(<Parent count={0} />, 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 < 4; commitIndex++) {
utils.act(() => {
TestRenderer.create(
<Validator
commitIndex={commitIndex}
previousCommitDetails={null}
rootID={rootID}
/>
);
});
}
expect(allCommitData).toHaveLength(4);
utils.exportImportHelper(bridge, store);
for (let commitIndex = 0; commitIndex < 4; 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$')];
const Grandparent = () => {
Scheduler.advanceTime(10);
return (
<React.Fragment>
<Parent key="one" />
<Parent key="two" />
</React.Fragment>
);
};
const Parent = () => {
Scheduler.advanceTime(2);
return <Child />;
};
const Child = () => {
Scheduler.advanceTime(1);
return null;
};
utils.act(() => store.profilerStore.startProfiling());
utils.act(() =>
ReactDOM.render(<Grandparent />, document.createElement('div'))
);
utils.act(() => store.profilerStore.stopProfiling());
let commitData = null;
function Validator({ commitIndex, rootID }) {
commitData = store.profilerStore.getCommitData(rootID, commitIndex);
expect(commitData).toMatchSnapshot(
`CommitDetails with filtered self durations`
);
return null;
}
const rootID = store.roots[0];
utils.act(() => {
TestRenderer.create(<Validator commitIndex={0} rootID={rootID} />);
});
expect(commitData).not.toBeNull();
});
it('should calculate self duration correctly for suspended views', async done => {
let data;
const getData = () => {
if (data) {
return data;
} else {
throw new Promise(resolve => {
data = 'abc';
resolve(data);
});
}
};
const Parent = () => {
Scheduler.advanceTime(10);
return (
<React.Suspense fallback={<Fallback />}>
<Async />
</React.Suspense>
);
};
const Fallback = () => {
Scheduler.advanceTime(2);
return 'Fallback...';
};
const Async = () => {
Scheduler.advanceTime(3);
const data = getData();
return data;
};
utils.act(() => store.profilerStore.startProfiling());
await utils.actAsync(() =>
ReactDOM.render(<Parent />, document.createElement('div'))
);
utils.act(() => store.profilerStore.stopProfiling());
const allCommitData = [];
function Validator({ commitIndex, rootID }) {
const commitData = store.profilerStore.getCommitData(rootID, commitIndex);
allCommitData.push(commitData);
expect(commitData).toMatchSnapshot(
`CommitDetails with filtered self durations`
);
return null;
}
const rootID = store.roots[0];
for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
utils.act(() => {
TestRenderer.create(
<Validator commitIndex={commitIndex} rootID={rootID} />
);
});
}
expect(allCommitData).toHaveLength(2);
done();
});
it('should collect data for each rendered fiber', () => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
const children = new Array(count)
.fill(true)
.map((_, index) => <Child key={index} duration={index} />);
return (
<React.Fragment>
{children}
<MemoizedChild duration={1} />
</React.Fragment>
);
};
const Child = ({ duration }) => {
Scheduler.advanceTime(duration);
return null;
};
const MemoizedChild = React.memo(Child);
const container = document.createElement('div');
utils.act(() => store.profilerStore.startProfiling());
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
utils.act(() => store.profilerStore.stopProfiling());
const allFiberCommits = [];
function Validator({ fiberID, previousFiberCommits, rootID }) {
const fiberCommits = store.profilerStore.profilingCache.getFiberCommits({
fiberID,
rootID,
});
if (previousFiberCommits != null) {
expect(fiberCommits).toEqual(previousFiberCommits);
} else {
allFiberCommits.push(fiberCommits);
expect(fiberCommits).toMatchSnapshot(
`FiberCommits: element ${fiberID}`
);
}
return null;
}
const rootID = store.roots[0];
for (let index = 0; index < store.numElements; index++) {
utils.act(() => {
const fiberID = store.getElementIDAtIndex(index);
if (fiberID == null) {
throw Error(`Unexpected null ID for element at index ${index}`);
}
TestRenderer.create(
<Validator
fiberID={fiberID}
previousFiberCommits={null}
rootID={rootID}
/>
);
});
}
expect(allFiberCommits).toHaveLength(store.numElements);
utils.exportImportHelper(bridge, store);
for (let index = 0; index < store.numElements; index++) {
utils.act(() => {
const fiberID = store.getElementIDAtIndex(index);
if (fiberID == null) {
throw Error(`Unexpected null ID for element at index ${index}`);
}
TestRenderer.create(
<Validator
fiberID={fiberID}
previousFiberCommits={allFiberCommits[index]}
rootID={rootID}
/>
);
});
}
});
it('should report every traced interaction', () => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
const children = new Array(count)
.fill(true)
.map((_, index) => <Child key={index} duration={index} />);
return (
<React.Fragment>
{children}
<MemoizedChild duration={1} />
</React.Fragment>
);
};
const Child = ({ duration }) => {
Scheduler.advanceTime(duration);
return null;
};
const MemoizedChild = React.memo(Child);
const container = document.createElement('div');
utils.act(() => store.profilerStore.startProfiling());
utils.act(() =>
SchedulerTracing.unstable_trace(
'mount: one child',
Scheduler.unstable_now(),
() => ReactDOM.render(<Parent count={1} />, container)
)
);
utils.act(() =>
SchedulerTracing.unstable_trace(
'update: two children',
Scheduler.unstable_now(),
() => ReactDOM.render(<Parent count={2} />, container)
)
);
utils.act(() => store.profilerStore.stopProfiling());
let interactions = null;
function Validator({ previousInteractions, rootID }) {
interactions = store.profilerStore.profilingCache.getInteractionsChartData(
{
rootID,
}
).interactions;
if (previousInteractions != null) {
expect(interactions).toEqual(previousInteractions);
} else {
expect(interactions).toMatchSnapshot('Interactions');
}
return null;
}
const rootID = store.roots[0];
utils.act(() =>
TestRenderer.create(
<Validator previousInteractions={null} rootID={rootID} />
)
);
expect(interactions).not.toBeNull();
utils.exportImportHelper(bridge, store);
utils.act(() =>
TestRenderer.create(
<Validator previousInteractions={interactions} rootID={rootID} />
)
);
});
});
+63 -117
View File
@@ -27,7 +27,7 @@ describe('profiling charts', () => {
});
describe('flamegraph chart', () => {
it('should contain valid data', async done => {
it('should contain valid data', () => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
return (
@@ -47,7 +47,7 @@ describe('profiling charts', () => {
const container = document.createElement('div');
utils.act(() => store.startProfiling());
utils.act(() => store.profilerStore.startProfiling());
utils.act(() =>
SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
ReactDOM.render(<Parent />, container)
@@ -60,68 +60,50 @@ describe('profiling charts', () => {
() => ReactDOM.render(<Parent />, container)
)
);
utils.act(() => store.stopProfiling());
utils.act(() => store.profilerStore.stopProfiling());
let suspenseResolved = false;
let renderFinished = false;
function Suspender({ commitIndex, rendererID, rootID }) {
const profilingSummary = store.profilingCache.ProfilingSummary.read({
rendererID,
function Validator({ commitIndex, rootID }) {
const commitTree = store.profilerStore.profilingCache.getCommitTree({
commitIndex,
rootID,
});
const commitDetails = store.profilingCache.CommitDetails.read({
commitIndex,
rendererID,
rootID,
});
suspenseResolved = true;
const commitTree = store.profilingCache.getCommitTree({
commitIndex,
profilingSummary,
});
const chartData = store.profilingCache.getFlamegraphChartData({
commitDetails,
commitIndex,
commitTree,
});
const chartData = store.profilerStore.profilingCache.getFlamegraphChartData(
{
commitIndex,
commitTree,
rootID,
}
);
expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`);
expect(chartData).toMatchSnapshot(
`${commitIndex}: FlamegraphChartData`
);
renderFinished = true;
return null;
}
const rendererID = utils.getRendererID();
const rootID = store.roots[0];
for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
suspenseResolved = false;
renderFinished = false;
await utils.actAsync(
() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
),
3
);
utils.act(() => {
TestRenderer.create(
<Validator commitIndex={commitIndex} rootID={rootID} />
);
});
expect(suspenseResolved).toBe(true);
expect(renderFinished).toBe(true);
}
expect(suspenseResolved).toBe(true);
done();
expect(renderFinished).toBe(true);
});
});
describe('ranked chart', () => {
it('should contain valid data', async done => {
it('should contain valid data', () => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
return (
@@ -141,7 +123,7 @@ describe('profiling charts', () => {
const container = document.createElement('div');
utils.act(() => store.startProfiling());
utils.act(() => store.profilerStore.startProfiling());
utils.act(() =>
SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
ReactDOM.render(<Parent />, container)
@@ -154,64 +136,46 @@ describe('profiling charts', () => {
() => ReactDOM.render(<Parent />, container)
)
);
utils.act(() => store.stopProfiling());
utils.act(() => store.profilerStore.stopProfiling());
let suspenseResolved = false;
let renderFinished = false;
function Suspender({ commitIndex, rendererID, rootID }) {
const profilingSummary = store.profilingCache.ProfilingSummary.read({
rendererID,
function Validator({ commitIndex, rootID }) {
const commitTree = store.profilerStore.profilingCache.getCommitTree({
commitIndex,
rootID,
});
const commitDetails = store.profilingCache.CommitDetails.read({
commitIndex,
rendererID,
rootID,
});
suspenseResolved = true;
const commitTree = store.profilingCache.getCommitTree({
commitIndex,
profilingSummary,
});
const chartData = store.profilingCache.getRankedChartData({
commitDetails,
commitIndex,
commitTree,
});
const chartData = store.profilerStore.profilingCache.getRankedChartData(
{
commitIndex,
commitTree,
rootID,
}
);
expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`);
expect(chartData).toMatchSnapshot(`${commitIndex}: RankedChartData`);
renderFinished = true;
return null;
}
const rendererID = utils.getRendererID();
const rootID = store.roots[0];
for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
suspenseResolved = false;
renderFinished = false;
await utils.actAsync(
() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
),
3
);
utils.act(() => {
TestRenderer.create(
<Validator commitIndex={commitIndex} rootID={rootID} />
);
});
expect(suspenseResolved).toBe(true);
expect(renderFinished).toBe(true);
}
done();
});
});
describe('interactions', () => {
it('should contain valid data', async done => {
it('should contain valid data', () => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
return (
@@ -231,7 +195,7 @@ describe('profiling charts', () => {
const container = document.createElement('div');
utils.act(() => store.startProfiling());
utils.act(() => store.profilerStore.startProfiling());
utils.act(() =>
SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
ReactDOM.render(<Parent />, container)
@@ -244,52 +208,34 @@ describe('profiling charts', () => {
() => ReactDOM.render(<Parent />, container)
)
);
utils.act(() => store.stopProfiling());
utils.act(() => store.profilerStore.stopProfiling());
let suspenseResolved = false;
let renderFinished = false;
function Suspender({ commitIndex, rendererID, rootID }) {
const profilingSummary = store.profilingCache.ProfilingSummary.read({
rendererID,
rootID,
});
const { interactions } = store.profilingCache.Interactions.read({
rendererID,
rootID,
});
suspenseResolved = true;
const chartData = store.profilingCache.getInteractionsChartData({
interactions,
profilingSummary,
});
function Validator({ commitIndex, rootID }) {
const chartData = store.profilerStore.profilingCache.getInteractionsChartData(
{
rootID,
}
);
expect(chartData).toMatchSnapshot('Interactions');
renderFinished = true;
return null;
}
const rendererID = utils.getRendererID();
const rootID = store.roots[0];
for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
suspenseResolved = false;
renderFinished = false;
await utils.actAsync(
() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
),
3
);
utils.act(() => {
TestRenderer.create(
<Validator commitIndex={commitIndex} rootID={rootID} />
);
});
expect(suspenseResolved).toBe(true);
expect(renderFinished).toBe(true);
}
done();
});
});
});
@@ -24,7 +24,7 @@ describe('commit tree', () => {
TestRenderer = utils.requireTestRenderer();
});
it('should be able to rebuild the store tree for each commit', async done => {
it('should be able to rebuild the store tree for each commit', () => {
const Parent = ({ count }) => {
Scheduler.advanceTime(10);
return new Array(count)
@@ -38,52 +38,37 @@ describe('commit tree', () => {
const container = document.createElement('div');
utils.act(() => store.startProfiling());
utils.act(() => store.profilerStore.startProfiling());
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
utils.act(() => ReactDOM.render(<Parent count={0} />, container));
utils.act(() => store.stopProfiling());
utils.act(() => store.profilerStore.stopProfiling());
let suspenseResolved = false;
let renderFinished = false;
function Suspender({ commitIndex, rendererID, rootID }) {
const profilingSummary = store.profilingCache.ProfilingSummary.read({
rendererID,
function Validator({ commitIndex, rootID }) {
const commitTree = store.profilerStore.profilingCache.getCommitTree({
commitIndex,
rootID,
});
suspenseResolved = true;
const commitTree = store.profilingCache.getCommitTree({
commitIndex,
profilingSummary,
});
expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`);
renderFinished = true;
return null;
}
const rendererID = utils.getRendererID();
const rootID = store.roots[0];
for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
suspenseResolved = false;
renderFinished = false;
await utils.actAsync(
() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
rendererID={rendererID}
rootID={rootID}
/>
</React.Suspense>
),
3
);
utils.act(() => {
TestRenderer.create(
<Validator commitIndex={commitIndex} rootID={rootID} />
);
});
expect(suspenseResolved).toBe(true);
expect(renderFinished).toBe(true);
}
done();
});
});
+20
View File
@@ -0,0 +1,20 @@
// @flow
describe('profiling utils', () => {
let utils;
beforeEach(() => {
utils = require('src/devtools/views/Profiler/utils');
});
it('should throw if importing older/unsupported data', () => {
expect(() =>
utils.prepareProfilingDataFrontendFromExport(
({
version: 0,
dataForRoots: [],
}: any)
)
).toThrow('Unsupported profiler export version "0"');
});
});
+4 -2
View File
@@ -1,11 +1,13 @@
// @flow
import type Store from 'src/devtools/store';
describe('Store component filters', () => {
let React;
let ReactDOM;
let TestUtils;
let Types;
let store;
let store: Store;
let utils;
const act = (callback: Function) => {
@@ -28,7 +30,7 @@ describe('Store component filters', () => {
});
it('should throw if filters are updated while profiling', () => {
act(() => store.startProfiling());
act(() => store.profilerStore.startProfiling());
expect(() => (store.componentFilters = [])).toThrow(
'Cannot modify filter preferences while profiling'
);
+27 -41
View File
@@ -2,10 +2,10 @@
import typeof ReactTestRenderer from 'react-test-renderer';
import type { ElementType } from 'src/types';
import type Bridge from 'src/bridge';
import type Store from 'src/devtools/store';
import type { ProfilingDataFrontend } from 'src/devtools/views/Profiler/types';
import type { ElementType } from 'src/types';
export function act(callback: Function): void {
const TestUtils = require('react-dom/test-utils');
@@ -133,57 +133,43 @@ export function requireTestRenderer(): ReactTestRenderer {
}
}
export function exportImportHelper(
bridge: Bridge,
store: Store,
rendererID: number,
rootID: number
): void {
export function exportImportHelper(bridge: Bridge, store: Store): void {
const { act } = require('./utils');
const {
prepareExportedProfilingSummary,
prepareImportedProfilingData,
prepareProfilingDataExport,
prepareProfilingDataFrontendFromExport,
} = require('src/devtools/views/Profiler/utils');
let exportedProfilingDataJsonString = '';
const onExportFile = ({ contents }) => {
if (typeof contents === 'string') {
exportedProfilingDataJsonString = (contents: string);
}
};
bridge.addListener('exportFile', onExportFile);
const { profilerStore } = store;
act(() => {
const exportProfilingSummary = prepareExportedProfilingSummary(
store.profilingOperations,
store.profilingSnapshots,
rendererID,
rootID
);
bridge.send('exportProfilingSummary', exportProfilingSummary);
});
expect(profilerStore.profilingData).not.toBeNull();
// Cleanup to be able to call this again on the same bridge without memory leaks.
bridge.removeListener('exportFile', onExportFile);
const profilingDataFrontendInitial = ((profilerStore.profilingData: any): ProfilingDataFrontend);
expect(typeof exportedProfilingDataJsonString).toBe('string');
expect(exportedProfilingDataJsonString).not.toBe('');
const importedProfilingData = prepareImportedProfilingData(
exportedProfilingDataJsonString
const profilingDataExport = prepareProfilingDataExport(
profilingDataFrontendInitial
);
// Simulate writing/reading to disk.
const serializedProfilingDataExport = JSON.stringify(
profilingDataExport,
null,
2
);
const parsedProfilingDataExport = JSON.parse(serializedProfilingDataExport);
const profilingDataFrontend = prepareProfilingDataFrontendFromExport(
(parsedProfilingDataExport: any)
);
// Sanity check that profiling snapshots are serialized correctly.
expect(store.profilingSnapshots.get(rootID)).toEqual(
importedProfilingData.profilingSnapshots.get(rootID)
);
expect(store.profilingOperations.get(rootID)).toEqual(
importedProfilingData.profilingOperations.get(rootID)
);
expect(profilingDataFrontendInitial).toEqual(profilingDataFrontend);
// Snapshot the JSON-parsed object, rather than the raw string, because Jest formats the diff nicer.
expect(importedProfilingData).toMatchSnapshot('imported data');
expect(parsedProfilingDataExport).toMatchSnapshot('imported data');
act(() => {
store.importedProfilingData = importedProfilingData;
// Apply the new exported-then-reimported data so tests can re-run assertions.
profilerStore.profilingData = profilingDataFrontend;
});
}
+3 -100
View File
@@ -10,7 +10,6 @@ import {
} from '../constants';
import { hideOverlay, showOverlay } from './views/Highlighter';
import type { ExportedProfilingSummaryFromFrontend } from 'src/devtools/views/Profiler/types';
import type {
PathFrame,
PathMatch,
@@ -20,8 +19,6 @@ import type {
import type { OwnersList } from 'src/devtools/views/Components/types';
import type { Bridge, ComponentFilter } from '../types';
import { prepareExportedProfilingData } from 'src/devtools/views/Profiler/utils';
const debug = (methodName, ...args) => {
if (__DEBUG__) {
console.log(
@@ -96,12 +93,8 @@ export default class Agent extends EventEmitter {
'clearHighlightedElementInDOM',
this.clearHighlightedElementInDOM
);
bridge.addListener('exportProfilingSummary', this.exportProfilingSummary);
bridge.addListener('getCommitDetails', this.getCommitDetails);
bridge.addListener('getFiberCommits', this.getFiberCommits);
bridge.addListener('getInteractions', this.getInteractions);
bridge.addListener('getProfilingData', this.getProfilingData);
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
bridge.addListener('getProfilingSummary', this.getProfilingSummary);
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
bridge.addListener('getOwnersList', this.getOwnersList);
bridge.addListener('inspectElement', this.inspectElement);
@@ -154,109 +147,19 @@ export default class Agent extends EventEmitter {
return null;
}
exportProfilingSummary = (
exportedProfilingSummary: ExportedProfilingSummaryFromFrontend
): void => {
const { rendererID, rootID } = exportedProfilingSummary;
getProfilingData = ({ rendererID }: {| rendererID: RendererID |}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}"`);
return;
}
try {
const exportedProfilingDataFromRenderer = renderer.getExportedProfilingData(
rootID
);
const exportedProfilingData = prepareExportedProfilingData(
exportedProfilingDataFromRenderer,
exportedProfilingSummary
);
this._bridge.send('exportFile', {
contents: JSON.stringify(exportedProfilingData, null, 2),
filename: 'profile-data.json',
});
} catch (error) {
console.warn(`Unable to export file: ${error.stack}`);
}
};
getCommitDetails = ({
commitIndex,
rendererID,
rootID,
}: {
commitIndex: number,
rendererID: number,
rootID: number,
}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}"`);
} else {
this._bridge.send(
'commitDetails',
renderer.getCommitDetails(rootID, commitIndex)
);
}
};
getFiberCommits = ({
fiberID,
rendererID,
rootID,
}: {
fiberID: number,
rendererID: number,
rootID: number,
}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}"`);
} else {
this._bridge.send(
'fiberCommits',
renderer.getFiberCommits(rootID, fiberID)
);
}
};
getInteractions = ({
rendererID,
rootID,
}: {
rendererID: number,
rootID: number,
}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}"`);
} else {
this._bridge.send('interactions', renderer.getInteractions(rootID));
}
this._bridge.send('profilingData', renderer.getProfilingData());
};
getProfilingStatus = () => {
this._bridge.send('profilingStatus', this._isProfiling);
};
getProfilingSummary = ({
rendererID,
rootID,
}: {
rendererID: number,
rootID: number,
}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}"`);
} else {
this._bridge.send(
'profilingSummary',
renderer.getProfilingSummary(rootID)
);
}
};
clearHighlightedElementInDOM = () => {
hideOverlay();
};
+137 -201
View File
@@ -18,7 +18,6 @@ import {
ElementTypeRoot,
ElementTypeSuspense,
} from 'src/types';
import { PROFILER_EXPORT_VERSION } from 'src/constants';
import {
getDisplayName,
getDefaultComponentFilters,
@@ -37,17 +36,13 @@ import {
import { inspectHooksOfFiber } from './ReactDebugHooks';
import type {
CommitDetailsBackend,
CommitDataBackend,
DevToolsHook,
ExportedProfilingDataFromRenderer,
Fiber,
FiberCommitsBackend,
InteractionBackend,
InteractionsBackend,
InteractionWithCommitsBackend,
PathFrame,
PathMatch,
ProfilingSummaryBackend,
ProfilingDataBackend,
ProfilingDataForRootBackend,
ReactRenderer,
RendererInterface,
} from './types';
@@ -55,6 +50,7 @@ import type {
InspectedElement,
Owner,
} from 'src/devtools/views/Components/types';
import type { Interaction } from 'src/devtools/views/Profiler/types';
import type { ComponentFilter, ElementType } from 'src/types';
function getInternalReactConstants(version) {
@@ -820,6 +816,12 @@ export function attach(
pushOperation(ElementTypeRoot);
pushOperation(isProfilingSupported ? 1 : 0);
pushOperation(hasOwnerMetadata ? 1 : 0);
if (isProfiling) {
if (displayNamesByRootID !== null) {
displayNamesByRootID.set(id, getDisplayNameForRoot(fiber));
}
}
} else {
const { key } = fiber;
const displayName = getDisplayNameForFiber(fiber);
@@ -1286,7 +1288,7 @@ export function attach(
durations: [],
commitTime: performance.now() - profilingStartTime,
interactions: Array.from(root.memoizedInteractions).map(
(interaction: InteractionBackend) => ({
(interaction: Interaction) => ({
...interaction,
timestamp: interaction.timestamp - profilingStartTime,
})
@@ -1329,7 +1331,7 @@ export function attach(
durations: [],
commitTime: performance.now() - profilingStartTime,
interactions: Array.from(root.memoizedInteractions).map(
(interaction: InteractionBackend) => ({
(interaction: Interaction) => ({
...interaction,
timestamp: interaction.timestamp - profilingStartTime,
})
@@ -1981,188 +1983,113 @@ export function attach(
type CommitProfilingData = {|
commitTime: number,
durations: Array<number>,
interactions: Array<InteractionBackend>,
interactions: Array<Interaction>,
maxActualDuration: number,
priorityLevel: string | null,
|};
type CommitProfilingMetadataMap = Map<number, Array<CommitProfilingData>>;
type DisplayNamesByRootID = Map<number, string>;
let currentCommitProfilingMetadata: CommitProfilingData | null = null;
let displayNamesByRootID: DisplayNamesByRootID | 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 rootToCommitProfilingMetadataMap: CommitProfilingMetadataMap | null = null;
function getCommitDetails(
rootID: number,
commitIndex: number
): CommitDetailsBackend {
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
rootID
);
if (commitProfilingMetadata != null) {
const commitProfilingData = commitProfilingMetadata[commitIndex];
if (commitProfilingData != null) {
return {
commitIndex,
durations: commitProfilingData.durations,
interactions: commitProfilingData.interactions,
priorityLevel: commitProfilingData.priorityLevel,
function getProfilingData(): ProfilingDataBackend {
const dataForRoots: Array<ProfilingDataForRootBackend> = [];
if (rootToCommitProfilingMetadataMap === null) {
throw Error(
'getProfilingData() called before any profiling data was recorded'
);
}
rootToCommitProfilingMetadataMap.forEach(
(commitProfilingMetadata, rootID) => {
const commitData: Array<CommitDataBackend> = [];
const initialTreeBaseDurations: Array<[number, number]> = [];
const allInteractions: Map<number, Interaction> = new Map();
const interactionCommits: Map<number, Array<number>> = new Map();
const displayName =
(displayNamesByRootID !== null && displayNamesByRootID.get(rootID)) ||
'Unknown';
if (initialTreeBaseDurationsMap != null) {
initialTreeBaseDurationsMap.forEach((treeBaseDuration, id) => {
if (
initialIDToRootMap != null &&
initialIDToRootMap.get(id) === rootID
) {
// We don't need to convert milliseconds to microseconds in this case,
// because the profiling summary is JSON serialized.
initialTreeBaseDurations.push([id, treeBaseDuration]);
}
});
}
commitProfilingMetadata.forEach((commitProfilingData, commitIndex) => {
const {
durations,
interactions,
maxActualDuration,
priorityLevel,
commitTime,
} = commitProfilingData;
const interactionIDs: Array<number> = [];
interactions.forEach(interaction => {
if (!allInteractions.has(interaction.id)) {
allInteractions.set(interaction.id, interaction);
}
interactionIDs.push(interaction.id);
const commitIndices = interactionCommits.get(interaction.id);
if (commitIndices != null) {
commitIndices.push(commitIndex);
} else {
interactionCommits.set(interaction.id, [commitIndex]);
}
});
const fiberActualDurations: Array<[number, number]> = [];
const fiberSelfDurations: Array<[number, number]> = [];
for (let i = 0; i < durations.length; i += 3) {
const fiberID = durations[i];
fiberActualDurations.push([fiberID, durations[i + 1]]);
fiberSelfDurations.push([fiberID, durations[i + 2]]);
}
commitData.push({
duration: maxActualDuration,
fiberActualDurations,
fiberSelfDurations,
interactionIDs,
priorityLevel,
timestamp: commitTime,
});
});
dataForRoots.push({
commitData,
displayName,
initialTreeBaseDurations,
interactionCommits: Array.from(interactionCommits.entries()),
interactions: Array.from(allInteractions.entries()),
rootID,
};
}
}
console.warn(
`getCommitDetails(): No profiling info recorded for root "${rootID}" and commit ${commitIndex}`
);
return {
commitIndex,
durations: [],
interactions: [],
priorityLevel: null,
rootID,
};
}
function getFiberCommits(
rootID: number,
fiberID: number
): FiberCommitsBackend {
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
rootID
);
if (commitProfilingMetadata != null) {
const commitDurations = [];
commitProfilingMetadata.forEach(({ durations }, commitIndex) => {
for (let i = 0; i < durations.length; i += 3) {
if (durations[i] === fiberID) {
commitDurations.push(commitIndex, durations[i + 2]);
break;
}
}
});
return {
commitDurations,
fiberID,
rootID,
};
}
console.warn(
`getFiberCommits(): No profiling info recorded for root "${rootID}"`
);
return {
commitDurations: [],
fiberID,
rootID,
};
}
function getInteractions(rootID: number): InteractionsBackend {
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
rootID
);
if (commitProfilingMetadata != null) {
const interactionsMap: Map<
number,
InteractionWithCommitsBackend
> = new Map();
commitProfilingMetadata.forEach((commitProfilingData, commitIndex) => {
commitProfilingData.interactions.forEach(interaction => {
const interactionWithCommits = interactionsMap.get(interaction.id);
if (interactionWithCommits != null) {
interactionWithCommits.commits.push(commitIndex);
} else {
interactionsMap.set(interaction.id, {
...interaction,
commits: [commitIndex],
});
}
});
});
return {
interactions: Array.from(interactionsMap.values()),
rootID,
};
}
console.warn(
`getInteractions(): No interactions recorded for root "${rootID}"`
);
return {
interactions: [],
rootID,
};
}
function getExportedProfilingData(
rootID: number
): ExportedProfilingDataFromRenderer {
const commitDetailsForEachCommit = [];
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
rootID
);
if (commitProfilingMetadata != null) {
for (let index = 0; index < commitProfilingMetadata.length; index++) {
commitDetailsForEachCommit.push(getCommitDetails(rootID, index));
}
}
return {
version: PROFILER_EXPORT_VERSION,
profilingSummary: getProfilingSummary(rootID),
commitDetails: commitDetailsForEachCommit,
interactions: getInteractions(rootID),
};
}
function getProfilingSummary(rootID: number): ProfilingSummaryBackend {
const interactions = new Set();
const commitDurations = [];
const commitTimes = [];
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
rootID
);
if (commitProfilingMetadata != null) {
commitProfilingMetadata.forEach(metadata => {
commitDurations.push(metadata.maxActualDuration);
commitTimes.push(metadata.commitTime);
metadata.interactions.forEach(({ name, timestamp }) => {
interactions.add(`${timestamp}:${name}`);
});
});
}
const initialTreeBaseDurations = [];
if (initialTreeBaseDurationsMap != null) {
initialTreeBaseDurationsMap.forEach((treeBaseDuration, id) => {
if (
initialIDToRootMap != null &&
initialIDToRootMap.get(id) === rootID
) {
// We don't need to convert milliseconds to microseconds in this case,
// because the profiling summary is JSON serialized.
initialTreeBaseDurations.push(id, treeBaseDuration);
}
});
}
return {
commitDurations,
commitTimes,
initialTreeBaseDurations,
interactionCount: interactions.size,
rootID,
dataForRoots,
rendererID,
};
}
@@ -2175,9 +2102,18 @@ export function attach(
// It's important we snapshot both the durations and the id-to-root map,
// since either of these may change during the profiling session
// (e.g. when a fiber is re-rendered or when a fiber gets removed).
displayNamesByRootID = new Map();
initialTreeBaseDurationsMap = new Map(idToTreeBaseDurationMap);
initialIDToRootMap = new Map(idToRootMap);
hook.getFiberRoots(rendererID).forEach(root => {
const rootID = getFiberID(getPrimaryFiber(root.current));
((displayNamesByRootID: any): DisplayNamesByRootID).set(
rootID,
getDisplayNameForRoot(root.current)
);
});
isProfiling = true;
profilingStartTime = performance.now();
rootToCommitProfilingMetadataMap = new Map();
@@ -2313,6 +2249,32 @@ export function attach(
const rootDisplayNameCounter: Map<string, number> = new Map();
function setRootPseudoKey(id: number, fiber: Fiber) {
const name = getDisplayNameForRoot(fiber);
const counter = rootDisplayNameCounter.get(name) || 0;
rootDisplayNameCounter.set(name, counter + 1);
const pseudoKey = `${name}:${counter}`;
rootPseudoKeys.set(id, pseudoKey);
}
function removeRootPseudoKey(id: number) {
const pseudoKey = rootPseudoKeys.get(id);
if (pseudoKey === undefined) {
throw new Error('Expected root pseudo key to be known.');
}
const name = pseudoKey.substring(0, pseudoKey.lastIndexOf(':'));
const counter = rootDisplayNameCounter.get(name);
if (counter === undefined) {
throw new Error('Expected counter to be known.');
}
if (counter > 1) {
rootDisplayNameCounter.set(name, counter - 1);
} else {
rootDisplayNameCounter.delete(name);
}
rootPseudoKeys.delete(id);
}
function getDisplayNameForRoot(fiber: Fiber): string {
let preferredDisplayName = null;
let fallbackDisplayName = null;
let child = fiber.child;
@@ -2339,29 +2301,7 @@ export function attach(
}
child = child.child;
}
const name = preferredDisplayName || fallbackDisplayName || 'Anonymous';
const counter = rootDisplayNameCounter.get(name) || 0;
rootDisplayNameCounter.set(name, counter + 1);
const pseudoKey = `${name}:${counter}`;
rootPseudoKeys.set(id, pseudoKey);
}
function removeRootPseudoKey(id: number) {
const pseudoKey = rootPseudoKeys.get(id);
if (pseudoKey === undefined) {
throw new Error('Expected root pseudo key to be known.');
}
const name = pseudoKey.substring(0, pseudoKey.lastIndexOf(':'));
const counter = rootDisplayNameCounter.get(name);
if (counter === undefined) {
throw new Error('Expected counter to be known.');
}
if (counter > 1) {
rootDisplayNameCounter.set(name, counter - 1);
} else {
rootDisplayNameCounter.delete(name);
}
rootPseudoKeys.delete(id);
return preferredDisplayName || fallbackDisplayName || 'Anonymous';
}
function getPathFrame(fiber: Fiber): PathFrame {
@@ -2459,15 +2399,11 @@ export function attach(
cleanup,
flushInitialOperations,
getBestMatchForTrackedPath,
getCommitDetails,
getFiberIDFromNative,
getFiberCommits,
getInteractions,
findNativeByFiberID,
getOwnersList,
getPathForElement,
getExportedProfilingData,
getProfilingSummary,
getProfilingData,
handleCommitFiberRoot,
handleCommitFiberUnmount,
inspectElement,
+23 -49
View File
@@ -5,6 +5,7 @@ import type {
InspectedElement,
Owner,
} from 'src/devtools/views/Components/types';
import type { Interaction } from 'src/devtools/views/Profiler/types';
type BundleType =
| 0 // PROD
@@ -115,51 +116,33 @@ export type ReactRenderer = {
currentDispatcherRef?: {| current: null | Dispatcher |},
};
export type InteractionBackend = {|
id: number,
name: string,
export type CommitDataBackend = {|
duration: number,
// Tuple of fiber ID and actual duration
fiberActualDurations: Array<[number, number]>,
// Tuple of fiber ID and computed "self" duration
fiberSelfDurations: Array<[number, number]>,
interactionIDs: Array<number>,
priorityLevel: string | null,
timestamp: number,
|};
export type CommitDetailsBackend = {|
commitIndex: number,
// An interleaved array: fiberID at [i], actualDuration at [i + 1], computed selfDuration at [i + 2].
durations: Array<number>,
interactions: Array<InteractionBackend>,
priorityLevel: string | null,
export type ProfilingDataForRootBackend = {|
commitData: Array<CommitDataBackend>,
displayName: string,
// Tuple of Fiber ID and base duration
initialTreeBaseDurations: Array<[number, number]>,
// Tuple of Interaction ID and commit indices
interactionCommits: Array<[number, Array<number>]>,
interactions: Array<[number, Interaction]>,
rootID: number,
|};
export type FiberCommitsBackend = {|
commitDurations: Array<number>,
fiberID: number,
rootID: number,
|};
export type InteractionWithCommitsBackend = {|
...InteractionBackend,
commits: Array<number>,
|};
export type InteractionsBackend = {|
interactions: Array<InteractionWithCommitsBackend>,
rootID: number,
|};
export type ProfilingSummaryBackend = {|
commitDurations: Array<number>,
commitTimes: Array<number>,
// An interleaved array: fiberID at [i], initialTreeBaseDuration at [i + 1].
initialTreeBaseDurations: Array<number>,
interactionCount: number,
rootID: number,
|};
export type ExportedProfilingDataFromRenderer = {|
version: 3,
profilingSummary: ProfilingSummaryBackend,
commitDetails: Array<CommitDetailsBackend>,
interactions: InteractionsBackend,
// Profiling data collected by the renderer interface.
// This information will be passed to the frontend and combined with info it collects.
export type ProfilingDataBackend = {|
dataForRoots: Array<ProfilingDataForRootBackend>,
rendererID: number,
|};
export type PathFrame = {|
@@ -178,21 +161,12 @@ export type RendererInterface = {
findNativeByFiberID: (id: number) => ?Array<NativeType>,
flushInitialOperations: () => void,
getBestMatchForTrackedPath: () => PathMatch | null,
getCommitDetails: (
rootID: number,
commitIndex: number
) => CommitDetailsBackend,
getFiberIDFromNative: (
component: NativeType,
findNearestUnfilteredAncestor?: boolean
) => number | null,
getFiberCommits: (rootID: number, fiberID: number) => FiberCommitsBackend,
getInteractions: (rootID: number) => InteractionsBackend,
getProfilingData(): ProfilingDataBackend,
getOwnersList: (id: number) => Array<Owner> | null,
getExportedProfilingData: (
rootID: number
) => ExportedProfilingDataFromRenderer,
getProfilingSummary: (rootID: number) => ProfilingSummaryBackend,
getPathForElement: (id: number) => Array<PathFrame> | null,
handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void,
handleCommitFiberUnmount: (fiber: Object) => void,
+1 -1
View File
@@ -16,4 +16,4 @@ export const SESSION_STORAGE_LAST_SELECTION_KEY =
export const __DEBUG__ = false;
export const PROFILER_EXPORT_VERSION = 3;
export const PROFILER_EXPORT_VERSION = 4;
+346
View File
@@ -0,0 +1,346 @@
// @flow
import EventEmitter from 'events';
import memoize from 'memoize-one';
import throttle from 'lodash.throttle';
import { prepareProfilingDataFrontendFromBackendAndStore } from './views/Profiler/utils';
import ProfilingCache from './ProfilingCache';
import Store from './store';
import type { ProfilingDataBackend } from 'src/backend/types';
import type {
CommitDataFrontend,
ProfilingDataForRootFrontend,
ProfilingDataFrontend,
SnapshotNode,
} from './views/Profiler/types';
import type { Bridge } from '../types';
const THROTTLE_CAPTURE_SCREENSHOT_DURATION = 500;
export default class ProfilerStore extends EventEmitter {
_bridge: Bridge;
// Suspense cache for lazily calculating derived profiling data.
_cache: ProfilingCache;
// Temporary store of profiling data from the backend renderer(s).
// This data will be converted to the ProfilingDataFrontend format after being collected from all renderers.
_dataBackends: Array<ProfilingDataBackend> = [];
// Data from the most recently completed profiling session,
// or data that has been imported from a previously exported session.
// This object contains all necessary data to drive the Profiler UI interface,
// even though some of it is lazily parsed/derived via the ProfilingCache.
_dataFrontend: ProfilingDataFrontend | null = null;
// Snapshot of the state of the main Store (including all roots) when profiling started.
// Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling,
// to reconstruct the state of each root for each commit.
// It's okay to use a single root to store this information because node IDs are unique across all roots.
//
// This map is only updated while profiling is in progress;
// Upon completion, it is converted into the exportable ProfilingDataFrontend format.
_initialSnapshotsByRootID: Map<number, Map<number, SnapshotNode>> = new Map();
// Map of root (id) to a list of tree mutation that occur during profiling.
// Once profiling is finished, these mutations can be used, along with the initial tree snapshots,
// to reconstruct the state of each root for each commit.
//
// This map is only updated while profiling is in progress;
// Upon completion, it is converted into the exportable ProfilingDataFrontend format.
_inProgressOperationsByRootID: Map<number, Array<Uint32Array>> = new Map();
// Map of root (id) to a Map of screenshots by commit ID.
// Stores screenshots for each commit (when profiling).
//
// This map is only updated while profiling is in progress;
// Upon completion, it is converted into the exportable ProfilingDataFrontend format.
_inProgressScreenshotsByRootID: Map<number, Map<number, string>> = new Map();
// The backend is currently profiling.
// When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
_isProfiling: boolean = false;
// After profiling, data is requested from each attached renderer using this queue.
// So long as this queue is not empty, the store is retrieving and processing profiling data from the backend.
_rendererQueue: Set<number> = new Set();
_store: Store;
constructor(bridge: Bridge, store: Store, defaultIsProfiling: boolean) {
super();
this._bridge = bridge;
this._isProfiling = defaultIsProfiling;
this._store = store;
bridge.addListener('operations', this.onBridgeOperations);
bridge.addListener('profilingData', this.onBridgeProfilingData);
bridge.addListener('profilingStatus', this.onProfilingStatus);
bridge.addListener('shutdown', this.onBridgeShutdown);
// It's possible that profiling has already started (e.g. "reload and start profiling")
// so the frontend needs to ask the backend for its status after mounting.
bridge.send('getProfilingStatus');
this._cache = new ProfilingCache(this);
}
getCommitData(rootID: number, commitIndex: number): CommitDataFrontend {
if (this._dataFrontend !== null) {
const dataForRoot = this._dataFrontend.dataForRoots.get(rootID);
if (dataForRoot != null) {
const commitDatum = dataForRoot.commitData[commitIndex];
if (commitDatum != null) {
return commitDatum;
}
}
}
throw Error(
`Could not find commit data for root "${rootID}" and commit ${commitIndex}`
);
}
getDataForRoot(rootID: number): ProfilingDataForRootFrontend {
if (this._dataFrontend !== null) {
const dataForRoot = this._dataFrontend.dataForRoots.get(rootID);
if (dataForRoot != null) {
return dataForRoot;
}
}
throw Error(`Could not find commit data for root "${rootID}"`);
}
// Profiling data has been recorded for at least one root.
get hasProfilingData(): boolean {
return (
this._dataFrontend !== null && this._dataFrontend.dataForRoots.size > 0
);
}
get isProcessingData(): boolean {
return this._rendererQueue.size > 0 || this._dataBackends.length > 0;
}
get isProfiling(): boolean {
return this._isProfiling;
}
get profilingCache(): ProfilingCache {
return this._cache;
}
get profilingData(): ProfilingDataFrontend | null {
return this._dataFrontend;
}
set profilingData(value: ProfilingDataFrontend | null): void {
this._dataBackends.splice(0);
this._dataFrontend = value;
this._initialSnapshotsByRootID.clear();
this._inProgressOperationsByRootID.clear();
this._inProgressScreenshotsByRootID.clear();
this._cache.invalidate();
this.emit('profilingData');
}
clear(): void {
this._dataBackends.splice(0);
this._dataFrontend = null;
this._initialSnapshotsByRootID.clear();
this._inProgressOperationsByRootID.clear();
this._inProgressScreenshotsByRootID.clear();
this._rendererQueue.clear();
// Invalidate suspense cache if profiling data is being (re-)recorded.
// Note that we clear now because any existing data is "stale".
this._cache.invalidate();
this.emit('profilingData');
}
startProfiling(): void {
this._bridge.send('startProfiling');
// Don't actually update the local profiling boolean yet!
// Wait for onProfilingStatus() to confirm the status has changed.
// This ensures the frontend and backend are in sync wrt which commits were profiled.
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
}
stopProfiling(): void {
this._bridge.send('stopProfiling');
// Don't actually update the local profiling boolean yet!
// Wait for onProfilingStatus() to confirm the status has changed.
// This ensures the frontend and backend are in sync wrt which commits were profiled.
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
}
_captureScreenshot = throttle(
memoize((rootID: number, commitIndex: number) => {
this._bridge.send('captureScreenshot', { commitIndex, rootID });
}),
THROTTLE_CAPTURE_SCREENSHOT_DURATION
);
_takeProfilingSnapshotRecursive = (
elementID: number,
profilingSnapshots: Map<number, SnapshotNode>
) => {
const element = this._store.getElementByID(elementID);
if (element !== null) {
const snapshotNode: SnapshotNode = {
id: elementID,
children: element.children.slice(0),
displayName: element.displayName,
key: element.key,
type: element.type,
};
profilingSnapshots.set(elementID, snapshotNode);
element.children.forEach(childID =>
this._takeProfilingSnapshotRecursive(childID, profilingSnapshots)
);
}
};
onBridgeOperations = (operations: Uint32Array) => {
if (!(operations instanceof Uint32Array)) {
// $FlowFixMe TODO HACK Temporary workaround for the fact that Chrome is not transferring the typed array.
operations = Uint32Array.from(Object.values(operations));
}
// The first two values are always rendererID and rootID
const rootID = operations[1];
if (this._isProfiling) {
let profilingOperations = this._inProgressOperationsByRootID.get(rootID);
if (profilingOperations == null) {
profilingOperations = [operations];
this._inProgressOperationsByRootID.set(rootID, profilingOperations);
} else {
profilingOperations.push(operations);
}
if (!this._initialSnapshotsByRootID.has(rootID)) {
this._initialSnapshotsByRootID.set(rootID, new Map());
}
if (this._store.captureScreenshots) {
const commitIndex = profilingOperations.length - 1;
this._captureScreenshot(rootID, commitIndex);
}
}
};
onBridgeProfilingData = (dataBackend: ProfilingDataBackend) => {
if (this._isProfiling) {
// This should never happen, but if it does- ignore previous profiling data.
return;
}
const { rendererID } = dataBackend;
if (!this._rendererQueue.has(rendererID)) {
throw Error(
`Unexpected profiling data update from renderer "${rendererID}"`
);
}
this._dataBackends.push(dataBackend);
this._rendererQueue.delete(rendererID);
if (this._rendererQueue.size === 0) {
this._dataFrontend = prepareProfilingDataFrontendFromBackendAndStore(
this._dataBackends,
this._inProgressOperationsByRootID,
this._inProgressScreenshotsByRootID,
this._initialSnapshotsByRootID
);
this._dataBackends.splice(0);
this.emit('isProcessingData');
}
};
onBridgeShutdown = () => {
this._bridge.removeListener('operations', this.onBridgeOperations);
this._bridge.removeListener('profilingStatus', this.onProfilingStatus);
this._bridge.removeListener('shutdown', this.onBridgeShutdown);
};
onProfilingStatus = (isProfiling: boolean) => {
if (isProfiling) {
this._dataBackends.splice(0);
this._dataFrontend = null;
this._initialSnapshotsByRootID.clear();
this._inProgressOperationsByRootID.clear();
this._inProgressScreenshotsByRootID.clear();
this._rendererQueue.clear();
// Record snapshot of tree at the time profiling is started.
// This info is required to handle cases of e.g. nodes being removed during profiling.
this._store.roots.forEach(rootID => {
const profilingSnapshots = new Map();
this._initialSnapshotsByRootID.set(rootID, profilingSnapshots);
this._takeProfilingSnapshotRecursive(rootID, profilingSnapshots);
});
}
if (this._isProfiling !== isProfiling) {
this._isProfiling = isProfiling;
// Invalidate suspense cache if profiling data is being (re-)recorded.
// Note that we clear again, in case any views read from the cache while profiling.
// (That would have resolved a now-stale value without any profiling data.)
this._cache.invalidate();
this.emit('isProfiling');
// If we've just finished a profiling session, we need to fetch data stored in each renderer interface
// and re-assemble it on the front-end into a format (ProfilingDataFrontend) that can power the Profiler UI.
// During this time, DevTools UI should probably not be interactive.
if (!isProfiling) {
this._dataBackends.splice(0);
this._rendererQueue.clear();
for (let rendererID of this._store.rootIDToRendererID.values()) {
if (!this._rendererQueue.has(rendererID)) {
this._rendererQueue.add(rendererID);
this._bridge.send('getProfilingData', { rendererID });
}
}
this.emit('isProcessingData');
}
}
};
onScreenshotCaptured = ({
commitIndex,
dataURL,
rootID,
}: {|
commitIndex: number,
dataURL: string,
rootID: number,
|}) => {
let screenshotsForRootByCommitIndex = this._inProgressScreenshotsByRootID.get(
rootID
);
if (!screenshotsForRootByCommitIndex) {
screenshotsForRootByCommitIndex = new Map();
this._inProgressScreenshotsByRootID.set(
rootID,
screenshotsForRootByCommitIndex
);
}
screenshotsForRootByCommitIndex.set(commitIndex, dataURL);
};
}
+55 -367
View File
@@ -1,7 +1,6 @@
// @flow
import { createResource } from './cache';
import Store from './store';
import ProfilerStore from './ProfilerStore';
import {
getCommitTree,
invalidateCommitTrees,
@@ -19,416 +18,105 @@ import {
invalidateChartData as invalidateRankedChartData,
} from 'src/devtools/views/Profiler/RankedChartBuilder';
import type { Resource } from './cache';
import type {
CommitDetailsBackend,
FiberCommitsBackend,
InteractionsBackend,
ProfilingSummaryBackend,
} from 'src/backend/types';
import type {
CommitDetailsFrontend,
FiberCommitsFrontend,
InteractionsFrontend,
InteractionWithCommitsFrontend,
CommitTreeFrontend,
ProfilingSummaryFrontend,
} from 'src/devtools/views/Profiler/types';
import type { CommitTree } from 'src/devtools/views/Profiler/types';
import type { ChartData as FlamegraphChartData } from 'src/devtools/views/Profiler/FlamegraphChartBuilder';
import type { ChartData as InteractionsChartData } from 'src/devtools/views/Profiler/InteractionsChartBuilder';
import type { ChartData as RankedChartData } from 'src/devtools/views/Profiler/RankedChartBuilder';
import type { Bridge } from 'src/types';
type CommitDetailsParams = {|
commitIndex: number,
rendererID: number,
rootID: number,
|};
type FiberCommitsParams = {|
fiberID: number,
rendererID: number,
rootID: number,
|};
type InteractionsParams = {|
rendererID: number,
rootID: number,
|};
type GetCommitTreeParams = {|
commitIndex: number,
profilingSummary: ProfilingSummaryFrontend,
|};
type ProfilingSummaryParams = {|
rendererID: number,
rootID: number,
|};
export default class ProfilingCache {
_bridge: Bridge;
_store: Store;
_fiberCommits: Map<number, Array<number>> = new Map();
_profilerStore: ProfilerStore;
_pendingCommitDetailsMap: Map<
string,
(commitDetails: CommitDetailsFrontend) => void
> = new Map();
_pendingFiberCommitsMap: Map<
string,
(fiberCommits: FiberCommitsFrontend) => void
> = new Map();
_pendingInteractionsMap: Map<
number,
(interactions: InteractionsFrontend) => void
> = new Map();
_pendingProfileSummaryMap: Map<
number,
(profilingSummary: ProfilingSummaryFrontend) => void
> = new Map();
CommitDetails: Resource<
CommitDetailsParams,
string,
CommitDetailsFrontend
> = createResource(
({ commitIndex, rendererID, rootID }: CommitDetailsParams) => {
return new Promise(resolve => {
const pendingKey = `${rootID}-${commitIndex}`;
const importedProfilingData = this._store.importedProfilingData;
if (importedProfilingData !== null) {
const commitDetailsByCommitIndex =
importedProfilingData.commitDetails;
if (
commitDetailsByCommitIndex != null &&
commitIndex < commitDetailsByCommitIndex.length
) {
const commitDetails = commitDetailsByCommitIndex[commitIndex];
if (commitDetails != null) {
this._pendingCommitDetailsMap.delete(pendingKey);
resolve(commitDetails);
return;
}
}
} else if (this._store.profilingOperations.has(rootID)) {
this._pendingCommitDetailsMap.set(pendingKey, resolve);
this._bridge.send('getCommitDetails', {
commitIndex,
rendererID,
rootID,
});
return;
}
this._pendingCommitDetailsMap.delete(pendingKey);
// If no profiling data was recorded for this root, skip the round trip.
resolve({
rootID,
commitIndex,
actualDurations: new Map(),
priorityLevel: null,
interactions: [],
selfDurations: new Map(),
});
});
},
({ commitIndex, rendererID, rootID }: CommitDetailsParams) =>
`${rootID}-${commitIndex}`
);
FiberCommits: Resource<
FiberCommitsParams,
string,
FiberCommitsFrontend
> = createResource(
({ fiberID, rendererID, rootID }: FiberCommitsParams) => {
return new Promise(resolve => {
const pendingKey = `${rootID}-${fiberID}`;
const importedProfilingData = this._store.importedProfilingData;
if (importedProfilingData !== null) {
const { commitDetails } = importedProfilingData;
const commitDurations = [];
commitDetails.forEach(({ selfDurations }, commitIndex) => {
const selfDuration = selfDurations.get(fiberID);
if (selfDuration != null) {
commitDurations.push(commitIndex, selfDuration);
}
});
this._pendingFiberCommitsMap.delete(pendingKey);
resolve({
commitDurations,
fiberID,
rootID,
});
return;
} else if (this._store.profilingOperations.has(rootID)) {
this._pendingFiberCommitsMap.set(pendingKey, resolve);
this._bridge.send('getFiberCommits', {
fiberID,
rendererID,
rootID,
});
return;
}
this._pendingFiberCommitsMap.delete(pendingKey);
// If no profiling data was recorded for this root, skip the round trip.
resolve({
commitDurations: [],
fiberID,
rootID,
});
});
},
({ fiberID, rendererID, rootID }: FiberCommitsParams) =>
`${rootID}-${fiberID}`
);
Interactions: Resource<
InteractionsParams,
number,
InteractionsFrontend
> = createResource(
({ rendererID, rootID }: InteractionsParams) => {
return new Promise(resolve => {
const pendingKey = rootID;
const importedProfilingData = this._store.importedProfilingData;
if (importedProfilingData !== null) {
const interactionsFrontend: InteractionsFrontend =
importedProfilingData.interactions;
if (interactionsFrontend != null) {
this._pendingInteractionsMap.delete(pendingKey);
resolve(interactionsFrontend);
return;
}
} else if (this._store.profilingOperations.has(rootID)) {
this._pendingInteractionsMap.set(pendingKey, resolve);
this._bridge.send('getInteractions', {
rendererID,
rootID,
});
return;
}
this._pendingInteractionsMap.delete(pendingKey);
// If no profiling data was recorded for this root, skip the round trip.
resolve({
interactions: [],
rootID,
});
});
},
({ rendererID, rootID }: ProfilingSummaryParams) => rootID
);
ProfilingSummary: Resource<
ProfilingSummaryParams,
number,
ProfilingSummaryFrontend
> = createResource(
({ rendererID, rootID }: ProfilingSummaryParams) => {
return new Promise(resolve => {
const pendingKey = rootID;
const importedProfilingData = this._store.importedProfilingData;
if (importedProfilingData !== null) {
const profilingSummaryFrontend: ProfilingSummaryFrontend =
importedProfilingData.profilingSummary;
if (profilingSummaryFrontend != null) {
this._pendingProfileSummaryMap.delete(pendingKey);
resolve(profilingSummaryFrontend);
return;
}
} else if (this._store.profilingOperations.has(rootID)) {
this._pendingProfileSummaryMap.set(pendingKey, resolve);
this._bridge.send('getProfilingSummary', { rendererID, rootID });
return;
}
this._pendingProfileSummaryMap.delete(pendingKey);
// If no profiling data was recorded for this root, skip the round trip.
resolve({
rootID,
commitDurations: [],
commitTimes: [],
initialTreeBaseDurations: new Map(),
interactionCount: 0,
});
});
},
({ rendererID, rootID }: ProfilingSummaryParams) => rootID
);
constructor(bridge: Bridge, store: Store) {
this._bridge = bridge;
this._store = store;
bridge.addListener('commitDetails', this.onCommitDetails);
bridge.addListener('fiberCommits', this.onFiberCommits);
bridge.addListener('interactions', this.onInteractions);
bridge.addListener('profilingSummary', this.onProfileSummary);
constructor(profilerStore: ProfilerStore) {
this._profilerStore = profilerStore;
}
getCommitTree = ({ commitIndex, profilingSummary }: GetCommitTreeParams) =>
getCommitTree = ({
commitIndex,
rootID,
}: {|
commitIndex: number,
rootID: number,
|}) =>
getCommitTree({
commitIndex,
profilingSummary,
store: this._store,
profilerStore: this._profilerStore,
rootID,
});
getFiberCommits = ({
fiberID,
rootID,
}: {|
fiberID: number,
rootID: number,
|}): Array<number> => {
const cachedFiberCommits = this._fiberCommits.get(fiberID);
if (cachedFiberCommits != null) {
return cachedFiberCommits;
}
const fiberCommits = [];
const dataForRoot = this._profilerStore.getDataForRoot(rootID);
dataForRoot.commitData.forEach((commitDatum, commitIndex) => {
if (commitDatum.fiberActualDurations.has(fiberID)) {
fiberCommits.push(commitIndex);
}
});
this._fiberCommits.set(fiberID, fiberCommits);
return fiberCommits;
};
getFlamegraphChartData = ({
commitDetails,
commitIndex,
commitTree,
rootID,
}: {|
commitDetails: CommitDetailsFrontend,
commitIndex: number,
commitTree: CommitTreeFrontend,
commitTree: CommitTree,
rootID: number,
|}): FlamegraphChartData =>
getFlamegraphChartData({
commitDetails,
commitIndex,
commitTree,
profilerStore: this._profilerStore,
rootID,
});
getInteractionsChartData = ({
interactions,
profilingSummary,
rootID,
}: {|
interactions: Array<InteractionWithCommitsFrontend>,
profilingSummary: ProfilingSummaryFrontend,
rootID: number,
|}): InteractionsChartData =>
getInteractionsChartData({
interactions,
profilingSummary,
profilerStore: this._profilerStore,
rootID,
});
getRankedChartData = ({
commitDetails,
commitIndex,
commitTree,
rootID,
}: {|
commitDetails: CommitDetailsFrontend,
commitIndex: number,
commitTree: CommitTreeFrontend,
commitTree: CommitTree,
rootID: number,
|}): RankedChartData =>
getRankedChartData({
commitDetails,
commitIndex,
commitTree,
profilerStore: this._profilerStore,
rootID,
});
invalidate() {
// Invalidate Suspense caches.
this.CommitDetails.clear();
this.FiberCommits.clear();
this.Interactions.clear();
this.ProfilingSummary.clear();
this._fiberCommits.clear();
// Invalidate non-Suspense caches too.
invalidateCommitTrees();
invalidateFlamegraphChartData();
invalidateInteractionsChartData();
invalidateRankedChartData();
this._pendingCommitDetailsMap.clear();
this._pendingFiberCommitsMap.clear();
this._pendingInteractionsMap.clear();
this._pendingProfileSummaryMap.clear();
}
onCommitDetails = ({
commitIndex,
durations,
interactions,
priorityLevel,
rootID,
}: CommitDetailsBackend) => {
const key = `${rootID}-${commitIndex}`;
const resolve = this._pendingCommitDetailsMap.get(key);
if (resolve != null) {
this._pendingCommitDetailsMap.delete(key);
const actualDurationsMap = new Map<number, number>();
const selfDurationsMap = new Map<number, number>();
for (let i = 0; i < durations.length; i += 3) {
const fiberID = durations[i];
actualDurationsMap.set(fiberID, durations[i + 1]);
selfDurationsMap.set(fiberID, durations[i + 2]);
}
resolve({
actualDurations: actualDurationsMap,
commitIndex,
interactions,
priorityLevel,
rootID,
selfDurations: selfDurationsMap,
});
}
};
onFiberCommits = ({
commitDurations,
fiberID,
rootID,
}: FiberCommitsBackend) => {
const key = `${rootID}-${fiberID}`;
const resolve = this._pendingFiberCommitsMap.get(key);
if (resolve != null) {
this._pendingFiberCommitsMap.delete(key);
resolve({
commitDurations,
fiberID,
rootID,
});
}
};
onInteractions = ({ interactions, rootID }: InteractionsBackend) => {
const resolve = this._pendingInteractionsMap.get(rootID);
if (resolve != null) {
this._pendingInteractionsMap.delete(rootID);
resolve({
interactions,
rootID,
});
}
};
onProfileSummary = ({
commitDurations,
commitTimes,
initialTreeBaseDurations,
interactionCount,
rootID,
}: ProfilingSummaryBackend) => {
const resolve = this._pendingProfileSummaryMap.get(rootID);
if (resolve != null) {
this._pendingProfileSummaryMap.delete(rootID);
const initialTreeBaseDurationsMap = new Map();
for (let i = 0; i < initialTreeBaseDurations.length; i += 2) {
const fiberID = initialTreeBaseDurations[i];
const initialTreeBaseDuration = initialTreeBaseDurations[i + 1];
initialTreeBaseDurationsMap.set(fiberID, initialTreeBaseDuration);
}
resolve({
commitDurations,
commitTimes,
initialTreeBaseDurations: initialTreeBaseDurationsMap,
interactionCount,
rootID,
});
}
};
}
+14 -245
View File
@@ -1,8 +1,6 @@
// @flow
import EventEmitter from 'events';
import memoize from 'memoize-one';
import throttle from 'lodash.throttle';
import { inspect } from 'util';
import {
TREE_OPERATION_ADD,
@@ -17,14 +15,10 @@ import {
utfDecodeString,
} from '../utils';
import { __DEBUG__ } from '../constants';
import ProfilingCache from './ProfilingCache';
import { printStore } from 'src/__tests__/storeSerializer';
import ProfilerStore from './ProfilerStore';
import type { Element } from './views/Components/types';
import type {
ImportedProfilingData,
ProfilingSnapshotNode,
} from './views/Profiler/types';
import type { Bridge, ComponentFilter, ElementType } from '../types';
const debug = (methodName, ...args) => {
@@ -43,12 +37,9 @@ const LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY =
const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =
'React::DevTools::collapseNodesByDefault';
const THROTTLE_CAPTURE_SCREENSHOT_DURATION = 500;
type Config = {|
isProfiling?: boolean,
supportsCaptureScreenshots?: boolean,
supportsFileDownloads?: boolean,
supportsReloadAndProfile?: boolean,
supportsProfiling?: boolean,
|};
@@ -80,37 +71,11 @@ export default class Store extends EventEmitter {
// The InspectedElementContext also relies on this mutability for its WeakMap usage.
_idToElement: Map<number, Element> = new Map();
// The user has imported a previously exported profiling session.
_importedProfilingData: ImportedProfilingData | null = null;
// The backend is currently profiling.
// When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
_isProfiling: boolean = false;
// Map of element (id) to the set of elements (ids) it owns.
// This map enables getOwnersListForElement() to avoid traversing the entire tree.
_ownersMap: Map<number, Set<number>> = new Map();
// Suspense cache for reading profiling data.
_profilingCache: ProfilingCache;
// Map of root (id) to a list of tree mutation that occur during profiling.
// Once profiling is finished, these mutations can be used, along with the initial tree snapshots,
// to reconstruct the state of each root for each commit.
_profilingOperationsByRootID: Map<number, Array<Uint32Array>> = new Map();
// Map of root (id) to a Map of screenshots by commit ID.
// Stores screenshots for each commit (when profiling).
_profilingScreenshotsByRootID: Map<number, Map<number, string>> = new Map();
// Snapshot of the state of the main Store (including all roots) when profiling started.
// Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling,
// to reconstruct the state of each root for each commit.
// It's okay to use a single root to store this information because node IDs are unique across all roots.
_profilingSnapshotsByRootID: Map<
number,
Map<number, ProfilingSnapshotNode>
> = new Map();
_profilerStore: ProfilerStore;
// Incremented each time the store is mutated.
// This enables a passive effect to detect a mutation between render and commit phase.
@@ -128,7 +93,6 @@ export default class Store extends EventEmitter {
// These options may be initially set by a confiugraiton option when constructing the Store.
// In the case of "supportsProfiling", the option may be updated based on the injected renderers.
_supportsCaptureScreenshots: boolean = false;
_supportsFileDownloads: boolean = false;
_supportsProfiling: boolean = false;
_supportsReloadAndProfile: boolean = false;
@@ -150,26 +114,21 @@ export default class Store extends EventEmitter {
this._componentFilters = getSavedComponentFilters();
let isProfiling = false;
if (config != null) {
isProfiling = config.isProfiling === true;
const {
isProfiling,
supportsCaptureScreenshots,
supportsFileDownloads,
supportsProfiling,
supportsReloadAndProfile,
} = config;
if (isProfiling) {
this._isProfiling = true;
}
if (supportsCaptureScreenshots) {
this._supportsCaptureScreenshots = true;
this._captureScreenshots =
localStorage.getItem(LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY) ===
'true';
}
if (supportsFileDownloads) {
this._supportsFileDownloads = true;
}
if (supportsProfiling) {
this._supportsProfiling = true;
}
@@ -180,15 +139,9 @@ export default class Store extends EventEmitter {
this._bridge = bridge;
bridge.addListener('operations', this.onBridgeOperations);
bridge.addListener('profilingStatus', this.onProfilingStatus);
bridge.addListener('screenshotCaptured', this.onScreenshotCaptured);
bridge.addListener('shutdown', this.onBridgeShutdown);
// It's possible that profiling has already started (e.g. "reload and start profiling")
// so the frontend needs to ask the backend for its status after mounting.
bridge.send('getProfilingStatus');
this._profilingCache = new ProfilingCache(bridge, this);
this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
}
// This is only used in tests to avoid memory leaks.
@@ -197,23 +150,6 @@ export default class Store extends EventEmitter {
// The only safe time to assert these maps are empty is when the store is empty.
this.assertMapSizeMatchesRootCount(this._idToElement, '_idToElement');
this.assertMapSizeMatchesRootCount(this._ownersMap, '_ownersMap');
// These maps will be empty unless profiling mode has been started.
// After this, their size should always match the number of roots,
// but unless we want to track additional metadata about profiling history,
// the only safe time to assert this is when the store is empty.
this.assertMapSizeMatchesRootCount(
this._profilingOperationsByRootID,
'_profilingOperationsByRootID'
);
this.assertMapSizeMatchesRootCount(
this._profilingScreenshotsByRootID,
'_profilingScreenshotsByRootID'
);
this.assertMapSizeMatchesRootCount(
this._profilingSnapshotsByRootID,
'_profilingSnapshotsByRootID'
);
}
// These maps should always be the same size as the number of roots
@@ -273,7 +209,7 @@ export default class Store extends EventEmitter {
return this._componentFilters;
}
set componentFilters(value: Array<ComponentFilter>): void {
if (this._isProfiling) {
if (this._profilerStore.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');
@@ -295,55 +231,22 @@ export default class Store extends EventEmitter {
return this._hasOwnerMetadata;
}
// Profiling data has been recorded for at least one root.
get hasProfilingData(): boolean {
return (
this._importedProfilingData !== null ||
this._profilingOperationsByRootID.size > 0
);
}
get importedProfilingData(): ImportedProfilingData | null {
return this._importedProfilingData;
}
set importedProfilingData(value: ImportedProfilingData | null): void {
this._importedProfilingData = value;
this._profilingOperationsByRootID = new Map();
this._profilingScreenshotsByRootID = new Map();
this._profilingSnapshotsByRootID = new Map();
this._profilingCache.invalidate();
this.emit('importedProfilingData');
}
get isProfiling(): boolean {
return this._isProfiling;
}
get numElements(): number {
return this._weightAcrossRoots;
}
get profilingCache(): ProfilingCache {
return this._profilingCache;
}
get profilingOperations(): Map<number, Array<Uint32Array>> {
return this._profilingOperationsByRootID;
}
get profilingScreenshots(): Map<number, Map<number, string>> {
return this._profilingScreenshotsByRootID;
}
get profilingSnapshots(): Map<number, Map<number, ProfilingSnapshotNode>> {
return this._profilingSnapshotsByRootID;
get profilerStore(): ProfilerStore {
return this._profilerStore;
}
get revision(): number {
return this._revision;
}
get rootIDToRendererID(): Map<number, number> {
return this._rootIDToRendererID;
}
get roots(): $ReadOnlyArray<number> {
return this._roots;
}
@@ -352,10 +255,6 @@ export default class Store extends EventEmitter {
return this._supportsCaptureScreenshots;
}
get supportsFileDownloads(): boolean {
return this._supportsFileDownloads;
}
get supportsProfiling(): boolean {
return this._supportsProfiling;
}
@@ -364,19 +263,6 @@ export default class Store extends EventEmitter {
return this._supportsReloadAndProfile;
}
clearProfilingData(): void {
this._importedProfilingData = null;
this._profilingOperationsByRootID = new Map();
this._profilingScreenshotsByRootID = new Map();
this._profilingSnapshotsByRootID = new Map();
// Invalidate suspense cache if profiling data is being (re-)recorded.
// Note that we clear now because any existing data is "stale".
this._profilingCache.invalidate();
this.emit('isProfiling');
}
containsElement(id: number): boolean {
return this._idToElement.get(id) != null;
}
@@ -602,24 +488,6 @@ export default class Store extends EventEmitter {
return false;
}
startProfiling(): void {
this._bridge.send('startProfiling');
// Don't actually update the local profiling boolean yet!
// Wait for onProfilingStatus() to confirm the status has changed.
// This ensures the frontend and backend are in sync wrt which commits were profiled.
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
}
stopProfiling(): void {
this._bridge.send('stopProfiling');
// Don't actually update the local profiling boolean yet!
// Wait for onProfilingStatus() to confirm the status has changed.
// This ensures the frontend and backend are in sync wrt which commits were profiled.
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
}
// TODO Maybe split this into two methods: expand() and collapse()
toggleIsCollapsed(id: number, isCollapsed: boolean): void {
let didMutate = false;
@@ -702,34 +570,6 @@ export default class Store extends EventEmitter {
}
}
_captureScreenshot = throttle(
memoize((rootID: number, commitIndex: number) => {
this._bridge.send('captureScreenshot', { commitIndex, rootID });
}),
THROTTLE_CAPTURE_SCREENSHOT_DURATION
);
_takeProfilingSnapshotRecursive = (
elementID: number,
profilingSnapshots: Map<number, ProfilingSnapshotNode>
) => {
const element = this.getElementByID(elementID);
if (element !== null) {
const snapshotNode: ProfilingSnapshotNode = {
id: elementID,
children: element.children.slice(0),
displayName: element.displayName,
key: element.key,
type: element.type,
};
profilingSnapshots.set(elementID, snapshotNode);
element.children.forEach(childID =>
this._takeProfilingSnapshotRecursive(childID, profilingSnapshots)
);
}
};
_adjustParentTreeWeight = (
parentElement: Element | null,
weightDelta: number
@@ -770,23 +610,8 @@ export default class Store extends EventEmitter {
let haveRootsChanged = false;
// The first two values are always rendererID and rootID
const rendererID = operations[0];
const rootID = operations[1];
if (this._isProfiling) {
let profilingOperations = this._profilingOperationsByRootID.get(rootID);
if (profilingOperations == null) {
profilingOperations = [operations];
this._profilingOperationsByRootID.set(rootID, profilingOperations);
} else {
profilingOperations.push(operations);
}
if (this._captureScreenshots) {
const commitIndex = profilingOperations.length - 1;
this._captureScreenshot(rootID, commitIndex);
}
}
const addedElementIDs: Array<number> = [];
// This is a mapping of removed ID -> parent ID:
@@ -858,10 +683,6 @@ export default class Store extends EventEmitter {
weight: 0,
});
if (this._isProfiling) {
this._profilingSnapshotsByRootID.set(id, new Map());
}
haveRootsChanged = true;
} else {
parentID = ((operations[i]: any): number);
@@ -957,10 +778,6 @@ export default class Store extends EventEmitter {
this._rootIDToRendererID.delete(id);
this._rootIDToCapabilities.delete(id);
this._profilingOperationsByRootID.delete(id);
this._profilingScreenshotsByRootID.delete(id);
this._profilingSnapshotsByRootID.delete(id);
haveRootsChanged = true;
} else {
if (__DEBUG__) {
@@ -1066,60 +883,12 @@ export default class Store extends EventEmitter {
this.emit('mutated', [addedElementIDs, removedElementIDs]);
};
onProfilingStatus = (isProfiling: boolean) => {
if (isProfiling) {
this._importedProfilingData = null;
this._profilingOperationsByRootID = new Map();
this._profilingScreenshotsByRootID = new Map();
this._profilingSnapshotsByRootID = new Map();
this.roots.forEach(rootID => {
const profilingSnapshots = new Map();
this._profilingSnapshotsByRootID.set(rootID, profilingSnapshots);
this._takeProfilingSnapshotRecursive(rootID, profilingSnapshots);
});
}
if (this._isProfiling !== isProfiling) {
this._isProfiling = isProfiling;
// Invalidate suspense cache if profiling data is being (re-)recorded.
// Note that we clear again, in case any views read from the cache while profiling.
// (That would have resolved a now-stale value without any profiling data.)
this._profilingCache.invalidate();
this.emit('isProfiling');
}
};
onScreenshotCaptured = ({
commitIndex,
dataURL,
rootID,
}: {|
commitIndex: number,
dataURL: string,
rootID: number,
|}) => {
let profilingScreenshotsForRootByCommitIndex = this._profilingScreenshotsByRootID.get(
rootID
);
if (!profilingScreenshotsForRootByCommitIndex) {
profilingScreenshotsForRootByCommitIndex = new Map();
this._profilingScreenshotsByRootID.set(
rootID,
profilingScreenshotsForRootByCommitIndex
);
}
profilingScreenshotsForRootByCommitIndex.set(commitIndex, dataURL);
};
onBridgeShutdown = () => {
if (__DEBUG__) {
debug('onBridgeShutdown', 'unsubscribing from Bridge');
}
this._bridge.removeListener('operations', this.onBridgeOperations);
this._bridge.removeListener('profilingStatus', this.onProfilingStatus);
this._bridge.removeListener('shutdown', this.onBridgeShutdown);
};
}
@@ -41,6 +41,7 @@ import type {
export default function ComponentFiltersModalWrapper(_: {||}) {
const store = useContext(StoreContext);
const { profilerStore } = store;
const { isModalShowing, setIsModalShowing } = useContext(
ComponentFiltersModalContext
@@ -50,13 +51,13 @@ export default function ComponentFiltersModalWrapper(_: {||}) {
// If necessary, we could support this- but it doesn't seem like a necessary use case.
const isProfilingSubscription = useMemo(
() => ({
getCurrentValue: () => store.isProfiling,
getCurrentValue: () => profilerStore.isProfiling,
subscribe: (callback: Function) => {
store.addListener('isProfiling', callback);
return () => store.removeListener('isProfiling', callback);
profilerStore.addListener('isProfiling', callback);
return () => profilerStore.removeListener('isProfiling', callback);
},
}),
[store]
[profilerStore]
);
const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
if (isProfiling && isModalShowing) {
@@ -14,6 +14,7 @@ import type { ComponentFilter } from 'src/types';
export default function ToggleComponentFiltersModalButton() {
const store = useContext(StoreContext);
const { profilerStore } = store;
const { isModalShowing, setIsModalShowing } = useContext(
ComponentFiltersModalContext
@@ -23,13 +24,13 @@ export default function ToggleComponentFiltersModalButton() {
// If necessary, we could support this- but it doesn't seem like a necessary use case.
const isProfilingSubscription = useMemo(
() => ({
getCurrentValue: () => store.isProfiling,
getCurrentValue: () => profilerStore.isProfiling,
subscribe: (callback: Function) => {
store.addListener('isProfiling', callback);
return () => store.removeListener('isProfiling', callback);
profilerStore.addListener('isProfiling', callback);
return () => profilerStore.removeListener('isProfiling', callback);
},
}),
[store]
[profilerStore]
);
const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
@@ -8,13 +8,14 @@ import { StoreContext } from '../context';
export default function ClearProfilingDataButton() {
const store = useContext(StoreContext);
const { isProfiling } = useContext(ProfilerContext);
const { hasProfilingData, isProfiling } = useContext(ProfilerContext);
const { profilerStore } = store;
const clear = useCallback(() => store.clearProfilingData(), [store]);
const clear = useCallback(() => profilerStore.clear(), [profilerStore]);
return (
<Button
disabled={isProfiling || !store.hasProfilingData}
disabled={isProfiling || !hasProfilingData}
onClick={clear}
title="Clear profiling data"
>
@@ -8,6 +8,16 @@ import { useModalDismissSignal } from '../hooks';
import styles from './CommitFilterModal.css';
export default function FilterModal(_: {||}) {
const { isModalShowing } = useContext(CommitFilterModalContext);
if (!isModalShowing) {
return null;
}
return <FilterModalImpl />;
}
function FilterModalImpl(_: {||}) {
const {
isCommitFilterEnabled,
minCommitDuration,
+11 -35
View File
@@ -13,7 +13,7 @@ import { StoreContext } from '../context';
import styles from './CommitFlamegraph.css';
import type { ChartData, ChartNode } from './FlamegraphChartBuilder';
import type { CommitDetailsFrontend, CommitTreeFrontend } from './types';
import type { CommitTree } from './types';
export type ItemData = {|
chartData: ChartData,
@@ -25,10 +25,11 @@ export type ItemData = {|
|};
export default function CommitFlamegraphAutoSizer(_: {||}) {
const { profilingCache } = useContext(StoreContext);
const { rendererID, rootID, selectedCommitIndex, selectFiber } = useContext(
const { profilerStore } = useContext(StoreContext);
const { rootID, selectedCommitIndex, selectFiber } = useContext(
ProfilerContext
);
const { profilingCache } = profilerStore;
const deselectCurrentFiber = useCallback(
event => {
@@ -38,39 +39,22 @@ export default function CommitFlamegraphAutoSizer(_: {||}) {
[selectFiber]
);
const profilingSummary = profilingCache.ProfilingSummary.read({
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
});
let commitDetails: CommitDetailsFrontend | null = null;
let commitTree: CommitTreeFrontend | null = null;
let commitTree: CommitTree | null = null;
let chartData: ChartData | null = null;
if (selectedCommitIndex !== null) {
commitDetails = profilingCache.CommitDetails.read({
commitTree = profilingCache.getCommitTree({
commitIndex: selectedCommitIndex,
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
});
commitTree = profilingCache.getCommitTree({
commitIndex: selectedCommitIndex,
profilingSummary,
});
chartData = profilingCache.getFlamegraphChartData({
commitDetails,
commitIndex: selectedCommitIndex,
commitTree,
rootID: ((rootID: any): number),
});
}
if (
commitDetails != null &&
commitTree != null &&
chartData != null &&
chartData.depth > 0
) {
if (commitTree != null && chartData != null && chartData.depth > 0) {
return (
<div className={styles.Container} onClick={deselectCurrentFiber}>
<AutoSizer>
@@ -79,8 +63,7 @@ export default function CommitFlamegraphAutoSizer(_: {||}) {
// by the time this render prop function is called, the values of the `let` variables have not changed.
<CommitFlamegraph
chartData={((chartData: any): ChartData)}
commitDetails={((commitDetails: any): CommitDetailsFrontend)}
commitTree={((commitTree: any): CommitTreeFrontend)}
commitTree={((commitTree: any): CommitTree)}
height={height}
width={width}
/>
@@ -95,19 +78,12 @@ export default function CommitFlamegraphAutoSizer(_: {||}) {
type Props = {|
chartData: ChartData,
commitDetails: CommitDetailsFrontend,
commitTree: CommitTreeFrontend,
commitTree: CommitTree,
height: number,
width: number,
|};
function CommitFlamegraph({
chartData,
commitDetails,
commitTree,
height,
width,
}: Props) {
function CommitFlamegraph({ chartData, commitTree, height, width }: Props) {
const { selectFiber, selectedFiberID } = useContext(ProfilerContext);
const selectedChartNodeIndex = useMemo<number>(() => {
+11 -35
View File
@@ -13,7 +13,7 @@ import { StoreContext } from '../context';
import styles from './CommitRanked.css';
import type { ChartData } from './RankedChartBuilder';
import type { CommitDetailsFrontend, CommitTreeFrontend } from './types';
import type { CommitTree } from './types';
export type ItemData = {|
chartData: ChartData,
@@ -25,10 +25,11 @@ export type ItemData = {|
|};
export default function CommitRankedAutoSizer(_: {||}) {
const { profilingCache } = useContext(StoreContext);
const { rendererID, rootID, selectedCommitIndex, selectFiber } = useContext(
const { profilerStore } = useContext(StoreContext);
const { rootID, selectedCommitIndex, selectFiber } = useContext(
ProfilerContext
);
const { profilingCache } = profilerStore;
const deselectCurrentFiber = useCallback(
event => {
@@ -38,47 +39,29 @@ export default function CommitRankedAutoSizer(_: {||}) {
[selectFiber]
);
const profilingSummary = profilingCache.ProfilingSummary.read({
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
});
let commitDetails: CommitDetailsFrontend | null = null;
let commitTree: CommitTreeFrontend | null = null;
let commitTree: CommitTree | null = null;
let chartData: ChartData | null = null;
if (selectedCommitIndex !== null) {
commitDetails = profilingCache.CommitDetails.read({
commitTree = profilingCache.getCommitTree({
commitIndex: selectedCommitIndex,
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
});
commitTree = profilingCache.getCommitTree({
commitIndex: selectedCommitIndex,
profilingSummary,
});
chartData = profilingCache.getRankedChartData({
commitDetails,
commitIndex: selectedCommitIndex,
commitTree,
rootID: ((rootID: any): number),
});
}
if (
commitDetails != null &&
commitTree != null &&
chartData != null &&
chartData.nodes.length > 0
) {
if (commitTree != null && chartData != null && chartData.nodes.length > 0) {
return (
<div className={styles.Container} onClick={deselectCurrentFiber}>
<AutoSizer>
{({ height, width }) => (
<CommitRanked
chartData={((chartData: any): ChartData)}
commitDetails={((commitDetails: any): CommitDetailsFrontend)}
commitTree={((commitTree: any): CommitTreeFrontend)}
commitTree={((commitTree: any): CommitTree)}
height={height}
width={width}
/>
@@ -93,19 +76,12 @@ export default function CommitRankedAutoSizer(_: {||}) {
type Props = {|
chartData: ChartData,
commitDetails: CommitDetailsFrontend,
commitTree: CommitTreeFrontend,
commitTree: CommitTree,
height: number,
width: number,
|};
function CommitRanked({
chartData,
commitDetails,
commitTree,
height,
width,
}: Props) {
function CommitRanked({ chartData, commitTree, height, width }: Props) {
const { selectedFiberID, selectFiber } = useContext(ProfilerContext);
const selectedFiberIndex = useMemo(
@@ -9,14 +9,13 @@ import {
} from 'src/constants';
import { utfDecodeString } from 'src/utils';
import { ElementTypeRoot } from 'src/types';
import Store from 'src/devtools/store';
import ProfilerStore from 'src/devtools/ProfilerStore';
import type { ElementType } from 'src/types';
import type {
CommitTreeFrontend,
CommitTreeNodeFrontend,
ProfilingSnapshotNode,
ProfilingSummaryFrontend,
CommitTree,
CommitTreeNode,
ProfilingDataForRootFrontend,
} from 'src/devtools/views/Profiler/types';
const debug = (methodName, ...args) => {
@@ -30,36 +29,40 @@ const debug = (methodName, ...args) => {
}
};
const rootToCommitTreeMap: Map<number, Array<CommitTreeFrontend>> = new Map();
const rootToCommitTreeMap: Map<number, Array<CommitTree>> = new Map();
export function getCommitTree({
commitIndex,
profilingSummary,
store,
profilerStore,
rootID,
}: {|
commitIndex: number,
profilingSummary: ProfilingSummaryFrontend,
store: Store,
|}): CommitTreeFrontend {
const { rootID } = profilingSummary;
profilerStore: ProfilerStore,
rootID: number,
|}): CommitTree {
if (!rootToCommitTreeMap.has(rootID)) {
rootToCommitTreeMap.set(rootID, []);
}
const commitTrees = ((rootToCommitTreeMap.get(
rootID
): any): Array<CommitTreeFrontend>);
): any): Array<CommitTree>);
if (commitIndex < commitTrees.length) {
return commitTrees[commitIndex];
}
const { importedProfilingData } = store;
const profilingOperations =
importedProfilingData != null
? importedProfilingData.profilingOperations
: store.profilingOperations;
const { profilingData } = profilerStore;
if (profilingData === null) {
throw Error(`No profiling data available`);
}
const dataForRoot = profilingData.dataForRoots.get(rootID);
if (dataForRoot == null) {
throw Error(`Could not find profiling data for root "${rootID}"`);
}
const { operations } = dataForRoot;
// Commits are generated sequentially and cached.
// If this is the very first commit, start with the cached snapshot and apply the first mutation.
@@ -67,32 +70,12 @@ export function getCommitTree({
if (commitIndex === 0) {
const nodes = new Map();
const { importedProfilingData } = store;
const profilingSnapshots =
importedProfilingData != null
? importedProfilingData.profilingSnapshots.get(rootID)
: store.profilingSnapshots.get(rootID);
if (profilingSnapshots == null) {
throw Error(`Could not find profiling snapshot for root "${rootID}"`);
}
// Construct the initial tree.
recursivelyInitializeTree(
rootID,
0,
nodes,
profilingSummary.initialTreeBaseDurations,
profilingSnapshots
);
recursivelyInitializeTree(rootID, 0, nodes, dataForRoot);
// Mutate the tree
const commitOperations = profilingOperations.get(rootID);
if (commitOperations != null && commitIndex < commitOperations.length) {
const commitTree = updateTree(
{ nodes, rootID },
commitOperations[commitIndex]
);
if (operations != null && commitIndex < operations.length) {
const commitTree = updateTree({ nodes, rootID }, operations[commitIndex]);
if (__DEBUG__) {
__printTree(commitTree);
@@ -104,14 +87,14 @@ export function getCommitTree({
} else {
const previousCommitTree = getCommitTree({
commitIndex: commitIndex - 1,
profilingSummary,
store,
profilerStore,
rootID,
});
const commitOperations = profilingOperations.get(rootID);
if (commitOperations != null && commitIndex < commitOperations.length) {
if (operations != null && commitIndex < operations.length) {
const commitTree = updateTree(
previousCommitTree,
commitOperations[commitIndex]
operations[commitIndex]
);
if (__DEBUG__) {
@@ -131,11 +114,10 @@ export function getCommitTree({
function recursivelyInitializeTree(
id: number,
parentID: number,
nodes: Map<number, CommitTreeNodeFrontend>,
initialTreeBaseDurations: Map<number, number>,
profilingSnapshots: Map<number, ProfilingSnapshotNode>
nodes: Map<number, CommitTreeNode>,
dataForRoot: ProfilingDataForRootFrontend
): void {
const node = profilingSnapshots.get(id);
const node = dataForRoot.snapshots.get(id);
if (node != null) {
nodes.set(id, {
id,
@@ -143,35 +125,31 @@ function recursivelyInitializeTree(
displayName: node.displayName,
key: node.key,
parentID,
treeBaseDuration: ((initialTreeBaseDurations.get(id): any): number),
treeBaseDuration: ((dataForRoot.initialTreeBaseDurations.get(
id
): any): number),
type: node.type,
});
node.children.forEach(childID =>
recursivelyInitializeTree(
childID,
id,
nodes,
initialTreeBaseDurations,
profilingSnapshots
)
recursivelyInitializeTree(childID, id, nodes, dataForRoot)
);
}
}
function updateTree(
commitTree: CommitTreeFrontend,
commitTree: CommitTree,
operations: Uint32Array
): CommitTreeFrontend {
): CommitTree {
// Clone the original tree so edits don't affect it.
const nodes = new Map(commitTree.nodes);
// Clone nodes before mutating them so edits don't affect them.
const getClonedNode = (id: number): CommitTreeNodeFrontend => {
const getClonedNode = (id: number): CommitTreeNode => {
const clonedNode = ((Object.assign(
{},
nodes.get(id)
): any): CommitTreeNodeFrontend);
): any): CommitTreeNode);
nodes.set(id, clonedNode);
return clonedNode;
};
@@ -219,7 +197,7 @@ function updateTree(
debug('Add', `new root fiber ${id}`);
}
const node: CommitTreeNodeFrontend = {
const node: CommitTreeNode = {
children: [],
displayName: null,
id,
@@ -254,7 +232,7 @@ function updateTree(
const parentNode = getClonedNode(parentID);
parentNode.children = parentNode.children.concat(id);
const node: CommitTreeNodeFrontend = {
const node: CommitTreeNode = {
children: [],
displayName,
id,
@@ -355,7 +333,7 @@ export function invalidateCommitTrees(): void {
}
// DEBUG
const __printTree = (commitTree: CommitTreeFrontend) => {
const __printTree = (commitTree: CommitTree) => {
if (__DEBUG__) {
const { nodes, rootID } = commitTree;
console.group('__printTree()');
@@ -2,8 +2,9 @@
import { ElementTypeForwardRef, ElementTypeMemo } from 'src/types';
import { formatDuration } from './utils';
import ProfilerStore from 'src/devtools/ProfilerStore';
import type { CommitDetailsFrontend, CommitTreeFrontend } from './types';
import type { CommitTree } from './types';
export type ChartNode = {|
actualDuration: number,
@@ -28,15 +29,19 @@ export type ChartData = {|
const cachedChartData: Map<string, ChartData> = new Map();
export function getChartData({
commitDetails,
commitIndex,
commitTree,
profilerStore,
rootID,
}: {|
commitDetails: CommitDetailsFrontend,
commitIndex: number,
commitTree: CommitTreeFrontend,
commitTree: CommitTree,
profilerStore: ProfilerStore,
rootID: number,
|}): ChartData {
const { actualDurations, rootID, selfDurations } = commitDetails;
const commitDatum = profilerStore.getCommitData(rootID, commitIndex);
const { fiberActualDurations, fiberSelfDurations } = commitDatum;
const { nodes } = commitTree;
const key = `${rootID}-${commitIndex}`;
@@ -62,9 +67,9 @@ export function getChartData({
const { children, displayName, key, treeBaseDuration, type } = node;
const actualDuration = actualDurations.get(id) || 0;
const selfDuration = selfDurations.get(id) || 0;
const didRender = actualDurations.has(id);
const actualDuration = fiberActualDurations.get(id) || 0;
const selfDuration = fiberSelfDurations.get(id) || 0;
const didRender = fiberActualDurations.has(id);
const name = displayName || 'Anonymous';
const maybeKey = key !== null ? ` key="${key}"` : '';
@@ -131,7 +136,7 @@ export function getChartData({
walkTree(id, baseDuration, 1);
}
actualDurations.forEach((duration, id) => {
fiberActualDurations.forEach((duration, id) => {
const node = nodes.get(id);
if (node != null) {
let currentID = node.parentID;
@@ -17,9 +17,8 @@ type Props = {
function InteractionListItem({ data: itemData, index, style }: Props) {
const {
chartData,
interactions,
dataForRoot,
labelWidth,
profilingSummary,
scaleX,
selectedInteractionID,
selectCommitIndex,
@@ -27,20 +26,22 @@ function InteractionListItem({ data: itemData, index, style }: Props) {
selectTab,
} = itemData;
const { maxCommitDuration } = chartData;
const { commitDurations, commitTimes } = profilingSummary;
const { commitData, interactionCommits } = dataForRoot;
const { interactions, lastInteractionTime, maxCommitDuration } = chartData;
const interaction = interactions[index];
if (interaction == null) {
throw Error(`Could not find interaction #${index}`);
}
const handleClick = useCallback(() => {
selectInteraction(interaction.id);
}, [interaction, selectInteraction]);
const commits = interactionCommits.get(interaction.id) || [];
const startTime = interaction.timestamp;
const stopTime =
interaction.commits.length > 0
? commitTimes[interaction.commits[interaction.commits.length - 1]]
: interaction.timestamp;
const stopTime = lastInteractionTime;
const viewCommit = (commitIndex: number) => {
selectTab('flame-chart');
@@ -71,7 +72,7 @@ function InteractionListItem({ data: itemData, index, style }: Props) {
width: scaleX(stopTime - startTime, 0),
}}
/>
{interaction.commits.map(commitIndex => (
{commits.map(commitIndex => (
<div
className={styles.CommitBox}
key={commitIndex}
@@ -80,10 +81,13 @@ function InteractionListItem({ data: itemData, index, style }: Props) {
backgroundColor: getGradientColor(
Math.min(
1,
Math.max(0, commitDurations[commitIndex] / maxCommitDuration)
Math.max(
0,
commitData[commitIndex].duration / maxCommitDuration
)
) || 0
),
left: labelWidth + scaleX(commitTimes[commitIndex], 0),
left: labelWidth + scaleX(commitData[commitIndex].timestamp, 0),
}}
/>
))}
+10 -23
View File
@@ -11,18 +11,14 @@ import { scale } from './utils';
import styles from './Interactions.css';
import type { ProfilingDataForRootFrontend } from './types';
import type { ChartData } from './InteractionsChartBuilder';
import type { TabID } from './ProfilerContext';
import type {
InteractionWithCommitsFrontend,
ProfilingSummaryFrontend,
} from './types';
export type ItemData = {|
chartData: ChartData,
interactions: Array<InteractionWithCommitsFrontend>,
dataForRoot: ProfilingDataForRootFrontend,
labelWidth: number,
profilingSummary: ProfilingSummaryFrontend,
scaleX: (value: number, fallbackValue: number) => number,
selectedInteractionID: number | null,
selectCommitIndex: (id: number | null) => void,
@@ -42,30 +38,23 @@ export default function InteractionsAutoSizer(_: {||}) {
function Interactions({ height, width }: {| height: number, width: number |}) {
const {
rendererID,
rootID,
selectedInteractionID,
selectInteraction,
selectCommitIndex,
selectTab,
} = useContext(ProfilerContext);
const { profilingCache } = useContext(StoreContext);
const { profilerStore } = useContext(StoreContext);
const { profilingCache } = profilerStore;
const { interactions } = profilingCache.Interactions.read({
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
});
const profilingSummary = profilingCache.ProfilingSummary.read({
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
});
const dataForRoot = profilerStore.getDataForRoot(((rootID: any): number));
const chartData = profilingCache.getInteractionsChartData({
interactions,
profilingSummary,
rootID: ((rootID: any): number),
});
const { interactions } = chartData;
const handleKeyDown = useCallback(
event => {
let index;
@@ -110,9 +99,8 @@ function Interactions({ height, width }: {| height: number, width: number |}) {
return {
chartData,
interactions,
dataForRoot,
labelWidth,
profilingSummary,
scaleX: scale(0, chartData.lastInteractionTime, 0, timelineWidth),
selectedInteractionID,
selectCommitIndex,
@@ -121,8 +109,7 @@ function Interactions({ height, width }: {| height: number, width: number |}) {
};
}, [
chartData,
interactions,
profilingSummary,
dataForRoot,
selectedInteractionID,
selectCommitIndex,
selectInteraction,
@@ -1,11 +1,11 @@
// @flow
import type {
InteractionWithCommitsFrontend,
ProfilingSummaryFrontend,
} from './types';
import ProfilerStore from 'src/devtools/ProfilerStore';
import type { Interaction } from './types';
export type ChartData = {|
interactions: Array<Interaction>,
lastInteractionTime: number,
maxCommitDuration: number,
|};
@@ -13,30 +13,37 @@ export type ChartData = {|
const cachedChartData: Map<number, ChartData> = new Map();
export function getChartData({
interactions,
profilingSummary,
profilerStore,
rootID,
}: {|
interactions: Array<InteractionWithCommitsFrontend>,
profilingSummary: ProfilingSummaryFrontend,
profilerStore: ProfilerStore,
rootID: number,
|}): ChartData {
const { rootID } = profilingSummary;
if (cachedChartData.has(rootID)) {
return ((cachedChartData.get(rootID): any): ChartData);
}
const { commitDurations, commitTimes } = profilingSummary;
const dataForRoot = profilerStore.getDataForRoot(rootID);
if (dataForRoot == null) {
throw Error(`Could not find profiling data for root "${rootID}"`);
}
const { commitData, interactions } = dataForRoot;
const lastInteractionTime =
commitTimes.length > 0 ? commitTimes[commitTimes.length - 1] : 0;
commitData.length > 0 ? commitData[commitData.length - 1].timestamp : 0;
let maxCommitDuration = 0;
commitDurations.forEach(commitDuration => {
maxCommitDuration = Math.max(maxCommitDuration, commitDuration);
commitData.forEach(commitDatum => {
maxCommitDuration = Math.max(maxCommitDuration, commitDatum.duration);
});
const chartData = { lastInteractionTime, maxCommitDuration };
const chartData = {
interactions: Array.from(interactions.values()),
lastInteractionTime,
maxCommitDuration,
};
cachedChartData.set(rootID, chartData);
+9 -27
View File
@@ -1,37 +1,19 @@
// @flow
import React, { useContext } from 'react';
import { ProfilerContext } from './ProfilerContext';
import React from 'react';
import ToggleCommitFilterModalButton from './ToggleCommitFilterModalButton';
import styles from './NoCommitData.css';
export default function NoCommitData(_: {||}) {
const { rootHasProfilingData } = useContext(ProfilerContext);
if (rootHasProfilingData) {
return (
<div className={styles.NoCommitData}>
<div className={styles.Header}>
There is no data matching the current filter criteria.
</div>
<div className={styles.FilterMessage}>
Try adjusting the commit filter <ToggleCommitFilterModalButton />
</div>
return (
<div className={styles.NoCommitData}>
<div className={styles.Header}>
There is no data matching the current filter criteria.
</div>
);
} else {
return (
<div className={styles.NoCommitData}>
<div className={styles.Header}>
There is no timing data to display for the currently selected commit.
</div>
<div>
This can indicate that a render occurred too quickly for the timing
API to measure. Try selecting another commit in the upper, right-hand
corner.
</div>
<div className={styles.FilterMessage}>
Try adjusting the commit filter <ToggleCommitFilterModalButton />
</div>
);
}
</div>
);
}
+80 -159
View File
@@ -1,10 +1,7 @@
// @flow
import React, { Suspense, useContext } from 'react';
import {
CommitFilterModalContext,
CommitFilterModalContextController,
} from './CommitFilterModalContext';
import React, { useContext } from 'react';
import { CommitFilterModalContextController } from './CommitFilterModalContext';
import { ModalDialog } from '../ModalDialog';
import { ProfilerContext } from './ProfilerContext';
import TabBar from '../TabBar';
@@ -13,6 +10,7 @@ import CommitFlamegraph from './CommitFlamegraph';
import CommitRanked from './CommitRanked';
import CommitFilterModal from './CommitFilterModal';
import Interactions from './Interactions';
import RootSelector from './RootSelector';
import RecordToggle from './RecordToggle';
import ReloadAndProfileButton from './ReloadAndProfileButton';
import ProfilingImportExportButtons from './ProfilingImportExportButtons';
@@ -30,163 +28,91 @@ export type Props = {|
|};
function Profiler({ supportsProfiling }: Props) {
const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext(
ProfilerContext
);
const {
hasProfilingData,
isProcessingData,
isProfiling,
selectedFiberID,
selectedTabID,
selectTab,
} = useContext(ProfilerContext);
if (isProfiling || !rootHasProfilingData) {
return (
<NonSuspendingProfiler
hasProfilingData={hasProfilingData}
isProfiling={isProfiling}
supportsProfiling={supportsProfiling}
/>
);
} else {
return (
<CommitFilterModalContextController>
<SuspendingProfiler />
</CommitFilterModalContextController>
);
}
}
// This view is rendered when there is no profiler data (either we haven't profiled yet or we're currently profiling).
// Nothing in this view's subtree suspends.
// By not suspending while profiling is in progress, we avoid potential cache invalidation trickiness.
// NOTE that the structure of this UI should mirror SuspendingProfiler.
function NonSuspendingProfiler({
hasProfilingData,
isProfiling,
supportsProfiling,
}: {|
hasProfilingData: boolean,
isProfiling: boolean,
supportsProfiling: boolean,
|}) {
let view = null;
if (!supportsProfiling) {
view = <ProfilingNotSupported />;
} else if (isProfiling) {
view = <RecortdingInProgress />;
view = <RecordingInProgress />;
} else if (isProcessingData) {
view = <ProcessingData />;
} else if (!hasProfilingData) {
view = <NoProfilingData />;
} else {
view = <NoProfilingDataForRoot />;
}
return (
<div className={styles.Profiler}>
<div className={styles.LeftColumn}>
<div className={styles.Toolbar}>
<RecordToggle disabled={!supportsProfiling} />
<ReloadAndProfileButton />
<ClearProfilingDataButton />
<ProfilingImportExportButtons />
<div className={styles.VRule} />
<TabBar
currentTab={null}
disabled
id="Profiler"
selectTab={() => {}}
size="small"
tabs={tabs}
/>
</div>
<div className={styles.Content}>
{view}
<ModalDialog />
</div>
</div>
</div>
);
}
function ContentFallback() {
return <div className={styles.ContentFallback}>Loading...</div>;
}
function SnapshotSelectorFallback() {
return <div className={styles.SnapshotSelectorFallback}>Loading...</div>;
}
// This view is rendered when there is profiler data (even though there may not be any for the currently selected root).
// This view's subtree uses suspense to request profiler data from the backend.
// NOTE that the structure of this UI should mirror NonSuspendingProfiler.
function SuspendingProfiler() {
const { selectedFiberID, selectedTabID, selectTab } = useContext(
ProfilerContext
);
const { isModalShowing: isFilterModalShowing } = useContext(
CommitFilterModalContext
);
let view = null;
switch (selectedTabID) {
case 'flame-chart':
view = <CommitFlamegraph />;
break;
case 'ranked-chart':
view = <CommitRanked />;
break;
case 'interactions':
view = <Interactions />;
break;
default:
break;
switch (selectedTabID) {
case 'flame-chart':
view = <CommitFlamegraph />;
break;
case 'ranked-chart':
view = <CommitRanked />;
break;
case 'interactions':
view = <Interactions />;
break;
default:
break;
}
}
let sidebar = null;
switch (selectedTabID) {
case 'interactions':
sidebar = <SidebarInteractions />;
break;
case 'flame-chart':
case 'ranked-chart':
if (selectedFiberID !== null) {
sidebar = <SidebarSelectedFiberInfo />;
} else {
sidebar = <SidebarCommitInfo />;
}
break;
default:
break;
if (!isProfiling && !isProcessingData && hasProfilingData) {
switch (selectedTabID) {
case 'interactions':
sidebar = <SidebarInteractions />;
break;
case 'flame-chart':
case 'ranked-chart':
if (selectedFiberID !== null) {
sidebar = <SidebarSelectedFiberInfo />;
} else {
sidebar = <SidebarCommitInfo />;
}
break;
default:
break;
}
}
return (
<div className={styles.Profiler}>
<div className={styles.LeftColumn}>
<div className={styles.Toolbar}>
<RecordToggle />
<ReloadAndProfileButton />
<ClearProfilingDataButton />
<ProfilingImportExportButtons />
<div className={styles.VRule} />
<TabBar
currentTab={selectedTabID}
id="Profiler"
selectTab={selectTab}
size="small"
tabs={tabs}
/>
<div className={styles.Spacer} />
<ToggleCommitFilterModalButton />
<div className={styles.VRule} />
<Suspense fallback={<SnapshotSelectorFallback />}>
<SnapshotSelector />
</Suspense>
</div>
<div className={styles.Content}>
<Suspense fallback={<ContentFallback />}>{view}</Suspense>
{isFilterModalShowing && <CommitFilterModal />}
<ModalDialog />
<CommitFilterModalContextController>
<div className={styles.Profiler}>
<div className={styles.LeftColumn}>
<div className={styles.Toolbar}>
<RecordToggle />
<ReloadAndProfileButton />
<ClearProfilingDataButton />
<ProfilingImportExportButtons />
<div className={styles.VRule} />
<TabBar
currentTab={selectedTabID}
id="Profiler"
selectTab={selectTab}
size="small"
tabs={tabs}
/>
<RootSelector />
<div className={styles.Spacer} />
<ToggleCommitFilterModalButton />
<div className={styles.VRule} />
{hasProfilingData && <SnapshotSelector />}
</div>
<div className={styles.Content}>
{view}
<CommitFilterModal />
<ModalDialog />
</div>
</div>
<div className={styles.RightColumn}>{sidebar}</div>
</div>
<div className={styles.RightColumn}>
<Suspense fallback={<ContentFallback />}>{sidebar}</Suspense>
</div>
</div>
</CommitFilterModalContextController>
);
}
@@ -220,18 +146,6 @@ const NoProfilingData = () => (
</div>
);
const NoProfilingDataForRoot = () => (
<div className={styles.Column}>
<div className={styles.Header}>
No profiling data has been recorded for the selected root.
</div>
<div className={styles.Row}>
Select a different root in the elements panel, or click the record button{' '}
<RecordToggle /> to start recording.
</div>
</div>
);
const ProfilingNotSupported = () => (
<div className={styles.Column}>
<div className={styles.Header}>Profiling not supported.</div>
@@ -256,7 +170,14 @@ const ProfilingNotSupported = () => (
</div>
);
const RecortdingInProgress = () => (
const ProcessingData = () => (
<div className={styles.Column}>
<div className={styles.Header}>Processing data...</div>
<div className={styles.Row}>This should only take a minute.</div>
</div>
);
const RecordingInProgress = () => (
<div className={styles.Column}>
<div className={styles.Header}>Profiling is in progress...</div>
<div className={styles.Row}>
+60 -38
View File
@@ -16,7 +16,7 @@ import {
import { StoreContext } from '../context';
import Store from '../../store';
import type { ImportedProfilingData } from './types';
import type { ProfilingDataFrontend } from './types';
export type TabID = 'flame-chart' | 'ranked-chart' | 'interactions';
@@ -31,16 +31,18 @@ type Context = {|
// or from the backend itself (after a reload-and-profile action).
// It is synced between the backend and frontend via a Store subscription.
hasProfilingData: boolean,
isProcessingData: boolean,
isProfiling: boolean,
profilingData: ProfilingDataFrontend | null,
startProfiling(value: boolean): void,
stopProfiling(value: boolean): void,
// Which renderer and root should profiling data be shown for?
// Often this will correspond to the selected renderer and root in the Elements panel.
// If nothing is selected though, this will default to the first root.
rendererID: number | null,
// Which root should profiling data be shown for?
// This value should be initialized to either:
// 1. The selected root in the Components tree (if it has any profiling data) or
// 2. The first root in the list with profiling data.
rootID: number | null,
rootHasProfilingData: boolean,
setRootID: (id: number) => void,
// Controls whether commits are filtered by duration.
// This value is controlled by a filter toggle UI in the Profiler toolbar.
@@ -72,8 +74,9 @@ ProfilerContext.displayName = 'ProfilerContext';
type StoreProfilingState = {|
hasProfilingData: boolean,
importedProfilingData: ImportedProfilingData | null,
isProcessingData: boolean,
isProfiling: boolean,
profilingData: ProfilingDataFrontend | null,
|};
type Props = {|
@@ -85,49 +88,66 @@ function ProfilerContextController({ children }: Props) {
const { selectedElementID } = useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const { profilerStore } = store;
const subscription = useMemo(
() => ({
getCurrentValue: () => ({
hasProfilingData: store.hasProfilingData,
importedProfilingData: store.importedProfilingData,
isProfiling: store.isProfiling,
hasProfilingData: profilerStore.hasProfilingData,
isProcessingData: profilerStore.isProcessingData,
isProfiling: profilerStore.isProfiling,
profilingData: profilerStore.profilingData,
}),
subscribe: (callback: Function) => {
store.addListener('importedProfilingData', callback);
store.addListener('isProfiling', callback);
profilerStore.addListener('profilingData', callback);
profilerStore.addListener('isProcessingData', callback);
profilerStore.addListener('isProfiling', callback);
return () => {
store.removeListener('importedProfilingData', callback);
store.removeListener('isProfiling', callback);
profilerStore.removeListener('profilingData', callback);
profilerStore.removeListener('isProcessingData', callback);
profilerStore.removeListener('isProfiling', callback);
};
},
}),
[store]
[profilerStore]
);
const {
isProfiling,
hasProfilingData,
importedProfilingData,
isProcessingData,
isProfiling,
profilingData,
} = useSubscription<StoreProfilingState, Store>(subscription);
let rendererID = null;
let rootID = null;
let rootHasProfilingData = false;
if (importedProfilingData !== null) {
rootHasProfilingData = true;
} else if (selectedElementID !== null) {
rendererID = store.getRendererIDForElement(selectedElementID);
rootID = store.getRootIDForElement(selectedElementID);
rootHasProfilingData =
rootID === null ? false : store.profilingOperations.has(rootID);
} else if (store.roots.length > 0) {
// If no root is selected, assume the first root; many React apps are single root anyway.
rootID = store.roots[0];
rootHasProfilingData = store.profilingOperations.has(rootID);
rendererID = store.getRendererIDForElement(rootID);
const [rootID, setRootID] = useState<number | null>(null);
const dataForRoots =
profilingData !== null ? profilingData.dataForRoots : null;
if (dataForRoots != null) {
const firstRootID = dataForRoots.keys().next().value || null;
if (rootID === null || !dataForRoots.has(rootID)) {
let selectedElementRootID = null;
if (selectedElementID !== null) {
selectedElementRootID = store.getRootIDForElement(selectedElementID);
}
if (
selectedElementRootID !== null &&
dataForRoots.has(selectedElementRootID)
) {
setRootID(selectedElementRootID);
} else {
setRootID(firstRootID);
}
}
}
const startProfiling = useCallback(() => store.startProfiling(), [store]);
const stopProfiling = useCallback(() => store.stopProfiling(), [store]);
const startProfiling = useCallback(
() => store.profilerStore.startProfiling(),
[store]
);
const stopProfiling = useCallback(() => store.profilerStore.stopProfiling(), [
store,
]);
const [
isCommitFilterEnabled,
@@ -187,13 +207,14 @@ function ProfilerContextController({ children }: Props) {
selectTab,
hasProfilingData,
isProcessingData,
isProfiling,
profilingData,
startProfiling,
stopProfiling,
rendererID,
rootID,
rootHasProfilingData,
setRootID,
isCommitFilterEnabled,
setIsCommitFilterEnabled,
@@ -215,13 +236,14 @@ function ProfilerContextController({ children }: Props) {
selectTab,
hasProfilingData,
isProcessingData,
isProfiling,
profilingData,
startProfiling,
stopProfiling,
rendererID,
rootID,
rootHasProfilingData,
setRootID,
isCommitFilterEnabled,
setIsCommitFilterEnabled,
@@ -5,44 +5,52 @@ import { ProfilerContext } from './ProfilerContext';
import { ModalDialogContext } from '../ModalDialog';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import { BridgeContext, StoreContext } from '../context';
import { StoreContext } from '../context';
import {
prepareExportedProfilingSummary,
prepareImportedProfilingData,
prepareProfilingDataExport,
prepareProfilingDataFrontendFromExport,
} from './utils';
import { downloadFile } from '../utils';
import styles from './ProfilingImportExportButtons.css';
import type { ProfilingDataExport } from './types';
export default function ProfilingImportExportButtons() {
const bridge = useContext(BridgeContext);
const { isProfiling, rendererID, rootHasProfilingData, rootID } = useContext(
ProfilerContext
);
const { isProfiling, profilingData, rootID } = useContext(ProfilerContext);
const store = useContext(StoreContext);
const { profilerStore } = store;
const inputRef = useRef<HTMLInputElement | null>(null);
const { dispatch: modalDialogDispatch } = useContext(ModalDialogContext);
const downloadData = useCallback(() => {
if (rendererID === null || rootID === null) {
if (rootID === null) {
return;
}
const exportedProfilingSummary = prepareExportedProfilingSummary(
store.profilingOperations,
store.profilingSnapshots,
rootID,
rendererID
);
bridge.send('exportProfilingSummary', exportedProfilingSummary);
}, [
bridge,
rendererID,
rootID,
store.profilingOperations,
store.profilingSnapshots,
]);
if (profilingData !== null) {
const profilingDataExport = prepareProfilingDataExport(profilingData);
const date = new Date();
const dateString = date
.toLocaleDateString(undefined, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
.replace(/\//g, '-');
const timeString = date
.toLocaleTimeString(undefined, {
hour12: false,
})
.replace(/:/g, '-');
downloadFile(
`profiling-data.${dateString}.${timeString}.json`,
JSON.stringify(profilingDataExport, null, 2)
);
}
}, [rootID, profilingData]);
const uploadData = useCallback(() => {
if (inputRef.current !== null) {
@@ -57,7 +65,12 @@ export default function ProfilingImportExportButtons() {
fileReader.addEventListener('load', () => {
try {
const raw = ((fileReader.result: any): string);
store.importedProfilingData = prepareImportedProfilingData(raw);
const profilingDataExport = ((JSON.parse(
raw
): any): ProfilingDataExport);
profilerStore.profilingData = prepareProfilingDataFrontendFromExport(
profilingDataExport
);
} catch (error) {
modalDialogDispatch({
type: 'SHOW',
@@ -76,7 +89,7 @@ export default function ProfilingImportExportButtons() {
// TODO (profiling) Handle fileReader errors.
fileReader.readAsText(input.files[0]);
}
}, [modalDialogDispatch, store]);
}, [modalDialogDispatch, profilerStore]);
return (
<Fragment>
@@ -95,15 +108,13 @@ export default function ProfilingImportExportButtons() {
>
<ButtonIcon type="import" />
</Button>
{store.supportsFileDownloads && (
<Button
disabled={isProfiling || !rootHasProfilingData}
onClick={downloadData}
title="Save profile..."
>
<ButtonIcon type="export" />
</Button>
)}
<Button
disabled={isProfiling || !profilerStore.hasProfilingData}
onClick={downloadData}
title="Save profile..."
>
<ButtonIcon type="export" />
</Button>
</Fragment>
);
}
@@ -2,8 +2,9 @@
import { ElementTypeForwardRef, ElementTypeMemo } from 'src/types';
import { formatDuration } from './utils';
import ProfilerStore from 'src/devtools/ProfilerStore';
import type { CommitDetailsFrontend, CommitTreeFrontend } from './types';
import type { CommitTree } from './types';
export type ChartNode = {|
id: number,
@@ -20,15 +21,19 @@ export type ChartData = {|
const cachedChartData: Map<string, ChartData> = new Map();
export function getChartData({
commitDetails,
commitIndex,
commitTree,
profilerStore,
rootID,
}: {|
commitDetails: CommitDetailsFrontend,
commitIndex: number,
commitTree: CommitTreeFrontend,
commitTree: CommitTree,
profilerStore: ProfilerStore,
rootID: number,
|}): ChartData {
const { actualDurations, rootID, selfDurations } = commitDetails;
const commitDatum = profilerStore.getCommitData(rootID, commitIndex);
const { fiberActualDurations, fiberSelfDurations } = commitDatum;
const { nodes } = commitTree;
const key = `${rootID}-${commitIndex}`;
@@ -39,7 +44,7 @@ export function getChartData({
let maxSelfDuration = 0;
const chartNodes: Array<ChartNode> = [];
actualDurations.forEach((actualDuration, id) => {
fiberActualDurations.forEach((actualDuration, id) => {
const node = nodes.get(id);
if (node == null) {
@@ -52,7 +57,7 @@ export function getChartData({
if (parentID === 0) {
return;
}
const selfDuration = selfDurations.get(id) || 0;
const selfDuration = fiberSelfDurations.get(id) || 0;
maxSelfDuration = Math.max(maxSelfDuration, selfDuration);
const name = displayName || 'Anonymous';
@@ -0,0 +1,3 @@
.Spacer {
flex: 1;
}
@@ -0,0 +1,42 @@
// @flow
import React, { Fragment, useCallback, useContext } from 'react';
import { ProfilerContext } from './ProfilerContext';
import styles from './RootSelector.css';
export default function RootSelector(_: {||}) {
const { profilingData, rootID, setRootID } = useContext(ProfilerContext);
const options = [];
if (profilingData !== null) {
profilingData.dataForRoots.forEach((dataForRoot, rootID) => {
options.push(
<option key={rootID} value={rootID}>
{dataForRoot.displayName}
</option>
);
});
}
const handleChange = useCallback(
({ currentTarget }) => {
setRootID(parseInt(currentTarget.value, 10));
},
[setRootID]
);
if (profilingData === null || profilingData.dataForRoots.size <= 1) {
// Don't take up visual space if there's only one root.
return null;
}
return (
<Fragment>
<div className={styles.Spacer} />
<select value={rootID} onChange={handleChange}>
{options}
</select>
</Fragment>
);
}
@@ -12,24 +12,12 @@ export type Props = {||};
export default function SidebarCommitInfo(_: Props) {
const {
selectedCommitIndex,
rendererID,
rootID,
selectInteraction,
selectTab,
} = useContext(ProfilerContext);
const {
captureScreenshots,
profilingCache,
profilingScreenshots,
} = useContext(StoreContext);
const screenshotsByCommitIndex =
rootID !== null ? profilingScreenshots.get(rootID) : null;
const screenshot =
screenshotsByCommitIndex != null && selectedCommitIndex !== null
? screenshotsByCommitIndex.get(selectedCommitIndex)
: null;
const { captureScreenshots, profilerStore } = useContext(StoreContext);
const [
isScreenshotModalVisible,
@@ -45,26 +33,22 @@ export default function SidebarCommitInfo(_: Props) {
[]
);
if (selectedCommitIndex === null) {
if (rootID === null || selectedCommitIndex === null) {
return <div className={styles.NothingSelected}>Nothing selected</div>;
}
const { commitDurations, commitTimes } = profilingCache.ProfilingSummary.read(
{
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
}
);
const { interactions } = profilerStore.getDataForRoot(rootID);
const {
duration,
interactionIDs,
priorityLevel,
screenshot,
timestamp,
} = profilerStore.getCommitData(rootID, selectedCommitIndex);
const { interactions, priorityLevel } = profilingCache.CommitDetails.read({
commitIndex: selectedCommitIndex,
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
});
const viewInteraction = interaction => {
const viewInteraction = interactionID => {
selectTab('interactions');
selectInteraction(interaction.id);
selectInteraction(interactionID);
};
return (
@@ -80,34 +64,33 @@ export default function SidebarCommitInfo(_: Props) {
)}
<li className={styles.ListItem}>
<label className={styles.Label}>Committed at</label>:{' '}
<span className={styles.Value}>
{formatTime(commitTimes[((selectedCommitIndex: any): number)])}s
</span>
<span className={styles.Value}>{formatTime(timestamp)}s</span>
</li>
<li className={styles.ListItem}>
<label className={styles.Label}>Render duration</label>:{' '}
<span className={styles.Value}>
{formatDuration(
commitDurations[((selectedCommitIndex: any): number)]
)}
ms
</span>
<span className={styles.Value}>{formatDuration(duration)}ms</span>
</li>
<li className={styles.Interactions}>
<label className={styles.Label}>Interactions</label>:
<div className={styles.InteractionList}>
{interactions.length === 0 ? (
{interactionIDs.length === 0 ? (
<div className={styles.NoInteractions}>None</div>
) : null}
{interactions.map((interaction, index) => (
<button
key={index}
className={styles.Interaction}
onClick={() => viewInteraction(interaction)}
>
{interaction.name}
</button>
))}
{interactionIDs.map(interactionID => {
const interaction = interactions.get(interactionID);
if (interaction == null) {
throw Error(`Invalid interaction "${interactionID}"`);
}
return (
<button
key={interactionID}
className={styles.Interaction}
onClick={() => viewInteraction(interactionID)}
>
{interaction.name}
</button>
);
})}
</div>
</li>
{captureScreenshots && (
@@ -13,48 +13,70 @@ export type Props = {||};
export default function SidebarInteractions(_: Props) {
const {
selectedInteractionID,
rendererID,
rootID,
selectCommitIndex,
selectTab,
} = useContext(ProfilerContext);
const { profilingCache } = useContext(StoreContext);
const { profilerStore } = useContext(StoreContext);
const { profilingCache } = profilerStore;
if (selectedInteractionID === null) {
return <div className={styles.NothingSelected}>Nothing selected</div>;
}
const { interactions } = profilingCache.Interactions.read({
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
});
const interaction = interactions.find(
interaction => interaction.id === selectedInteractionID
const { interactionCommits, interactions } = profilerStore.getDataForRoot(
((rootID: any): number)
);
const interaction = interactions.get(selectedInteractionID);
if (interaction == null) {
throw Error(
`Could not find interaction by selected interaction id "${selectedInteractionID}"`
);
}
const profilingSummary = profilingCache.ProfilingSummary.read({
rendererID: ((rendererID: any): number),
const { maxCommitDuration } = profilingCache.getInteractionsChartData({
rootID: ((rootID: any): number),
});
const { maxCommitDuration } = profilingCache.getInteractionsChartData({
interactions,
profilingSummary,
});
const { commitDurations, commitTimes } = profilingSummary;
const viewCommit = (commitIndex: number) => {
selectTab('flame-chart');
selectCommitIndex(commitIndex);
};
const listItems: Array<React$Node> = [];
const commitIndices = interactionCommits.get(selectedInteractionID);
if (commitIndices != null) {
commitIndices.forEach(commitIndex => {
const { duration, timestamp } = profilerStore.getCommitData(
((rootID: any): number),
commitIndex
);
listItems.push(
<li
key={commitIndex}
className={styles.ListItem}
onClick={() => viewCommit(commitIndex)}
>
<div
className={styles.CommitBox}
style={{
backgroundColor: getGradientColor(
Math.min(1, Math.max(0, duration / maxCommitDuration)) || 0
),
}}
/>
<div>
timestamp: {formatTime(timestamp)}s
<br />
duration: {formatDuration(duration)}ms
</div>
</li>
);
});
}
return (
<Fragment>
<div className={styles.Toolbar}>
@@ -62,35 +84,7 @@ export default function SidebarInteractions(_: Props) {
</div>
<div className={styles.Content}>
<div className={styles.Commits}>Commits:</div>
<ul className={styles.List}>
{interaction.commits.map(commitIndex => (
<li
key={commitIndex}
className={styles.ListItem}
onClick={() => viewCommit(commitIndex)}
>
<div
className={styles.CommitBox}
style={{
backgroundColor: getGradientColor(
Math.min(
1,
Math.max(
0,
commitDurations[commitIndex] / maxCommitDuration
)
) || 0
),
}}
/>
<div>
timestamp: {formatTime(commitTimes[commitIndex])}s
<br />
duration: {formatDuration(commitDurations[commitIndex])}ms
</div>
</li>
))}
</ul>
<ul className={styles.List}>{listItems}</ul>
</div>
</Fragment>
);
@@ -12,9 +12,8 @@ import styles from './SidebarSelectedFiberInfo.css';
export type Props = {||};
export default function SidebarSelectedFiberInfo(_: Props) {
const { profilingCache } = useContext(StoreContext);
const { profilerStore } = useContext(StoreContext);
const {
rendererID,
rootID,
selectCommitIndex,
selectedCommitIndex,
@@ -22,23 +21,21 @@ export default function SidebarSelectedFiberInfo(_: Props) {
selectedFiberName,
selectFiber,
} = useContext(ProfilerContext);
const { profilingCache } = profilerStore;
const { commitTimes } = profilingCache.ProfilingSummary.read({
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
});
const { commitDurations } = profilingCache.FiberCommits.read({
const commitIndices = profilingCache.getFiberCommits({
fiberID: ((selectedFiberID: any): number),
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
});
const listItems = [];
for (let i = 0; i < commitDurations.length; i += 2) {
const commitIndex = commitDurations[i];
const duration = commitDurations[i + 1];
const time = commitTimes[commitIndex];
for (let i = 0; i < commitIndices.length; i += 2) {
const commitIndex = commitIndices[i];
const { duration, timestamp } = profilerStore.getCommitData(
((rootID: any): number),
commitIndex
);
listItems.push(
<button
@@ -50,7 +47,7 @@ export default function SidebarSelectedFiberInfo(_: Props) {
}
onClick={() => selectCommitIndex(commitIndex)}
>
{formatTime(time)}s for {formatDuration(duration)}ms
{formatTime(timestamp)}s for {formatDuration(duration)}ms
</button>
);
}
+16 -12
View File
@@ -16,29 +16,33 @@ export default function SnapshotSelector(_: Props) {
const {
isCommitFilterEnabled,
minCommitDuration,
rendererID,
rootID,
selectedCommitIndex,
selectCommitIndex,
} = useContext(ProfilerContext);
const { profilingCache } = useContext(StoreContext);
const { commitDurations, commitTimes } = profilingCache.ProfilingSummary.read(
{
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
}
);
const { profilerStore } = useContext(StoreContext);
const { commitData } = profilerStore.getDataForRoot(((rootID: any): number));
const commitDurations: Array<number> = [];
const commitTimes: Array<number> = [];
commitData.forEach(commitDatum => {
commitDurations.push(commitDatum.duration);
commitTimes.push(commitDatum.timestamp);
});
const filteredCommitIndices = useMemo(
() =>
commitDurations.reduce((reduced, commitDuration, index) => {
if (!isCommitFilterEnabled || commitDuration >= minCommitDuration) {
commitData.reduce((reduced, commitDatum, index) => {
if (
!isCommitFilterEnabled ||
commitDatum.duration >= minCommitDuration
) {
reduced.push(index);
}
return reduced;
}, []),
[commitDurations, isCommitFilterEnabled, minCommitDuration]
[commitData, isCommitFilterEnabled, minCommitDuration]
);
const numFilteredCommits = filteredCommitIndices.length;
@@ -112,7 +116,7 @@ export default function SnapshotSelector(_: Props) {
[viewNextCommit, viewPrevCommit]
);
if (commitDurations.length === 0) {
if (commitData.length === 0) {
return null;
}
+89 -75
View File
@@ -1,13 +1,8 @@
// @flow
import type { ElementType } from 'src/types';
import type {
CommitDetailsBackend,
InteractionsBackend,
ProfilingSummaryBackend,
} from 'src/backend/types';
export type CommitTreeNodeFrontend = {|
export type CommitTreeNode = {|
id: number,
children: Array<number>,
displayName: string | null,
@@ -17,58 +12,18 @@ export type CommitTreeNodeFrontend = {|
type: ElementType,
|};
export type CommitTreeFrontend = {|
nodes: Map<number, CommitTreeNodeFrontend>,
export type CommitTree = {|
nodes: Map<number, CommitTreeNode>,
rootID: number,
|};
export type InteractionFrontend = {|
export type Interaction = {|
id: number,
name: string,
timestamp: number,
|};
export type InteractionWithCommitsFrontend = {|
...InteractionFrontend,
commits: Array<number>,
|};
export type InteractionsFrontend = {|
interactions: Array<InteractionWithCommitsFrontend>,
rootID: number,
|};
export type CommitDetailsFrontend = {|
actualDurations: Map<number, number>,
commitIndex: number,
interactions: Array<InteractionFrontend>,
priorityLevel: string | null,
rootID: number,
selfDurations: Map<number, number>,
|};
export type FiberCommitsFrontend = {|
commitDurations: Array<number>,
fiberID: number,
rootID: number,
|};
export type ProfilingSummaryFrontend = {|
rootID: number,
// Commit durations
commitDurations: Array<number>,
// Commit times (relative to when profiling started)
commitTimes: Array<number>,
// Map of fiber id to (initial) tree base duration
initialTreeBaseDurations: Map<number, number>,
interactionCount: number,
|};
export type ProfilingSnapshotNode = {|
export type SnapshotNode = {|
id: number,
children: Array<number>,
displayName: string | null,
@@ -76,35 +31,94 @@ export type ProfilingSnapshotNode = {|
type: ElementType,
|};
export type ImportedProfilingData = {|
version: 3,
profilingOperations: Map<number, Array<Uint32Array>>,
profilingSnapshots: Map<number, Map<number, ProfilingSnapshotNode>>,
commitDetails: Array<CommitDetailsFrontend>,
interactions: InteractionsFrontend,
profilingSummary: ProfilingSummaryFrontend,
export type CommitDataFrontend = {|
// How long was this commit?
duration: number,
// Map of Fiber (ID) to actual duration for this commit;
// Fibers that did not render will not have entries in this Map.
fiberActualDurations: Map<number, number>,
// Map of Fiber (ID) to "self duration" for this commit;
// Fibers that did not render will not have entries in this Map.
fiberSelfDurations: Map<number, number>,
// Which interactions (IDs) were associated with this commit.
interactionIDs: Array<number>,
// Priority level of the commit (if React provided this info)
priorityLevel: string | null,
// Screenshot data for this commit (if available).
screenshot: string | null,
// When did this commit occur (relative to the start of profiling)
timestamp: number,
|};
export type SerializableProfilingDataOperationsByRootID = Array<
[number, Array<Array<number>>]
>;
export type SerializableProfilingDataSnapshotsByRootID = Array<
[number, Array<[number, ProfilingSnapshotNode]>]
>;
export type ProfilingDataForRootFrontend = {|
// Timing, duration, and other metadata about each commit.
commitData: Array<CommitDataFrontend>,
export type ExportedProfilingSummaryFromFrontend = {|
version: 3,
profilingOperationsByRootID: SerializableProfilingDataOperationsByRootID,
profilingSnapshotsByRootID: SerializableProfilingDataSnapshotsByRootID,
rendererID: number,
// Display name of the nearest descendant component (ideally a function or class component).
// This value is used by the root selector UI.
displayName: string,
// Map of fiber id to (initial) tree base duration when Profiling session was started.
// This info can be used along with commitOperations to reconstruct the tree for any commit.
initialTreeBaseDurations: Map<number, number>,
// All interactions recorded (for this root) during the current session.
interactionCommits: Map<number, Array<number>>,
// All interactions recorded (for this root) during the current session.
interactions: Map<number, Interaction>,
// List of tree mutation that occur during profiling.
// These mutations can be used along with initial snapshots to reconstruct the tree for any commit.
operations: Array<Uint32Array>,
// Identifies the root this profiler data corresponds to.
rootID: number,
// Map of fiber id to node when the Profiling session was started.
// This info can be used along with commitOperations to reconstruct the tree for any commit.
snapshots: Map<number, SnapshotNode>,
|};
export type ExportedProfilingData = {|
version: 3,
profilingOperationsByRootID: SerializableProfilingDataOperationsByRootID,
profilingSnapshotsByRootID: SerializableProfilingDataSnapshotsByRootID,
commitDetails: Array<CommitDetailsBackend>,
interactions: InteractionsBackend,
profilingSummary: ProfilingSummaryBackend,
// Combination of profiling data collected by the renderer interface (backend) and Store (frontend).
export type ProfilingDataFrontend = {|
// Profiling data per root.
dataForRoots: Map<number, ProfilingDataForRootFrontend>,
|};
export type CommitDataExport = {|
duration: number,
// Tuple of fiber ID and actual duration
fiberActualDurations: Array<[number, number]>,
// Tuple of fiber ID and computed "self" duration
fiberSelfDurations: Array<[number, number]>,
interactionIDs: Array<number>,
priorityLevel: string | null,
screenshot: string | null,
timestamp: number,
|};
export type ProfilingDataForRootExport = {|
commitData: Array<CommitDataExport>,
displayName: string,
// Tuple of Fiber ID and base duration
initialTreeBaseDurations: Array<[number, number]>,
// Tuple of Interaction ID and commit indices
interactionCommits: Array<[number, Array<number>]>,
interactions: Array<[number, Interaction]>,
operations: Array<Array<number>>,
rootID: number,
snapshots: Array<[number, SnapshotNode]>,
|};
// Serializable vefrsion of ProfilingDataFrontend data.
export type ProfilingDataExport = {|
version: 4,
dataForRoots: Array<ProfilingDataForRootExport>,
|};
+171 -148
View File
@@ -2,15 +2,15 @@
import { PROFILER_EXPORT_VERSION } from 'src/constants';
import type { ProfilingDataBackend } from 'src/backend/types';
import type {
ExportedProfilingSummaryFromFrontend,
ExportedProfilingData,
ImportedProfilingData,
ProfilingSnapshotNode,
ProfilingDataExport,
ProfilingDataForRootExport,
ProfilingDataForRootFrontend,
ProfilingDataFrontend,
SnapshotNode,
} from './types';
import type { ExportedProfilingDataFromRenderer } from 'src/backend/types';
const commitGradient = [
'var(--color-commit-gradient-0)',
'var(--color-commit-gradient-1)',
@@ -24,153 +24,176 @@ const commitGradient = [
'var(--color-commit-gradient-9)',
];
export const prepareExportedProfilingSummary = (
profilingOperations: Map<number, Array<Uint32Array>>,
profilingSnapshots: Map<number, Map<number, ProfilingSnapshotNode>>,
rendererID: number,
rootID: number
) => {
const profilingOperationsForRoot = [];
const operations = profilingOperations.get(rootID);
if (operations != null) {
operations.forEach(operationsTypedArray => {
// Convert typed array to plain array before JSON serialization, or it will be converted to an Object.
const operationsPlainArray = Array.from(operationsTypedArray);
profilingOperationsForRoot.push(operationsPlainArray);
});
}
// Combines info from the Store (frontend) and renderer interfaces (backend) into the format required by the Profiler UI.
// This format can then be quickly exported (and re-imported).
export function prepareProfilingDataFrontendFromBackendAndStore(
dataBackends: Array<ProfilingDataBackend>,
operationsByRootID: Map<number, Array<Uint32Array>>,
screenshotsByRootID: Map<number, Map<number, string>>,
snapshotsByRootID: Map<number, Map<number, SnapshotNode>>
): ProfilingDataFrontend {
const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map();
// Convert Map to Array of key-value pairs or JSON.stringify will clobber the contents.
const profilingSnapshotsForRoot = [];
const profilingSnapshotsMap = profilingSnapshots.get(rootID);
if (profilingSnapshotsMap != null) {
for (const [elementID, snapshotNode] of profilingSnapshotsMap.entries()) {
profilingSnapshotsForRoot.push([elementID, snapshotNode]);
}
}
dataBackends.forEach(dataBackend => {
dataBackend.dataForRoots.forEach(
({
commitData,
displayName,
initialTreeBaseDurations,
interactionCommits,
interactions,
rootID,
}) => {
const screenshots = screenshotsByRootID.get(rootID) || null;
const exportedProfilingSummary: ExportedProfilingSummaryFromFrontend = {
version: PROFILER_EXPORT_VERSION,
profilingOperationsByRootID: [[rootID, profilingOperationsForRoot]],
profilingSnapshotsByRootID: [[rootID, profilingSnapshotsForRoot]],
rendererID,
rootID,
};
return exportedProfilingSummary;
};
export const prepareExportedProfilingData = (
exportedProfilingDataFromRenderer: ExportedProfilingDataFromRenderer,
exportedProfilingSummary: ExportedProfilingSummaryFromFrontend
): ExportedProfilingData => {
if (exportedProfilingDataFromRenderer.version !== PROFILER_EXPORT_VERSION) {
throw new Error(
`Unsupported profiling data version ${
exportedProfilingDataFromRenderer.version
} from renderer with id "${exportedProfilingSummary.rendererID}"`
);
}
if (exportedProfilingSummary.version !== PROFILER_EXPORT_VERSION) {
throw new Error(
`Unsupported profiling summary version ${
exportedProfilingSummary.version
} from renderer with id "${exportedProfilingSummary.rendererID}"`
);
}
const exportedProfilingData: ExportedProfilingData = {
version: PROFILER_EXPORT_VERSION,
profilingSummary: exportedProfilingDataFromRenderer.profilingSummary,
commitDetails: exportedProfilingDataFromRenderer.commitDetails,
interactions: exportedProfilingDataFromRenderer.interactions,
profilingOperationsByRootID:
exportedProfilingSummary.profilingOperationsByRootID,
profilingSnapshotsByRootID:
exportedProfilingSummary.profilingSnapshotsByRootID,
};
return exportedProfilingData;
};
/**
* This function should mirror `prepareExportedProfilingData` and `prepareExportedProfilingSummary`.
*/
export const prepareImportedProfilingData = (
exportedProfilingDataJsonString: string
) => {
const parsed = JSON.parse(exportedProfilingDataJsonString);
if (parsed.version !== PROFILER_EXPORT_VERSION) {
throw Error(`Unsupported profiler export version "${parsed.version}".`);
}
// Some "exported" types in `parsed` are `...Backend`, see `prepareExportedProfilingData`,
// they come to `ExportedProfilingData` from `ExportedProfilingDataFromRenderer`.
// But the "imported" types in `ImportedProfilingData` are `...Frontend`,
// and some of them aren't exactly the same as `...Backend` (i.e. an interleaved array versus a map).
// The type annotations here help us to spot the incompatibilities and properly convert.
const exportedProfilingData: ExportedProfilingData = parsed;
const profilingSummaryExported = exportedProfilingData.profilingSummary;
const initialTreeBaseDurations =
profilingSummaryExported.initialTreeBaseDurations;
const initialTreeBaseDurationsMap = new Map();
for (let i = 0; i < initialTreeBaseDurations.length; i += 2) {
const fiberID = initialTreeBaseDurations[i];
const initialTreeBaseDuration = initialTreeBaseDurations[i + 1];
initialTreeBaseDurationsMap.set(fiberID, initialTreeBaseDuration);
}
const importedProfilingData: ImportedProfilingData = {
version: parsed.version,
profilingOperations: new Map(
exportedProfilingData.profilingOperationsByRootID.map(
([rootID, profilingOperationsForRoot]) => [
rootID,
profilingOperationsForRoot.map(operations =>
Uint32Array.from(operations)
),
]
)
),
profilingSnapshots: new Map(
exportedProfilingData.profilingSnapshotsByRootID.map(
([rootID, profilingSnapshotsForRoot]) => [
rootID,
new Map(profilingSnapshotsForRoot),
]
)
),
commitDetails: exportedProfilingData.commitDetails.map(
commitDetailsBackendItem => {
const durations = commitDetailsBackendItem.durations;
const actualDurationsMap = new Map<number, number>();
const selfDurationsMap = new Map<number, number>();
for (let i = 0; i < durations.length; i += 3) {
const fiberID = durations[i];
actualDurationsMap.set(fiberID, durations[i + 1]);
selfDurationsMap.set(fiberID, durations[i + 2]);
const operations = operationsByRootID.get(rootID);
if (operations == null) {
throw Error(`Could not find profiling operations for root ${rootID}`);
}
return {
actualDurations: actualDurationsMap,
commitIndex: commitDetailsBackendItem.commitIndex,
interactions: commitDetailsBackendItem.interactions,
priorityLevel: commitDetailsBackendItem.priorityLevel,
rootID: commitDetailsBackendItem.rootID,
selfDurations: selfDurationsMap,
};
const snapshots = snapshotsByRootID.get(rootID);
if (snapshots == null) {
throw Error(`Could not find profiling snapshots for root ${rootID}`);
}
dataForRoots.set(rootID, {
commitData: commitData.map((commitDataBackend, commitIndex) => ({
duration: commitDataBackend.duration,
fiberActualDurations: new Map(
commitDataBackend.fiberActualDurations
),
fiberSelfDurations: new Map(commitDataBackend.fiberSelfDurations),
interactionIDs: commitDataBackend.interactionIDs,
priorityLevel: commitDataBackend.priorityLevel,
screenshot:
(screenshots !== null && screenshots.get(commitIndex)) || null,
timestamp: commitDataBackend.timestamp,
})),
displayName,
initialTreeBaseDurations: new Map(initialTreeBaseDurations),
interactionCommits: new Map(interactionCommits),
interactions: new Map(interactions),
operations,
rootID,
snapshots,
});
}
),
interactions: exportedProfilingData.interactions,
profilingSummary: {
rootID: profilingSummaryExported.rootID,
commitDurations: profilingSummaryExported.commitDurations,
commitTimes: profilingSummaryExported.commitTimes,
initialTreeBaseDurations: initialTreeBaseDurationsMap,
interactionCount: profilingSummaryExported.interactionCount,
},
);
});
return { dataForRoots };
}
// Converts a Profiling data export into the format required by the Store.
export function prepareProfilingDataFrontendFromExport(
profilingDataExport: ProfilingDataExport
): ProfilingDataFrontend {
const { version } = profilingDataExport;
if (version !== PROFILER_EXPORT_VERSION) {
throw Error(`Unsupported profiler export version "${version}"`);
}
const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map();
profilingDataExport.dataForRoots.forEach(
({
commitData,
displayName,
initialTreeBaseDurations,
interactionCommits,
interactions,
operations,
rootID,
snapshots,
}) => {
dataForRoots.set(rootID, {
commitData: commitData.map(
({
duration,
fiberActualDurations,
fiberSelfDurations,
interactionIDs,
priorityLevel,
screenshot,
timestamp,
}) => ({
duration,
fiberActualDurations: new Map(fiberActualDurations),
fiberSelfDurations: new Map(fiberSelfDurations),
interactionIDs,
priorityLevel,
screenshot,
timestamp,
})
),
displayName,
initialTreeBaseDurations: new Map(initialTreeBaseDurations),
interactionCommits: new Map(interactionCommits),
interactions: new Map(interactions),
operations: operations.map(array => Uint32Array.from(array)), // Convert Array back to Uint32Array
rootID,
snapshots: new Map(snapshots),
});
}
);
return { dataForRoots };
}
// Converts a Store Profiling data into a format that can be safely (JSON) serialized for export.
export function prepareProfilingDataExport(
profilingDataFrontend: ProfilingDataFrontend
): ProfilingDataExport {
const dataForRoots: Array<ProfilingDataForRootExport> = [];
profilingDataFrontend.dataForRoots.forEach(
({
commitData,
displayName,
initialTreeBaseDurations,
interactionCommits,
interactions,
operations,
rootID,
snapshots,
}) => {
dataForRoots.push({
commitData: commitData.map(
({
duration,
fiberActualDurations,
fiberSelfDurations,
interactionIDs,
priorityLevel,
screenshot,
timestamp,
}) => ({
duration,
fiberActualDurations: Array.from(fiberActualDurations.entries()),
fiberSelfDurations: Array.from(fiberSelfDurations.entries()),
interactionIDs,
priorityLevel,
screenshot,
timestamp,
})
),
displayName,
initialTreeBaseDurations: Array.from(
initialTreeBaseDurations.entries()
),
interactionCommits: Array.from(interactionCommits.entries()),
interactions: Array.from(interactions.entries()),
operations: operations.map(array => Array.from(array)), // Convert Uint32Array to Array for serialization
rootID,
snapshots: Array.from(snapshots.entries()),
});
}
);
return {
version: PROFILER_EXPORT_VERSION,
dataForRoots,
};
return importedProfilingData;
};
}
export const getGradientColor = (value: number) => {
const maxIndex = commitGradient.length - 1;
+1 -1
View File
@@ -22,7 +22,7 @@
--light-color-commit-did-not-render-fill: #cfd1d5;
--light-color-commit-did-not-render-fill-text: #000000;
--light-color-commit-did-not-render-pattern: #cfd1d5;
--light-color-commit-did-not-render-pattern-text: #000000;
--light-color-commit-did-not-render-pattern-text: #333333;
--light-color-commit-gradient-0: #37afa9;
--light-color-commit-gradient-1: #63b19e;
--light-color-commit-gradient-2: #80b393;
+24
View File
@@ -146,3 +146,27 @@ export function serializeHooksForCopy(hooks: HooksTree | null): string {
return '';
}
}
// Keeping this in memory seems to be enough to enable the browser to download larger profiles.
// Without this, we would see a "Download failed: network error" failure.
let downloadUrl = null;
export function downloadFile(filename: string, text: string): void {
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
if (downloadUrl !== null) {
URL.revokeObjectURL(downloadUrl);
}
downloadUrl = URL.createObjectURL(blob);
const element = document.createElement('a');
element.setAttribute('href', downloadUrl);
element.setAttribute('download', filename);
element.style.display = 'none';
((document.body: any): HTMLBodyElement).appendChild(element);
element.click();
((document.body: any): HTMLBodyElement).removeChild(element);
}