mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
First pass at flame graph chart
This commit is contained in:
@@ -631,10 +631,14 @@ export function attach(
|
||||
}
|
||||
|
||||
if (isProfiling) {
|
||||
// Tree base duration updates are included in the operations typed array.
|
||||
// So we have to convert them from milliseconds to microseconds so we can send them as ints.
|
||||
const treeBaseDuration = Math.floor(fiber.treeBaseDuration * 1000);
|
||||
|
||||
const operation = new Uint32Array(3);
|
||||
operation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION;
|
||||
operation[1] = id;
|
||||
operation[2] = fiber.treeBaseDuration;
|
||||
operation[2] = treeBaseDuration;
|
||||
addOperation(operation);
|
||||
}
|
||||
}
|
||||
@@ -702,6 +706,10 @@ export function attach(
|
||||
|
||||
if (isProfiling) {
|
||||
if (treeBaseDuration !== fiber.alternate.treeBaseDuration) {
|
||||
// Tree base duration updates are included in the operations typed array.
|
||||
// So we have to convert them from milliseconds to microseconds so we can send them as ints.
|
||||
const treeBaseDuration = Math.floor(fiber.treeBaseDuration * 1000);
|
||||
|
||||
const operation = new Uint32Array(3);
|
||||
operation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION;
|
||||
operation[1] = getFiberID(getPrimaryFiber(fiber));
|
||||
@@ -1443,6 +1451,8 @@ export function attach(
|
||||
((initialTreeBaseDurationsMap: any): Map<number, number>).forEach(
|
||||
(treeBaseDuration, id) => {
|
||||
if (idToRootMap.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
getCommitTree,
|
||||
invalidateCommitTrees,
|
||||
} from 'src/devtools/views/Profiler/CommitTreeBuilder';
|
||||
import {
|
||||
getChartData as getFlamegraphChartData,
|
||||
invalidateChartData as invalidateFlamegraphChartData,
|
||||
} from 'src/devtools/views/Profiler/FlamegraphChartBuilder';
|
||||
import {
|
||||
getChartData as getRankedChartData,
|
||||
invalidateChartData as invalidateRankedChartData,
|
||||
@@ -22,6 +26,7 @@ import type {
|
||||
CommitTree as CommitTreeFrontend,
|
||||
ProfilingSummary as ProfilingSummaryFrontend,
|
||||
} from 'src/devtools/views/Profiler/types';
|
||||
import type { ChartData as FlamegraphChartData } from 'src/devtools/views/Profiler/FlamegraphChartBuilder';
|
||||
import type { ChartData as RankedChartData } from 'src/devtools/views/Profiler/RankedChartBuilder';
|
||||
|
||||
type CommitDetailsParams = {|
|
||||
@@ -130,6 +135,24 @@ export default class ProfilingCache {
|
||||
store: this._store,
|
||||
});
|
||||
|
||||
getFlamegraphChartData = ({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
rootID,
|
||||
}: {|
|
||||
commitDetails: CommitDetailsFrontend,
|
||||
commitIndex: number,
|
||||
commitTree: CommitTreeFrontend,
|
||||
rootID: number,
|
||||
|}): FlamegraphChartData =>
|
||||
getFlamegraphChartData({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
rootID,
|
||||
});
|
||||
|
||||
getRankedChartData = ({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
@@ -154,6 +177,7 @@ export default class ProfilingCache {
|
||||
|
||||
// Invalidate non-Suspense caches too.
|
||||
invalidateCommitTrees();
|
||||
invalidateFlamegraphChartData();
|
||||
invalidateRankedChartData();
|
||||
|
||||
this._pendingCommitDetailsMap.clear();
|
||||
|
||||
@@ -13,7 +13,6 @@ type Props = {|
|
||||
onClick: Function,
|
||||
onDoubleClick?: Function,
|
||||
placeLabelAboveNode?: boolean,
|
||||
title: string,
|
||||
width: number,
|
||||
x: number,
|
||||
y: number,
|
||||
@@ -28,14 +27,13 @@ export default function ChartNode({
|
||||
label,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
title,
|
||||
width,
|
||||
x,
|
||||
y,
|
||||
}: Props) {
|
||||
return (
|
||||
<g className={styles.Group} transform={`translate(${x},${y})`}>
|
||||
<title>{title}</title>
|
||||
<title>{label}</title>
|
||||
<rect
|
||||
width={width}
|
||||
height={height}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.Container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,64 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { ProfilerContext } from './ProfilerContext';
|
||||
import NoCommitData from './NoCommitData';
|
||||
import CommitFlamegraphListItem from './CommitFlamegraphListItem';
|
||||
import { barHeight } from './constants';
|
||||
import { scale } from './utils';
|
||||
import { StoreContext } from '../context';
|
||||
|
||||
import styles from './CommitFlamegraph.css';
|
||||
|
||||
export default function CommitFlamegraph(_: {||}) {
|
||||
const { rendererID, rootID, selectedCommitIndex } = useContext(
|
||||
ProfilerContext
|
||||
import type { ChartData, ChartNode } from './FlamegraphChartBuilder';
|
||||
|
||||
export type ItemData = {|
|
||||
chartData: ChartData,
|
||||
scaleX: (value: number, fallbackValue: number) => number,
|
||||
selectedChartNode: ChartNode,
|
||||
selectedChartNodeIndex: number,
|
||||
selectFiber: (id: number | null) => void,
|
||||
width: number,
|
||||
|};
|
||||
|
||||
export default function CommitFlamegraphAutoSizer(_: {||}) {
|
||||
const { selectFiber } = useContext(ProfilerContext);
|
||||
const deselectCurrentFiber = useCallback(
|
||||
event => {
|
||||
event.stopPropagation();
|
||||
selectFiber(null);
|
||||
},
|
||||
[selectFiber]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.Container} onClick={deselectCurrentFiber}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<CommitFlamegraph height={height} width={width} />
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommitFlamegraph({
|
||||
height,
|
||||
width,
|
||||
}: {|
|
||||
height: number,
|
||||
width: number,
|
||||
|}) {
|
||||
const {
|
||||
rendererID,
|
||||
rootID,
|
||||
selectedCommitIndex,
|
||||
selectFiber,
|
||||
selectedFiberID,
|
||||
} = useContext(ProfilerContext);
|
||||
|
||||
const { profilingCache } = useContext(StoreContext);
|
||||
|
||||
const profilingSummary = profilingCache.ProfilingSummary.read({
|
||||
@@ -31,5 +79,58 @@ export default function CommitFlamegraph(_: {||}) {
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
return 'Coming soon: Flamegraph';
|
||||
const chartData = profilingCache.getFlamegraphChartData({
|
||||
commitDetails,
|
||||
commitIndex: ((selectedCommitIndex: any): number),
|
||||
commitTree,
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
const selectedChartNodeIndex = useMemo(
|
||||
() =>
|
||||
selectedFiberID === null
|
||||
? 0
|
||||
: ((chartData.idToDepthMap.get(selectedFiberID): any): number) - 1,
|
||||
[chartData, selectedFiberID]
|
||||
);
|
||||
|
||||
const selectedChartNode = useMemo(() => {
|
||||
if (selectedFiberID === null) {
|
||||
return chartData.rows[0][0];
|
||||
}
|
||||
return ((chartData.rows[selectedChartNodeIndex].find(
|
||||
chartNode => chartNode.id === selectedFiberID
|
||||
): any): ChartNode);
|
||||
}, [chartData, selectedFiberID, selectedChartNodeIndex]);
|
||||
|
||||
const itemData = useMemo<ItemData>(
|
||||
() => ({
|
||||
chartData,
|
||||
scaleX: scale(0, selectedChartNode.treeBaseDuration, 0, width),
|
||||
selectedChartNode,
|
||||
selectedChartNodeIndex,
|
||||
selectFiber,
|
||||
width,
|
||||
}),
|
||||
[chartData, selectedChartNode, selectedChartNodeIndex, selectFiber, width]
|
||||
);
|
||||
|
||||
// If a commit contains no fibers with an actualDuration > 0,
|
||||
// Display a fallback message.
|
||||
if (chartData.depth === 0) {
|
||||
return <NoCommitData />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
innerElementType="svg"
|
||||
itemCount={chartData.depth}
|
||||
itemData={itemData}
|
||||
itemSize={barHeight}
|
||||
width={width}
|
||||
>
|
||||
{CommitFlamegraphListItem}
|
||||
</FixedSizeList>
|
||||
);
|
||||
}
|
||||
|
||||
102
src/devtools/views/Profiler/CommitFlamegraphListItem.js
Normal file
102
src/devtools/views/Profiler/CommitFlamegraphListItem.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment, useCallback } from 'react';
|
||||
import { barHeight, barWidthThreshold } from './constants';
|
||||
import { getGradientColor } from './utils';
|
||||
import ChartNode from './ChartNode';
|
||||
|
||||
import type { ItemData } from './CommitFlamegraph';
|
||||
|
||||
export default function CommitFlamegraphListItem({
|
||||
data,
|
||||
index,
|
||||
style,
|
||||
}: {
|
||||
data: ItemData,
|
||||
index: number,
|
||||
style: Object,
|
||||
}) {
|
||||
const {
|
||||
chartData,
|
||||
scaleX,
|
||||
selectedChartNode,
|
||||
selectedChartNodeIndex,
|
||||
selectFiber,
|
||||
width,
|
||||
} = data;
|
||||
const { maxSelfDuration, rows } = chartData;
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: MouseEvent, id: number) => {
|
||||
event.stopPropagation();
|
||||
selectFiber(id);
|
||||
},
|
||||
[selectFiber]
|
||||
);
|
||||
|
||||
// List items are absolutely positioned using the CSS "top" attribute.
|
||||
// The "left" value will always be 0.
|
||||
// Since height is fixed, and width is based on the node's duration,
|
||||
// We can ignore those values as well.
|
||||
const top = parseInt(style.top, 10);
|
||||
|
||||
const row = rows[index];
|
||||
|
||||
let selectedNodeOffset = scaleX(selectedChartNode.offset, width);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{row.map(chartNode => {
|
||||
const {
|
||||
actualDuration,
|
||||
didRender,
|
||||
id,
|
||||
name,
|
||||
offset,
|
||||
selfDuration,
|
||||
treeBaseDuration,
|
||||
} = chartNode;
|
||||
|
||||
const nodeOffset = scaleX(offset, width);
|
||||
const nodeWidth = scaleX(treeBaseDuration, width);
|
||||
|
||||
// Filter out nodes that are too small to see or click.
|
||||
// This also helps render large trees faster.
|
||||
if (nodeWidth < barWidthThreshold) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Filter out nodes that are outside of the horizontal window.
|
||||
if (
|
||||
nodeOffset + nodeWidth < selectedNodeOffset ||
|
||||
nodeOffset > selectedNodeOffset + width
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let color = 'var(--color-commit-did-not-render)';
|
||||
let label = name;
|
||||
if (didRender) {
|
||||
color = getGradientColor(selfDuration / maxSelfDuration);
|
||||
label = `${name} (${selfDuration.toFixed(
|
||||
1
|
||||
)}ms of ${actualDuration.toFixed(1)}ms)`;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartNode
|
||||
color={color}
|
||||
height={barHeight}
|
||||
isDimmed={index < selectedChartNodeIndex}
|
||||
key={id}
|
||||
label={label}
|
||||
onClick={event => handleClick(event, id)}
|
||||
width={nodeWidth}
|
||||
x={nodeOffset - selectedNodeOffset}
|
||||
y={top}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -42,7 +42,6 @@ export default function CommitRankedListItem({
|
||||
key={node.id}
|
||||
label={node.label}
|
||||
onClick={handleClick}
|
||||
title={node.title}
|
||||
width={Math.max(minBarWidth, scaleX(node.value, width))}
|
||||
x={0}
|
||||
y={top}
|
||||
|
||||
@@ -264,7 +264,7 @@ function updateTree(
|
||||
id = operations[i + 1];
|
||||
|
||||
node = ((nodes.get(id): any): Node);
|
||||
node.treeBaseDuration = operations[i + 2];
|
||||
node.treeBaseDuration = operations[i + 2] / 1000; // Convert microseconds back to milliseconds;
|
||||
|
||||
debug(
|
||||
'Update',
|
||||
|
||||
112
src/devtools/views/Profiler/FlamegraphChartBuilder.js
Normal file
112
src/devtools/views/Profiler/FlamegraphChartBuilder.js
Normal file
@@ -0,0 +1,112 @@
|
||||
// @flow
|
||||
|
||||
import { calculateSelfDuration } from './utils';
|
||||
|
||||
import type { CommitDetails, CommitTree, Node } from './types';
|
||||
|
||||
export type ChartNode = {|
|
||||
actualDuration: number,
|
||||
didRender: boolean,
|
||||
id: number,
|
||||
label: string,
|
||||
name: string,
|
||||
offset: number,
|
||||
selfDuration: number,
|
||||
treeBaseDuration: number,
|
||||
|};
|
||||
|
||||
export type ChartData = {|
|
||||
depth: number,
|
||||
idToDepthMap: Map<number, number>,
|
||||
maxSelfDuration: number,
|
||||
rows: Array<Array<ChartNode>>,
|
||||
|};
|
||||
|
||||
const cachedChartData: Map<string, ChartData> = new Map();
|
||||
|
||||
export function getChartData({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
rootID,
|
||||
}: {|
|
||||
commitDetails: CommitDetails,
|
||||
commitIndex: number,
|
||||
commitTree: CommitTree,
|
||||
rootID: number,
|
||||
|}): ChartData {
|
||||
const key = `${rootID}-${commitIndex}`;
|
||||
|
||||
if (cachedChartData.has(key)) {
|
||||
return ((cachedChartData.get(key): any): ChartData);
|
||||
}
|
||||
|
||||
const { nodes } = commitTree;
|
||||
const { actualDurations } = commitDetails;
|
||||
|
||||
const idToDepthMap: Map<number, number> = new Map();
|
||||
const rows: Array<Array<ChartNode>> = [];
|
||||
|
||||
let maxDepth = 0;
|
||||
let maxSelfDuration = 0;
|
||||
|
||||
// Generate flame graph structure using tree base durations.
|
||||
const walkTree = (
|
||||
id: number,
|
||||
parentOffset: number = 0,
|
||||
currentDepth: number = 1
|
||||
) => {
|
||||
idToDepthMap.set(id, currentDepth);
|
||||
|
||||
const node = ((nodes.get(id): any): Node);
|
||||
const name = node.displayName || 'Unknown';
|
||||
|
||||
const selfDuration = calculateSelfDuration(id, commitTree, commitDetails);
|
||||
|
||||
maxDepth = Math.max(maxDepth, currentDepth);
|
||||
maxSelfDuration = Math.max(maxSelfDuration, selfDuration);
|
||||
|
||||
const chartNode: ChartNode = {
|
||||
actualDuration: actualDurations.get(id) || 0,
|
||||
didRender: actualDurations.has(id),
|
||||
id,
|
||||
label: `${name} (${selfDuration.toFixed(1)}ms)`,
|
||||
name,
|
||||
offset: parentOffset,
|
||||
selfDuration,
|
||||
treeBaseDuration: node.treeBaseDuration,
|
||||
};
|
||||
|
||||
if (currentDepth > rows.length) {
|
||||
rows.push([chartNode]);
|
||||
} else {
|
||||
rows[currentDepth - 1].push(chartNode);
|
||||
}
|
||||
|
||||
node.children.forEach(childID => {
|
||||
const childChartNode = walkTree(childID, parentOffset, currentDepth + 1);
|
||||
parentOffset += childChartNode.treeBaseDuration;
|
||||
});
|
||||
|
||||
return chartNode;
|
||||
};
|
||||
|
||||
// Skip over the root; we don't want to show it in the flamegraph.
|
||||
const root = ((nodes.get(rootID): any): Node);
|
||||
walkTree(root.children[0]);
|
||||
|
||||
const chartData = {
|
||||
depth: maxDepth,
|
||||
idToDepthMap,
|
||||
maxSelfDuration,
|
||||
rows,
|
||||
};
|
||||
|
||||
cachedChartData.set(key, chartData);
|
||||
|
||||
return chartData;
|
||||
}
|
||||
|
||||
export function invalidateChartData(): void {
|
||||
cachedChartData.clear();
|
||||
}
|
||||
@@ -8,7 +8,6 @@ export type ChartNode = {|
|
||||
id: number,
|
||||
label: string,
|
||||
name: string,
|
||||
title: string,
|
||||
value: number,
|
||||
|};
|
||||
|
||||
@@ -58,7 +57,6 @@ export function getChartData({
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
title: label,
|
||||
value: selfDuration,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
export const barHeight = 20;
|
||||
export const barWidthThreshold = 2;
|
||||
export const maxBarWidth = 30;
|
||||
export const minBarHeight = 5;
|
||||
export const minBarWidth = 5;
|
||||
|
||||
Reference in New Issue
Block a user