Massively overhauled profiling data architecture

This commit is contained in:
Brian Vaughn
2019-05-22 06:05:25 -07:00
parent 7ce9f4859c
commit c6de014a9a
33 changed files with 3276 additions and 3289 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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,
}

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.startProfiling());
utils.act(() => {
ReactDOM.render(<Parent key="A" count={4} />, containerA);
ReactDOM.render(<Parent key="B" count={1} />, containerB);
});
utils.act(() => store.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();
});
});

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();
});
});

View File

@@ -0,0 +1,465 @@
// @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', 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 profilingDataForRoot = null;
// TODO (profarc) Add multi roots
function Suspender({ previousProfilingDataForRoot, rootID }) {
profilingDataForRoot = store.profilerStore.getDataForRoot(rootID);
if (previousProfilingDataForRoot != null) {
expect(profilingDataForRoot).toEqual(previousProfilingDataForRoot);
} else {
expect(profilingDataForRoot).toMatchSnapshot('ProfilingSummary');
}
return null;
}
const rootID = store.roots[0];
await utils.actAsync(() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender previousProfilingDataForRoot={null} rootID={rootID} />
</React.Suspense>
)
);
expect(profilingDataForRoot).not.toBeNull();
utils.exportImportHelper(bridge, store, rootID);
await utils.actAsync(() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
previousProfilingDataForRoot={profilingDataForRoot}
rootID={rootID}
/>
</React.Suspense>
)
);
done();
});
it('should collect data 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 allCommitData = [];
function Suspender({ 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++) {
await utils.actAsync(() => {
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
previousCommitDetails={null}
rootID={rootID}
/>
</React.Suspense>
);
});
}
expect(allCommitData).toHaveLength(4);
utils.exportImportHelper(bridge, store, rootID);
for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
await utils.actAsync(() => {
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender
commitIndex={commitIndex}
previousCommitDetails={allCommitData[commitIndex]}
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 commitData = null;
function Suspender({ commitIndex, rootID }) {
commitData = store.profilerStore.getCommitData(rootID, commitIndex);
expect(commitData).toMatchSnapshot(
`CommitDetails with filtered self durations`
);
return null;
}
const rootID = store.roots[0];
await utils.actAsync(() => {
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender commitIndex={0} rootID={rootID} />
</React.Suspense>
);
});
expect(commitData).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 allCommitData = [];
function Suspender({ 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++) {
await utils.actAsync(() => {
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender commitIndex={commitIndex} rootID={rootID} />
</React.Suspense>
);
});
}
expect(allCommitData).toHaveLength(2);
done();
});
it('should collect data 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, rootID }) {
const fiberCommits = store.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++) {
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}
rootID={rootID}
/>
</React.Suspense>
);
});
}
expect(allFiberCommits).toHaveLength(store.numElements);
utils.exportImportHelper(bridge, store, 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]}
rootID={rootID}
/>
</React.Suspense>
);
});
}
done();
});
it('should report 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, rootID }) {
interactions = store.profilingCache.getInteractionsChartData({
rootID,
}).interactions;
if (previousInteractions != null) {
expect(interactions).toEqual(previousInteractions);
} else {
expect(interactions).toMatchSnapshot('Interactions');
}
return null;
}
const rootID = store.roots[0];
await utils.actAsync(() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender previousInteractions={null} rootID={rootID} />
</React.Suspense>
)
);
expect(interactions).not.toBeNull();
utils.exportImportHelper(bridge, store, rootID);
await utils.actAsync(() =>
TestRenderer.create(
<React.Suspense fallback={null}>
<Suspender previousInteractions={interactions} rootID={rootID} />
</React.Suspense>
)
);
done();
});
});

View File

@@ -62,59 +62,45 @@ describe('profiling charts', () => {
);
utils.act(() => store.stopProfiling());
let suspenseResolved = false;
let renderFinished = false;
function Suspender({ commitIndex, rendererID, rootID }) {
const profilingSummary = store.profilingCache.ProfilingSummary.read({
rendererID,
rootID,
});
const commitDetails = store.profilingCache.CommitDetails.read({
commitIndex,
rendererID,
rootID,
});
suspenseResolved = true;
function Suspender({ commitIndex, rootID }) {
const commitTree = store.profilingCache.getCommitTree({
commitIndex,
profilingSummary,
rootID,
});
const chartData = store.profilingCache.getFlamegraphChartData({
commitDetails,
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}
/>
<Suspender commitIndex={commitIndex} rootID={rootID} />
</React.Suspense>
),
3
);
expect(suspenseResolved).toBe(true);
expect(renderFinished).toBe(true);
}
expect(suspenseResolved).toBe(true);
expect(renderFinished).toBe(true);
done();
});
@@ -156,54 +142,40 @@ describe('profiling charts', () => {
);
utils.act(() => store.stopProfiling());
let suspenseResolved = false;
let renderFinished = false;
function Suspender({ commitIndex, rendererID, rootID }) {
const profilingSummary = store.profilingCache.ProfilingSummary.read({
rendererID,
rootID,
});
const commitDetails = store.profilingCache.CommitDetails.read({
commitIndex,
rendererID,
rootID,
});
suspenseResolved = true;
function Suspender({ commitIndex, rootID }) {
const commitTree = store.profilingCache.getCommitTree({
commitIndex,
profilingSummary,
rootID,
});
const chartData = store.profilingCache.getRankedChartData({
commitDetails,
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}
/>
<Suspender commitIndex={commitIndex} rootID={rootID} />
</React.Suspense>
),
3
);
expect(suspenseResolved).toBe(true);
expect(renderFinished).toBe(true);
}
done();
@@ -246,47 +218,33 @@ describe('profiling charts', () => {
);
utils.act(() => store.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;
function Suspender({ commitIndex, rootID }) {
const chartData = store.profilingCache.getInteractionsChartData({
interactions,
profilingSummary,
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}
/>
<Suspender commitIndex={commitIndex} rootID={rootID} />
</React.Suspense>
),
3
);
expect(suspenseResolved).toBe(true);
expect(renderFinished).toBe(true);
}
done();

View File

@@ -45,43 +45,34 @@ describe('commit tree', () => {
utils.act(() => ReactDOM.render(<Parent count={0} />, container));
utils.act(() => store.stopProfiling());
let suspenseResolved = false;
let renderFinished = false;
function Suspender({ commitIndex, rendererID, rootID }) {
const profilingSummary = store.profilingCache.ProfilingSummary.read({
rendererID,
rootID,
});
suspenseResolved = true;
function Suspender({ commitIndex, rootID }) {
const commitTree = store.profilingCache.getCommitTree({
commitIndex,
profilingSummary,
rootID,
});
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}
/>
<Suspender commitIndex={commitIndex} rootID={rootID} />
</React.Suspense>
),
3
);
expect(suspenseResolved).toBe(true);
expect(renderFinished).toBe(true);
}
done();

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"');
});
});

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');
@@ -136,54 +136,44 @@ export function requireTestRenderer(): ReactTestRenderer {
export function exportImportHelper(
bridge: Bridge,
store: Store,
rendererID: number,
rootID: number
): 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 profilingData = 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(
profilingData.profilingSnapshots.get(rootID)
);
expect(store.profilingOperations.get(rootID)).toEqual(
profilingData.profilingOperations.get(rootID)
);
expect(profilingDataFrontendInitial).toEqual(profilingDataFrontend);
// Snapshot the JSON-parsed object, rather than the raw string, because Jest formats the diff nicer.
expect(profilingData).toMatchSnapshot('imported data');
expect(serializedProfilingDataExport).toMatchSnapshot('imported data');
act(() => {
store.profilingData = profilingData;
// Apply the new exported-then-reimported data so tests can re-run assertions.
profilerStore.profilingData = profilingDataFrontend;
});
}

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();
};

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,

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,

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 = 5;

View File

@@ -0,0 +1,370 @@
// @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}"`);
}
get cache(): ProfilingCache {
return this._cache;
}
// Profiling data has been recorded for at least one root.
get hasProfilingData(): boolean {
return (
this._dataFrontend !== null && this._dataFrontend.dataForRoots.size > 0
);
}
// TODO (profarc) Remove this getter
get initialSnapshotsByRootID(): Map<number, Map<number, SnapshotNode>> {
return this._initialSnapshotsByRootID;
}
// TODO (profarc) Remove this getter
get inProgressOperationsByRootID(): Map<number, Array<Uint32Array>> {
return this._inProgressOperationsByRootID;
}
// TODO (profarc) Remove this getter
get inProgressScreenshotsByRootID(): Map<number, Map<number, string>> {
return this._inProgressScreenshotsByRootID;
}
get isProcessingData(): boolean {
return this._rendererQueue.size > 0 || this._dataBackends.length > 0;
}
get isProfiling(): boolean {
return this._isProfiling;
}
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();
// TODO (profarc) Remove subscriptions to Store for this
this._store.emit('profilingData');
this.emit('profilingData');
}
clear(): void {
this._dataBackends.splice(0);
this._dataFrontend = null;
this._initialSnapshotsByRootID.clear();
this._inProgressOperationsByRootID.clear();
this._inProgressScreenshotsByRootID.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();
// TODO (profarc) Remove subscriptions to Store for this
this._store.emit('isProfiling');
this.emit('isProfiling');
}
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);
// TODO (profarc) Remove subscriptions to Store for this
this._store.emit('isProcessingData');
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();
// TODO (profarc) Remove subscriptions to Store for this
this._store.emit('isProfiling');
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 });
}
}
// TODO (profarc) Remove subscriptions to Store for this
this._store.emit('isProcessingData');
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);
};
}

View File

@@ -1,7 +1,6 @@
// @flow
import { createResource } from './cache';
import Store from './store';
import ProfilerStore from './ProfilerStore';
import {
getCommitTree,
invalidateCommitTrees,
@@ -19,415 +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 profilingData = this._store.profilingData;
if (profilingData !== null) {
const commitDetailsByCommitIndex = profilingData.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 profilingData = this._store.profilingData;
if (profilingData !== null) {
const { commitDetails } = profilingData;
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 profilingData = this._store.profilingData;
if (profilingData !== null) {
const interactionsFrontend: InteractionsFrontend =
profilingData.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 profilingData = this._store.profilingData;
if (profilingData !== null) {
const profilingSummaryFrontend: ProfilingSummaryFrontend =
profilingData.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,
});
}
};
}

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,
@@ -19,11 +17,12 @@ import {
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,
ProfilingDataFrontend,
SnapshotNode,
} from './views/Profiler/types';
import type { Bridge, ComponentFilter, ElementType } from '../types';
@@ -43,8 +42,6 @@ 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,
@@ -80,37 +77,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 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;
// The user has imported a previously exported profiling session.
_profilingData: ImportedProfilingData | null = null;
// 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.
@@ -150,17 +121,16 @@ 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 =
@@ -180,15 +150,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 +161,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 +220,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,54 +242,65 @@ export default class Store extends EventEmitter {
return this._hasOwnerMetadata;
}
// Profiling data has been recorded for at least one root.
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get hasProfilingData(): boolean {
return (
this._profilingData !== null || this._profilingOperationsByRootID.size > 0
);
return this._profilerStore.hasProfilingData;
}
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get isProcessingProfilingData(): boolean {
return this._profilerStore.isProcessingData;
}
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get isProfiling(): boolean {
return this._isProfiling;
return this._profilerStore.isProfiling;
}
get numElements(): number {
return this._weightAcrossRoots;
}
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get profilingCache(): ProfilingCache {
return this._profilingCache;
return this._profilerStore.cache;
}
get profilingData(): ImportedProfilingData | null {
return this._profilingData;
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get profilingData(): ProfilingDataFrontend | null {
return this._profilerStore.profilingData;
}
set profilingData(value: ImportedProfilingData | null): void {
this._profilingData = value;
this._profilingOperationsByRootID = new Map();
this._profilingScreenshotsByRootID = new Map();
this._profilingSnapshotsByRootID = new Map();
this._profilingCache.invalidate();
this.emit('profilingData');
set profilingData(value: ProfilingDataFrontend | null): void {
this._profilerStore.profilingData = value;
}
get profilingOperations(): Map<number, Array<Uint32Array>> {
return this._profilingOperationsByRootID;
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get profilingOperationsByRootID(): Map<number, Array<Uint32Array>> {
return this._profilerStore.inProgressOperationsByRootID;
}
get profilingScreenshots(): Map<number, Map<number, string>> {
return this._profilingScreenshotsByRootID;
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get profilingScreenshotsByRootID(): Map<number, Map<number, string>> {
return this._profilerStore.inProgressScreenshotsByRootID;
}
get profilingSnapshots(): Map<number, Map<number, ProfilingSnapshotNode>> {
return this._profilingSnapshotsByRootID;
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get profilingSnapshotsByRootID(): Map<number, Map<number, SnapshotNode>> {
return this._profilerStore.initialSnapshotsByRootID;
}
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;
}
@@ -363,17 +321,9 @@ export default class Store extends EventEmitter {
return this._supportsReloadAndProfile;
}
// TODO (profarc) Update views to use ProfilerStore directly to access this method.
clearProfilingData(): void {
this._profilingData = 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');
this._profilerStore.clear();
}
containsElement(id: number): boolean {
@@ -601,22 +551,14 @@ export default class Store extends EventEmitter {
return false;
}
// TODO (profarc) Update views to use ProfilerStore directly to access this method.
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.
this._profilerStore.startProfiling();
}
// TODO (profarc) Update views to use ProfilerStore directly to access this method.
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.
this._profilerStore.stopProfiling();
}
// TODO Maybe split this into two methods: expand() and collapse()
@@ -701,34 +643,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
@@ -769,23 +683,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:
@@ -857,10 +756,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);
@@ -956,10 +851,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__) {
@@ -1065,60 +956,12 @@ export default class Store extends EventEmitter {
this.emit('mutated', [addedElementIDs, removedElementIDs]);
};
onProfilingStatus = (isProfiling: boolean) => {
if (isProfiling) {
this._profilingData = 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);
};
}

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,
@@ -26,7 +26,7 @@ export type ItemData = {|
export default function CommitFlamegraphAutoSizer(_: {||}) {
const { profilingCache } = useContext(StoreContext);
const { rendererID, rootID, selectedCommitIndex, selectFiber } = useContext(
const { rootID, selectedCommitIndex, selectFiber } = useContext(
ProfilerContext
);
@@ -38,39 +38,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 +62,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 +77,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>(() => {

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,
@@ -26,7 +26,7 @@ export type ItemData = {|
export default function CommitRankedAutoSizer(_: {||}) {
const { profilingCache } = useContext(StoreContext);
const { rendererID, rootID, selectedCommitIndex, selectFiber } = useContext(
const { rootID, selectedCommitIndex, selectFiber } = useContext(
ProfilerContext
);
@@ -38,47 +38,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 +75,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(

View File

@@ -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 { profilingData } = store;
const profilingOperations =
profilingData != null
? profilingData.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 { profilingData } = store;
const profilingSnapshots =
profilingData != null
? profilingData.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()');

View File

@@ -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;

View File

@@ -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),
}}
/>
))}

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,29 +38,21 @@ 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 { interactions } = profilingCache.Interactions.read({
rendererID: ((rendererID: any): number),
const dataForRoot = profilerStore.getDataForRoot(((rootID: any): number));
const chartData = profilerStore.cache.getInteractionsChartData({
rootID: ((rootID: any): number),
});
const profilingSummary = profilingCache.ProfilingSummary.read({
rendererID: ((rendererID: any): number),
rootID: ((rootID: any): number),
});
const chartData = profilingCache.getInteractionsChartData({
interactions,
profilingSummary,
});
const { interactions } = chartData;
const handleKeyDown = useCallback(
event => {
@@ -110,9 +98,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 +108,7 @@ function Interactions({ height, width }: {| height: number, width: number |}) {
};
}, [
chartData,
interactions,
profilingSummary,
dataForRoot,
selectedInteractionID,
selectCommitIndex,
selectInteraction,

View File

@@ -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);

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';
@@ -72,7 +72,7 @@ ProfilerContext.displayName = 'ProfilerContext';
type StoreProfilingState = {|
hasProfilingData: boolean,
profilingData: ImportedProfilingData | null,
profilingData: ProfilingDataFrontend | null,
isProfiling: boolean,
|};
@@ -117,11 +117,11 @@ function ProfilerContextController({ children }: Props) {
rendererID = store.getRendererIDForElement(selectedElementID);
rootID = store.getRootIDForElement(selectedElementID);
rootHasProfilingData =
rootID === null ? false : store.profilingOperations.has(rootID);
rootID === null ? false : store.profilingOperationsByRootID.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);
rootHasProfilingData = store.profilingOperationsByRootID.has(rootID);
rendererID = store.getRendererIDForElement(rootID);
}

View File

@@ -5,20 +5,20 @@ 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 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, rendererID, rootID } = useContext(ProfilerContext);
const store = useContext(StoreContext);
const { profilerStore } = store;
const inputRef = useRef<HTMLInputElement | null>(null);
@@ -29,20 +29,15 @@ export default function ProfilingImportExportButtons() {
return;
}
const exportedProfilingSummary = prepareExportedProfilingSummary(
store.profilingOperations,
store.profilingSnapshots,
rootID,
rendererID
);
bridge.send('exportProfilingSummary', exportedProfilingSummary);
}, [
bridge,
rendererID,
rootID,
store.profilingOperations,
store.profilingSnapshots,
]);
if (profilerStore.profilingData !== null) {
const profilingDataExport = prepareProfilingDataExport(
profilerStore.profilingData
);
// TODO (profarc) Generate anchor "download" tag and click it
console.log('profilingDataExport:', profilingDataExport);
}
}, [rendererID, rootID, profilerStore.profilingData]);
const uploadData = useCallback(() => {
if (inputRef.current !== null) {
@@ -57,7 +52,12 @@ export default function ProfilingImportExportButtons() {
fileReader.addEventListener('load', () => {
try {
const raw = ((fileReader.result: any): string);
store.profilingData = prepareImportedProfilingData(raw);
const profilingDataExport = ((JSON.parse(
raw
): any): ProfilingDataExport);
store.profilingData = prepareProfilingDataFrontendFromExport(
profilingDataExport
);
} catch (error) {
modalDialogDispatch({
type: 'SHOW',
@@ -97,7 +97,7 @@ export default function ProfilingImportExportButtons() {
</Button>
{store.supportsFileDownloads && (
<Button
disabled={isProfiling || !rootHasProfilingData}
disabled={isProfiling || !profilerStore.hasProfilingData}
onClick={downloadData}
title="Save profile..."
>

View File

@@ -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';

View File

@@ -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 && (

View File

@@ -13,48 +13,69 @@ export type Props = {||};
export default function SidebarInteractions(_: Props) {
const {
selectedInteractionID,
rendererID,
rootID,
selectCommitIndex,
selectTab,
} = useContext(ProfilerContext);
const { profilingCache } = useContext(StoreContext);
const { profilingCache, profilerStore } = useContext(StoreContext);
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 +83,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>
);

View File

@@ -12,9 +12,8 @@ import styles from './SidebarSelectedFiberInfo.css';
export type Props = {||};
export default function SidebarSelectedFiberInfo(_: Props) {
const { profilingCache } = useContext(StoreContext);
const { profilingCache, profilerStore } = useContext(StoreContext);
const {
rendererID,
rootID,
selectCommitIndex,
selectedCommitIndex,
@@ -23,22 +22,19 @@ export default function SidebarSelectedFiberInfo(_: Props) {
selectFiber,
} = useContext(ProfilerContext);
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 +46,7 @@ export default function SidebarSelectedFiberInfo(_: Props) {
}
onClick={() => selectCommitIndex(commitIndex)}
>
{formatTime(time)}s for {formatDuration(duration)}ms
{formatTime(timestamp)}s for {formatDuration(duration)}ms
</button>
);
}

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;
}

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: 5,
dataForRoots: Array<ProfilingDataForRootExport>,
|};

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 profilingData: 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 profilingData;
};
}
export const getGradientColor = (value: number) => {
const maxIndex = commitGradient.length - 1;