diff --git a/.prettierignore b/.prettierignore index 3114af9092..05990ac880 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,6 @@ src/types/libdef/npm docs-user -src/test/fixtures/ +src/test/fixtures/upgrades res/zee-worker.js dist coverage diff --git a/.prettierrc.js b/.prettierrc.js index e18ac86656..231799b8f4 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -15,7 +15,7 @@ module.exports = { }, }, { - files: 'bin/*', + files: 'bin/*.js', options: { // Files in bin/ are javascript files that may use Flow comments. We // don't want the content of these Flow comments to be output outside of diff --git a/package.json b/package.json index 397e029300..19094e03a6 100644 --- a/package.json +++ b/package.json @@ -58,20 +58,20 @@ "@codemirror/lang-rust": "^6.0.1", "@codemirror/language": "^6.9.2", "@codemirror/state": "^6.3.1", - "@codemirror/view": "^6.21.3", + "@codemirror/view": "^6.22.0", "@firefox-devtools/react-contextmenu": "^5.1.1", "@fluent/bundle": "^0.18.0", "@fluent/langneg": "^0.7.0", "@fluent/react": "^0.15.2", - "@lezer/highlight": "^1.1.6", - "@tgwf/co2": "^0.13.8", + "@lezer/highlight": "^1.2.0", + "@tgwf/co2": "^0.13.9", "array-move": "^3.0.1", "array-range": "^1.0.1", "clamp": "^1.0.1", "classnames": "^2.3.2", "common-tags": "^1.8.2", "copy-to-clipboard": "^3.3.3", - "core-js": "^3.33.1", + "core-js": "^3.33.2", "escape-string-regexp": "^4.0.0", "gecko-profiler-demangle": "^0.3.3", "idb": "^7.1.1", @@ -84,7 +84,7 @@ "query-string": "^8.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-intersection-observer": "^9.5.2", + "react-intersection-observer": "^9.5.3", "react-redux": "^8.1.3", "react-splitter-layout": "^4.0.0", "react-transition-group": "^4.4.5", @@ -98,16 +98,16 @@ }, "devDependencies": { "@babel/cli": "^7.23.0", - "@babel/core": "^7.23.2", - "@babel/eslint-parser": "^7.22.15", + "@babel/core": "^7.23.3", + "@babel/eslint-parser": "^7.23.3", "@babel/eslint-plugin": "^7.22.10", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.23.2", - "@babel/preset-flow": "^7.22.15", - "@babel/preset-react": "^7.22.15", + "@babel/preset-env": "^7.23.3", + "@babel/preset-flow": "^7.23.3", + "@babel/preset-react": "^7.23.3", "@testing-library/dom": "^9.3.3", "@testing-library/jest-dom": "^6.1.4", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^14.1.2", "alex": "^11.0.1", "autoprefixer": "^10.4.16", "babel-jest": "^29.7.0", @@ -122,7 +122,7 @@ "css-loader": "^6.8.1", "cssnano": "^6.0.1", "devtools-license-check": "^0.9.0", - "eslint": "^8.52.0", + "eslint": "^8.53.0", "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-flowtype": "^8.0.3", @@ -131,7 +131,7 @@ "eslint-plugin-jest-dom": "^5.1.0", "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-react": "^7.33.2", - "eslint-plugin-testing-library": "^6.1.0", + "eslint-plugin-testing-library": "^6.1.2", "espree": "^9.6.1", "fake-indexeddb": "^4.0.2", "fetch-mock-jest": "^1.5.1", @@ -154,7 +154,7 @@ "open": "^9.1.0", "postcss": "^8.4.31", "postcss-loader": "^7.3.3", - "prettier": "^3.0.3", + "prettier": "^3.1.0", "raw-loader": "^4.0.2", "rimraf": "^5.0.5", "style-loader": "^3.3.3", diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 0367d88af7..b7122d4395 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -105,26 +105,30 @@ export function changeSelectedCallNode( selectedCallNodePath: CallNodePath, context: SelectionContext = { source: 'auto' }, optionalExpandedToCallNodePath?: CallNodePath -): Action { - if (optionalExpandedToCallNodePath) { - for (let i = 0; i < selectedCallNodePath.length; i++) { - if (selectedCallNodePath[i] !== optionalExpandedToCallNodePath[i]) { - // This assertion ensures that the selectedCallNode will be correctly expanded. - throw new Error( - oneLine` - The optional expanded call node path provided to the changeSelectedCallNode - must contain the selected call node path. - ` - ); +): ThunkAction { + return (dispatch, getState) => { + if (optionalExpandedToCallNodePath) { + for (let i = 0; i < selectedCallNodePath.length; i++) { + if (selectedCallNodePath[i] !== optionalExpandedToCallNodePath[i]) { + // This assertion ensures that the selectedCallNode will be correctly expanded. + throw new Error( + oneLine` + The optional expanded call node path provided to the changeSelectedCallNode + must contain the selected call node path. + ` + ); + } } } - } - return { - type: 'CHANGE_SELECTED_CALL_NODE', - selectedCallNodePath, - optionalExpandedToCallNodePath, - threadsKey, - context, + const isInverted = getInvertCallstack(getState()); + dispatch({ + type: 'CHANGE_SELECTED_CALL_NODE', + isInverted, + selectedCallNodePath, + optionalExpandedToCallNodePath, + threadsKey, + context, + }); }; } @@ -1637,14 +1641,17 @@ export function expandAllCallNodeDescendants( export function changeExpandedCallNodes( threadsKey: ThreadsKey, expandedCallNodePaths: Array -): Action { - return { - type: 'CHANGE_EXPANDED_CALL_NODES', - threadsKey, - expandedCallNodePaths, +): ThunkAction { + return (dispatch, getState) => { + const isInverted = getInvertCallstack(getState()); + dispatch({ + type: 'CHANGE_EXPANDED_CALL_NODES', + isInverted, + threadsKey, + expandedCallNodePaths, + }); }; } - export function changeSelectedMarker( threadsKey: ThreadsKey, selectedMarker: MarkerIndex | null, @@ -1772,13 +1779,16 @@ export function changeInvertCallstack( eventCategory: 'profile', eventAction: 'change invert callstack', }); + const callTree = selectedThreadSelectors.getCallTree(getState()); + const selectedCallNode = + selectedThreadSelectors.getSelectedCallNodeIndex(getState()); + const newSelectedCallNodePath = + callTree.findHeavyPathToSameFunctionAfterInversion(selectedCallNode); dispatch({ type: 'CHANGE_INVERT_CALLSTACK', invertCallstack, selectedThreadIndexes: getSelectedThreadIndexes(getState()), - callTree: selectedThreadSelectors.getCallTree(getState()), - callNodeTable: selectedThreadSelectors.getCallNodeInfo(getState()) - .callNodeTable, + newSelectedCallNodePath, }); }; } diff --git a/src/components/calltree/CallTree.js b/src/components/calltree/CallTree.js index 658b392152..2b6feceb95 100644 --- a/src/components/calltree/CallTree.js +++ b/src/components/calltree/CallTree.js @@ -71,7 +71,7 @@ type StateProps = {| +disableOverscan: boolean, +invertCallstack: boolean, +implementationFilter: ImplementationFilter, - +callNodeMaxDepth: number, + +callNodeMaxDepthPlusOne: number, +weightType: WeightType, +tableViewOptions: TableViewOptions, |}; @@ -359,7 +359,7 @@ class CallTreeImpl extends PureComponent { expandedCallNodeIndexes, searchStringsRegExp, disableOverscan, - callNodeMaxDepth, + callNodeMaxDepthPlusOne, weightType, tableViewOptions, onTableViewOptionsChange, @@ -383,7 +383,7 @@ class CallTreeImpl extends PureComponent { disableOverscan={disableOverscan} ref={this._takeTreeViewRef} contextMenuId="CallNodeContextMenu" - maxNodeDepth={callNodeMaxDepth} + maxNodeDepth={callNodeMaxDepthPlusOne} rowHeight={16} indentWidth={10} onKeyDown={this._onKeyDown} @@ -417,8 +417,8 @@ export const CallTree = explicitConnect<{||}, StateProps, DispatchProps>({ // Use the filtered call node max depth, rather than the preview filtered call node // max depth so that the width of the TreeView component is stable across preview // selections. - callNodeMaxDepth: - selectedThreadSelectors.getFilteredCallNodeMaxDepth(state), + callNodeMaxDepthPlusOne: + selectedThreadSelectors.getFilteredCallNodeMaxDepthPlusOne(state), weightType: selectedThreadSelectors.getWeightTypeForCallTree(state), tableViewOptions: getCurrentTableViewOptions(state), }), diff --git a/src/components/flame-graph/Canvas.js b/src/components/flame-graph/Canvas.js index c06b8a0acc..ae6798d403 100644 --- a/src/components/flame-graph/Canvas.js +++ b/src/components/flame-graph/Canvas.js @@ -57,7 +57,7 @@ export type OwnProps = {| +innerWindowIDToPageMap: Map | null, +unfilteredThread: Thread, +sampleIndexOffset: number, - +maxStackDepth: number, + +maxStackDepthPlusOne: number, +flameGraphTiming: FlameGraphTiming, +callNodeInfo: CallNodeInfo, +callTree: CallTree, @@ -128,10 +128,11 @@ class FlameGraphCanvasImpl extends React.PureComponent { // selection or applying a transform), move the viewport // vertically so that its offset from the base of the flame graph // is maintained. - if (prevProps.maxStackDepth !== this.props.maxStackDepth) { + if (prevProps.maxStackDepthPlusOne !== this.props.maxStackDepthPlusOne) { this.props.viewport.moveViewport( 0, - (prevProps.maxStackDepth - this.props.maxStackDepth) * ROW_HEIGHT + (prevProps.maxStackDepthPlusOne - this.props.maxStackDepthPlusOne) * + ROW_HEIGHT ); } @@ -155,7 +156,7 @@ class FlameGraphCanvasImpl extends React.PureComponent { _scrollSelectionIntoView = () => { const { selectedCallNodeIndex, - maxStackDepth, + maxStackDepthPlusOne, callNodeInfo: { callNodeTable }, } = this.props; @@ -164,7 +165,7 @@ class FlameGraphCanvasImpl extends React.PureComponent { } const depth = callNodeTable.depth[selectedCallNodeIndex]; - const y = (maxStackDepth - depth - 1) * ROW_HEIGHT; + const y = (maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT; if (y < this.props.viewport.viewportTop) { this.props.viewport.moveViewport(0, this.props.viewport.viewportTop - y); @@ -186,7 +187,7 @@ class FlameGraphCanvasImpl extends React.PureComponent { flameGraphTiming, callNodeInfo: { callNodeTable }, stackFrameHeight, - maxStackDepth, + maxStackDepthPlusOne, rightClickedCallNodeIndex, selectedCallNodeIndex, categories, @@ -237,9 +238,11 @@ class FlameGraphCanvasImpl extends React.PureComponent { ctx.fillRect(0, 0, deviceContainerWidth, deviceContainerHeight); const startDepth = Math.floor( - maxStackDepth - viewportBottom / stackFrameHeight + maxStackDepthPlusOne - viewportBottom / stackFrameHeight + ); + const endDepth = Math.ceil( + maxStackDepthPlusOne - viewportTop / stackFrameHeight ); - const endDepth = Math.ceil(maxStackDepth - 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. @@ -252,9 +255,9 @@ class FlameGraphCanvasImpl extends React.PureComponent { } const cssRowTop: CssPixels = - (maxStackDepth - depth - 1) * ROW_HEIGHT - viewportTop; + (maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT - viewportTop; const cssRowBottom: CssPixels = - (maxStackDepth - depth) * ROW_HEIGHT - viewportTop; + (maxStackDepthPlusOne - depth) * ROW_HEIGHT - viewportTop; const deviceRowTop: DevicePixels = snap(cssRowTop * cssToDeviceScale); const deviceRowBottom: DevicePixels = snap(cssRowBottom * cssToDeviceScale) - 1; @@ -473,11 +476,13 @@ class FlameGraphCanvasImpl extends React.PureComponent { _hitTest = (x: CssPixels, y: CssPixels): HoveredStackTiming | null => { const { flameGraphTiming, - maxStackDepth, + maxStackDepthPlusOne, viewport: { viewportTop, containerWidth }, } = this.props; const pos = x / containerWidth; - const depth = Math.floor(maxStackDepth - (y + viewportTop) / ROW_HEIGHT); + const depth = Math.floor( + maxStackDepthPlusOne - (y + viewportTop) / ROW_HEIGHT + ); const stackTiming = flameGraphTiming[depth]; if (!stackTiming) { diff --git a/src/components/flame-graph/FlameGraph.js b/src/components/flame-graph/FlameGraph.js index 0cd10c5ab7..64210dc728 100644 --- a/src/components/flame-graph/FlameGraph.js +++ b/src/components/flame-graph/FlameGraph.js @@ -72,7 +72,7 @@ type StateProps = {| +innerWindowIDToPageMap: Map | null, +unfilteredThread: Thread, +sampleIndexOffset: number, - +maxStackDepth: number, + +maxStackDepthPlusOne: number, +timeRange: StartEndRange, +previewSelection: PreviewSelection, +flameGraphTiming: FlameGraphTiming, @@ -327,7 +327,7 @@ class FlameGraphImpl extends React.PureComponent { unfilteredThread, sampleIndexOffset, threadsKey, - maxStackDepth, + maxStackDepthPlusOne, flameGraphTiming, callTree, callNodeInfo, @@ -349,7 +349,7 @@ class FlameGraphImpl extends React.PureComponent { displayStackType, } = this.props; - const maxViewportHeight = maxStackDepth * STACK_FRAME_HEIGHT; + const maxViewportHeight = maxStackDepthPlusOne * STACK_FRAME_HEIGHT; return (
@@ -381,7 +381,7 @@ class FlameGraphImpl extends React.PureComponent { weightType, unfilteredThread, sampleIndexOffset, - maxStackDepth, + maxStackDepthPlusOne, flameGraphTiming, callTree, callNodeInfo, @@ -427,7 +427,8 @@ export const FlameGraph = explicitConnect<{||}, StateProps, DispatchProps>({ selectedThreadSelectors.getSampleIndexOffsetFromCommittedRange(state), // Use the filtered call node max depth, rather than the preview filtered one, so // that the viewport height is stable across preview selections. - maxStackDepth: selectedThreadSelectors.getFilteredCallNodeMaxDepth(state), + maxStackDepthPlusOne: + selectedThreadSelectors.getFilteredCallNodeMaxDepthPlusOne(state), flameGraphTiming: selectedThreadSelectors.getFlameGraphTiming(state), callTree: selectedThreadSelectors.getCallTree(state), timeRange: getCommittedRange(state), diff --git a/src/components/flame-graph/MaybeFlameGraph.js b/src/components/flame-graph/MaybeFlameGraph.js index 497b80b3ca..e8e15f278b 100644 --- a/src/components/flame-graph/MaybeFlameGraph.js +++ b/src/components/flame-graph/MaybeFlameGraph.js @@ -16,6 +16,10 @@ 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`. is +// only used in the "flame-graph" tab. + type StateProps = {| +isPreviewSelectionEmpty: boolean, +invertCallstack: boolean, @@ -72,8 +76,9 @@ export const MaybeFlameGraph = explicitConnect<{||}, StateProps, DispatchProps>( return { invertCallstack: getInvertCallstack(state), isPreviewSelectionEmpty: - selectedThreadSelectors.getPreviewFilteredCallNodeMaxDepth(state) === - 0, + selectedThreadSelectors.getPreviewFilteredCallNodeMaxDepthPlusOne( + state + ) === 0, }; }, mapDispatchToProps: { diff --git a/src/components/shared/CallNodeContextMenu.js b/src/components/shared/CallNodeContextMenu.js index 9bec11a57c..2a37ee7a60 100644 --- a/src/components/shared/CallNodeContextMenu.js +++ b/src/components/shared/CallNodeContextMenu.js @@ -18,6 +18,7 @@ import { getFunctionName } from 'firefox-profiler/profile-logic/function-info'; import { getBottomBoxInfoForCallNode, getOriginAnnotationForFunc, + getCallNodePathFromIndex, } from 'firefox-profiler/profile-logic/profile-data'; import { getCategories } from 'firefox-profiler/selectors'; @@ -226,25 +227,28 @@ class CallNodeContextMenuImpl extends React.PureComponent { callNodeInfo: { callNodeTable }, } = rightClickedCallNodeInfo; - let stack = ''; - let curCallNodeIndex = callNodeIndex; - - do { - // Match the style of MarkerContextMenu.js#convertStackToString which uses - // square brackets around [file:line:column] info. This isn't provided by - // getOriginAnnotationForFunc, so build the string in two parts: - const funcIndex = callNodeTable.func[curCallNodeIndex]; - const funcNameIndex = funcTable.name[funcIndex]; - const funcName = stringTable.getString(funcNameIndex); - const fileNameURL = getOriginAnnotationForFunc( - funcIndex, - funcTable, - resourceTable, - stringTable - ); - stack += funcName + (fileNameURL ? ` [${fileNameURL}]\n` : '\n'); - curCallNodeIndex = callNodeTable.prefix[curCallNodeIndex]; - } while (curCallNodeIndex !== -1); + const callPath = getCallNodePathFromIndex( + callNodeIndex, + callNodeTable + ).reverse(); + + const stack = callPath + .map((funcIndex) => { + // Match the style of MarkerContextMenu.js#convertStackToString which uses + // square brackets around [file:line:column] info. + // The square brackets aren't included in the string that's returned from + // getOriginAnnotationForFunc, so build the string in two parts: + const funcNameIndex = funcTable.name[funcIndex]; + const funcName = stringTable.getString(funcNameIndex); + const originAnnotation = getOriginAnnotationForFunc( + funcIndex, + funcTable, + resourceTable, + stringTable + ); + return funcName + (originAnnotation ? ` [${originAnnotation}]` : ''); + }) + .join('\n'); copy(stack); } diff --git a/src/components/shared/thread/CPUGraph.js b/src/components/shared/thread/CPUGraph.js index 32bcdf8e07..c0bb0cb9e0 100644 --- a/src/components/shared/thread/CPUGraph.js +++ b/src/components/shared/thread/CPUGraph.js @@ -14,16 +14,13 @@ import type { IndexIntoSamplesTable, Milliseconds, CallNodeInfo, - IndexIntoCallNodeTable, SelectedState, } from 'firefox-profiler/types'; -import type { HeightFunctionParams } from './HeightGraph'; type Props = {| +className: string, +thread: Thread, +samplesSelectedStates: null | SelectedState[], - +sampleCallNodes: Array, +interval: Milliseconds, +rangeStart: Milliseconds, +rangeEnd: Milliseconds, @@ -40,10 +37,7 @@ type Props = {| |}; export class ThreadCPUGraph extends PureComponent { - _heightFunction = ({ - sampleIndex, - yPixelsPerHeight, - }: HeightFunctionParams): number => { + _heightFunction = (sampleIndex: IndexIntoSamplesTable): number | null => { const { thread } = this.props; const { samples } = thread; @@ -56,14 +50,13 @@ export class ThreadCPUGraph extends PureComponent { const cpuDelta = ensureExists(samples.threadCPUDelta)[sampleIndex + 1] || 0; const interval = samples.time[sampleIndex + 1] - samples.time[sampleIndex]; const currentCPUPerMs = cpuDelta / interval; - return currentCPUPerMs * yPixelsPerHeight; + return currentCPUPerMs; }; render() { const { className, thread, - sampleCallNodes, samplesSelectedStates, interval, rangeStart, @@ -85,7 +78,6 @@ export class ThreadCPUGraph extends PureComponent { trackName={trackName} interval={interval} thread={thread} - sampleCallNodes={sampleCallNodes} samplesSelectedStates={samplesSelectedStates} rangeStart={rangeStart} rangeEnd={rangeEnd} diff --git a/src/components/shared/thread/HeightGraph.js b/src/components/shared/thread/HeightGraph.js index 99ffbd3173..fe097b784c 100644 --- a/src/components/shared/thread/HeightGraph.js +++ b/src/components/shared/thread/HeightGraph.js @@ -17,34 +17,15 @@ import type { CategoryList, IndexIntoSamplesTable, Milliseconds, - IndexIntoCallNodeTable, SelectedState, } from 'firefox-profiler/types'; -/** - * Parameters for the height function of the HeightGraph component. - * Shared HeightGraph component gets a `heightFunc` as a prop and executes this - * function for each sample to determine the height of the given sample in the graph. - * Therefore, each height per sample is determined by the parent component. This - * helps us reuse the same height graph component with different values. - * - * These parameters are exposed because the parent component needs them to find - * out the height of the sample with different logics. These parameters are added - * on an as-needed basis. - */ -export type HeightFunctionParams = {| - +sampleIndex: number, - +callNodeIndex: number, - +yPixelsPerHeight: number, -|}; - type Props = {| - +heightFunc: (HeightFunctionParams) => number, + +heightFunc: (IndexIntoSamplesTable) => number | null, +maxValue: number, +className: string, +thread: Thread, +samplesSelectedStates: null | SelectedState[], - +sampleCallNodes: Array, +interval: Milliseconds, +rangeStart: Milliseconds, +rangeEnd: Milliseconds, @@ -86,7 +67,6 @@ export class ThreadHeightGraph extends PureComponent { const { thread, samplesSelectedStates, - sampleCallNodes, interval, rangeStart, rangeEnd, @@ -149,16 +129,12 @@ export class ThreadHeightGraph extends PureComponent { if (sampleTime < nextMinTime) { continue; } - const callNodeIndex = sampleCallNodes[i]; - if (callNodeIndex === null) { + const heightFuncResult = heightFunc(i); + if (heightFuncResult === null) { continue; } - const height = heightFunc({ - sampleIndex: i, - callNodeIndex, - yPixelsPerHeight, - }); + const height = heightFuncResult * yPixelsPerHeight; const xPos = (sampleTime - range[0]) * xPixelsPerMs; let samplesBucket; @@ -190,6 +166,9 @@ export class ThreadHeightGraph extends PureComponent { xPos: number[], }; function drawSamples(samplesBucket: SamplesBucket, color: string) { + if (samplesBucket.xPos.length === 0) { + return; + } ctx.fillStyle = color; for (let i = 0; i < samplesBucket.height.length; i++) { const height = samplesBucket.height[i]; diff --git a/src/components/shared/thread/SampleGraph.js b/src/components/shared/thread/SampleGraph.js index e4d658d965..9b2525495e 100644 --- a/src/components/shared/thread/SampleGraph.js +++ b/src/components/shared/thread/SampleGraph.js @@ -6,7 +6,6 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; import { InView } from 'react-intersection-observer'; -import { ensureExists } from 'firefox-profiler/utils/flow'; import { timeCode } from 'firefox-profiler/utils/time-code'; import { getSampleIndexClosestToCenteredTime } from 'firefox-profiler/profile-logic/profile-data'; import { bisectionRight } from 'firefox-profiler/utils/bisect'; @@ -20,7 +19,6 @@ import type { CategoryList, IndexIntoSamplesTable, Milliseconds, - IndexIntoCallNodeTable, SelectedState, } from 'firefox-profiler/types'; import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; @@ -29,7 +27,6 @@ type Props = {| +className: string, +thread: Thread, +samplesSelectedStates: null | SelectedState[], - +sampleCallNodes: Array, +interval: Milliseconds, +rangeStart: Milliseconds, +rangeEnd: Milliseconds, @@ -95,7 +92,6 @@ export class ThreadSampleGraphImpl extends PureComponent { rangeStart, rangeEnd, samplesSelectedStates, - sampleCallNodes, categories, width, height, @@ -144,8 +140,8 @@ export class ThreadSampleGraphImpl extends PureComponent { if (sampleTime < nextMinTime) { continue; } - const callNodeIndex = sampleCallNodes[i]; - if (callNodeIndex === null) { + const stackIndex = thread.samples.stack[i]; + if (stackIndex === null) { continue; } const xPos = @@ -157,10 +153,6 @@ export class ThreadSampleGraphImpl extends PureComponent { ) { samplesBucket = highlightedSamples; } else { - const stackIndex = ensureExists( - thread.samples.stack[i], - 'A stack must exist for this sample, since a callNodeIndex exists.' - ); const categoryIndex = thread.stackTable.category[stackIndex]; const category = categories[categoryIndex]; if (category.name === 'Idle') { @@ -174,8 +166,11 @@ export class ThreadSampleGraphImpl extends PureComponent { } function drawSamples(samplePositions: number[], color: string) { + if (samplePositions.length === 0) { + return; + } + ctx.fillStyle = color; for (let i = 0; i < samplePositions.length; i++) { - ctx.fillStyle = color; const startY = 0; const xPos = samplePositions[i]; ctx.fillRect(xPos, startY, drawnSampleWidth, canvas.height); diff --git a/src/components/shared/thread/StackGraph.js b/src/components/shared/thread/StackGraph.js index ca39da4a93..381546da6b 100644 --- a/src/components/shared/thread/StackGraph.js +++ b/src/components/shared/thread/StackGraph.js @@ -16,7 +16,6 @@ import type { IndexIntoCallNodeTable, SelectedState, } from 'firefox-profiler/types'; -import type { HeightFunctionParams } from './HeightGraph'; type Props = {| +className: string, @@ -38,21 +37,20 @@ type Props = {| |}; export class ThreadStackGraph extends PureComponent { - _heightFunction = ({ - callNodeIndex, - yPixelsPerHeight, - }: HeightFunctionParams): number => { - const { callNodeInfo } = this.props; - const { callNodeTable } = callNodeInfo; + _heightFunction = (sampleIndex: IndexIntoSamplesTable): number | null => { + const { callNodeInfo, sampleCallNodes } = this.props; + const callNodeIndex = sampleCallNodes[sampleIndex]; + if (callNodeIndex === null) { + return null; + } - return callNodeTable.depth[callNodeIndex] * yPixelsPerHeight; + return callNodeInfo.callNodeTable.depth[callNodeIndex]; }; render() { const { className, thread, - sampleCallNodes, samplesSelectedStates, interval, rangeStart, @@ -79,7 +77,6 @@ export class ThreadStackGraph extends PureComponent { trackName={trackName} interval={interval} thread={thread} - sampleCallNodes={sampleCallNodes} samplesSelectedStates={samplesSelectedStates} rangeStart={rangeStart} rangeEnd={rangeEnd} diff --git a/src/components/stack-chart/index.js b/src/components/stack-chart/index.js index 9f88a953f8..368fe0d699 100644 --- a/src/components/stack-chart/index.js +++ b/src/components/stack-chart/index.js @@ -72,7 +72,7 @@ type StateProps = {| +thread: Thread, +weightType: WeightType, +innerWindowIDToPageMap: Map | null, - +maxStackDepth: number, + +maxStackDepthPlusOne: number, +combinedTimingRows: CombinedTimingRows, +timeRange: StartEndRange, +interval: Milliseconds, @@ -210,7 +210,7 @@ class StackChartImpl extends React.PureComponent { const { thread, threadsKey, - maxStackDepth, + maxStackDepthPlusOne, combinedTimingRows, timeRange, interval, @@ -229,7 +229,7 @@ class StackChartImpl extends React.PureComponent { displayStackType, } = this.props; - const maxViewportHeight = maxStackDepth * STACK_FRAME_HEIGHT; + const maxViewportHeight = maxStackDepthPlusOne * STACK_FRAME_HEIGHT; return (
{ role="tabpanel" aria-labelledby="stack-chart-tab-button" > - + - {maxStackDepth === 0 && userTimings.length === 0 ? ( + {maxStackDepthPlusOne === 0 && userTimings.length === 0 ? ( ) : ( ({ thread: selectedThreadSelectors.getFilteredThread(state), // Use the raw WeightType here, as the stack chart does not use the call tree weightType: selectedThreadSelectors.getSamplesWeightType(state), - maxStackDepth: selectedThreadSelectors.getFilteredCallNodeMaxDepth(state), + maxStackDepthPlusOne: + selectedThreadSelectors.getFilteredCallNodeMaxDepthPlusOne(state), combinedTimingRows, timeRange: getCommittedRange(state), interval: getProfileInterval(state), diff --git a/src/components/timeline/TrackThread.js b/src/components/timeline/TrackThread.js index f8113c075c..e8ad4e35b6 100644 --- a/src/components/timeline/TrackThread.js +++ b/src/components/timeline/TrackThread.js @@ -285,7 +285,6 @@ class TimelineTrackThreadImpl extends PureComponent { thread={filteredThread} rangeStart={rangeStart} rangeEnd={rangeEnd} - sampleCallNodes={sampleCallNodes} samplesSelectedStates={samplesSelectedStates} categories={categories} onSampleClick={this._onSampleClick} @@ -301,7 +300,6 @@ class TimelineTrackThreadImpl extends PureComponent { rangeStart={rangeStart} rangeEnd={rangeEnd} callNodeInfo={callNodeInfo} - sampleCallNodes={sampleCallNodes} samplesSelectedStates={samplesSelectedStates} categories={categories} onSampleClick={this._onSampleClick} diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index 5f1499784b..af553d6686 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -10,6 +10,7 @@ import { getOriginAnnotationForFunc, getCategoryPairLabel, getBottomBoxInfoForCallNode, + getCallNodePathFromIndex, } from './profile-data'; import { resourceTypes } from './data-structures'; import { getFunctionName } from './function-info'; @@ -20,6 +21,7 @@ import type { SamplesLikeTable, WeightType, CallNodeTable, + CallNodePath, IndexIntoCallNodeTable, CallNodeInfo, CallNodeData, @@ -40,10 +42,11 @@ import type { CallTreeSummaryStrategy } from '../types/actions'; type CallNodeChildren = IndexIntoCallNodeTable[]; type CallNodeSummary = { self: Float32Array, + leaf: Float32Array, total: Float32Array, }; -export type CallTreeCountsAndSummary = { - callNodeChildCount: Uint32Array, +export type CallTreeTimings = { + callNodeHasChildren: Uint8Array, callNodeSummary: CallNodeSummary, rootCount: number, rootTotalSummary: number, @@ -71,7 +74,7 @@ export class CallTree { _callNodeInfo: CallNodeInfo; _callNodeTable: CallNodeTable; _callNodeSummary: CallNodeSummary; - _callNodeChildCount: Uint32Array; // A table column matching the callNodeTable + _callNodeHasChildren: Uint8Array; // A table column matching the callNodeTable _thread: Thread; _rootTotalSummary: number; _rootCount: number; @@ -87,7 +90,7 @@ export class CallTree { categories: CategoryList, callNodeInfo: CallNodeInfo, callNodeSummary: CallNodeSummary, - callNodeChildCount: Uint32Array, + callNodeHasChildren: Uint8Array, rootTotalSummary: number, rootCount: number, isHighPrecision: boolean, @@ -97,7 +100,7 @@ export class CallTree { this._callNodeInfo = callNodeInfo; this._callNodeTable = callNodeInfo.callNodeTable; this._callNodeSummary = callNodeSummary; - this._callNodeChildCount = callNodeChildCount; + this._callNodeHasChildren = callNodeHasChildren; this._thread = thread; this._rootTotalSummary = rootTotalSummary; this._rootCount = rootCount; @@ -107,6 +110,19 @@ export class CallTree { this._weightType = weightType; } + _getFirstChildIndex( + callNodeIndex: IndexIntoCallNodeTable | -1 + ): IndexIntoCallNodeTable | -1 { + if (callNodeIndex === -1) { + return this._callNodeTable.length !== 0 ? 0 : -1; + } + const subtreeRangeEnd = this._callNodeTable.subtreeRangeEnd[callNodeIndex]; + if (subtreeRangeEnd !== callNodeIndex + 1) { + return callNodeIndex + 1; + } + return -1; + } + getRoots() { return this.getChildren(-1); } @@ -114,26 +130,18 @@ export class CallTree { getChildren(callNodeIndex: IndexIntoCallNodeTable): CallNodeChildren { let children = this._children[callNodeIndex]; if (children === undefined) { - const childCount = - callNodeIndex === -1 - ? this._rootCount - : this._callNodeChildCount[callNodeIndex]; children = []; + const firstChild = this._getFirstChildIndex(callNodeIndex); for ( - let childCallNodeIndex = callNodeIndex + 1; - childCallNodeIndex < this._callNodeTable.length && - children.length < childCount; - childCallNodeIndex++ + let childCallNodeIndex = firstChild; + childCallNodeIndex !== -1; + childCallNodeIndex = this._callNodeTable.nextSibling[childCallNodeIndex] ) { - const childPrefixIndex = this._callNodeTable.prefix[childCallNodeIndex]; const childTotalSummary = this._callNodeSummary.total[childCallNodeIndex]; - const childChildCount = this._callNodeChildCount[childCallNodeIndex]; + const childHasChildren = this._callNodeHasChildren[childCallNodeIndex]; - if ( - childPrefixIndex === callNodeIndex && - (childTotalSummary !== 0 || childChildCount !== 0) - ) { + if (childTotalSummary !== 0 || childHasChildren !== 0) { children.push(childCallNodeIndex); } } @@ -148,7 +156,7 @@ export class CallTree { } hasChildren(callNodeIndex: IndexIntoCallNodeTable): boolean { - return this.getChildren(callNodeIndex).length !== 0; + return this._callNodeHasChildren[callNodeIndex] !== 0; } _addDescendantsToSet( @@ -362,6 +370,53 @@ export class CallTree { this._thread ); } + + /** + * Take a CallNodeIndex, and compute an inverted path for it. + * + * e.g: + * (invertedPath, invertedCallTree) => path + * (path, callTree) => invertedPath + * + * Call trees are sorted with the CallNodes with the heaviest total time as the first + * entry. This function walks to the tip of the heaviest branches to find the leaf node, + * then construct an inverted CallNodePath with the result. This gives a pretty decent + * result, but it doesn't guarantee that it will select the heaviest CallNodePath for the + * INVERTED call tree. This would require doing a round trip through the reducers or + * some other mechanism in order to first calculate the next inverted call tree. This is + * probably not worth it, so go ahead and use the uninverted call tree, as it's probably + * good enough. + */ + findHeavyPathToSameFunctionAfterInversion( + callNodeIndex: IndexIntoCallNodeTable | null + ): CallNodePath { + if (callNodeIndex === null) { + return []; + } + const heaviestPath = this.findHeaviestPathInSubtree(callNodeIndex); + const startingDepth = this._callNodeTable.depth[callNodeIndex]; + const partialPath = heaviestPath.slice(startingDepth); + return partialPath.reverse(); + } + + findHeaviestPathInSubtree( + callNodeIndex: IndexIntoCallNodeTable + ): CallNodePath { + const rangeEnd = this._callNodeTable.subtreeRangeEnd[callNodeIndex]; + + // Find the call node with the highest leaf time. + let maxNode = -1; + let maxAbs = 0; + for (let nodeIndex = callNodeIndex; nodeIndex < rangeEnd; nodeIndex++) { + const nodeLeaf = Math.abs(this._callNodeSummary.leaf[nodeIndex]); + if (maxNode === -1 || nodeLeaf > maxAbs) { + maxNode = nodeIndex; + maxAbs = nodeLeaf; + } + } + + return getCallNodePathFromIndex(maxNode, this._callNodeTable); + } } function _getInvertedStackSelf( @@ -457,12 +512,12 @@ function _getStackSelf( * what type of weight is in the SamplesLikeTable. For instance, it could be * milliseconds, sample counts, or bytes. */ -export function computeCallTreeCountsAndSummary( +export function computeCallTreeTimings( samples: SamplesLikeTable, sampleIndexToCallNodeIndex: Array, { callNodeTable }: CallNodeInfo, invertCallstack: boolean -): CallTreeCountsAndSummary { +): CallTreeTimings { // Inverted trees need a different method for computing the timing. const { callNodeSelf, callNodeLeaf } = invertCallstack ? _getInvertedStackSelf(samples, callNodeTable, sampleIndexToCallNodeIndex) @@ -470,10 +525,13 @@ export function computeCallTreeCountsAndSummary( // Compute the following variables: const callNodeTotalSummary = new Float32Array(callNodeTable.length); - const callNodeChildCount = new Uint32Array(callNodeTable.length); + const callNodeHasChildren = new Uint8Array(callNodeTable.length); let rootTotalSummary = 0; let rootCount = 0; + // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1858310 + const abs = Math.abs; + // We loop the call node table in reverse, so that we find the children // before their parents, and the total is known at the time we reach a // node. @@ -483,8 +541,8 @@ export function computeCallTreeCountsAndSummary( callNodeIndex-- ) { callNodeTotalSummary[callNodeIndex] += callNodeLeaf[callNodeIndex]; - rootTotalSummary += Math.abs(callNodeLeaf[callNodeIndex]); - const hasChildren = callNodeChildCount[callNodeIndex] !== 0; + rootTotalSummary += abs(callNodeLeaf[callNodeIndex]); + const hasChildren = callNodeHasChildren[callNodeIndex] !== 0; const hasTotalValue = callNodeTotalSummary[callNodeIndex] !== 0; if (!hasChildren && !hasTotalValue) { @@ -497,16 +555,17 @@ export function computeCallTreeCountsAndSummary( } else { callNodeTotalSummary[prefixCallNode] += callNodeTotalSummary[callNodeIndex]; - callNodeChildCount[prefixCallNode]++; + callNodeHasChildren[prefixCallNode] = 1; } } return { callNodeSummary: { self: callNodeSelf, + leaf: callNodeLeaf, total: callNodeTotalSummary, }, - callNodeChildCount, + callNodeHasChildren, rootTotalSummary, rootCount, }; @@ -519,19 +578,23 @@ export function getCallTree( thread: Thread, callNodeInfo: CallNodeInfo, categories: CategoryList, - callTreeCountsAndSummary: CallTreeCountsAndSummary, + callTreeTimings: CallTreeTimings, weightType: WeightType ): CallTree { return timeCode('getCallTree', () => { - const { callNodeSummary, callNodeChildCount, rootTotalSummary, rootCount } = - callTreeCountsAndSummary; + const { + callNodeSummary, + callNodeHasChildren, + rootTotalSummary, + rootCount, + } = callTreeTimings; return new CallTree( thread, categories, callNodeInfo, callNodeSummary, - callNodeChildCount, + callNodeHasChildren, rootTotalSummary, rootCount, Boolean(thread.isJsTracer), @@ -611,7 +674,7 @@ export function extractSamplesLikeTable( } /** - * This function is extremely similar to computeCallTreeCountsAndSummary, + * This function is extremely similar to computeCallTreeTimings, * but is specialized for converting sample counts into traced timing. Samples * don't have duration information associated with them, it's mostly how long they * were observed to be running. This function computes the timing the exact same @@ -671,7 +734,7 @@ export function computeTracedTiming( // Compute the following variables: const callNodeTotalSummary = new Float32Array(callNodeTable.length); - const callNodeChildCount = new Uint32Array(callNodeTable.length); + const callNodeHasChildren = new Uint8Array(callNodeTable.length); // We loop the call node table in reverse, so that we find the children // before their parents, and the total time is known at the time we reach a @@ -682,7 +745,7 @@ export function computeTracedTiming( callNodeIndex-- ) { callNodeTotalSummary[callNodeIndex] += callNodeLeaf[callNodeIndex]; - const hasChildren = callNodeChildCount[callNodeIndex] !== 0; + const hasChildren = callNodeHasChildren[callNodeIndex] !== 0; const hasTotalValue = callNodeTotalSummary[callNodeIndex] !== 0; if (!hasChildren && !hasTotalValue) { @@ -693,7 +756,7 @@ export function computeTracedTiming( if (prefixCallNode !== -1) { callNodeTotalSummary[prefixCallNode] += callNodeTotalSummary[callNodeIndex]; - callNodeChildCount[prefixCallNode]++; + callNodeHasChildren[prefixCallNode] = 1; } } diff --git a/src/profile-logic/data-structures.js b/src/profile-logic/data-structures.js index 6bac3c124b..0a50aea02d 100644 --- a/src/profile-logic/data-structures.js +++ b/src/profile-logic/data-structures.js @@ -25,6 +25,7 @@ import type { ExtensionTable, CategoryList, JsTracerTable, + CallNodeTable, } from 'firefox-profiler/types'; /** @@ -413,3 +414,23 @@ export function getEmptyProfile(): Profile { threads: [], }; } + +export function getEmptyCallNodeTable(): CallNodeTable { + return { + // Important! + // If modifying this structure, please update all callers of this function to ensure + // that they are pushing on correctly to the data structure. These pushes may not + // be caught by the type system. + prefix: new Int32Array(0), + subtreeRangeEnd: new Uint32Array(0), + nextSibling: new Int32Array(0), + func: new Int32Array(0), + category: new Int32Array(0), + subcategory: new Int32Array(0), + innerWindowID: new Float64Array(0), + sourceFramesInlinedIntoSymbol: [], + depth: [], + maxDepth: -1, + length: 0, + }; +} diff --git a/src/profile-logic/flame-graph.js b/src/profile-logic/flame-graph.js index f894a3b652..4d85d96774 100644 --- a/src/profile-logic/flame-graph.js +++ b/src/profile-logic/flame-graph.js @@ -5,12 +5,14 @@ // @flow import type { UnitIntervalOfProfileRange, - CallNodeInfo, CallNodeTable, + FuncTable, IndexIntoCallNodeTable, - Thread, } from 'firefox-profiler/types'; -import type { CallTreeCountsAndSummary } from './call-tree'; +import type { UniqueStringArray } from 'firefox-profiler/utils/unique-string-array'; +import type { CallTreeTimings } from './call-tree'; + +import { bisectionRightByStrKey } from 'firefox-profiler/utils/bisect'; export type FlameGraphDepth = number; export type IndexIntoFlameGraphTiming = number; @@ -38,226 +40,270 @@ export type FlameGraphTiming = Array<{ length: number, }>; -type RootsAndChildren = { - /** - * Conceptually, `children` is a collection of arrays, one for each - * callnode in the tree. Each array contains all immediate callnode - * children (with a non-zero total time) of a given callnode. - * - * To avoid heavy allocations for large call trees, the elements of - * this collection are not real array instances. Instead, one has to - * work with slices of one large array. - * - * Given a callnode `p` with callnode index `pi`, and `start` and - * `end` defined as: - * let start = children.offsets[pi]; - * let end = children.offsets[pi + 1]; - * - * then children.array.slice(start, end) is a sorted list of all - * callnode indices whose callnodes have `p` as their direct parent. - * The list is sorted in descending order with respect to the - * function names of the callnodes. - */ - children: { - // Array of IndexIntoCallNodeTable. This is a concatenation of all - // children sub-arrays. - array: Uint32Array, - // This array maps a given IndexIntoCallNodeTable to a slice - // within `array` by providing start and end indices. - offsets: Uint32Array, - }, - - // A list of every root CallNodeIndex in the call tree. - roots: IndexIntoCallNodeTable[], -}; - /** - * Obtain collections of callnode indices needed for building the - * flame graph. + * FlameGraphRows is an array of rows, where each row is an array of call node + * indexes. This is a timing-invariant representation of the flame graph and can + * be cached independently of the sample / timing information. + * + * When combined with the timing information, it is used to produce FlameGraphTiming. + * + * In FlameGraphRows, rows[depth] contains all the call nodes of that depth. + * The call nodes are ordered in the same order that they'll be displayed in the + * flame graph: + * + * - Siblings are ordered by function name. + * - Siblings are grouped, i.e. all nodes with the same prefix are next to each other. + * - The order of these groups with respect to each other is determined by how + * their prefix call nodes are ordered in the previous row. + * + * # Example ([call node index] [function name]) * - * The returned object contains two arrays, one for the roots and one - * for all children of the call tree. Along with the children array is - * an offset array used to index into it. + * ``` + * - 0 A + * - 1 D + * - 2 I + * - 3 B + * - 4 C + * - 5 F + * - 6 E + * - 7 G + * - 8 H + * ``` + * + * The call node table above produces the following FlameGraphRows: + * Depth 0: [0, 8] // ["A", "H"] + * Depth 1: [3, 1, 7] // ["B", "D", "G"] + * Depth 2: [4, 6, 5, 2] // ["C", "E", "F", "I"] + * + * Note that [4, 6, 5] are the children of 3 ("B"), sorted by name, and these + * children have been moved before 2 ("I") to match the order of their parents. + */ +export type FlameGraphRows = IndexIntoCallNodeTable[][]; + +/** + * Compute the FlameGraphRows. The result is independent of timing information. */ -export function getRootsAndChildren( - thread: Thread, +export function computeFlameGraphRows( callNodeTable: CallNodeTable, - callNodeChildCount: Uint32Array, - totalTime: Float32Array -): RootsAndChildren { - const roots = []; - const array = new Uint32Array(callNodeTable.length); - const offsets = new Uint32Array(callNodeTable.length + 1); - - /* For performance reasons the array is of type Uint32Array. This - * means we cannot use values such as `undefined` or `null` to - * indicate uninitialized values, as we build up the array. But - * since `callNodeTable` is ordered is such a way that a given - * callnode index always comes _after_ its parent callnode index, we - * know that callnode index zero never can be a child. It is always - * a root. (Not counting the special -1 root, but we don't need it - * here). Hence, we are free to use the value 0 in the children - * array to mark elements as not initialized, since 0 is never a - * valid child. Since the default values of Uint32Array is 0, we - * conveniently get an array where all its values are uninitialized - * from start. */ - - let callNodeIndex = 0; - let ptr = 0; - for (; callNodeIndex < callNodeTable.length; callNodeIndex++) { - offsets[callNodeIndex] = ptr; - ptr += callNodeChildCount[callNodeIndex]; - - if (totalTime[callNodeIndex] === 0) { - continue; - } + funcTable: FuncTable, + stringTable: UniqueStringArray +): FlameGraphRows { + if (callNodeTable.length === 0) { + return [[]]; + } + + const { func, nextSibling, subtreeRangeEnd, maxDepth } = callNodeTable; + const funcTableNameColumn = funcTable.name; + + // flameGraphRows is what we'll return from this function. + // + // Each row is conceptually partitioned into two parts: "Finished nodes" and + // "pending nodes". + // + // For row d, flameGraphRows[d] is partitioned as follows (a..b is the half-open + // range which includes a but excludes b): + // + // - flameGraphRows[d][0..pendingRangeStartAtDepth[d]] are "finished" + // - flameGraphRows[d][pendingRangeStartAtDepth[d]..] are "pending" + // + // A node starts out as "pending" when we initially add it to the row. + // A node becomes "finished" once we've decided to process its children. + // + // This is used to queue up a bunch of siblings before we process their + // children. + // We need to queue up nodes before we can process their children because + // we can only process children once their parents are in the right order. + const flameGraphRows = Array.from({ length: maxDepth + 1 }, () => []); + const pendingRangeStartAtDepth = new Int32Array(maxDepth + 1); - const parent = callNodeTable.prefix[callNodeIndex]; - if (parent === -1) { - roots.push(callNodeIndex); - continue; + // At the beginning of each turn of this loop, add currentCallNode and all its + // siblings as "pending" to row[currentDepth], ordered by name. Then find the + // first pending call node with children, and go to the next iteration. + let currentCallNode = 0; + let currentDepth = 0; // always set to depth[currentCallNode] + outer: while (true) { + // assert(depth[currentCallNode] === currentDepth); + + // Add currentCallNode and all its siblings to the current row. Ensure correct + // ordering when inserting each sibling. + const rowAtThisDepth = flameGraphRows[currentDepth]; + const siblingIndexRangeStart = rowAtThisDepth.length; // index into rowAtThisDepth + for ( + let currentSibling = currentCallNode; + currentSibling !== -1; + currentSibling = nextSibling[currentSibling] + ) { + const siblingIndexRangeEnd = rowAtThisDepth.length; + if (siblingIndexRangeStart === siblingIndexRangeEnd) { + // This is the first sibling that we see. We don't need to compute an + // insertion index because we don't have any other siblings to compare + // to yet. + rowAtThisDepth.push(currentSibling); + } else { + // There are other siblings already present in rowAtThisDepth[siblingIndexRangeStart..]. + // Do an ordered insert, to keep siblings ordered by function name. + // assert(siblingIndexRangeStart < siblingIndexRangeEnd) + const thisFunc = func[currentSibling]; + const funcName = stringTable.getString(funcTableNameColumn[thisFunc]); + const insertionIndex = bisectionRightByStrKey( + rowAtThisDepth, + funcName, + (cn) => stringTable.getString(funcTableNameColumn[func[cn]]), + siblingIndexRangeStart, + siblingIndexRangeEnd + ); + rowAtThisDepth.splice(insertionIndex, 0, currentSibling); + } } - const funcName = thread.stringTable.getString( - thread.funcTable.name[callNodeTable.func[callNodeIndex]] - ); - - /* From the parent, we can now know the slice allotted for all - * its children. */ - const start = offsets[parent]; - const end = offsets[parent] + callNodeChildCount[parent] - 1; - - /* Find the place in `array` where this callnode should be - * inserted, swapping elements in the array as we go - * along. Continue as long as this callnode's function name is - * lexically smaller than the function names of the callnodes - * already placed in the array. This ensures that all slices have - * children in descending order. Any callnode indices equal to 0 - * means that they are uninitialized, so just breeze through - * them. When we stop, when have found the right position to - * insert our callnode. - * - * This effectively is an insertion sort, which is O(n^2), but - * since n is typically small (the number of children of a given - * callnode), it should be just fine. - */ - let i = start; - while (i < end) { - if ( - array[i + 1] !== 0 && - funcName > - thread.stringTable.getString( - thread.funcTable.name[callNodeTable.func[array[i + 1]]] - ) - ) { - // We've found our spot if the next slot in the array is - // occupied with a callnode whose function name is less than - // ours. - break; + // Now currentCallNode and all its siblings have been added to the row, and + // they are ordered correctly. They are all marked as pending; + // pendingRangeStartAtDepth has not been advanced. + + // In the remainder of this loop iteration, all we'll be doing is to find + // the next node for processing. Starting at the current depth, but going to + // to more shallow depths if needed, we want to find the first pending node + // which has children. + + // We know that the current row has at least one remaining pending node + // (currentCallNode) so we start with this row. + let candidateDepth = currentDepth; + let candidateRow = rowAtThisDepth; + let indexInCandidateRow = pendingRangeStartAtDepth[candidateDepth]; + let candidateNode = candidateRow[indexInCandidateRow]; + + // candidateNode may not have any children. Keep searching, in this row and + // in more shallow rows, until we find a node which does have children. + + // At the end of this loop, candidateNode will be set to a node which has + // children, and the following will be true: + // candidateNode === flameGraphRows[candidateDepth][pendingRangeStartAtDepth[candidateDepth]] + // + // "while (!hasChildren(candidateNode))" + while (subtreeRangeEnd[candidateNode] === candidateNode + 1) { + // candidateNode does not have any children. + // "Finish" candidateNode by incrementing pendingRangeStartAtDepth[candidateDepth]. + indexInCandidateRow++; + pendingRangeStartAtDepth[candidateDepth] = indexInCandidateRow; + + // Find the next row which still has pending nodes, going to shallower + // depths until we hit the end. + while (indexInCandidateRow === candidateRow.length) { + // There are no more pending nodes in the current row - all nodes at + // this depth are already finished. + if (candidateDepth === 0) { + // We must have processed the entire tree at this point, and we are done. + break outer; + } + // Go to a shallower depth and continue the search there. + candidateDepth--; + candidateRow = flameGraphRows[candidateDepth]; + indexInCandidateRow = pendingRangeStartAtDepth[candidateDepth]; } - array[i] = array[i + 1]; - i++; + // candidateRow now has at least one pending node left. + candidateNode = candidateRow[indexInCandidateRow]; } - array[i] = callNodeIndex; + + // Now candidateNode is a pending node which has at least one child. + // assert(candidateNode === flameGraphRows[candidateDepth][pendingRangeStartAtDepth[candidateDepth]]) + // assert(subtreeRangeEnd[candidateNode] !== candidateNode + 1) + + // We have now decided to process this node, i.e. we know that we will add + // this node's children in the next loop iteration. + // "Finish" candidateNode by incrementing pendingRangeStartAtDepth[candidateDepth]. + pendingRangeStartAtDepth[candidateDepth] = indexInCandidateRow + 1; + + // Advance to candidateNode's first child. Due to the way call nodes are ordered, + // the first child of x (if present) is always at x + 1. + currentCallNode = candidateNode + 1; // "currentCallNode = firstChild[candidateNode];" + currentDepth = candidateDepth + 1; } - offsets[callNodeIndex] = ptr; - - // The children are already sorted, but the roots aren't. - // Let's sort the roots in descending order, just like the children. - roots.sort((rootA, rootB) => { - const [nameA, nameB] = [rootA, rootB].map((callNodeIndex) => - thread.stringTable.getString( - thread.funcTable.name[callNodeTable.func[callNodeIndex]] - ) - ); - if (nameA < nameB) { - return 1; - } - if (nameA === nameB) { - return 0; - } - return -1; - }); - return { roots, children: { array, offsets } }; + + return flameGraphRows; } /** * Build a FlameGraphTiming table from a call tree. */ export function getFlameGraphTiming( - thread: Thread, - callNodeInfo: CallNodeInfo, - callTreeCountsAndSummary: CallTreeCountsAndSummary + flameGraphRows: FlameGraphRows, + callNodeTable: CallNodeTable, + callTreeTimings: CallTreeTimings ): FlameGraphTiming { - const { callNodeChildCount, callNodeSummary, rootTotalSummary } = - callTreeCountsAndSummary; - - const { roots, children } = getRootsAndChildren( - thread, - callNodeInfo.callNodeTable, - callNodeChildCount, - callNodeSummary.total - ); + const { callNodeSummary, rootTotalSummary } = callTreeTimings; + const { total, self } = callNodeSummary; + const { prefix } = callNodeTable; + + // This is where we build up the return value, one row at a time. const timing = []; - // Array of call nodes to recursively process in the loop below. - // Start with the roots of the call tree. - const stack: Array<{ - depth: number, - nodeIndex: IndexIntoCallNodeTable, - }> = roots.map((nodeIndex) => ({ nodeIndex, depth: 0 })); - - // Keep track of time offset by depth level. - const timeOffset = [0.0]; - - while (stack.length) { - const { depth, nodeIndex } = stack.pop(); - - // Select an existing row, or create a new one. - let row = timing[depth]; - if (row === undefined) { - row = { - start: [], - end: [], - selfRelative: [], - callNode: [], - length: 0, - }; - timing[depth] = row; - } + // This is used to adjust the start position of a call node's box based on the + // start position of its prefix node's box. + const startPerCallNode = new Float32Array(callNodeTable.length); - // Take the absolute value, as native deallocations can be negative. - const totalRelative = Math.abs( - callNodeSummary.total[nodeIndex] / rootTotalSummary - ); - const selfRelative = Math.abs( - callNodeSummary.self[nodeIndex] / rootTotalSummary - ); - - // Compute the timing information. - row.start.push(timeOffset[depth]); - row.end.push(timeOffset[depth] + totalRelative); - row.selfRelative.push(selfRelative); - row.callNode.push(nodeIndex); - row.length++; - - // Before we add the total time of this node to the time offset, - // we'll make sure that the first child (if any) begins with the - // same time offset. - timeOffset[depth + 1] = timeOffset[depth]; - timeOffset[depth] += totalRelative; - - // The items in the children array are sorted in descending order, - // but since they are popped from the stack at the top of the - // while loop they'll be processed in ascending order. - for ( - let offset = children.offsets[nodeIndex]; - offset < children.offsets[nodeIndex + 1]; - offset++ - ) { - stack.push({ nodeIndex: children.array[offset], depth: depth + 1 }); + // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1858310 + const abs = Math.abs; + + // Go row by row. + for (let depth = 0; depth < flameGraphRows.length; depth++) { + const rowNodes = flameGraphRows[depth]; + + const start = []; + const end = []; + const selfRelative = []; + const timingCallNodes = []; + + // Process the call nodes in this row. Sibling boxes are adjacent to each other. + // Whenever the prefix changes, we need to add a gap so that the child boxes + // start at the same position as the parent box. + // + // Previous row: [B ][D ] [G ] + // Current row: [C][E][F] [I ] + // (Note that this is upside down from how the flame graph is usually displayed) + let currentStart = 0; + let previousPrefixCallNode = -1; + for (let indexInRow = 0; indexInRow < rowNodes.length; indexInRow++) { + const nodeIndex = rowNodes[indexInRow]; + const totalVal = total[nodeIndex]; + if (totalVal === 0) { + // Skip boxes with zero width. + continue; + } + + const nodePrefix = prefix[nodeIndex]; + if (nodePrefix !== previousPrefixCallNode) { + // We have advanced to a node with a different parent, so we need to + // jump ahead to the parent box's start position. + currentStart = startPerCallNode[nodePrefix]; + previousPrefixCallNode = nodePrefix; + } + + // Write down the start position of this call node so that it can be + // checked later by this node's children. + startPerCallNode[nodeIndex] = currentStart; + + // Take the absolute value, as native deallocations can be negative. + const totalRelativeVal = abs(totalVal / rootTotalSummary); + const selfRelativeVal = abs(self[nodeIndex] / rootTotalSummary); + + const currentEnd = currentStart + totalRelativeVal; + start.push(currentStart); + end.push(currentEnd); + selfRelative.push(selfRelativeVal); + timingCallNodes.push(nodeIndex); + + // The start position of the next box is the end position of the current box. + currentStart = currentEnd; } + timing[depth] = { + start, + end, + selfRelative, + callNode: timingCallNodes, + length: timingCallNodes.length, + }; } + return timing; } diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index 5f4749069c..a16ae62193 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -12,6 +12,7 @@ import { getEmptyUnbalancedNativeAllocationsTable, getEmptyBalancedNativeAllocationsTable, getEmptyStackTable, + getEmptyCallNodeTable, shallowCloneFrameTable, shallowCloneFuncTable, } from './data-structures'; @@ -106,23 +107,30 @@ export function getCallNodeInfo( ): CallNodeInfo { return timeCode('getCallNodeInfo', () => { const stackIndexToCallNodeIndex = new Uint32Array(stackTable.length); - const funcCount = funcTable.length; - // Maps can't key off of two items, so combine the prefixCallNode and the funcIndex - // using the following formula: prefixCallNode * funcCount + funcIndex => callNode - const prefixCallNodeAndFuncToCallNodeMap = new Map(); // The callNodeTable components. const prefix: Array = []; + const firstChild: Array = []; + const nextSibling: Array = []; const func: Array = []; const category: Array = []; const subcategory: Array = []; const innerWindowID: Array = []; - const depth: Array = []; const sourceFramesInlinedIntoSymbol: Array< IndexIntoNativeSymbolTable | -1 | null, > = []; let length = 0; + // An extra column that only gets used while the table is built up: For each + // node A, currentLastChild[A] tracks the last currently-known child node of A. + // It is updated whenever a new node is created; e.g. creating node B updates + // currentLastChild[prefix[B]]. + // currentLastChild[A] is -1 while A has no children. + const currentLastChild: Array = []; + + // The last currently-known root node, i.e. the last known "child of -1". + let currentLastRoot = -1; + function addCallNode( prefixIndex: IndexIntoCallNodeTable, funcIndex: IndexIntoFuncTable, @@ -138,10 +146,34 @@ export function getCallNodeInfo( subcategory[index] = subcategoryIndex; innerWindowID[index] = windowID; sourceFramesInlinedIntoSymbol[index] = inlinedIntoSymbol; + + // Initialize these firstChild and nextSibling to -1. They will be updated + // once this node's first child or next sibling gets created. + firstChild[index] = -1; + nextSibling[index] = -1; + currentLastChild[index] = -1; + + // Update the next sibling of our previous sibling, and the first child of + // our prefix (if we're the first child). + // Also set this node's depth. if (prefixIndex === -1) { - depth[index] = 0; + // This node is a root. Just update the previous root's nextSibling. Because + // this node has no parent, there's also no firstChild information to update. + if (currentLastRoot !== -1) { + nextSibling[currentLastRoot] = index; + } + currentLastRoot = index; } else { - depth[index] = depth[prefixIndex] + 1; + // This node is not a root: update both firstChild and nextSibling information + // when appropriate. + const prevSiblingIndex = currentLastChild[prefixIndex]; + if (prevSiblingIndex === -1) { + // This is the first child for this prefix. + firstChild[prefixIndex] = index; + } else { + nextSibling[prevSiblingIndex] = index; + } + currentLastChild[prefixIndex] = index; } } @@ -156,19 +188,32 @@ export function getCallNodeInfo( const frameIndex = stackTable.frame[stackIndex]; const categoryIndex = stackTable.category[stackIndex]; const subcategoryIndex = stackTable.subcategory[stackIndex]; - const windowID = frameTable.innerWindowID[frameIndex] || 0; const inlinedIntoSymbol = frameTable.inlineDepth[frameIndex] > 0 ? frameTable.nativeSymbol[frameIndex] : null; const funcIndex = frameTable.func[frameIndex]; - const prefixCallNodeAndFuncIndex = prefixCallNode * funcCount + funcIndex; // Check if the call node for this stack already exists. - let callNodeIndex = prefixCallNodeAndFuncToCallNodeMap.get( - prefixCallNodeAndFuncIndex - ); - if (callNodeIndex === undefined) { + let callNodeIndex = -1; + if (stackIndex !== 0) { + const currentFirstSibling = + prefixCallNode === -1 ? 0 : firstChild[prefixCallNode]; + for ( + let currentSibling = currentFirstSibling; + currentSibling !== -1; + currentSibling = nextSibling[currentSibling] + ) { + if (func[currentSibling] === funcIndex) { + callNodeIndex = currentSibling; + break; + } + } + } + + if (callNodeIndex === -1) { + const windowID = frameTable.innerWindowID[frameIndex] || 0; + // New call node. callNodeIndex = length; addCallNode( @@ -179,10 +224,6 @@ export function getCallNodeInfo( windowID, inlinedIntoSymbol ); - prefixCallNodeAndFuncToCallNodeMap.set( - prefixCallNodeAndFuncIndex, - callNodeIndex - ); } else { // There is already a call node for this function. Use it, and check if // there are any conflicts between the various stack nodes that have been @@ -213,19 +254,144 @@ export function getCallNodeInfo( } stackIndexToCallNodeIndex[stackIndex] = callNodeIndex; } + return _createCallNodeInfoFromUnorderedComponents( + prefix, + firstChild, + nextSibling, + func, + category, + subcategory, + innerWindowID, + sourceFramesInlinedIntoSymbol, + length, + stackIndexToCallNodeIndex + ); + }); +} + +/** + * Create a CallNodeInfo with an ordered call node table based on the pieces of + * an unordered call node table. + * + * The order of siblings is maintained. + * If a node A has children, its first child B directly follows A. + * Otherwise, the node following A is A's next sibling (if it has one), or the + * next sibling of the closest ancestor which has a next sibling. + * This means that any node and all its descendants are laid out contiguously. + */ +function _createCallNodeInfoFromUnorderedComponents( + prefix: Array, + firstChild: Array, + nextSibling: Array, + func: Array, + category: Array, + subcategory: Array, + innerWindowID: Array, + sourceFramesInlinedIntoSymbol: Array, + length: number, + stackIndexToCallNodeIndex: Uint32Array +): CallNodeInfo { + return timeCode('createCallNodeInfoFromUnorderedComponents', () => { + if (length === 0) { + return { + callNodeTable: getEmptyCallNodeTable(), + stackIndexToCallNodeIndex: new Uint32Array(0), + }; + } + + const prefixSorted = new Int32Array(length); + const nextSiblingSorted = new Int32Array(length); + const subtreeRangeEndSorted = new Uint32Array(length); + const funcSorted = new Int32Array(length); + const categorySorted = new Int32Array(length); + const subcategorySorted = new Int32Array(length); + const innerWindowIDSorted = new Float64Array(length); + const sourceFramesInlinedIntoSymbolSorted = new Array(length); + const depthSorted = new Array(length); + let maxDepth = 0; + + // Traverse the entire tree, as follows: + // 1. nextOldIndex is the next node in DFS order. Copy over all values from + // the unsorted columns into the sorted columns. + // 2. Find the next node in DFS order, set nextOldIndex to it, and continue + // to the next loop iteration. + const oldIndexToNewIndex = new Uint32Array(length); + let nextOldIndex = 0; + let nextNewIndex = 0; + let currentDepth = 0; + let currentOldPrefix = -1; + let currentNewPrefix = -1; + while (nextOldIndex !== -1) { + const oldIndex = nextOldIndex; + const newIndex = nextNewIndex; + oldIndexToNewIndex[oldIndex] = newIndex; + nextNewIndex++; + + prefixSorted[newIndex] = currentNewPrefix; + funcSorted[newIndex] = func[oldIndex]; + categorySorted[newIndex] = category[oldIndex]; + subcategorySorted[newIndex] = subcategory[oldIndex]; + innerWindowIDSorted[newIndex] = innerWindowID[oldIndex]; + sourceFramesInlinedIntoSymbolSorted[newIndex] = + sourceFramesInlinedIntoSymbol[oldIndex]; + depthSorted[newIndex] = currentDepth; + // The remaining two columns, nextSiblingSorted and subtreeRangeEndSorted, + // will be filled in when we get to the end of the current subtree. + + // Find the next index in DFS order: If we have children, then our first child + // is next. Otherwise, we need to advance to our next sibling, if we have one, + // otherwise to the next sibling of the first ancestor which has one. + const oldFirstChild = firstChild[oldIndex]; + if (oldFirstChild !== -1) { + // We have children. Our first child is the next node in DFS order. + currentOldPrefix = oldIndex; + currentNewPrefix = newIndex; + nextOldIndex = oldFirstChild; + currentDepth++; + if (currentDepth > maxDepth) { + maxDepth = currentDepth; + } + continue; + } + + // We have no children. The next node is the next sibling of this node or + // of an ancestor node. Now is also a good time to fill in the values for + // subtreeRangeEnd and nextSibling. + subtreeRangeEndSorted[newIndex] = nextNewIndex; + nextOldIndex = nextSibling[oldIndex]; + nextSiblingSorted[newIndex] = nextOldIndex === -1 ? -1 : nextNewIndex; + while (nextOldIndex === -1 && currentOldPrefix !== -1) { + subtreeRangeEndSorted[currentNewPrefix] = nextNewIndex; + const oldPrefixNextSibling = nextSibling[currentOldPrefix]; + nextSiblingSorted[currentNewPrefix] = + oldPrefixNextSibling === -1 ? -1 : nextNewIndex; + nextOldIndex = oldPrefixNextSibling; + currentOldPrefix = prefix[currentOldPrefix]; + currentNewPrefix = prefixSorted[currentNewPrefix]; + currentDepth--; + } + } const callNodeTable: CallNodeTable = { - prefix: new Int32Array(prefix), - func: new Int32Array(func), - category: new Int32Array(category), - subcategory: new Int32Array(subcategory), - innerWindowID: new Float64Array(innerWindowID), - sourceFramesInlinedIntoSymbol, - depth, + prefix: prefixSorted, + subtreeRangeEnd: subtreeRangeEndSorted, + nextSibling: nextSiblingSorted, + func: funcSorted, + category: categorySorted, + subcategory: subcategorySorted, + innerWindowID: innerWindowIDSorted, + sourceFramesInlinedIntoSymbol: sourceFramesInlinedIntoSymbolSorted, + depth: depthSorted, + maxDepth, length, }; - return { callNodeTable, stackIndexToCallNodeIndex }; + return { + callNodeTable, + stackIndexToCallNodeIndex: stackIndexToCallNodeIndex.map( + (oldIndex) => oldIndexToNewIndex[oldIndex] + ), + }; }); } @@ -281,129 +447,57 @@ function getSamplesSelectedStatesForNoSelection( } /** - * Compute a Map, represented as a - * Uint8Array, which describes the relation of each call node with respect to - * the selected call node: - * - * ```typescript - * const enum CallNodeFlag { - * Unvisited = 0, - * InsideSelectedSubtree = 1, - * AncestorOfSelectedNode = 2, - * BeforeSelected = 3, - * AfterSelected = 4, - * } - * ``` - * - * The ordering BeforeSelected / AfterSelected is determined in the same way - * as in the function compareCallNodes in getTreeOrderComparator: Any nodes that - * would be visited before the selected subtree in a depth-first traversal are - * "before", and sibling nodes are ordered by call node index. - * - * Example: + * Given the call node for each sample and the call node selected states, + * compute each sample's selected state. * - * Here's an example tree where each node is represented as [flag], [callNodeIndex]. - * The tree nodes are written down in depth-first traversal order, but their call - * node indexes are not in that order. The only guaranteed relationship between - * call node indexes is, as usual, that every node has a higher index than its - * prefix node. + * For samples that are not filtered out, the sample's selected state is based + * on the relation of the sample's call node to the selected call node: Any call + * nodes in the selected node's subtree are "selected"; all other nodes are + * either "before" or "after" the selected subtree. * - * In this example, call node 14 is the selected call node. All descendants of - * that node (including the selected node itself) are InsideSelectedSubtree. + * Call node tables are ordered in depth-first traversal order, so we can + * determine whether a node is before, inside or after a subtree simply by + * comparing the call node index to the "selected index range". Example: * * ``` - * BeforeSelected, 0 - * BeforeSelected, 1 - * BeforeSelected, 3 - * BeforeSelected, 4 - * AncestorOfSelectedNode, 2 - * BeforeSelected, 6 - * BeforeSelected, 10 - * BeforeSelected, 11 - * BeforeSelected, 12 - * AncestorOfSelectedNode, 7 - * BeforeSelected, 13 - * BeforeSelected, 22 - * BeforeSelected, 23 - * InsideSelectedSubtree, 14 - * InsideSelectedSubtree, 15 - * InsideSelectedSubtree, 17 - * InsideSelectedSubtree, 18 - * InsideSelectedSubtree, 20 - * InsideSelectedSubtree, 16 - * InsideSelectedSubtree, 19 - * InsideSelectedSubtree, 27 - * AfterSelected, 21 - * AfterSelected, 24 - * AfterSelected, 25 - * AfterSelected, 8 - * AfterSelected, 9 - * AfterSelected, 5 - * AfterSelected, 26 + * before, 0 + * before, 1 + * before, 2 + * before, 3 + * before, 4 + * before, 5 + * before, 6 + * before, 7 + * before, 8 + * before, 9 + * before, 10 + * before, 11 + * before, 12 + * selected, 13 <-- selected node + * selected, 14 + * selected, 15 + * selected, 16 + * selected, 17 + * selected, 18 + * selected, 19 + * selected, 20 + * after, 21 + * after, 22 + * after, 23 + * after, 24 + * after, 25 + * after, 26 + * after, 27 * ``` * - * The activity graph treats AncestorOfSelectedNode and BeforeSelected the - * same; both are "before the selection". They just have two different flag values - * here because the loop in this function needs to treat them differently. - */ -function getCallNodeSelectedStates( - callNodeTable: CallNodeTable, - selectedCallNodeIndex: IndexIntoCallNodeTable -): Uint8Array { - // Precompute an array containing the call node indexes for the selected call - // node and its ancestors. - const selectedCallNodeDepth = callNodeTable.depth[selectedCallNodeIndex]; - const selectionAncestorAtDepth: IndexIntoCallNodeTable[] = new Array( - selectedCallNodeDepth - ); - for ( - let callNodeIndex = selectedCallNodeIndex, depth = selectedCallNodeDepth; - depth >= 0; - depth--, callNodeIndex = callNodeTable.prefix[callNodeIndex] - ) { - selectionAncestorAtDepth[depth] = callNodeIndex; - } - - // Do a single pass over the call nodes and compute each node's selected state. - const callNodeCount = callNodeTable.length; - const callNodePrefixes = callNodeTable.prefix; - const callNodeDepths = callNodeTable.depth; - const callNodeSelectedStates = new Uint8Array(callNodeCount); - for (let callNodeIndex = 0; callNodeIndex < callNodeCount; callNodeIndex++) { - const prefix = callNodePrefixes[callNodeIndex]; - let callNodeFlag = prefix !== -1 ? callNodeSelectedStates[prefix] : 2; - if (callNodeFlag === 2 /* CallNodeFlag.AncestorOfSelectedNode */) { - const depth = callNodeDepths[callNodeIndex]; - // assert(depth <= selectedCallNodeDepth); - const selectionAncestorAtThisDepth = selectionAncestorAtDepth[depth]; - if (callNodeIndex === selectionAncestorAtThisDepth) { - if (depth === selectedCallNodeDepth) { - callNodeFlag = 1 /* CallNodeFlag.InsideSelectedSubtree */; - } else { - callNodeFlag = 2 /* CallNodeFlag.AncestorOfSelectedNode */; - } - } else if (callNodeIndex < selectionAncestorAtThisDepth) { - callNodeFlag = 3 /* CallNodeFlag.BeforeSelected */; - } else { - callNodeFlag = 4 /* CallNodeFlag.AfterSelected */; - } - } else { - // All other flags are inherited. For example, if the parent is - // BeforeSelected, then so is its entire subtree. Do nothing. - } - callNodeSelectedStates[callNodeIndex] = callNodeFlag; - } - return callNodeSelectedStates; -} - -/** - * Given the call node for each sample and the call node selected states, - * compute each sample's selected state. + * In this example, the selected node has index 13 and the "selected index range" + * is the range from 13 to 21 (not including 21). */ function mapCallNodeSelectedStatesToSamples( sampleCallNodes: Array, activeTabFilteredCallNodes: Array, - callNodeSelectedStates: Uint8Array + selectedCallNodeIndex: IndexIntoCallNodeTable, + selectedCallNodeDescendantsEndIndex: IndexIntoCallNodeTable ): SelectedState[] { const sampleCount = sampleCallNodes.length; const samplesSelectedStates = new Array(sampleCount); @@ -411,21 +505,12 @@ function mapCallNodeSelectedStatesToSamples( let sampleSelectedState: SelectedState = 'SELECTED'; const callNodeIndex = sampleCallNodes[sampleIndex]; if (callNodeIndex !== null) { - switch (callNodeSelectedStates[callNodeIndex]) { - case 1 /* CallNodeFlags.InsideSelectedSubtree */: - sampleSelectedState = 'SELECTED'; - break; - case 2 /* CallNodeFlags.AncestorOfSelectedNode */: - case 3 /* CallNodeFlags.BeforeSelected */: - sampleSelectedState = 'UNSELECTED_ORDERED_BEFORE_SELECTED'; - break; - case 4 /* CallNodeFlags.AfterSelected */: - sampleSelectedState = 'UNSELECTED_ORDERED_AFTER_SELECTED'; - break; - default: - throw new Error( - `Unexpected value ${callNodeSelectedStates[callNodeIndex]} at callNodeSelectedStates[${callNodeIndex}]` - ); + if (callNodeIndex < selectedCallNodeIndex) { + sampleSelectedState = 'UNSELECTED_ORDERED_BEFORE_SELECTED'; + } else if (callNodeIndex < selectedCallNodeDescendantsEndIndex) { + sampleSelectedState = 'SELECTED'; + } else { + sampleSelectedState = 'UNSELECTED_ORDERED_AFTER_SELECTED'; } } else { // This sample was filtered out. @@ -461,14 +546,11 @@ export function getSamplesSelectedStates( ); } - const callNodeSelectedStates = getCallNodeSelectedStates( - callNodeTable, - selectedCallNodeIndex - ); return mapCallNodeSelectedStatesToSamples( sampleCallNodes, activeTabFilteredCallNodes, - callNodeSelectedStates + selectedCallNodeIndex, + callNodeTable.subtreeRangeEnd[selectedCallNodeIndex] ); } @@ -484,30 +566,6 @@ export function getLeafFuncIndex(path: CallNodePath): IndexIntoFuncTable { return path[path.length - 1]; } -/** - * Compute the descendants of a call node. Each item of the returned array is - * either 0 or 1; 1 means "this node is a descendant of subtreeRoot". - */ -export function computeDescendantCallNodes( - callNodeTable: CallNodeTable, - subtreeRoot: IndexIntoCallNodeTable -): Uint8Array { - const callNodeCount = callNodeTable.length; - const callNodePrefixes = callNodeTable.prefix; - const result = new Uint8Array(callNodeCount); - for (let callNodeIndex = 0; callNodeIndex < callNodeCount; callNodeIndex++) { - if (callNodeIndex === subtreeRoot) { - result[callNodeIndex] = 1; - } else { - const callNodePrefix = callNodePrefixes[callNodeIndex]; - const isDescendant = - callNodePrefix !== -1 && result[callNodePrefix] === 1; - result[callNodeIndex] = isDescendant ? 1 : 0; - } - } - return result; -} - export type JsImplementation = | 'interpreter' | 'blinterp' @@ -777,12 +835,8 @@ export function getTimingsForCallNodeIndex( return { forPath: pathTimings, rootTime }; } - // Do one pass over the call node table to compute which call nodes are - // descendants of needleNodeIndex. - const isDescendantOfNeedle = computeDescendantCallNodes( - callNodeTable, - needleNodeIndex - ); + const needleDescendantsEndIndex = + callNodeTable.subtreeRangeEnd[needleNodeIndex]; const needleNodeIsRootOfInvertedTree = isInvertedTree && callNodeTable.prefix[needleNodeIndex] === -1; @@ -810,7 +864,10 @@ export function getTimingsForCallNodeIndex( } } - if (isDescendantOfNeedle[thisNodeIndex] === 1) { + if ( + thisNodeIndex >= needleNodeIndex && + thisNodeIndex < needleDescendantsEndIndex + ) { // One of the parents is the exact passed path. accumulateDataToTimings(pathTimings.totalTime, sampleIndex, weight); @@ -1880,6 +1937,7 @@ function _getCallNodeIndexFromPathWithCache( // start from the start, and use `-1` which is the prefix we use to indicate // the root node. if (index === undefined) { + // assert(i === 0); index = -1; } @@ -1913,23 +1971,29 @@ function _getCallNodeIndexFromPathWithCache( // Returns the CallNodeIndex that matches the function `func` and whose parent's // CallNodeIndex is `parent`. export function getCallNodeIndexFromParentAndFunc( - parent: IndexIntoCallNodeTable, + parent: IndexIntoCallNodeTable | -1, func: IndexIntoFuncTable, callNodeTable: CallNodeTable ): IndexIntoCallNodeTable | null { + if (parent === -1) { + if (callNodeTable.length === 0) { + return null; + } + } else if (callNodeTable.subtreeRangeEnd[parent] === parent + 1) { + // parent has no children. + return null; + } // Node children always come after their parents in the call node table, // that's why we start looping at `parent + 1`. // Note that because the root parent is `-1`, we correctly start at `0` when - // we look for a top-level item. + // we look for a root. + const firstChild = parent + 1; for ( - let callNodeIndex = parent + 1; // the root parent is -1 - callNodeIndex < callNodeTable.length; - callNodeIndex++ + let callNodeIndex = firstChild; + callNodeIndex !== -1; + callNodeIndex = callNodeTable.nextSibling[callNodeIndex] ) { - if ( - callNodeTable.prefix[callNodeIndex] === parent && - callNodeTable.func[callNodeIndex] === func - ) { + if (callNodeTable.func[callNodeIndex] === func) { return callNodeIndex; } } @@ -1991,22 +2055,24 @@ export function convertStackToCallNodeAndCategoryPath( } /** - * Compute maximum depth of call stack for a given thread. + * Compute maximum depth of call stack for a given thread, and return maxDepth+1. + * This value can be used as the length for any per-depth arrays. * - * Returns the depth of the deepest call node, but with a one-based - * depth instead of a zero-based. + * The depth for a root node is zero. + * So if you only a single sample whose call node is a root node, this function + * returns 1. * - * If there are no samples, or the stacks are all filtered out for the samples, then - * 0 is returned. + * If there are no samples, or the stacks are all filtered out for the samples, + * then 0 is returned. */ -export function computeCallNodeMaxDepth( +export function computeCallNodeMaxDepthPlusOne( samples: SamplesLikeTable, callNodeInfo: CallNodeInfo ): number { // Compute the depth on a per-sample basis. This is done since the callNodeInfo is // 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 max = -1; + let maxDepth = -1; const { callNodeTable, stackIndexToCallNodeIndex } = callNodeInfo; for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) { const stackIndex = samples.stack[sampleIndex]; @@ -2015,10 +2081,12 @@ export function computeCallNodeMaxDepth( } const callNodeIndex = stackIndexToCallNodeIndex[stackIndex]; const depth = callNodeTable.depth[callNodeIndex]; - max = Math.max(max, depth); + if (depth > maxDepth) { + maxDepth = depth; + } } - return max + 1; + return maxDepth + 1; } export function invertCallstack( @@ -2540,61 +2608,26 @@ export function getFuncNamesAndOriginsForPath( } /** - * Return a function that can compare two samples' call nodes, and determine a sort order. + * Return a function that can compare two samples' call nodes, and determine + * which node is "before" the other. + * We use the call node index for this order. In the call node table, call nodes + * are ordered in depth-first traversal order, so we can just compare those + * indexes. + * + * This order is used for the activity graph. The tree order comparator is used + * specifically for hit testing, but we also compare call nodes in the same way + * in mapCallNodeSelectedStatesToSamples, which is what gets used for determining + * which areas of the graph to draw in with the selection highlight fill. * - * The order is determined as follows: - * - Ancestor call nodes are ordered before their descendants. - * - Sibling call nodes are ordered by their call node index. - * This order can be different than the order of the rows that are displayed in the - * call tree, because it does not take any sample information into account. This - * makes it independent of any range selection and cheaper to compute. + * "Ordered after" means "swims on top in the activity graph". + * + * The depth-first traversal order has the nice property that the nodes of a + * subtree are located in a contiguous index range. This means that the + * highlighted area for a selected subtree is contiguous in the graph. */ export function getTreeOrderComparator( - callNodeTable: CallNodeTable, sampleCallNodes: Array ): (IndexIntoSamplesTable, IndexIntoSamplesTable) => number { - /** - * Determine the ordering of two non-null call nodes. - */ - function compareCallNodes( - callNodeA: IndexIntoCallNodeTable, - callNodeB: IndexIntoCallNodeTable - ): number { - const initialDepthA = callNodeTable.depth[callNodeA]; - const initialDepthB = callNodeTable.depth[callNodeB]; - let depthA = initialDepthA; - let depthB = initialDepthB; - - // Walk call tree towards the roots until the call nodes are at the same depth. - while (depthA > depthB) { - callNodeA = callNodeTable.prefix[callNodeA]; - depthA--; - } - while (depthB > depthA) { - callNodeB = callNodeTable.prefix[callNodeB]; - depthB--; - } - - // Sort the call nodes by the initial depth. - if (callNodeA === callNodeB) { - return initialDepthA - initialDepthB; - } - - // The call nodes are at the same depth, walk towards the roots until a match is - // is found, then sort them based on stack order. - while (true) { - const parentNodeA = callNodeTable.prefix[callNodeA]; - const parentNodeB = callNodeTable.prefix[callNodeB]; - if (parentNodeA === parentNodeB) { - break; - } - callNodeA = parentNodeA; - callNodeB = parentNodeB; - } - - return callNodeA - callNodeB; - } - /** * Determine the ordering of (possibly null) call nodes for two given samples. * Returns a value < 0 if sampleA is ordered before sampleB, @@ -2622,7 +2655,7 @@ export function getTreeOrderComparator( // B filtered out, A not filtered out. B goes after A. return -1; } - return compareCallNodes(callNodeA, callNodeB); + return callNodeA - callNodeB; }; } diff --git a/src/profile-logic/stack-timing.js b/src/profile-logic/stack-timing.js index a8ef4553ed..47b0a80ed2 100644 --- a/src/profile-logic/stack-timing.js +++ b/src/profile-logic/stack-timing.js @@ -7,7 +7,6 @@ import type { SamplesLikeTable, Milliseconds, CallNodeInfo, - CallNodeTable, IndexIntoCallNodeTable, } from 'firefox-profiler/types'; /** @@ -57,135 +56,123 @@ export type StackTiming = {| export type StackTimingByDepth = Array; -type LastSeen = { - startTimeByDepth: number[], - callNodeIndexByDepth: IndexIntoCallNodeTable[], -}; - /** * Build a StackTimingByDepth table from a given thread. */ export function getStackTimingByDepth( samples: SamplesLikeTable, + sampleCallNodes: Array, callNodeInfo: CallNodeInfo, - maxDepth: number, + maxDepthPlusOne: number, interval: Milliseconds ): StackTimingByDepth { - const { callNodeTable, stackIndexToCallNodeIndex } = callNodeInfo; - const stackTimingByDepth = Array.from({ length: maxDepth }, () => ({ + const { callNodeTable } = callNodeInfo; + const { + prefix: callNodeTablePrefixColumn, + subtreeRangeEnd: callNodeTableSubtreeRangeEndColumn, + depth: callNodeTableDepthColumn, + } = callNodeTable; + const stackTimingByDepth = Array.from({ length: maxDepthPlusOne }, () => ({ start: [], end: [], callNode: [], length: 0, })); - const lastSeen: LastSeen = { - startTimeByDepth: [], - callNodeIndexByDepth: [], - }; - - // Go through each sample, and push/pop it on the stack to build up - // the stackTimingByDepth. - let previousDepth = -1; - for (let i = 0; i < samples.length; i++) { - const stackIndex = samples.stack[i]; - const sampleTime = samples.time[i]; - - // If this stack index is null (for instance if it was filtered out) then pop back - // down to the base stack. - if (stackIndex === null) { - _popStacks(stackTimingByDepth, lastSeen, -1, previousDepth, sampleTime); - previousDepth = -1; - } else { - const callNodeIndex = stackIndexToCallNodeIndex[stackIndex]; - const depth = callNodeTable.depth[callNodeIndex]; - - // Find the depth of the nearest shared stack. - const depthToPop = _findNearestSharedCallNodeDepth( - callNodeTable, - callNodeIndex, - lastSeen, - depth - ); - _popStacks( - stackTimingByDepth, - lastSeen, - depthToPop, - previousDepth, - sampleTime - ); - _pushStacks(callNodeTable, lastSeen, depth, callNodeIndex, sampleTime); - previousDepth = depth; - } + if (samples.length === 0) { + return stackTimingByDepth; } - // Pop the remaining stacks - const lastIndex = samples.length - 1; - const endingTime = samples.time[lastIndex] + interval; - _popStacks(stackTimingByDepth, lastSeen, -1, previousDepth, endingTime); - - return stackTimingByDepth; -} + // Overview of the algorithm: + // We go sample by sample. + // At the end of each iteration, we have a stack of "open boxes" which are + // available for sharing with the next sample; each open box has a call node + // and a start time. The number of open boxes matches the length of the call + // path. + // At the beginning of each iteration, we pick which of the open boxes from + // the previous sample we want to share (these boxes remain "open") and which + // ones we can't share. + // The ones we can't share need to be "committed", i.e. added to stackTimingByDepth. + // We share the boxes whose call nodes are ancestors of the current sample's + // call node, and commit the rest. Then we open new boxes for the unshared part + // of the current sample's call node path. + + // We remember the stack of open boxes by remembering only the deepest call + // node; and the start time for each box in the stack. + // The call nodes of the remaining "open boxes" are implicit; i.e. the call + // node of the open box at depth d is the ancestor at depth d of + // deepestOpenBoxCallNodeIndex. + let deepestOpenBoxCallNodeIndex = -1; + let deepestOpenBoxDepth = -1; + const openBoxStartTimeByDepth = new Float32Array(maxDepthPlusOne); + + for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) { + const sampleTime = samples.time[sampleIndex]; + const thisCallNodeIndex = sampleCallNodes[sampleIndex] ?? -1; + if (thisCallNodeIndex === deepestOpenBoxCallNodeIndex) { + continue; + } -function _findNearestSharedCallNodeDepth( - callNodeTable: CallNodeTable, - callNodeIndex: IndexIntoCallNodeTable, - lastSeen: LastSeen, - depthStart: number -): number { - let nextCallNodeIndex = callNodeIndex; - for (let depth = depthStart; depth >= 0; depth--) { - if (lastSeen.callNodeIndexByDepth[depth] === nextCallNodeIndex) { - return depth; + // Phase 1: Commit open boxes which are not shared by the current call node, + // i.e. any boxes whose call nodes are not ancestors of the current call node. + // These unshared boxes will be committed and added to stackTimingForThisDepth. + // + // We walk up from the previous sample's depth until we find the lowest + // common ancestor with the current sample's call node, commiting all boxes + // along the way. + // + // Here we use the call node table ordering for a cheap "is in subtree of" check. + // Any boxes which can stay open are the ones whose call nodes contain + // thisCallNodeIndex in their subtree, i.e. the ones which are ancestors af + // thisCallNodeIndex. + while ( + deepestOpenBoxDepth !== -1 && + (thisCallNodeIndex < deepestOpenBoxCallNodeIndex || + thisCallNodeIndex >= + callNodeTableSubtreeRangeEndColumn[deepestOpenBoxCallNodeIndex]) + ) { + // deepestOpenBoxCallNodeIndex is *not* an ancestors of thisCallNodeIndex. + // Commit this box. + const start = openBoxStartTimeByDepth[deepestOpenBoxDepth]; + const stackTimingForThisDepth = stackTimingByDepth[deepestOpenBoxDepth]; + const index = stackTimingForThisDepth.length++; + stackTimingForThisDepth.start[index] = start; + stackTimingForThisDepth.end[index] = sampleTime; + stackTimingForThisDepth.callNode[index] = deepestOpenBoxCallNodeIndex; + deepestOpenBoxCallNodeIndex = + callNodeTablePrefixColumn[deepestOpenBoxCallNodeIndex]; + deepestOpenBoxDepth--; } - nextCallNodeIndex = callNodeTable.prefix[nextCallNodeIndex]; - } - return -1; -} -function _popStacks( - stackTimingByDepth: StackTimingByDepth, - lastSeen: LastSeen, - depth: number, - previousDepth: number, - sampleTime: number -) { - // "Pop" off the stack, and commit the timing of the frames - for (let stackDepth = depth + 1; stackDepth <= previousDepth; stackDepth++) { - // Push on the new information. - stackTimingByDepth[stackDepth].start.push( - lastSeen.startTimeByDepth[stackDepth] - ); - stackTimingByDepth[stackDepth].end.push(sampleTime); - stackTimingByDepth[stackDepth].callNode.push( - lastSeen.callNodeIndexByDepth[stackDepth] - ); - stackTimingByDepth[stackDepth].length++; + // Phase 2: Enter new boxes for the current call node. + // New boxes start from depth `deepestOpenBoxDepth`, which is the depth of + // the lowest common ancestor of thisCallNodeIndex and the previous sample's + // call node. We "open" boxes going down all the way to thisCallNodeIndex. + if (thisCallNodeIndex !== -1) { + const thisCallNodeDepth = callNodeTableDepthColumn[thisCallNodeIndex]; + while (deepestOpenBoxDepth < thisCallNodeDepth) { + deepestOpenBoxDepth++; + openBoxStartTimeByDepth[deepestOpenBoxDepth] = sampleTime; + } + } - // Delete that this stack frame has been seen. - delete lastSeen.callNodeIndexByDepth[stackDepth]; - delete lastSeen.startTimeByDepth[stackDepth]; + deepestOpenBoxCallNodeIndex = thisCallNodeIndex; } -} -function _pushStacks( - callNodeTable: CallNodeTable, - lastSeen: LastSeen, - depth: number, - startingCallNodeIndex: IndexIntoCallNodeTable, - sampleTime: number -) { - let callNodeIndex = startingCallNodeIndex; - // "Push" onto the stack with new frames - for (let parentDepth = depth; parentDepth >= 0; parentDepth--) { - if ( - callNodeIndex === -1 || - lastSeen.callNodeIndexByDepth[parentDepth] !== undefined - ) { - break; - } - lastSeen.callNodeIndexByDepth[parentDepth] = callNodeIndex; - lastSeen.startTimeByDepth[parentDepth] = sampleTime; - callNodeIndex = callNodeTable.prefix[callNodeIndex]; + // We've processed all samples. + // Commit the boxes that were left open by the last sample. + const endTime = samples.time[samples.length - 1] + interval; + while (deepestOpenBoxDepth !== -1) { + const stackTimingForThisDepth = stackTimingByDepth[deepestOpenBoxDepth]; + const index = stackTimingForThisDepth.length++; + const start = openBoxStartTimeByDepth[deepestOpenBoxDepth]; + stackTimingForThisDepth.start[index] = start; + stackTimingForThisDepth.end[index] = endTime; + stackTimingForThisDepth.callNode[index] = deepestOpenBoxCallNodeIndex; + deepestOpenBoxCallNodeIndex = + callNodeTablePrefixColumn[deepestOpenBoxCallNodeIndex]; + deepestOpenBoxDepth--; } + + return stackTimingByDepth; } diff --git a/src/profile-logic/transforms.js b/src/profile-logic/transforms.js index 3934daba2f..288ec897d2 100644 --- a/src/profile-logic/transforms.js +++ b/src/profile-logic/transforms.js @@ -9,7 +9,6 @@ import { } from '../utils/uintarray-encoding'; import { toValidImplementationFilter, - getCallNodeIndexFromPath, updateThreadStacks, updateThreadStacksByGeneratingNewStackColumns, getMapStackUpdater, @@ -18,7 +17,6 @@ import { import { timeCode } from '../utils/time-code'; import { assertExhaustiveCheck, convertToTransformType } from '../utils/flow'; import { canonicalizeRangeSet } from '../utils/range-set'; -import { CallTree } from '../profile-logic/call-tree'; import { getSearchFilteredMarkerIndexes } from '../profile-logic/marker-data'; import { shallowCloneFrameTable, getEmptyStackTable } from './data-structures'; import { getFunctionName } from './function-info'; @@ -699,53 +697,6 @@ function _callNodePathHasPrefixPath( ); } -/** - * Take a CallNodePath, and invert it given a CallTree. Note that if the CallTree - * is itself inverted, you will get back the uninverted CallNodePath to the regular - * CallTree. - * - * e.g: - * (invertedPath, invertedCallTree) => path - * (path, callTree) => invertedPath - * - * Call trees are sorted with the CallNodes with the heaviest total time as the first - * entry. This function walks to the tip of the heaviest branches to find the leaf node, - * then construct an inverted CallNodePath with the result. This gives a pretty decent - * result, but it doesn't guarantee that it will select the heaviest CallNodePath for the - * INVERTED call tree. This would require doing a round trip through the reducers or - * some other mechanism in order to first calculate the next inverted call tree. This is - * probably not worth it, so go ahead and use the uninverted call tree, as it's probably - * good enough. - */ -export function invertCallNodePath( - path: CallNodePath, - callTree: CallTree, - callNodeTable: CallNodeTable -): CallNodePath { - let callNodeIndex = getCallNodeIndexFromPath(path, callNodeTable); - if (callNodeIndex === null) { - // No path was found, return an empty CallNodePath. - return []; - } - let children = [callNodeIndex]; - const pathToLeaf = []; - do { - // Walk down the tree's depth to construct a path to the leaf node, this should - // be the heaviest branch of the tree. - callNodeIndex = children[0]; - pathToLeaf.push(callNodeIndex); - children = callTree.getChildren(callNodeIndex); - } while (children && children.length > 0); - - return ( - pathToLeaf - // Map the CallNodeIndex to FuncIndex. - .map((index) => callNodeTable.func[index]) - // Reverse it so that it's in the proper inverted order. - .reverse() - ); -} - /** * Transform a thread's stacks to merge stacks that match the CallNodePath into * the calling stack. See `src/types/transforms.js` for more information about the diff --git a/src/reducers/profile-view.js b/src/reducers/profile-view.js index d0e205f0b0..139774e456 100644 --- a/src/reducers/profile-view.js +++ b/src/reducers/profile-view.js @@ -140,9 +140,11 @@ const symbolicationStatus: Reducer = ( } }; -export const defaultThreadViewOptions = { - selectedCallNodePath: [], - expandedCallNodePaths: new PathSet(), +export const defaultThreadViewOptions: ThreadViewOptions = { + selectedNonInvertedCallNodePath: [], + selectedInvertedCallNodePath: [], + expandedNonInvertedCallNodePaths: new PathSet(), + expandedInvertedCallNodePaths: new PathSet(), selectedMarker: null, selectedNetworkMarker: null, }; @@ -150,7 +152,7 @@ export const defaultThreadViewOptions = { function _getThreadViewOptions( state: ThreadViewOptionsPerThreads, threadsKey: ThreadsKey -) { +): ThreadViewOptions { const options = state[threadsKey]; if (options) { return options; @@ -202,14 +204,23 @@ const viewOptionsPerThread: Reducer = ( return { ...threadViewOptions, - selectedCallNodePath: applyFuncSubstitutionToCallPath( + selectedNonInvertedCallNodePath: applyFuncSubstitutionToCallPath( oldFuncToNewFuncsMap, - threadViewOptions.selectedCallNodePath + threadViewOptions.selectedNonInvertedCallNodePath ), - expandedCallNodePaths: + selectedInvertedCallNodePath: applyFuncSubstitutionToCallPath( + oldFuncToNewFuncsMap, + threadViewOptions.selectedInvertedCallNodePath + ), + expandedNonInvertedCallNodePaths: applyFuncSubstitutionToPathSetAndIncludeNewAncestors( oldFuncToNewFuncsMap, - threadViewOptions.expandedCallNodePaths + threadViewOptions.expandedNonInvertedCallNodePaths + ), + expandedInvertedCallNodePaths: + applyFuncSubstitutionToPathSetAndIncludeNewAncestors( + oldFuncToNewFuncsMap, + threadViewOptions.expandedInvertedCallNodePaths ), }; }); @@ -218,6 +229,7 @@ const viewOptionsPerThread: Reducer = ( } case 'CHANGE_SELECTED_CALL_NODE': { const { + isInverted, selectedCallNodePath, threadsKey, optionalExpandedToCallNodePath, @@ -225,7 +237,9 @@ const viewOptionsPerThread: Reducer = ( const threadState = _getThreadViewOptions(state, threadsKey); - const previousSelectedCallNodePath = threadState.selectedCallNodePath; + const previousSelectedCallNodePath = isInverted + ? threadState.selectedInvertedCallNodePath + : threadState.selectedNonInvertedCallNodePath; // If the selected node doesn't actually change, let's return the previous // state to avoid rerenders. @@ -236,7 +250,9 @@ const viewOptionsPerThread: Reducer = ( return state; } - let { expandedCallNodePaths } = threadState; + let expandedCallNodePaths = isInverted + ? threadState.expandedInvertedCallNodePaths + : threadState.expandedNonInvertedCallNodePaths; const expandToNode = optionalExpandedToCallNodePath ? optionalExpandedToCallNodePath : selectedCallNodePath; @@ -260,13 +276,26 @@ const viewOptionsPerThread: Reducer = ( ); } - return _updateThreadViewOptions(state, threadsKey, { - selectedCallNodePath, - expandedCallNodePaths, - }); + return _updateThreadViewOptions( + state, + threadsKey, + isInverted + ? { + selectedInvertedCallNodePath: selectedCallNodePath, + expandedInvertedCallNodePaths: expandedCallNodePaths, + } + : { + selectedNonInvertedCallNodePath: selectedCallNodePath, + expandedNonInvertedCallNodePaths: expandedCallNodePaths, + } + ); } case 'CHANGE_INVERT_CALLSTACK': { - const { callTree, callNodeTable, selectedThreadIndexes } = action; + const { + newSelectedCallNodePath, + selectedThreadIndexes, + invertCallstack, + } = action; return objectMap(state, (viewOptions, threadsKey) => { if ( // `Object.entries` converts number threadsKeys into strings, so @@ -274,35 +303,37 @@ const viewOptionsPerThread: Reducer = ( threadsKey === ProfileData.getThreadsKey(selectedThreadIndexes).toString() ) { - // Only attempt this on the current thread, as we need the transformed thread - // There is no guarantee that this has been calculated on all the other threads, - // and we shouldn't attempt to expect it, as that could be quite a perf cost. - const selectedCallNodePath = Transforms.invertCallNodePath( - viewOptions.selectedCallNodePath, - callTree, - callNodeTable - ); - const expandedCallNodePaths = new PathSet(); - for (let i = 1; i < selectedCallNodePath.length; i++) { - expandedCallNodePaths.add(selectedCallNodePath.slice(0, i)); + for (let i = 1; i < newSelectedCallNodePath.length; i++) { + expandedCallNodePaths.add(newSelectedCallNodePath.slice(0, i)); } - return { - ...viewOptions, - selectedCallNodePath, - expandedCallNodePaths, - }; + return invertCallstack + ? { + ...viewOptions, + selectedInvertedCallNodePath: newSelectedCallNodePath, + expandedInvertedCallNodePaths: expandedCallNodePaths, + } + : { + ...viewOptions, + selectedNonInvertedCallNodePath: newSelectedCallNodePath, + expandedNonInvertedCallNodePaths: expandedCallNodePaths, + }; } return viewOptions; }); } case 'CHANGE_EXPANDED_CALL_NODES': { - const { threadsKey, expandedCallNodePaths } = action; + const { threadsKey, isInverted } = action; + const expandedCallNodePaths = new PathSet(action.expandedCallNodePaths); - return _updateThreadViewOptions(state, threadsKey, { - expandedCallNodePaths: new PathSet(expandedCallNodePaths), - }); + return _updateThreadViewOptions( + state, + threadsKey, + isInverted + ? { expandedInvertedCallNodePaths: expandedCallNodePaths } + : { expandedNonInvertedCallNodePaths: expandedCallNodePaths } + ); } case 'CHANGE_SELECTED_MARKER': { const { threadsKey, selectedMarker } = action; @@ -317,30 +348,50 @@ const viewOptionsPerThread: Reducer = ( case 'ADD_TRANSFORM_TO_STACK': { const { threadsKey, transform, transformedThread, callNodeTable } = action; - const threadViewOptions = _getThreadViewOptions(state, threadsKey); - const expandedCallNodePaths = new PathSet( - Array.from(threadViewOptions.expandedCallNodePaths) - .map((path) => - Transforms.applyTransformToCallNodePath( - path, - transform, - transformedThread, - callNodeTable + + const getFilteredPathSet = function (pathSet: PathSet): PathSet { + return new PathSet( + Array.from(pathSet) + .map((path) => + Transforms.applyTransformToCallNodePath( + path, + transform, + transformedThread, + callNodeTable + ) ) - ) - .filter((path) => path.length > 0) - ); + .filter((path) => path.length > 0) + ); + }; - const selectedCallNodePath = Transforms.applyTransformToCallNodePath( - threadViewOptions.selectedCallNodePath, - transform, - transformedThread, - callNodeTable + const getFilteredPath = function (path: CallNodePath): CallNodePath { + return Transforms.applyTransformToCallNodePath( + path, + transform, + transformedThread, + callNodeTable + ); + }; + + const threadViewOptions = _getThreadViewOptions(state, threadsKey); + const selectedNonInvertedCallNodePath = getFilteredPath( + threadViewOptions.selectedNonInvertedCallNodePath + ); + const selectedInvertedCallNodePath = getFilteredPath( + threadViewOptions.selectedInvertedCallNodePath + ); + const expandedNonInvertedCallNodePaths = getFilteredPathSet( + threadViewOptions.expandedNonInvertedCallNodePaths + ); + const expandedInvertedCallNodePaths = getFilteredPathSet( + threadViewOptions.expandedInvertedCallNodePaths ); return _updateThreadViewOptions(state, threadsKey, { - selectedCallNodePath, - expandedCallNodePaths, + selectedNonInvertedCallNodePath, + selectedInvertedCallNodePath, + expandedNonInvertedCallNodePaths, + expandedInvertedCallNodePaths, }); } case 'POP_TRANSFORMS_FROM_STACK': { @@ -348,8 +399,10 @@ const viewOptionsPerThread: Reducer = ( // https://github.com/firefox-devtools/profiler/issues/882 const { threadsKey } = action; return _updateThreadViewOptions(state, threadsKey, { - selectedCallNodePath: [], - expandedCallNodePaths: new PathSet(), + selectedNonInvertedCallNodePath: [], + selectedInvertedCallNodePath: [], + expandedNonInvertedCallNodePaths: new PathSet(), + expandedInvertedCallNodePaths: new PathSet(), }); } case 'CHANGE_IMPLEMENTATION_FILTER': { @@ -366,41 +419,65 @@ const viewOptionsPerThread: Reducer = ( const viewOptions = _getThreadViewOptions(state, threadsKey); - // This CallNodePath may need to be updated twice. - let selectedCallNodePath: CallNodePath = viewOptions.selectedCallNodePath; - if (implementation === 'combined') { - // Restore the full CallNodePaths - selectedCallNodePath = Transforms.restoreAllFunctionsInCallNodePath( - transformedThread, - previousImplementation, - selectedCallNodePath - ); - } else { - if (previousImplementation !== 'combined') { - // Restore the CallNodePath back to an unfiltered state before re-filtering - // it on the next implementation. - selectedCallNodePath = Transforms.restoreAllFunctionsInCallNodePath( + const getUpdatedPath = function getUpdatedPath( + callNodePath: CallNodePath + ): CallNodePath { + // This CallNodePath may need to be updated twice. + if (implementation === 'combined') { + // Restore the full CallNodePaths + callNodePath = Transforms.restoreAllFunctionsInCallNodePath( transformedThread, previousImplementation, - selectedCallNodePath + callNodePath + ); + } else { + if (previousImplementation !== 'combined') { + // Restore the CallNodePath back to an unfiltered state before re-filtering + // it on the next implementation. + callNodePath = Transforms.restoreAllFunctionsInCallNodePath( + transformedThread, + previousImplementation, + callNodePath + ); + } + // Take the full CallNodePath, and strip out anything not in this implementation. + callNodePath = Transforms.filterCallNodePathByImplementation( + transformedThread, + implementation, + callNodePath ); } - // Take the full CallNodePath, and strip out anything not in this implementation. - selectedCallNodePath = Transforms.filterCallNodePathByImplementation( - transformedThread, - implementation, - selectedCallNodePath - ); - } + return callNodePath; + }; - const expandedCallNodePaths = new PathSet(); - for (let i = 1; i < selectedCallNodePath.length; i++) { - expandedCallNodePaths.add(selectedCallNodePath.slice(0, i)); - } + const getAncestorPathSet = function getAncestorPathSet( + callNodePath: CallNodePath + ): PathSet { + const ancestorCallNodePaths = new PathSet(); + for (let i = 1; i < callNodePath.length; i++) { + ancestorCallNodePaths.add(callNodePath.slice(0, i)); + } + return ancestorCallNodePaths; + }; + + const selectedNonInvertedCallNodePath = getUpdatedPath( + viewOptions.selectedNonInvertedCallNodePath + ); + const selectedInvertedCallNodePath = getUpdatedPath( + viewOptions.selectedInvertedCallNodePath + ); + const expandedNonInvertedCallNodePaths = getAncestorPathSet( + selectedNonInvertedCallNodePath + ); + const expandedInvertedCallNodePaths = getAncestorPathSet( + selectedInvertedCallNodePath + ); return _updateThreadViewOptions(state, threadsKey, { - selectedCallNodePath, - expandedCallNodePaths, + selectedNonInvertedCallNodePath, + selectedInvertedCallNodePath, + expandedNonInvertedCallNodePaths, + expandedInvertedCallNodePaths, }); } default: diff --git a/src/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index 92b265443f..e77712db35 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -178,7 +178,11 @@ export function getStackAndSampleSelectorsPerThread( const getSelectedCallNodePath: Selector = createSelector( threadSelectors.getViewOptions, - (threadViewOptions): CallNodePath => threadViewOptions.selectedCallNodePath + UrlState.getInvertCallstack, + (threadViewOptions, invertCallStack): CallNodePath => + invertCallStack + ? threadViewOptions.selectedInvertedCallNodePath + : threadViewOptions.selectedNonInvertedCallNodePath ); const getSelectedCallNodeIndex: Selector = @@ -195,7 +199,11 @@ export function getStackAndSampleSelectorsPerThread( const getExpandedCallNodePaths: Selector = createSelector( threadSelectors.getViewOptions, - (threadViewOptions) => threadViewOptions.expandedCallNodePaths + UrlState.getInvertCallstack, + (threadViewOptions, invertCallStack) => + invertCallStack + ? threadViewOptions.expandedInvertedCallNodePaths + : threadViewOptions.expandedNonInvertedCallNodePaths ); const getExpandedCallNodeIndexes: Selector< @@ -259,32 +267,22 @@ export function getStackAndSampleSelectorsPerThread( const getTreeOrderComparatorInFilteredThread: Selector< (IndexIntoSamplesTable, IndexIntoSamplesTable) => number, > = createSelector( - threadSelectors.getFilteredThread, - getCallNodeInfo, - (thread, { callNodeTable, stackIndexToCallNodeIndex }) => { - const sampleIndexToCallNodeIndex = - ProfileData.getSampleIndexToCallNodeIndex( - thread.samples.stack, - stackIndexToCallNodeIndex - ); - return ProfileData.getTreeOrderComparator( - callNodeTable, - sampleIndexToCallNodeIndex - ); - } + getSampleIndexToCallNodeIndexForFilteredThread, + ProfileData.getTreeOrderComparator ); - const getFilteredCallNodeMaxDepth: Selector = createSelector( + const getFilteredCallNodeMaxDepthPlusOne: Selector = createSelector( threadSelectors.getFilteredSamplesForCallTree, getCallNodeInfo, - ProfileData.computeCallNodeMaxDepth + ProfileData.computeCallNodeMaxDepthPlusOne ); - const getPreviewFilteredCallNodeMaxDepth: Selector = createSelector( - threadSelectors.getPreviewFilteredSamplesForCallTree, - getCallNodeInfo, - ProfileData.computeCallNodeMaxDepth - ); + const getPreviewFilteredCallNodeMaxDepthPlusOne: Selector = + createSelector( + threadSelectors.getPreviewFilteredSamplesForCallTree, + getCallNodeInfo, + ProfileData.computeCallNodeMaxDepthPlusOne + ); /** * When computing the call tree, a "samples" table is used, which @@ -298,32 +296,31 @@ export function getStackAndSampleSelectorsPerThread( (samples) => samples.weightType || 'samples' ); - const getCallTreeCountsAndSummary: Selector = - createSelector( - threadSelectors.getPreviewFilteredSamplesForCallTree, - getCallNodeInfo, - ProfileSelectors.getProfileInterval, - UrlState.getInvertCallstack, - (samples, callNodeInfo, interval, invertCallStack) => { - const sampleIndexToCallNodeIndex = - ProfileData.getSampleIndexToCallNodeIndex( - samples.stack, - callNodeInfo.stackIndexToCallNodeIndex - ); - return CallTree.computeCallTreeCountsAndSummary( - samples, - sampleIndexToCallNodeIndex, - callNodeInfo, - invertCallStack + const getCallTreeTimings: Selector = createSelector( + threadSelectors.getPreviewFilteredSamplesForCallTree, + getCallNodeInfo, + ProfileSelectors.getProfileInterval, + UrlState.getInvertCallstack, + (samples, callNodeInfo, interval, invertCallStack) => { + const sampleIndexToCallNodeIndex = + ProfileData.getSampleIndexToCallNodeIndex( + samples.stack, + callNodeInfo.stackIndexToCallNodeIndex ); - } - ); + return CallTree.computeCallTreeTimings( + samples, + sampleIndexToCallNodeIndex, + callNodeInfo, + invertCallStack + ); + } + ); const getCallTree: Selector = createSelector( threadSelectors.getPreviewFilteredThread, getCallNodeInfo, ProfileSelectors.getCategories, - getCallTreeCountsAndSummary, + getCallTreeTimings, getWeightTypeForCallTree, CallTree.getCallTree ); @@ -342,7 +339,7 @@ export function getStackAndSampleSelectorsPerThread( ); const getTracedTiming: Selector = createSelector( - threadSelectors.getFilteredSamplesForCallTree, + threadSelectors.getPreviewFilteredSamplesForCallTree, getCallNodeInfo, ProfileSelectors.getProfileInterval, UrlState.getInvertCallstack, @@ -352,17 +349,25 @@ export function getStackAndSampleSelectorsPerThread( const getStackTimingByDepth: Selector = createSelector( threadSelectors.getFilteredSamplesForCallTree, + getSampleIndexToCallNodeIndexForFilteredThread, getCallNodeInfo, - getFilteredCallNodeMaxDepth, + getFilteredCallNodeMaxDepthPlusOne, ProfileSelectors.getProfileInterval, StackTiming.getStackTimingByDepth ); + const getFlameGraphRows: Selector = createSelector( + (state) => getCallNodeInfo(state).callNodeTable, + (state) => threadSelectors.getFilteredThread(state).funcTable, + (state) => threadSelectors.getFilteredThread(state).stringTable, + FlameGraph.computeFlameGraphRows + ); + const getFlameGraphTiming: Selector = createSelector( - threadSelectors.getPreviewFilteredThread, - getCallNodeInfo, - getCallTreeCountsAndSummary, + getFlameGraphRows, + (state) => getCallNodeInfo(state).callNodeTable, + getCallTreeTimings, FlameGraph.getFlameGraphTiming ); @@ -405,8 +410,8 @@ export function getStackAndSampleSelectorsPerThread( getAssemblyViewAddressTimings, getTracedTiming, getStackTimingByDepth, - getFilteredCallNodeMaxDepth, - getPreviewFilteredCallNodeMaxDepth, + getFilteredCallNodeMaxDepthPlusOne, + getPreviewFilteredCallNodeMaxDepthPlusOne, getFlameGraphTiming, getRightClickedCallNodeIndex, }; diff --git a/src/selectors/per-thread/thread.js b/src/selectors/per-thread/thread.js index 43c4e56481..e41c7f3cd5 100644 --- a/src/selectors/per-thread/thread.js +++ b/src/selectors/per-thread/thread.js @@ -467,17 +467,18 @@ export function getThreadSelectorsWithMarkersPerThread( } ); - const getFilteredThread: Selector = createSelector( + const _getInvertedThread: Selector = createSelector( _getImplementationAndSearchFilteredThread, - UrlState.getInvertCallstack, ProfileSelectors.getDefaultCategory, - (thread, shouldInvertCallstack, defaultCategory) => { - return shouldInvertCallstack - ? ProfileData.invertCallstack(thread, defaultCategory) - : thread; - } + ProfileData.invertCallstack ); + const getFilteredThread: Selector = (state) => { + return UrlState.getInvertCallstack(state) + ? _getInvertedThread(state) + : _getImplementationAndSearchFilteredThread(state); + }; + const getPreviewFilteredThread: Selector = createSelector( getFilteredThread, ProfileSelectors.getPreviewSelection, diff --git a/src/selectors/url-state.js b/src/selectors/url-state.js index 59ff65febc..e9462386d4 100644 --- a/src/selectors/url-state.js +++ b/src/selectors/url-state.js @@ -73,8 +73,6 @@ export const getLastSelectedCallTreeSummaryStrategy: Selector< CallTreeSummaryStrategy, > = (state) => getProfileSpecificState(state).lastSelectedCallTreeSummaryStrategy; -export const getInvertCallstack: Selector = (state) => - getProfileSpecificState(state).invertCallstack; export const getShowUserTimings: Selector = (state) => getProfileSpecificState(state).showUserTimings; export const getSourceViewFile: Selector = (state) => @@ -109,9 +107,12 @@ export const getMarkersSearchString: Selector = (state) => getProfileSpecificState(state).markersSearchString; export const getNetworkSearchString: Selector = (state) => getProfileSpecificState(state).networkSearchString; - export const getSelectedTab: Selector = (state) => getUrlState(state).selectedTab; +export const getInvertCallstack: Selector = (state) => + getSelectedTab(state) === 'calltree' && + getProfileSpecificState(state).invertCallstack; + export const getSelectedThreadIndexesOrNull: Selector< Set | null, > = (state) => getProfileSpecificState(state).selectedThreads; diff --git a/src/test/components/CallNodeContextMenu.test.js b/src/test/components/CallNodeContextMenu.test.js index 3f140dbd34..635b776893 100644 --- a/src/test/components/CallNodeContextMenu.test.js +++ b/src/test/components/CallNodeContextMenu.test.js @@ -197,7 +197,7 @@ describe('calltree/CallNodeContextMenu', function () { // Copy is a mocked module, clear it both before and after. fireFullClick(getByText('Copy stack')); expect(copy).toHaveBeenCalledWith( - `B.js [https://example.com/script.js:2:222]\nA.js [https://example.com/script.js:1:111]\n` + `B.js [https://example.com/script.js:2:222]\nA.js [https://example.com/script.js:1:111]` ); }); }); diff --git a/src/test/components/FlameGraph.test.js b/src/test/components/FlameGraph.test.js index 91e5cf00e5..33630e1c04 100644 --- a/src/test/components/FlameGraph.test.js +++ b/src/test/components/FlameGraph.test.js @@ -29,6 +29,7 @@ import { updatePreviewSelection, changeImplementationFilter, } from '../../actions/profile-view'; +import { changeSelectedTab } from '../../actions/app'; import { selectedThreadSelectors } from '../../selectors/per-thread'; import { @@ -67,17 +68,12 @@ describe('FlameGraph', function () { expect(drawCalls).toMatchSnapshot(); }); - it('renders a message instead of the graph when call stack is inverted', () => { - const { getByText, dispatch } = setupFlameGraph(); - dispatch(changeInvertCallstack(true)); - expect(getByText(/The Flame Graph is not available/)).toBeInTheDocument(); - }); - - it('switches back to uninverted mode when clicking the button', () => { - const { getByText, dispatch, getState } = setupFlameGraph(); + it('ignores invertCallstack and always displays non-inverted', () => { + const { getState, dispatch } = setupFlameGraph(); + expect(getInvertCallstack(getState())).toBe(false); dispatch(changeInvertCallstack(true)); - expect(getInvertCallstack(getState())).toBe(true); - fireFullClick(getByText(/Switch to the normal call stack/)); + expect(getInvertCallstack(getState())).toBe(false); + dispatch(changeInvertCallstack(false)); expect(getInvertCallstack(getState())).toBe(false); }); @@ -141,9 +137,8 @@ describe('FlameGraph', function () { const div = getContentDiv(); function selectedNode() { - const callNodeIndex = selectedThreadSelectors.getSelectedCallNodeIndex( - getState() - ); + const callNodeIndex = + selectedThreadSelectors.getSelectedCallNodeIndex(getState()); return callNodeIndex && funcNames[callNodeIndex]; } @@ -298,6 +293,7 @@ function setupFlameGraph(addImplementationData: boolean = true) { } const store = storeWithProfile(profile); + store.dispatch(changeSelectedTab('flame-graph')); const renderResult = render( diff --git a/src/test/components/NetworkChart.test.js b/src/test/components/NetworkChart.test.js index 9f96a6885a..37f99acb04 100644 --- a/src/test/components/NetworkChart.test.js +++ b/src/test/components/NetworkChart.test.js @@ -629,9 +629,8 @@ describe('Network Chart/tooltip behavior', () => { afterEach(removeRootOverlayElement); it('shows a tooltip when the mouse hovers the line', () => { - const { rowItem, queryByTestId, getByTestId } = setupWithPayload( - getNetworkMarkers() - ); + const { rowItem, queryByTestId, getByTestId } = + setupWithPayload(getNetworkMarkers()); expect(queryByTestId('tooltip')).not.toBeInTheDocument(); // React uses mouseover/mouseout events to implement mouseenter/mouseleave. diff --git a/src/test/components/__snapshots__/CPUGraph.test.js.snap b/src/test/components/__snapshots__/CPUGraph.test.js.snap index 56d0efb730..c3e03bc122 100644 --- a/src/test/components/__snapshots__/CPUGraph.test.js.snap +++ b/src/test/components/__snapshots__/CPUGraph.test.js.snap @@ -2,10 +2,6 @@ exports[`CPUGraph matches the 2d canvas draw snapshot 1`] = ` Array [ - Array [ - "set fillStyle", - "#45a1ff", - ], Array [ "set fillStyle", "#003eaa", @@ -66,10 +62,6 @@ Array [ 10, 0, ], - Array [ - "set fillStyle", - "#c5e1fe", - ], Array [ "clearRect", 0, diff --git a/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap b/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap index 68f178b13c..18baa59885 100644 --- a/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap +++ b/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap @@ -987,7 +987,7 @@ allocated or deallocated in the program." class="react-contextmenu-wrapper treeViewContextMenu" >
@@ -1424,7 +1424,7 @@ allocated or deallocated in the program." aria-level="4" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns odd" - id="treeViewRow-6" + id="treeViewRow-8" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -1454,7 +1454,7 @@ allocated or deallocated in the program." aria-level="5" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns even" - id="treeViewRow-7" + id="treeViewRow-9" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -1485,7 +1485,7 @@ allocated or deallocated in the program." aria-level="6" aria-selected="true" class="treeViewRow treeViewRowScrolledColumns odd isSelected" - id="treeViewRow-8" + id="treeViewRow-10" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -1771,7 +1771,7 @@ allocated or deallocated in the program." class="react-contextmenu-wrapper treeViewContextMenu" >
@@ -2208,7 +2208,7 @@ allocated or deallocated in the program." aria-level="4" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns odd" - id="treeViewRow-6" + id="treeViewRow-8" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -2238,7 +2238,7 @@ allocated or deallocated in the program." aria-level="5" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns even" - id="treeViewRow-7" + id="treeViewRow-9" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -2269,7 +2269,7 @@ allocated or deallocated in the program." aria-level="6" aria-selected="true" class="treeViewRow treeViewRowScrolledColumns odd isSelected" - id="treeViewRow-8" + id="treeViewRow-10" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -2543,7 +2543,7 @@ allocated or deallocated in the program." class="react-contextmenu-wrapper treeViewContextMenu" >
@@ -2980,7 +2980,7 @@ allocated or deallocated in the program." aria-level="4" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns odd" - id="treeViewRow-6" + id="treeViewRow-8" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -3010,7 +3010,7 @@ allocated or deallocated in the program." aria-level="5" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns even" - id="treeViewRow-7" + id="treeViewRow-9" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -3041,7 +3041,7 @@ allocated or deallocated in the program." aria-level="6" aria-selected="true" class="treeViewRow treeViewRowScrolledColumns odd isSelected" - id="treeViewRow-8" + id="treeViewRow-10" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -3315,7 +3315,7 @@ allocated or deallocated in the program." class="react-contextmenu-wrapper treeViewContextMenu" >
@@ -3693,7 +3693,7 @@ allocated or deallocated in the program." aria-level="4" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns even" - id="treeViewRow-6" + id="treeViewRow-8" role="treeitem" style="height: 16px; line-height: 16px;" > diff --git a/src/test/components/__snapshots__/SampleGraph.test.js.snap b/src/test/components/__snapshots__/SampleGraph.test.js.snap index 062f0f241f..847abd8b71 100644 --- a/src/test/components/__snapshots__/SampleGraph.test.js.snap +++ b/src/test/components/__snapshots__/SampleGraph.test.js.snap @@ -24,10 +24,6 @@ Array [ 10, 10, ], - Array [ - "set fillStyle", - "#003eaa", - ], Array [ "fillRect", 5, @@ -35,10 +31,6 @@ Array [ 10, 10, ], - Array [ - "set fillStyle", - "#003eaa", - ], Array [ "fillRect", 15, @@ -46,10 +38,6 @@ Array [ 10, 10, ], - Array [ - "set fillStyle", - "#003eaa", - ], Array [ "fillRect", 25, @@ -57,10 +45,6 @@ Array [ 10, 10, ], - Array [ - "set fillStyle", - "#003eaa", - ], Array [ "fillRect", 35, @@ -68,10 +52,6 @@ Array [ 10, 10, ], - Array [ - "set fillStyle", - "#003eaa", - ], Array [ "fillRect", 45, @@ -79,10 +59,6 @@ Array [ 10, 10, ], - Array [ - "set fillStyle", - "#003eaa", - ], Array [ "fillRect", 55, @@ -90,10 +66,6 @@ Array [ 10, 10, ], - Array [ - "set fillStyle", - "#003eaa", - ], Array [ "fillRect", 65, diff --git a/src/test/components/__snapshots__/StackChart.test.js.snap b/src/test/components/__snapshots__/StackChart.test.js.snap index acfe572952..d73e7e7b9d 100644 --- a/src/test/components/__snapshots__/StackChart.test.js.snap +++ b/src/test/components/__snapshots__/StackChart.test.js.snap @@ -63,19 +63,6 @@ exports[`CombinedChart renders combined stack chart 1`] = `
  • -
  • -
  • -