mirror of
https://github.com/facebook/react.git
synced 2026-02-23 20:23:02 +00:00
Massively overhauled profiling data architecture
This commit is contained in:
File diff suppressed because it is too large
Load Diff
1495
src/__tests__/__snapshots__/profilingCache-test.js.snap
Normal file
1495
src/__tests__/__snapshots__/profilingCache-test.js.snap
Normal file
File diff suppressed because it is too large
Load Diff
@@ -247,6 +247,20 @@ Object {
|
||||
|
||||
exports[`profiling charts interactions should contain valid data: Interactions 1`] = `
|
||||
Object {
|
||||
"interactions": Array [
|
||||
Object {
|
||||
"__count": 1,
|
||||
"id": 0,
|
||||
"name": "mount",
|
||||
"timestamp": 0,
|
||||
},
|
||||
Object {
|
||||
"__count": 0,
|
||||
"id": 1,
|
||||
"name": "update",
|
||||
"timestamp": 15,
|
||||
},
|
||||
],
|
||||
"lastInteractionTime": 25,
|
||||
"maxCommitDuration": 15,
|
||||
}
|
||||
@@ -254,6 +268,20 @@ Object {
|
||||
|
||||
exports[`profiling charts interactions should contain valid data: Interactions 2`] = `
|
||||
Object {
|
||||
"interactions": Array [
|
||||
Object {
|
||||
"__count": 1,
|
||||
"id": 0,
|
||||
"name": "mount",
|
||||
"timestamp": 0,
|
||||
},
|
||||
Object {
|
||||
"__count": 0,
|
||||
"id": 1,
|
||||
"name": "update",
|
||||
"timestamp": 15,
|
||||
},
|
||||
],
|
||||
"lastInteractionTime": 25,
|
||||
"maxCommitDuration": 15,
|
||||
}
|
||||
|
||||
57
src/__tests__/profilerStore-test.js
Normal file
57
src/__tests__/profilerStore-test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
465
src/__tests__/profilingCache-test.js
Normal file
465
src/__tests__/profilingCache-test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
20
src/__tests__/profilingUtils-test.js
Normal file
20
src/__tests__/profilingUtils-test.js
Normal 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"');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
370
src/devtools/ProfilerStore.js
Normal file
370
src/devtools/ProfilerStore.js
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>(() => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()');
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { ElementTypeForwardRef, ElementTypeMemo } from 'src/types';
|
||||
import { formatDuration } from './utils';
|
||||
import ProfilerStore from 'src/devtools/ProfilerStore';
|
||||
|
||||
import type { CommitDetailsFrontend, CommitTreeFrontend } from './types';
|
||||
import type { CommitTree } from './types';
|
||||
|
||||
export type ChartNode = {|
|
||||
actualDuration: number,
|
||||
@@ -28,15 +29,19 @@ export type ChartData = {|
|
||||
const cachedChartData: Map<string, ChartData> = new Map();
|
||||
|
||||
export function getChartData({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
profilerStore,
|
||||
rootID,
|
||||
}: {|
|
||||
commitDetails: CommitDetailsFrontend,
|
||||
commitIndex: number,
|
||||
commitTree: CommitTreeFrontend,
|
||||
commitTree: CommitTree,
|
||||
profilerStore: ProfilerStore,
|
||||
rootID: number,
|
||||
|}): ChartData {
|
||||
const { actualDurations, rootID, selfDurations } = commitDetails;
|
||||
const commitDatum = profilerStore.getCommitData(rootID, commitIndex);
|
||||
|
||||
const { fiberActualDurations, fiberSelfDurations } = commitDatum;
|
||||
const { nodes } = commitTree;
|
||||
|
||||
const key = `${rootID}-${commitIndex}`;
|
||||
@@ -62,9 +67,9 @@ export function getChartData({
|
||||
|
||||
const { children, displayName, key, treeBaseDuration, type } = node;
|
||||
|
||||
const actualDuration = actualDurations.get(id) || 0;
|
||||
const selfDuration = selfDurations.get(id) || 0;
|
||||
const didRender = actualDurations.has(id);
|
||||
const actualDuration = fiberActualDurations.get(id) || 0;
|
||||
const selfDuration = fiberSelfDurations.get(id) || 0;
|
||||
const didRender = fiberActualDurations.has(id);
|
||||
|
||||
const name = displayName || 'Anonymous';
|
||||
const maybeKey = key !== null ? ` key="${key}"` : '';
|
||||
@@ -131,7 +136,7 @@ export function getChartData({
|
||||
walkTree(id, baseDuration, 1);
|
||||
}
|
||||
|
||||
actualDurations.forEach((duration, id) => {
|
||||
fiberActualDurations.forEach((duration, id) => {
|
||||
const node = nodes.get(id);
|
||||
if (node != null) {
|
||||
let currentID = node.parentID;
|
||||
|
||||
@@ -17,9 +17,8 @@ type Props = {
|
||||
function InteractionListItem({ data: itemData, index, style }: Props) {
|
||||
const {
|
||||
chartData,
|
||||
interactions,
|
||||
dataForRoot,
|
||||
labelWidth,
|
||||
profilingSummary,
|
||||
scaleX,
|
||||
selectedInteractionID,
|
||||
selectCommitIndex,
|
||||
@@ -27,20 +26,22 @@ function InteractionListItem({ data: itemData, index, style }: Props) {
|
||||
selectTab,
|
||||
} = itemData;
|
||||
|
||||
const { maxCommitDuration } = chartData;
|
||||
const { commitDurations, commitTimes } = profilingSummary;
|
||||
const { commitData, interactionCommits } = dataForRoot;
|
||||
const { interactions, lastInteractionTime, maxCommitDuration } = chartData;
|
||||
|
||||
const interaction = interactions[index];
|
||||
if (interaction == null) {
|
||||
throw Error(`Could not find interaction #${index}`);
|
||||
}
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
selectInteraction(interaction.id);
|
||||
}, [interaction, selectInteraction]);
|
||||
|
||||
const commits = interactionCommits.get(interaction.id) || [];
|
||||
|
||||
const startTime = interaction.timestamp;
|
||||
const stopTime =
|
||||
interaction.commits.length > 0
|
||||
? commitTimes[interaction.commits[interaction.commits.length - 1]]
|
||||
: interaction.timestamp;
|
||||
const stopTime = lastInteractionTime;
|
||||
|
||||
const viewCommit = (commitIndex: number) => {
|
||||
selectTab('flame-chart');
|
||||
@@ -71,7 +72,7 @@ function InteractionListItem({ data: itemData, index, style }: Props) {
|
||||
width: scaleX(stopTime - startTime, 0),
|
||||
}}
|
||||
/>
|
||||
{interaction.commits.map(commitIndex => (
|
||||
{commits.map(commitIndex => (
|
||||
<div
|
||||
className={styles.CommitBox}
|
||||
key={commitIndex}
|
||||
@@ -80,10 +81,13 @@ function InteractionListItem({ data: itemData, index, style }: Props) {
|
||||
backgroundColor: getGradientColor(
|
||||
Math.min(
|
||||
1,
|
||||
Math.max(0, commitDurations[commitIndex] / maxCommitDuration)
|
||||
Math.max(
|
||||
0,
|
||||
commitData[commitIndex].duration / maxCommitDuration
|
||||
)
|
||||
) || 0
|
||||
),
|
||||
left: labelWidth + scaleX(commitTimes[commitIndex], 0),
|
||||
left: labelWidth + scaleX(commitData[commitIndex].timestamp, 0),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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..."
|
||||
>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -12,24 +12,12 @@ export type Props = {||};
|
||||
export default function SidebarCommitInfo(_: Props) {
|
||||
const {
|
||||
selectedCommitIndex,
|
||||
rendererID,
|
||||
rootID,
|
||||
selectInteraction,
|
||||
selectTab,
|
||||
} = useContext(ProfilerContext);
|
||||
|
||||
const {
|
||||
captureScreenshots,
|
||||
profilingCache,
|
||||
profilingScreenshots,
|
||||
} = useContext(StoreContext);
|
||||
|
||||
const screenshotsByCommitIndex =
|
||||
rootID !== null ? profilingScreenshots.get(rootID) : null;
|
||||
const screenshot =
|
||||
screenshotsByCommitIndex != null && selectedCommitIndex !== null
|
||||
? screenshotsByCommitIndex.get(selectedCommitIndex)
|
||||
: null;
|
||||
const { captureScreenshots, profilerStore } = useContext(StoreContext);
|
||||
|
||||
const [
|
||||
isScreenshotModalVisible,
|
||||
@@ -45,26 +33,22 @@ export default function SidebarCommitInfo(_: Props) {
|
||||
[]
|
||||
);
|
||||
|
||||
if (selectedCommitIndex === null) {
|
||||
if (rootID === null || selectedCommitIndex === null) {
|
||||
return <div className={styles.NothingSelected}>Nothing selected</div>;
|
||||
}
|
||||
|
||||
const { commitDurations, commitTimes } = profilingCache.ProfilingSummary.read(
|
||||
{
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
}
|
||||
);
|
||||
const { interactions } = profilerStore.getDataForRoot(rootID);
|
||||
const {
|
||||
duration,
|
||||
interactionIDs,
|
||||
priorityLevel,
|
||||
screenshot,
|
||||
timestamp,
|
||||
} = profilerStore.getCommitData(rootID, selectedCommitIndex);
|
||||
|
||||
const { interactions, priorityLevel } = profilingCache.CommitDetails.read({
|
||||
commitIndex: selectedCommitIndex,
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
const viewInteraction = interaction => {
|
||||
const viewInteraction = interactionID => {
|
||||
selectTab('interactions');
|
||||
selectInteraction(interaction.id);
|
||||
selectInteraction(interactionID);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -80,34 +64,33 @@ export default function SidebarCommitInfo(_: Props) {
|
||||
)}
|
||||
<li className={styles.ListItem}>
|
||||
<label className={styles.Label}>Committed at</label>:{' '}
|
||||
<span className={styles.Value}>
|
||||
{formatTime(commitTimes[((selectedCommitIndex: any): number)])}s
|
||||
</span>
|
||||
<span className={styles.Value}>{formatTime(timestamp)}s</span>
|
||||
</li>
|
||||
<li className={styles.ListItem}>
|
||||
<label className={styles.Label}>Render duration</label>:{' '}
|
||||
<span className={styles.Value}>
|
||||
{formatDuration(
|
||||
commitDurations[((selectedCommitIndex: any): number)]
|
||||
)}
|
||||
ms
|
||||
</span>
|
||||
<span className={styles.Value}>{formatDuration(duration)}ms</span>
|
||||
</li>
|
||||
<li className={styles.Interactions}>
|
||||
<label className={styles.Label}>Interactions</label>:
|
||||
<div className={styles.InteractionList}>
|
||||
{interactions.length === 0 ? (
|
||||
{interactionIDs.length === 0 ? (
|
||||
<div className={styles.NoInteractions}>None</div>
|
||||
) : null}
|
||||
{interactions.map((interaction, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={styles.Interaction}
|
||||
onClick={() => viewInteraction(interaction)}
|
||||
>
|
||||
{interaction.name}
|
||||
</button>
|
||||
))}
|
||||
{interactionIDs.map(interactionID => {
|
||||
const interaction = interactions.get(interactionID);
|
||||
if (interaction == null) {
|
||||
throw Error(`Invalid interaction "${interactionID}"`);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={interactionID}
|
||||
className={styles.Interaction}
|
||||
onClick={() => viewInteraction(interactionID)}
|
||||
>
|
||||
{interaction.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</li>
|
||||
{captureScreenshots && (
|
||||
|
||||
@@ -13,48 +13,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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user