Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 33 additions & 23 deletions src/components/flame-graph/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
// selection or applying a transform), move the viewport
// vertically so that its offset from the base of the flame graph
// is maintained.
if (prevProps.maxStackDepthPlusOne !== this.props.maxStackDepthPlusOne) {
if (
!this.props.isInverted &&
prevProps.maxStackDepthPlusOne !== this.props.maxStackDepthPlusOne
) {
this.props.viewport.moveViewport(
0,
(prevProps.maxStackDepthPlusOne - this.props.maxStackDepthPlusOne) *
Expand All @@ -151,16 +154,21 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
}

_scrollSelectionIntoView = () => {
const { selectedCallNodeIndex, maxStackDepthPlusOne, callNodeInfo } =
this.props;
const {
selectedCallNodeIndex,
maxStackDepthPlusOne,
callNodeInfo,
isInverted,
} = this.props;

if (selectedCallNodeIndex === null) {
return;
}

const callNodeTable = callNodeInfo.getCallNodeTable();
const depth = callNodeTable.depth[selectedCallNodeIndex];
const y = (maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT;
const depth = callNodeInfo.depthForNode(selectedCallNodeIndex);
const y = isInverted
? depth * ROW_HEIGHT
: (maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT;

if (y < this.props.viewport.viewportTop) {
this.props.viewport.moveViewport(0, this.props.viewport.viewportTop - y);
Expand Down Expand Up @@ -192,6 +200,7 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
viewportTop,
viewportBottom,
},
isInverted,
} = this.props;

const { hoveredItem } = hoverInfo;
Expand Down Expand Up @@ -232,14 +241,12 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
fastFillStyle.set(getBackgroundColor());
ctx.fillRect(0, 0, deviceContainerWidth, deviceContainerHeight);

const callNodeTable = callNodeInfo.getCallNodeTable();

const startDepth = Math.floor(
maxStackDepthPlusOne - viewportBottom / stackFrameHeight
);
const endDepth = Math.ceil(
maxStackDepthPlusOne - viewportTop / stackFrameHeight
);
const startDepth = isInverted
? Math.floor(viewportTop / stackFrameHeight)
: Math.floor(maxStackDepthPlusOne - viewportBottom / stackFrameHeight);
const endDepth = isInverted
? Math.ceil(viewportBottom / stackFrameHeight)
: Math.ceil(maxStackDepthPlusOne - viewportTop / stackFrameHeight);

// Only draw the stack frames that are vertically within view.
// The graph is drawn from bottom to top, in order of increasing depth.
Expand All @@ -251,10 +258,12 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
continue;
}

const cssRowTop: CssPixels =
(maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT - viewportTop;
const cssRowBottom: CssPixels =
(maxStackDepthPlusOne - depth) * ROW_HEIGHT - viewportTop;
const cssRowTop: CssPixels = isInverted
? depth * ROW_HEIGHT - viewportTop
: (maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT - viewportTop;
const cssRowBottom: CssPixels = isInverted
? (depth + 1) * ROW_HEIGHT - viewportTop
: (maxStackDepthPlusOne - depth) * ROW_HEIGHT - viewportTop;
const deviceRowTop: DevicePixels = snap(cssRowTop * cssToDeviceScale);
const deviceRowBottom: DevicePixels =
snap(cssRowBottom * cssToDeviceScale) - 1;
Expand Down Expand Up @@ -300,7 +309,7 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
i === hoveredItem.flameGraphTimingIndex;
const isHighlighted = isSelected || isRightClicked || isHovered;

const categoryIndex = callNodeTable.category[callNodeIndex];
const categoryIndex = callNodeInfo.categoryForNode(callNodeIndex);
const category = categories[categoryIndex];
const colorStyles = mapCategoryColorNameToStackChartStyles(
category.color
Expand All @@ -322,7 +331,7 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
deviceBoxLeft + deviceHorizontalPadding;
const deviceTextWidth: DevicePixels = deviceBoxRight - deviceTextLeft;
if (deviceTextWidth > textMeasurement.minWidth) {
const funcIndex = callNodeTable.func[callNodeIndex];
const funcIndex = callNodeInfo.funcForNode(callNodeIndex);
const funcName = thread.stringTable.getString(
thread.funcTable.name[funcIndex]
);
Expand Down Expand Up @@ -472,11 +481,12 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
flameGraphTiming,
maxStackDepthPlusOne,
viewport: { viewportTop, containerWidth },
isInverted,
} = this.props;
const pos = x / containerWidth;
const depth = Math.floor(
maxStackDepthPlusOne - (y + viewportTop) / ROW_HEIGHT
);
const depth = isInverted
? Math.floor((y + viewportTop) / ROW_HEIGHT)
: Math.floor(maxStackDepthPlusOne - (y + viewportTop) / ROW_HEIGHT);
const stackTiming = flameGraphTiming[depth];

if (!stackTiming) {
Expand Down
23 changes: 13 additions & 10 deletions src/components/flame-graph/FlameGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import * as React from 'react';

import { explicitConnectWithForwardRef } from '../../utils/connect';
import { FlameGraphCanvas } from './Canvas';
import {
FlameGraphCanvas,
type OwnProps as FlameGraphCanvasProps,
} from './Canvas';

import {
getCategories,
Expand All @@ -28,7 +31,6 @@ import {
updateBottomBoxContentsAndMaybeOpen,
} from 'firefox-profiler/actions/profile-view';
import { extractNonInvertedCallTreeTimings } from 'firefox-profiler/profile-logic/call-tree';
import { ensureExists } from 'firefox-profiler/utils/types';

import type {
Thread,
Expand Down Expand Up @@ -352,10 +354,7 @@ class FlameGraphImpl
// different, and the flame graph is only used with non-inverted timings.)
const tracedTimingNonInverted =
tracedTiming !== null
? ensureExists(
extractNonInvertedCallTreeTimings(tracedTiming),
'The flame graph should only ever see non-inverted timings, see UrlState.getInvertCallstack'
)
? extractNonInvertedCallTreeTimings(tracedTiming)
: null;

const maxViewportHeight = maxStackDepthPlusOne * STACK_FRAME_HEIGHT;
Expand All @@ -376,7 +375,7 @@ class FlameGraphImpl
maxViewportHeight,
maximumZoom: 1,
previewSelection,
startsAtBottom: true,
startsAtBottom: !isInverted,
disableHorizontalMovement: true,
viewportNeedsUpdate,
marginLeft: 0,
Expand Down Expand Up @@ -416,12 +415,16 @@ class FlameGraphImpl
}
}

function viewportNeedsUpdate() {
// By always returning false we prevent the viewport from being
function viewportNeedsUpdate(
prevProps: FlameGraphCanvasProps,
nextProps: FlameGraphCanvasProps
) {
// By returning false we prevent the viewport from being
// reset and scrolled all the way to the bottom when doing
// operations like changing the time selection or applying a
// transform.
return false;
// We only reset the viewport if the invertCallstack setting changes.
return prevProps.isInverted !== nextProps.isInverted;
}

export const FlameGraph = explicitConnectWithForwardRef<
Expand Down
38 changes: 3 additions & 35 deletions src/components/flame-graph/MaybeFlameGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,23 @@
import * as React from 'react';

import { explicitConnectWithForwardRef } from 'firefox-profiler/utils/connect';
import { getInvertCallstack } from '../../selectors/url-state';
import { selectedThreadSelectors } from '../../selectors/per-thread';
import { changeInvertCallstack } from '../../actions/profile-view';
import { FlameGraphEmptyReasons } from './FlameGraphEmptyReasons';
import { FlameGraph, type FlameGraphHandle } from './FlameGraph';

import type { ConnectedProps } from 'firefox-profiler/utils/connect';

import './MaybeFlameGraph.css';

// TODO: This component isn't needed any more. Whenever the selected tab
// is "flame-graph", `invertCallstack` will be `false`. <MaybeFlameGraph /> is
// only used in the "flame-graph" tab.

type StateProps = {
readonly isPreviewSelectionEmpty: boolean;
readonly invertCallstack: boolean;
};
type DispatchProps = {
readonly changeInvertCallstack: typeof changeInvertCallstack;
};
type DispatchProps = {};
type Props = ConnectedProps<{}, StateProps, DispatchProps>;

class MaybeFlameGraphImpl extends React.PureComponent<Props> {
_flameGraph: React.RefObject<FlameGraphHandle | null> = React.createRef();

_onSwitchToNormalCallstackClick = () => {
this.props.changeInvertCallstack(false);
};

override componentDidMount() {
const flameGraph = this._flameGraph.current;
if (flameGraph) {
Expand All @@ -42,28 +29,12 @@ class MaybeFlameGraphImpl extends React.PureComponent<Props> {
}

override render() {
const { isPreviewSelectionEmpty, invertCallstack } = this.props;
const { isPreviewSelectionEmpty } = this.props;

if (isPreviewSelectionEmpty) {
return <FlameGraphEmptyReasons />;
}

if (invertCallstack) {
return (
<div className="flameGraphDisabledMessage">
<h3>The Flame Graph is not available for inverted call stacks</h3>
<p>
<button
type="button"
onClick={this._onSwitchToNormalCallstackClick}
>
Switch to the normal call stack
</button>{' '}
to show the Flame Graph.
</p>
</div>
);
}
return <FlameGraph ref={this._flameGraph} />;
}
}
Expand All @@ -76,13 +47,10 @@ export const MaybeFlameGraph = explicitConnectWithForwardRef<
>({
mapStateToProps: (state) => {
return {
invertCallstack: getInvertCallstack(state),
isPreviewSelectionEmpty:
!selectedThreadSelectors.getHasPreviewFilteredCtssSamples(state),
};
},
mapDispatchToProps: {
changeInvertCallstack,
},
mapDispatchToProps: {},
component: MaybeFlameGraphImpl,
});
2 changes: 1 addition & 1 deletion src/components/flame-graph/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const FlameGraphView = () => (
role="tabpanel"
aria-labelledby="flame-graph-tab-button"
>
<StackSettings hideInvertCallstack={true} />
<StackSettings />
<TransformNavigator />
<MaybeFlameGraph />
</div>
Expand Down
74 changes: 73 additions & 1 deletion src/profile-logic/flame-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
IndexIntoCallNodeTable,
} from 'firefox-profiler/types';
import type { StringTable } from 'firefox-profiler/utils/string-table';
import type { CallTreeTimingsNonInverted } from './call-tree';
import type { CallTreeTimingsNonInverted, CallTree } from './call-tree';

import { bisectionRightByStrKey } from 'firefox-profiler/utils/bisect';

Expand Down Expand Up @@ -307,3 +307,75 @@ export function getFlameGraphTiming(

return timing;
}

export function computeFlameGraphTimingFromCallTree(
callTree: CallTree
): FlameGraphTiming {
const rootTotalSummary = callTree.getTotal();
if (rootTotalSummary === 0) {
return [];
}

const timing: FlameGraphTiming = [];

function traverse(
nodeIndex: IndexIntoCallNodeTable,
depth: number,
startX: number
): number {
const { self, total } = callTree.getNodeData(nodeIndex);
if (total === 0) {
return startX;
}

const totalRelative = Math.abs(total / rootTotalSummary);
const endX = startX + totalRelative;

if (!timing[depth]) {
timing[depth] = {
start: [],
end: [],
selfRelative: [],
callNode: [],
length: 0,
};
}

timing[depth].start.push(startX);
timing[depth].end.push(endX);
timing[depth].selfRelative.push(Math.abs(self / rootTotalSummary));
timing[depth].callNode.push(nodeIndex);
timing[depth].length++;

const children = [...callTree.getChildren(nodeIndex)];
if (children.length > 0) {
// Sort children alphabetically by function name.
children.sort((a, b) => {
const nameA = callTree.getNodeData(a).funcName;
const nameB = callTree.getNodeData(b).funcName;
return nameA.localeCompare(nameB);
});

let currentChildStart = startX;
for (const child of children) {
currentChildStart = traverse(child, depth + 1, currentChildStart);
}
}

return endX;
}

const roots = [...callTree.getRoots()];
roots.sort((a, b) => {
const nameA = callTree.getNodeData(a).funcName;
const nameB = callTree.getNodeData(b).funcName;
return nameA.localeCompare(nameB);
});

let currentStart = 0;
for (const root of roots) {
currentStart = traverse(root, 0, currentStart);
}

return timing;
}
3 changes: 1 addition & 2 deletions src/profile-logic/profile-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2686,7 +2686,6 @@ export function computeCallNodeMaxDepthPlusOne(
// computed for the filtered thread, but a samples-like table can use the preview
// filtered thread, which involves a subset of the total call nodes.
let maxDepth = -1;
const callNodeTable = callNodeInfo.getCallNodeTable();
// TODO: Use sampleCallNodes instead
const stackIndexToCallNodeIndex =
callNodeInfo.getStackIndexToNonInvertedCallNodeIndex();
Expand All @@ -2696,7 +2695,7 @@ export function computeCallNodeMaxDepthPlusOne(
continue;
}
const callNodeIndex = stackIndexToCallNodeIndex[stackIndex];
const depth = callNodeTable.depth[callNodeIndex];
const depth = callNodeInfo.depthForNode(callNodeIndex);
if (depth > maxDepth) {
maxDepth = depth;
}
Expand Down
Loading
Loading