diff --git a/src/components/flame-graph/Canvas.tsx b/src/components/flame-graph/Canvas.tsx index 73dd9394da..7d40744d9b 100644 --- a/src/components/flame-graph/Canvas.tsx +++ b/src/components/flame-graph/Canvas.tsx @@ -70,6 +70,7 @@ export type OwnProps = { readonly scrollToSelectionGeneration: number; readonly categories: CategoryList; readonly interval: Milliseconds; + readonly startsAtBottom: boolean; readonly callTreeSummaryStrategy: CallTreeSummaryStrategy; readonly ctssSamples: SamplesLikeTable; readonly ctssSampleCategoriesAndSubcategories: SampleCategoriesAndSubcategories; @@ -123,8 +124,12 @@ class FlameGraphCanvasImpl extends React.PureComponent { // If the stack depth changes (say, when changing the time range // 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) { + // is maintained. In top-down layout the base is at the top, so no + // adjustment is needed when depth grows or shrinks. + if ( + this.props.startsAtBottom && + prevProps.maxStackDepthPlusOne !== this.props.maxStackDepthPlusOne + ) { this.props.viewport.moveViewport( 0, (prevProps.maxStackDepthPlusOne - this.props.maxStackDepthPlusOne) * @@ -150,16 +155,21 @@ class FlameGraphCanvasImpl extends React.PureComponent { } _scrollSelectionIntoView = () => { - const { selectedCallNodeIndex, maxStackDepthPlusOne, callNodeInfo } = - this.props; + const { + selectedCallNodeIndex, + maxStackDepthPlusOne, + callNodeInfo, + startsAtBottom, + } = 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 = startsAtBottom + ? (maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT + : depth * ROW_HEIGHT; if (y < this.props.viewport.viewportTop) { this.props.viewport.moveViewport(0, this.props.viewport.viewportTop - y); @@ -191,6 +201,7 @@ class FlameGraphCanvasImpl extends React.PureComponent { viewportTop, viewportBottom, }, + startsAtBottom, } = this.props; const { hoveredItem } = hoverInfo; @@ -231,14 +242,12 @@ class FlameGraphCanvasImpl extends React.PureComponent { 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 = startsAtBottom + ? Math.floor(maxStackDepthPlusOne - viewportBottom / stackFrameHeight) + : Math.floor(viewportTop / stackFrameHeight); + const endDepth = startsAtBottom + ? Math.ceil(maxStackDepthPlusOne - viewportTop / stackFrameHeight) + : Math.ceil(viewportBottom / stackFrameHeight); // Only draw the stack frames that are vertically within view. // The graph is drawn from bottom to top, in order of increasing depth. @@ -250,10 +259,12 @@ class FlameGraphCanvasImpl extends React.PureComponent { // Get the timing information for a row of stack frames. const stackTiming = flameGraphTiming.getRow(depth); - const cssRowTop: CssPixels = - (maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT - viewportTop; - const cssRowBottom: CssPixels = - (maxStackDepthPlusOne - depth) * ROW_HEIGHT - viewportTop; + const cssRowTop: CssPixels = startsAtBottom + ? (maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT - viewportTop + : depth * ROW_HEIGHT - viewportTop; + const cssRowBottom: CssPixels = startsAtBottom + ? (maxStackDepthPlusOne - depth) * ROW_HEIGHT - viewportTop + : (depth + 1) * ROW_HEIGHT - viewportTop; const deviceRowTop: DevicePixels = snap(cssRowTop * cssToDeviceScale); const deviceRowBottom: DevicePixels = snap(cssRowBottom * cssToDeviceScale) - 1; @@ -299,7 +310,7 @@ class FlameGraphCanvasImpl extends React.PureComponent { 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 @@ -321,7 +332,7 @@ class FlameGraphCanvasImpl extends React.PureComponent { 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] ); @@ -472,11 +483,13 @@ class FlameGraphCanvasImpl extends React.PureComponent { flameGraphTiming, maxStackDepthPlusOne, viewport: { viewportTop, containerWidth }, + startsAtBottom, } = this.props; const pos = x / containerWidth; - const depth = Math.floor( - maxStackDepthPlusOne - (y + viewportTop) / ROW_HEIGHT - ); + const depth = startsAtBottom + ? Math.floor(maxStackDepthPlusOne - (y + viewportTop) / ROW_HEIGHT) + : Math.floor((y + viewportTop) / ROW_HEIGHT); + if (depth < 0 || depth >= flameGraphTiming.rowCount) { return null; } diff --git a/src/components/flame-graph/ConnectedFlameGraph.tsx b/src/components/flame-graph/ConnectedFlameGraph.tsx index 8895b8812b..d2dcd36f63 100644 --- a/src/components/flame-graph/ConnectedFlameGraph.tsx +++ b/src/components/flame-graph/ConnectedFlameGraph.tsx @@ -177,6 +177,7 @@ class ConnectedFlameGraphImpl scrollToSelectionGeneration={scrollToSelectionGeneration} categories={categories} interval={interval} + startsAtBottom={true} callTreeSummaryStrategy={callTreeSummaryStrategy} ctssSamples={ctssSamples} ctssSampleCategoriesAndSubcategories={ diff --git a/src/components/flame-graph/FlameGraph.tsx b/src/components/flame-graph/FlameGraph.tsx index 159aaa0ea5..dd6d888341 100644 --- a/src/components/flame-graph/FlameGraph.tsx +++ b/src/components/flame-graph/FlameGraph.tsx @@ -59,6 +59,7 @@ export type Props = { readonly scrollToSelectionGeneration: number; readonly categories: CategoryList; readonly interval: Milliseconds; + readonly startsAtBottom: boolean; readonly callTreeSummaryStrategy: CallTreeSummaryStrategy; readonly ctssSamples: SamplesLikeTable; readonly ctssSampleCategoriesAndSubcategories: SampleCategoriesAndSubcategories; @@ -118,8 +119,7 @@ export class FlameGraph _wideEnough = (callNodeIndex: IndexIntoCallNodeTable): boolean => { const { flameGraphTiming, callNodeInfo } = this.props; - const callNodeTable = callNodeInfo.getCallNodeTable(); - const depth = callNodeTable.depth[callNodeIndex]; + const depth = callNodeInfo.depthForNode(callNodeIndex); const row = flameGraphTiming.getRow(depth); const columnIndex = row.callNode.indexOf(callNodeIndex); return row.end[columnIndex] - row.start[columnIndex] > SELECTABLE_THRESHOLD; @@ -143,8 +143,7 @@ export class FlameGraph let callNodeIndex = startingCallNodeIndex; - const callNodeTable = callNodeInfo.getCallNodeTable(); - const depth = callNodeTable.depth[callNodeIndex]; + const depth = callNodeInfo.depthForNode(callNodeIndex); const row = flameGraphTiming.getRow(depth); let columnIndex = row.callNode.indexOf(callNodeIndex); @@ -173,8 +172,8 @@ export class FlameGraph onSelectedCallNodeChange, onCallNodeEnterOrDoubleClick, onKeyboardTransformShortcut, + startsAtBottom, } = this.props; - const callNodeTable = callNodeInfo.getCallNodeTable(); if ( // Please do not forget to update the switch/case below if changing the array to allow more keys. @@ -186,43 +185,45 @@ export class FlameGraph return; } - switch (event.key) { - case 'ArrowDown': { - const prefix = callNodeTable.prefix[selectedCallNodeIndex]; - if (prefix !== -1) { - onSelectedCallNodeChange(prefix); - } - break; + // In top-down layout the parent is visually above the selected box, so + // ArrowUp navigates to the parent and ArrowDown to the first child. + // In bottom-up layout it's the other way around. + const isGoToParent = startsAtBottom + ? event.key === 'ArrowDown' + : event.key === 'ArrowUp'; + const isGoToChild = startsAtBottom + ? event.key === 'ArrowUp' + : event.key === 'ArrowDown'; + + if (isGoToParent) { + const prefix = callNodeInfo.prefixForNode(selectedCallNodeIndex); + if (prefix !== -1) { + onSelectedCallNodeChange(prefix); } - case 'ArrowUp': { - const [callNodeIndex] = callTree.getChildren(selectedCallNodeIndex); - // The call nodes returned from getChildren are sorted by - // total time in descending order. The first one in the - // array, which is the one we pick, has the longest time and - // thus the widest box. - - if (callNodeIndex !== undefined && this._wideEnough(callNodeIndex)) { - onSelectedCallNodeChange(callNodeIndex); - } - break; + } else if (isGoToChild) { + const [callNodeIndex] = callTree.getChildren(selectedCallNodeIndex); + // The call nodes returned from getChildren are sorted by + // total time in descending order. The first one in the + // array, which is the one we pick, has the longest time and + // thus the widest box. + + if (callNodeIndex !== undefined && this._wideEnough(callNodeIndex)) { + onSelectedCallNodeChange(callNodeIndex); } - case 'ArrowLeft': - case 'ArrowRight': { - const callNodeIndex = this._nextSelectableInRow( - selectedCallNodeIndex, - event.key === 'ArrowLeft' ? -1 : 1 - ); - - if (callNodeIndex !== undefined) { - onSelectedCallNodeChange(callNodeIndex); - } - break; + } else if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { + const callNodeIndex = this._nextSelectableInRow( + selectedCallNodeIndex, + event.key === 'ArrowLeft' ? -1 : 1 + ); + + if (callNodeIndex !== undefined) { + onSelectedCallNodeChange(callNodeIndex); } - default: - // We shouldn't arrive here, thanks to the if block at the top. - console.error( - `An unknown key "${event.key}" was pressed, this shouldn't happen.` - ); + } else { + // We shouldn't arrive here, thanks to the if block at the top. + console.error( + `An unknown key "${event.key}" was pressed, this shouldn't happen.` + ); } return; } @@ -248,9 +249,8 @@ export class FlameGraph if (document.activeElement === this._viewport) { event.preventDefault(); const { callNodeInfo, selectedCallNodeIndex, thread } = this.props; - const callNodeTable = callNodeInfo.getCallNodeTable(); if (selectedCallNodeIndex !== null) { - const funcIndex = callNodeTable.func[selectedCallNodeIndex]; + const funcIndex = callNodeInfo.funcForNode(selectedCallNodeIndex); const funcName = thread.stringTable.getString( thread.funcTable.name[funcIndex] ); @@ -275,6 +275,7 @@ export class FlameGraph callTreeSummaryStrategy, categories, interval, + startsAtBottom, innerWindowIDToPageMap, weightType, ctssSamples, @@ -318,7 +319,7 @@ export class FlameGraph maxViewportHeight, maximumZoom: 1, previewSelection, - startsAtBottom: true, + startsAtBottom, disableHorizontalMovement: true, viewportNeedsUpdate, marginLeft: 0, @@ -345,6 +346,7 @@ export class FlameGraph onDoubleClick: onCallNodeEnterOrDoubleClick, shouldDisplayTooltips: this._shouldDisplayTooltips, interval, + startsAtBottom, ctssSamples, ctssSampleCategoriesAndSubcategories, tracedTiming: tracedTimingNonInverted,