diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 9d78b10029..d59f040dd4 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -75,6 +75,7 @@ import type { KeyboardModifiers, TableViewOptions, SelectionContext, + BottomBoxInfo, } from 'firefox-profiler/types'; import { funcHasDirectRecursiveCall, @@ -1948,11 +1949,35 @@ export function changeTableViewOptions( }; } -export function openSourceView(file: string, currentTab: TabSlug): Action { +export function updateBottomBoxContentsAndMaybeOpen( + currentTab: TabSlug, + { libIndex, sourceFile, nativeSymbols }: BottomBoxInfo +): Action { + // TODO: If the set has more than one element, pick the native symbol with + // the highest total sample count + const nativeSymbol = nativeSymbols.length !== 0 ? nativeSymbols[0] : null; + return { - type: 'OPEN_SOURCE_VIEW', - file, + type: 'UPDATE_BOTTOM_BOX', + libIndex, + sourceFile, + nativeSymbol, + allNativeSymbolsForInitiatingCallNode: nativeSymbols, currentTab, + shouldOpenBottomBox: sourceFile !== null || nativeSymbol !== null, + shouldOpenAssemblyView: sourceFile === null && nativeSymbol !== null, + }; +} + +export function openAssemblyView(): Action { + return { + type: 'OPEN_ASSEMBLY_VIEW', + }; +} + +export function closeAssemblyView(): Action { + return { + type: 'CLOSE_ASSEMBLY_VIEW', }; } diff --git a/src/app-logic/url-handling.js b/src/app-logic/url-handling.js index fc07965080..0297db06b1 100644 --- a/src/app-logic/url-handling.js +++ b/src/app-logic/url-handling.js @@ -38,6 +38,7 @@ import type { ThreadIndex, TimelineType, SourceViewState, + AssemblyViewState, } from 'firefox-profiler/types'; import { decodeUintArrayFromUrlComponent, @@ -411,8 +412,8 @@ export function getQueryStringFromUrlState(urlState: UrlState): string { : urlState.profileSpecific.lastSelectedCallTreeSummaryStrategy; const { sourceView, isBottomBoxOpenPerPanel } = urlState.profileSpecific; query.sourceView = - sourceView.file !== null && isBottomBoxOpenPerPanel[selectedTab] - ? sourceView.file + sourceView.sourceFile !== null && isBottomBoxOpenPerPanel[selectedTab] + ? sourceView.sourceFile : undefined; break; } @@ -576,12 +577,19 @@ export function stateFromLocation( toValidTabSlug(pathParts[selectedTabPathPart]) || 'calltree'; const sourceView: SourceViewState = { activationGeneration: 0, - file: null, + libIndex: null, + sourceFile: null, + }; + const assemblyView: AssemblyViewState = { + isOpen: false, + activationGeneration: 0, + nativeSymbol: null, + allNativeSymbolsForInitiatingCallNode: [], }; const isBottomBoxOpenPerPanel = {}; tabSlugs.forEach((tabSlug) => (isBottomBoxOpenPerPanel[tabSlug] = false)); if (query.sourceView) { - sourceView.file = query.sourceView; + sourceView.sourceFile = query.sourceView; isBottomBoxOpenPerPanel[selectedTab] = true; } @@ -617,6 +625,7 @@ export function stateFromLocation( networkSearchString: query.networkSearch || '', transforms, sourceView, + assemblyView, isBottomBoxOpenPerPanel, timelineType: validateTimelineType(query.timelineType), full: { diff --git a/src/components/calltree/CallTree.js b/src/components/calltree/CallTree.js index e4f71f7cc6..658b392152 100644 --- a/src/components/calltree/CallTree.js +++ b/src/components/calltree/CallTree.js @@ -30,8 +30,8 @@ import { changeExpandedCallNodes, addTransformToStack, handleCallNodeTransformShortcut, - openSourceView, changeTableViewOptions, + updateBottomBoxContentsAndMaybeOpen, } from 'firefox-profiler/actions/profile-view'; import { assertExhaustiveCheck } from 'firefox-profiler/utils/flow'; @@ -82,7 +82,7 @@ type DispatchProps = {| +changeExpandedCallNodes: typeof changeExpandedCallNodes, +addTransformToStack: typeof addTransformToStack, +handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut, - +openSourceView: typeof openSourceView, + +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, +onTableViewOptionsChange: (TableViewOptions) => any, |}; @@ -289,12 +289,9 @@ class CallTreeImpl extends PureComponent { }; _onEnterOrDoubleClick = (nodeId: IndexIntoCallNodeTable) => { - const { tree, openSourceView } = this.props; - const file = tree.getRawFileNameForCallNode(nodeId); - if (file === null) { - return; - } - openSourceView(file, 'calltree'); + const { tree, updateBottomBoxContentsAndMaybeOpen } = this.props; + const bottomBoxInfo = tree.getBottomBoxInfoForCallNode(nodeId); + updateBottomBoxContentsAndMaybeOpen('calltree', bottomBoxInfo); }; maybeProcureInterestingInitialSelection() { @@ -431,7 +428,7 @@ export const CallTree = explicitConnect<{||}, StateProps, DispatchProps>({ changeExpandedCallNodes, addTransformToStack, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, onTableViewOptionsChange: (options: TableViewOptions) => changeTableViewOptions('calltree', options), }, diff --git a/src/components/flame-graph/FlameGraph.js b/src/components/flame-graph/FlameGraph.js index 1589e5047f..0cd10c5ab7 100644 --- a/src/components/flame-graph/FlameGraph.js +++ b/src/components/flame-graph/FlameGraph.js @@ -29,7 +29,7 @@ import { changeSelectedCallNode, changeRightClickedCallNode, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, } from 'firefox-profiler/actions/profile-view'; import type { @@ -96,7 +96,7 @@ type DispatchProps = {| +changeSelectedCallNode: typeof changeSelectedCallNode, +changeRightClickedCallNode: typeof changeRightClickedCallNode, +handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut, - +openSourceView: typeof openSourceView, + +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, |}; type Props = ConnectedProps<{||}, StateProps, DispatchProps>; @@ -131,16 +131,15 @@ class FlameGraphImpl extends React.PureComponent { ); }; - _onCallNodeDoubleClick = (callNodeIndex: IndexIntoCallNodeTable | null) => { + _onCallNodeEnterOrDoubleClick = ( + callNodeIndex: IndexIntoCallNodeTable | null + ) => { if (callNodeIndex === null) { return; } - const { callTree, openSourceView } = this.props; - const file = callTree.getRawFileNameForCallNode(callNodeIndex); - if (file === null) { - return; - } - openSourceView(file, 'flame-graph'); + const { callTree, updateBottomBoxContentsAndMaybeOpen } = this.props; + const bottomBoxInfo = callTree.getBottomBoxInfoForCallNode(callNodeIndex); + updateBottomBoxContentsAndMaybeOpen('flame-graph', bottomBoxInfo); }; _shouldDisplayTooltips = () => this.props.rightClickedCallNodeIndex === null; @@ -222,7 +221,6 @@ class FlameGraphImpl extends React.PureComponent { rightClickedCallNodeIndex, changeSelectedCallNode, handleCallNodeTransformShortcut, - openSourceView, } = this.props; if ( @@ -298,10 +296,7 @@ class FlameGraphImpl extends React.PureComponent { } if (event.key === 'Enter') { - const file = callTree.getRawFileNameForCallNode(nodeIndex); - if (file !== null) { - openSourceView(file, 'flame-graph'); - } + this._onCallNodeEnterOrDoubleClick(nodeIndex); return; } @@ -398,7 +393,7 @@ class FlameGraphImpl extends React.PureComponent { stackFrameHeight: STACK_FRAME_HEIGHT, onSelectionChange: this._onSelectedCallNodeChange, onRightClick: this._onRightClickedCallNodeChange, - onDoubleClick: this._onCallNodeDoubleClick, + onDoubleClick: this._onCallNodeEnterOrDoubleClick, shouldDisplayTooltips: this._shouldDisplayTooltips, interval, isInverted, @@ -462,7 +457,7 @@ export const FlameGraph = explicitConnect<{||}, StateProps, DispatchProps>({ changeSelectedCallNode, changeRightClickedCallNode, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, }, options: { forwardRef: true }, component: FlameGraphImpl, diff --git a/src/components/shared/CallNodeContextMenu.js b/src/components/shared/CallNodeContextMenu.js index 9eea175baa..1673ab62cb 100644 --- a/src/components/shared/CallNodeContextMenu.js +++ b/src/components/shared/CallNodeContextMenu.js @@ -15,13 +15,14 @@ import { funcHasIndirectRecursiveCall, } from 'firefox-profiler/profile-logic/transforms'; import { getFunctionName } from 'firefox-profiler/profile-logic/function-info'; +import { getBottomBoxInfoForCallNode } from 'firefox-profiler/profile-logic/profile-data'; import { getCategories } from 'firefox-profiler/selectors'; import copy from 'copy-to-clipboard'; import { addTransformToStack, expandAllCallNodeDescendants, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, setContextMenuVisibility, } from 'firefox-profiler/actions/profile-view'; import { @@ -71,7 +72,7 @@ type StateProps = {| type DispatchProps = {| +addTransformToStack: typeof addTransformToStack, +expandAllCallNodeDescendants: typeof expandAllCallNodeDescendants, - +openSourceView: typeof openSourceView, + +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, +setContextMenuVisibility: typeof setContextMenuVisibility, |}; @@ -179,11 +180,23 @@ class CallNodeContextMenuImpl extends React.PureComponent { } showFile(): void { - const filePath = this._getFilePath(); - if (filePath) { - const { openSourceView, selectedTab } = this.props; - openSourceView(filePath, selectedTab); + const { updateBottomBoxContentsAndMaybeOpen, selectedTab } = this.props; + + const rightClickedCallNodeInfo = this.getRightClickedCallNodeInfo(); + + if (rightClickedCallNodeInfo === null) { + throw new Error( + "The context menu assumes there is a selected call node and there wasn't one." + ); } + + const { callNodeIndex, thread, callNodeInfo } = rightClickedCallNodeInfo; + const bottomBoxInfo = getBottomBoxInfoForCallNode( + callNodeIndex, + callNodeInfo, + thread + ); + updateBottomBoxContentsAndMaybeOpen(selectedTab, bottomBoxInfo); } copyUrl(): void { @@ -808,7 +821,7 @@ export const CallNodeContextMenu = explicitConnect< mapDispatchToProps: { addTransformToStack, expandAllCallNodeDescendants, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, setContextMenuVisibility, }, component: CallNodeContextMenuImpl, diff --git a/src/components/stack-chart/index.js b/src/components/stack-chart/index.js index 540f47e39e..8ed6344774 100644 --- a/src/components/stack-chart/index.js +++ b/src/components/stack-chart/index.js @@ -34,7 +34,7 @@ import { changeSelectedCallNode, changeRightClickedCallNode, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, } from '../../actions/profile-view'; import { getCallNodePathFromIndex } from '../../profile-logic/profile-data'; @@ -93,7 +93,7 @@ type DispatchProps = {| +changeRightClickedCallNode: typeof changeRightClickedCallNode, +updatePreviewSelection: typeof updatePreviewSelection, +handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut, - +openSourceView: typeof openSourceView, + +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, |}; type Props = ConnectedProps<{||}, StateProps, DispatchProps>; @@ -152,7 +152,7 @@ class StackChartImpl extends React.PureComponent { selectedCallNodeIndex, rightClickedCallNodeIndex, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, } = this.props; const nodeIndex = @@ -164,10 +164,8 @@ class StackChartImpl extends React.PureComponent { } if (event.key === 'Enter') { - const file = callTree.getRawFileNameForCallNode(nodeIndex); - if (file !== null) { - openSourceView(file, 'stack-chart'); - } + const bottomBoxInfo = callTree.getBottomBoxInfoForCallNode(nodeIndex); + updateBottomBoxContentsAndMaybeOpen('stack-chart', bottomBoxInfo); return; } @@ -325,7 +323,7 @@ export const StackChart = explicitConnect<{||}, StateProps, DispatchProps>({ changeRightClickedCallNode, updatePreviewSelection, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, }, component: StackChartImpl, }); diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index 87aa791147..27be4e874c 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -9,6 +9,7 @@ import { getSampleIndexToCallNodeIndex, getOriginAnnotationForFunc, getCategoryPairLabel, + getBottomBoxInfoForCallNode, } from './profile-data'; import { resourceTypes } from './data-structures'; import { getFunctionName } from './function-info'; @@ -27,6 +28,7 @@ import type { TracedTiming, SamplesTable, ExtraBadgeInfo, + BottomBoxInfo, } from 'firefox-profiler/types'; import ExtensionIcon from '../../res/img/svg/extension.svg'; @@ -66,6 +68,7 @@ function extractFaviconFromLibname(libname: string): string | null { export class CallTree { _categories: CategoryList; + _callNodeInfo: CallNodeInfo; _callNodeTable: CallNodeTable; _callNodeSummary: CallNodeSummary; _callNodeChildCount: Uint32Array; // A table column matching the callNodeTable @@ -84,7 +87,7 @@ export class CallTree { constructor( thread: Thread, categories: CategoryList, - callNodeTable: CallNodeTable, + callNodeInfo: CallNodeInfo, callNodeSummary: CallNodeSummary, callNodeChildCount: Uint32Array, rootTotalSummary: number, @@ -95,7 +98,8 @@ export class CallTree { weightType: WeightType ) { this._categories = categories; - this._callNodeTable = callNodeTable; + this._callNodeInfo = callNodeInfo; + this._callNodeTable = callNodeInfo.callNodeTable; this._callNodeSummary = callNodeSummary; this._callNodeChildCount = callNodeChildCount; this._thread = thread; @@ -359,15 +363,14 @@ export class CallTree { ); } - getRawFileNameForCallNode( + getBottomBoxInfoForCallNode( callNodeIndex: IndexIntoCallNodeTable - ): string | null { - const funcIndex = this._callNodeTable.func[callNodeIndex]; - const fileName = this._thread.funcTable.fileName[funcIndex]; - if (fileName === null) { - return null; - } - return this._thread.stringTable.getString(fileName); + ): BottomBoxInfo { + return getBottomBoxInfoForCallNode( + callNodeIndex, + this._callNodeInfo, + this._thread + ); } } @@ -546,7 +549,7 @@ export function getCallTree( return new CallTree( thread, categories, - callNodeInfo.callNodeTable, + callNodeInfo, callNodeSummary, callNodeChildCount, rootTotalSummary, diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index 53708c969a..57a54b4670 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -37,6 +37,7 @@ import type { StackTable, FrameTable, FuncTable, + NativeSymbolTable, ResourceTable, CategoryList, IndexIntoCategoryList, @@ -75,6 +76,9 @@ import type { Address, AddressProof, TimelineType, + NativeSymbolInfo, + BottomBoxInfo, + Bytes, } from 'firefox-profiler/types'; import type { UniqueStringArray } from 'firefox-profiler/utils/unique-string-array'; @@ -3371,6 +3375,138 @@ export function findAddressProofForFile( return null; } +/** + * Calculate a lower bound for the function size, in bytes, of a native symbol. + * This is used when the symbol server does not return a size for a function. + * We need to know the size when we want to show assembly code for the function, + * in order to know how many bytes to disassemble. + * We estimate the size by finding the highest known address for this symbol in + * the frame table, and adding one byte (because the instruction at that address + * is at least one byte long). + */ +export function calculateFunctionSizeLowerBound( + frameTable: FrameTable, + nativeSymbolAddress: Address, + nativeSymbolIndex: IndexIntoNativeSymbolTable +): Bytes { + let maxFrameAddress = nativeSymbolAddress; + for (let i = 0; i < frameTable.length; i++) { + if (frameTable.nativeSymbol[i] === nativeSymbolIndex) { + const frameAddress = frameTable.address[i]; + if (frameAddress > maxFrameAddress) { + maxFrameAddress = frameAddress; + } + } + } + return maxFrameAddress + 1 - nativeSymbolAddress; +} + +/** + * Gathers the native symbols for a given call node. In most cases, a call node + * just has one native symbol (or zero if it's not native code). But in some + * cases, a call node can have its native code in multiple different functions, + * for example in the inverted tree if it was inlined into multiple different + * functions. + */ +export function getNativeSymbolsForCallNode( + callNodeIndex: IndexIntoCallNodeTable, + { stackIndexToCallNodeIndex }: CallNodeInfo, + stackTable: StackTable, + frameTable: FrameTable +): IndexIntoNativeSymbolTable[] { + const set = new Set(); + for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { + if (stackIndexToCallNodeIndex[stackIndex] === callNodeIndex) { + const frame = stackTable.frame[stackIndex]; + const nativeSymbol = frameTable.nativeSymbol[frame]; + if (nativeSymbol !== null) { + set.add(nativeSymbol); + } + } + } + return [...set]; +} + +/** + * Convert a native symbol index into a NativeSymbolInfo object, to create + * something that's meaningful outside of its associated thread. + */ +export function getNativeSymbolInfo( + nativeSymbol: IndexIntoNativeSymbolTable, + nativeSymbols: NativeSymbolTable, + frameTable: FrameTable, + stringTable: UniqueStringArray +): NativeSymbolInfo { + const functionSizeOrNull = nativeSymbols.functionSize[nativeSymbol]; + const functionSize = + functionSizeOrNull ?? + calculateFunctionSizeLowerBound( + frameTable, + nativeSymbols.address[nativeSymbol], + nativeSymbol + ); + return { + libIndex: nativeSymbols.libIndex[nativeSymbol], + address: nativeSymbols.address[nativeSymbol], + name: stringTable.getString(nativeSymbols.name[nativeSymbol]), + functionSize, + functionSizeIsKnown: functionSizeOrNull !== null, + }; +} + +/** + * Calculate the BottomBoxInfo for a call node, i.e. information about which + * things should be shown in the profiler UI's "bottom box" when this call node + * is double-clicked. + * + * We always want to update all panes in the bottom box when a new call node is + * double-clicked, so that we don't show inconsistent information side-by-side. + */ +export function getBottomBoxInfoForCallNode( + callNodeIndex: IndexIntoCallNodeTable, + callNodeInfo: CallNodeInfo, + thread: Thread +): BottomBoxInfo { + const { + stackTable, + frameTable, + funcTable, + stringTable, + resourceTable, + nativeSymbols, + } = thread; + + const funcIndex = callNodeInfo.callNodeTable.func[callNodeIndex]; + const fileName = funcTable.fileName[funcIndex]; + const sourceFile = fileName !== null ? stringTable.getString(fileName) : null; + const resource = funcTable.resource[funcIndex]; + const libIndex = + resource !== -1 && resourceTable.type[resource] === resourceTypes.library + ? resourceTable.lib[resource] + : null; + const nativeSymbolsForCallNode = getNativeSymbolsForCallNode( + callNodeIndex, + callNodeInfo, + stackTable, + frameTable + ); + const nativeSymbolInfosForCallNode = nativeSymbolsForCallNode.map( + (nativeSymbolIndex) => + getNativeSymbolInfo( + nativeSymbolIndex, + nativeSymbols, + frameTable, + stringTable + ) + ); + + return { + libIndex, + sourceFile, + nativeSymbols: nativeSymbolInfosForCallNode, + }; +} + /** * Determines the timeline type by looking at the profile data. * diff --git a/src/reducers/app.js b/src/reducers/app.js index a860d229ca..73821f5c2e 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -144,7 +144,7 @@ const panelLayoutGeneration: Reducer = (state = 0, action) => { case 'COMMIT_RANGE': case 'POP_COMMITTED_RANGES': // Bottom box: (fallthrough) - case 'OPEN_SOURCE_VIEW': + case 'UPDATE_BOTTOM_BOX': case 'CLOSE_BOTTOM_BOX_FOR_TAB': return state + 1; default: diff --git a/src/reducers/url-state.js b/src/reducers/url-state.js index 69944016f3..6594ab0d26 100644 --- a/src/reducers/url-state.js +++ b/src/reducers/url-state.js @@ -22,6 +22,7 @@ import type { Reducer, TimelineTrackOrganization, SourceViewState, + AssemblyViewState, IsOpenPerPanelState, } from 'firefox-profiler/types'; @@ -554,14 +555,51 @@ const timelineTrackOrganization: Reducer = ( }; const sourceView: Reducer = ( - state = { activationGeneration: 0, file: null }, + state = { activationGeneration: 0, libIndex: null, sourceFile: null }, action ) => { switch (action.type) { - case 'OPEN_SOURCE_VIEW': { + case 'UPDATE_BOTTOM_BOX': { return { activationGeneration: state.activationGeneration + 1, - file: action.file, + libIndex: action.libIndex, + sourceFile: action.sourceFile, + }; + } + default: + return state; + } +}; + +const assemblyView: Reducer = ( + state = { + activationGeneration: 0, + nativeSymbol: null, + allNativeSymbolsForInitiatingCallNode: [], + isOpen: false, + }, + action +) => { + switch (action.type) { + case 'UPDATE_BOTTOM_BOX': { + return { + activationGeneration: state.activationGeneration + 1, + nativeSymbol: action.nativeSymbol, + allNativeSymbolsForInitiatingCallNode: + action.allNativeSymbolsForInitiatingCallNode, + isOpen: state.isOpen || action.shouldOpenAssemblyView, + }; + } + case 'OPEN_ASSEMBLY_VIEW': { + return { + ...state, + isOpen: true, + }; + } + case 'CLOSE_ASSEMBLY_VIEW': { + return { + ...state, + isOpen: false, }; } default: @@ -580,9 +618,9 @@ const isBottomBoxOpenPerPanel: Reducer = ( action ) => { switch (action.type) { - case 'OPEN_SOURCE_VIEW': { - const { currentTab } = action; - if (!state[currentTab]) { + case 'UPDATE_BOTTOM_BOX': { + const { currentTab, shouldOpenBottomBox } = action; + if (shouldOpenBottomBox && !state[currentTab]) { return { ...state, [currentTab]: true }; } return state; @@ -662,6 +700,7 @@ const profileSpecific = combineReducers({ networkSearchString, transforms, sourceView, + assemblyView, isBottomBoxOpenPerPanel, timelineType, full: fullProfileSpecific, diff --git a/src/selectors/url-state.js b/src/selectors/url-state.js index 33a1a0f27d..ccc70924c2 100644 --- a/src/selectors/url-state.js +++ b/src/selectors/url-state.js @@ -77,7 +77,7 @@ export const getInvertCallstack: Selector = (state) => export const getShowUserTimings: Selector = (state) => getProfileSpecificState(state).showUserTimings; export const getSourceViewFile: Selector = (state) => - getProfileSpecificState(state).sourceView.file; + getProfileSpecificState(state).sourceView.sourceFile; export const getSourceViewActivationGeneration: Selector = (state) => getProfileSpecificState(state).sourceView.activationGeneration; export const getShowJsTracerSummary: Selector = (state) => diff --git a/src/test/fixtures/profiles/processed-profile.js b/src/test/fixtures/profiles/processed-profile.js index 503809251c..763fc7ccf0 100644 --- a/src/test/fixtures/profiles/processed-profile.js +++ b/src/test/fixtures/profiles/processed-profile.js @@ -43,6 +43,7 @@ import type { ThreadCPUDeltaUnit, LineNumber, Address, + Bytes, CallNodePath, } from 'firefox-profiler/types'; import { @@ -503,14 +504,14 @@ export function getProfileWithNamedThreads(threadNames: string[]): Profile { * - [line:*] - The line, affects frameTable.line * - [address:*] - The frame address, affects frameTable.address * - [inl:*] - The inline depth, affects frameTable.inlineDepth - * - [sym:*] - The native symbol, affects frameTable.nativeSymbol + * - [sym:::] - The native symbol, affects frameTable.nativeSymbol (keyed on ) ```js // Execute the code below in the web console in the profiler to get a stack that's // ready to be pasted into getProfileFromTextSamples. function getFrame( - { stackTable, frameTable, funcTable, stringTable, resourceTable, libs }, + { stackTable, frameTable, funcTable, stringTable, resourceTable, nativeSymbols, libs }, frameIndex ) { const funcIndex = frameTable.func[frameIndex]; @@ -530,7 +531,15 @@ function getFrame( } const address = frameTable.address[frameIndex]; if (address !== -1) { - s += `[address:0x${address.toString(16)}]`; + s += `[address:${address.toString(16)}]`; + } + const nativeSymbol = frameTable.nativeSymbol[frameIndex]; + if (nativeSymbol !== null) { + const symName = stringTable.getString(nativeSymbols.name[nativeSymbol]); + const symAddrStr = nativeSymbols.address[nativeSymbol].toString(16); + const functionSize = nativeSymbols.functionSize[nativeSymbol]; + cost symSizeStr = functionSize !== null ? functionSize.toString(16) : ''; + s += `[sym:${symName}:${symAddrStr}:${symSizeStr}]`; } const inlineDepth = frameTable.inlineDepth[frameIndex]; if (inlineDepth !== 0) { @@ -802,7 +811,7 @@ function _findLineNumberFromFuncName( function _findAddressFromFuncName( funcNameWithModifier: string ): Address | null { - const findAddressResult = /\[address:0x([0-9a-f]+)\]/.exec( + const findAddressResult = /\[address:([0-9a-f]+)\]/.exec( funcNameWithModifier ); if (findAddressResult) { @@ -825,10 +834,20 @@ function _findInlineDepthFromFuncName( function _findNativeSymbolNameFromFuncName( funcNameWithModifier: string -): string | null { +): {| name: string, address: Address, functionSize: Bytes | null |} | null { const findNativeSymbolResult = /\[sym:([^\]]+)\]/.exec(funcNameWithModifier); if (findNativeSymbolResult) { - return findNativeSymbolResult[1]; + const s = findNativeSymbolResult[1]; + const symbolInfoResult = /([^:]+):([0-9a-f]+):([0-9a-f]*)/.exec(s); + if (!symbolInfoResult) { + throw new Error(`Incorrect [sym:...] syntax: ${s}`); + } + return { + name: symbolInfoResult[1], + address: parseInt(symbolInfoResult[2], 16), + functionSize: + symbolInfoResult[3] !== '' ? parseInt(symbolInfoResult[3], 16) : null, + }; } return null; @@ -932,11 +951,12 @@ function _buildThreadFromTextOnlyStacks( (funcName.startsWith('0x') ? parseInt(funcName.substr(2), 16) : -1); let nativeSymbol = null; - const nativeSymbolName = + const nativeSymbolInfo = _findNativeSymbolNameFromFuncName(funcNameWithModifier); - if (nativeSymbolName) { - const nativeSymbolNameStringIndex = - stringTable.indexForString(nativeSymbolName); + if (nativeSymbolInfo) { + const nativeSymbolNameStringIndex = stringTable.indexForString( + nativeSymbolInfo.name + ); const nativeSymbolIndex = nativeSymbols.name.indexOf( nativeSymbolNameStringIndex ); @@ -945,9 +965,9 @@ function _buildThreadFromTextOnlyStacks( } else if (libIndex !== null) { nativeSymbol = nativeSymbols.length++; nativeSymbols.libIndex.push(libIndex); - nativeSymbols.address.push(0); // todo + nativeSymbols.address.push(nativeSymbolInfo.address); nativeSymbols.name.push(nativeSymbolNameStringIndex); - nativeSymbols.functionSize.push(null); + nativeSymbols.functionSize.push(nativeSymbolInfo.functionSize); } else { throw new Error( `[sym:] has to be used together with [lib:] - missing lib in "${funcNameWithModifier}"` diff --git a/src/test/store/__snapshots__/profile-view.test.js.snap b/src/test/store/__snapshots__/profile-view.test.js.snap index 66f614406b..77cccba8d2 100644 --- a/src/test/store/__snapshots__/profile-view.test.js.snap +++ b/src/test/store/__snapshots__/profile-view.test.js.snap @@ -2344,6 +2344,91 @@ CallTree { 0, 0, ], + "_callNodeInfo": Object { + "callNodeTable": Object { + "category": Int32Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "depth": Array [ + 0, + 1, + 2, + 3, + 2, + 3, + 2, + 3, + ], + "func": Int32Array [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 8, + ], + "innerWindowID": Float64Array [ + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + ], + "length": 8, + "prefix": Int32Array [ + -1, + 0, + 1, + 2, + 1, + 4, + 1, + 6, + ], + "sourceFramesInlinedIntoSymbol": Array [ + null, + null, + null, + null, + null, + null, + null, + null, + ], + "subcategory": Int32Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + "stackIndexToCallNodeIndex": Uint32Array [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + ], + }, "_callNodeSummary": Object { "self": Float32Array [ 0, diff --git a/src/test/unit/address-timings.test.js b/src/test/unit/address-timings.test.js index c787dfdf3f..0e32133d84 100644 --- a/src/test/unit/address-timings.test.js +++ b/src/test/unit/address-timings.test.js @@ -26,10 +26,10 @@ import type { describe('getStackAddressInfo', function () { it('computes results for all stacks', function () { const { profile, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` - A[lib:one][address:0x20][sym:Asym] A[lib:one][address:0x21][sym:Asym] A[lib:one][address:0x20][sym:Asym] - B[lib:one][address:0x30][sym:Bsym] B[lib:one][address:0x30][sym:Bsym] B[lib:one][address:0x30][sym:Bsym] - C[lib:two][address:0x10][sym:Csym] C[lib:two][address:0x11][sym:Csym] D[lib:two][address:0x40][sym:Dsym] - B[lib:one][address:0x30][sym:Bsym] D[lib:two][address:0x40][sym:Dsym] + A[lib:one][address:20][sym:Asym:20:] A[lib:one][address:21][sym:Asym:20:] A[lib:one][address:20][sym:Asym:20:] + B[lib:one][address:30][sym:Bsym:30:] B[lib:one][address:30][sym:Bsym:30:] B[lib:one][address:30][sym:Bsym:30:] + C[lib:two][address:10][sym:Csym:10:] C[lib:two][address:11][sym:Csym:10:] D[lib:two][address:40][sym:Dsym:40:] + B[lib:one][address:30][sym:Bsym:30:] D[lib:two][address:40][sym:Dsym:40:] `); const [thread] = profile.threads; const [{ Asym }] = nativeSymbolsDictPerThread; @@ -71,8 +71,8 @@ describe('getAddressTimings for getStackAddressInfo', function () { // In this example, there's one self address hit at address 0x30. // Both address 0x20 and address 0x30 have one total time hit. const { profile, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` - A[lib:file][address:0x20][sym:Asym] - A[lib:file][address:0x30][sym:Asym] + A[lib:file][address:20][sym:Asym:20:] + A[lib:file][address:30][sym:Asym:20:] `); const [thread] = profile.threads; const [{ Asym }] = nativeSymbolsDictPerThread; @@ -89,10 +89,10 @@ describe('getAddressTimings for getStackAddressInfo', function () { // In this example, there's one self address hit at address 0x30. // Both address 0x20 and address 0x30 have one total time hit. const { profile, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` - A[lib:file][address:0x20][sym:Asym] - B[lib:file][address:0x20][sym:Asym][inl:1] - C[lib:file][address:0x20][sym:Asym][inl:2] - A[lib:file][address:0x30][sym:Asym] + A[lib:file][address:20][sym:Asym:20:] + B[lib:file][address:20][sym:Asym:20:][inl:1] + C[lib:file][address:20][sym:Asym:20:][inl:2] + A[lib:file][address:30][sym:Asym:20:] `); const [thread] = profile.threads; const [{ Asym }] = nativeSymbolsDictPerThread; @@ -107,10 +107,10 @@ describe('getAddressTimings for getStackAddressInfo', function () { it('passes a test with two files and recursion', function () { const { profile, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` - A[lib:one][address:0x20][sym:Asym] A[lib:one][address:0x21][sym:Asym] A[lib:one][address:0x20][sym:Asym] - B[lib:one][address:0x30][sym:Bsym] B[lib:one][address:0x30][sym:Bsym] B[lib:one][address:0x30][sym:Bsym] - C[lib:two][address:0x10][sym:Csym] C[lib:two][address:0x11][sym:Csym] D[lib:two][address:0x40][sym:Dsym] - B[lib:one][address:0x30][sym:Bsym] D[lib:two][address:0x40][sym:Dsym] + A[lib:one][address:20][sym:Asym:20:] A[lib:one][address:21][sym:Asym:20:] A[lib:one][address:20][sym:Asym:20:] + B[lib:one][address:30][sym:Bsym:30:] B[lib:one][address:30][sym:Bsym:30:] B[lib:one][address:30][sym:Bsym:30:] + C[lib:two][address:10][sym:Csym:10:] C[lib:two][address:11][sym:Csym:10:] D[lib:two][address:40][sym:Dsym:40:] + B[lib:one][address:30][sym:Bsym:30:] D[lib:two][address:40][sym:Dsym:40:] `); const [thread] = profile.threads; const [{ Asym, Bsym, Csym, Dsym }] = nativeSymbolsDictPerThread; @@ -157,10 +157,10 @@ describe('getAddressTimings for getStackAddressInfo', function () { it('computes the same values on an inverted thread', function () { const { profile, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` - A[lib:one][address:0x20][sym:Asym] A[lib:one][address:0x21][sym:Asym] A[lib:one][address:0x20][sym:Asym] - B[lib:one][address:0x30][sym:Bsym] B[lib:one][address:0x30][sym:Bsym] B[lib:one][address:0x30][sym:Bsym] - C[lib:two][address:0x10][sym:Csym] C[lib:two][address:0x11][sym:Csym] D[lib:two][address:0x40][sym:Dsym] - B[lib:one][address:0x30][sym:Bsym] D[lib:two][address:0x40][sym:Dsym] + A[lib:one][address:20][sym:Asym:20:] A[lib:one][address:21][sym:Asym:20:] A[lib:one][address:20][sym:Asym:20:] + B[lib:one][address:30][sym:Bsym:30:] B[lib:one][address:30][sym:Bsym:30:] B[lib:one][address:30][sym:Bsym:30:] + C[lib:two][address:10][sym:Csym:10:] C[lib:two][address:11][sym:Csym:10:] D[lib:two][address:40][sym:Dsym:40:] + B[lib:one][address:30][sym:Bsym:30:] D[lib:two][address:40][sym:Dsym:40:] `); const categories = ensureExists( profile.meta.categories, @@ -223,8 +223,8 @@ describe('getAddressTimings for getStackAddressInfoForCallNode', function () { it('passes a basic test', function () { const { profile, funcNamesDictPerThread, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` - A[lib:file][address:0x20][sym:Asym] - B[lib:file][address:0x30][sym:Bsym] + A[lib:file][address:20][sym:Asym:20:] + B[lib:file][address:30][sym:Bsym:30:] `); const categories = ensureExists( profile.meta.categories, @@ -261,9 +261,9 @@ describe('getAddressTimings for getStackAddressInfoForCallNode', function () { it('passes a basic test with recursion', function () { const { profile, funcNamesDictPerThread, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` - A[lib:file][address:0x20][sym:Asym] - B[lib:file][address:0x30][sym:Bsym] - A[lib:file][address:0x21][sym:Asym] + A[lib:file][address:20][sym:Asym:20:] + B[lib:file][address:30][sym:Bsym:30:] + A[lib:file][address:21][sym:Asym:20:] `); const categories = ensureExists( profile.meta.categories, @@ -302,10 +302,10 @@ describe('getAddressTimings for getStackAddressInfoForCallNode', function () { it('passes a test where the same function is called via different call paths', function () { const { profile, funcNamesDictPerThread, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` - A[lib:one][address:0x20][sym:Asym] A[lib:one][address:0x21][sym:Asym] A[lib:one][address:0x20][sym:Asym] - B[lib:one][address:0x30][sym:Bsym] D[lib:one][address:0x50][sym:Dsym] B[lib:one][address:0x31][sym:Bsym] - C[lib:two][address:0x10][sym:Csym] C[lib:two][address:0x11][sym:Csym] C[lib:two][address:0x12][sym:Csym] - D[lib:one][address:0x51][sym:Dsym] + A[lib:one][address:20][sym:Asym:20:] A[lib:one][address:21][sym:Asym:20:] A[lib:one][address:20][sym:Asym:20:] + B[lib:one][address:30][sym:Bsym:30:] D[lib:one][address:50][sym:Dsym:40:] B[lib:one][address:31][sym:Bsym:30:] + C[lib:two][address:10][sym:Csym:10:] C[lib:two][address:11][sym:Csym:10:] C[lib:two][address:12][sym:Csym:10:] + D[lib:one][address:51][sym:Dsym:40:] `); const categories = ensureExists( profile.meta.categories, @@ -334,10 +334,10 @@ describe('getAddressTimings for getStackAddressInfoForCallNode', function () { it('passes a test with an inverted thread', function () { const { profile, funcNamesDictPerThread, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` - A[lib:one][address:0x20][sym:Asym] A[lib:one][address:0x21][sym:Asym] A[lib:one][address:0x20][sym:Asym] - B[lib:one][address:0x30][sym:Bsym] D[lib:one][address:0x50][sym:Dsym] B[lib:one][address:0x31][sym:Bsym] - D[lib:one][address:0x51][sym:Dsym] D[lib:one][address:0x52][sym:Dsym] C[lib:two][address:0x12][sym:Csym] - D[lib:one][address:0x51][sym:Dsym] + A[lib:one][address:20][sym:Asym:20:] A[lib:one][address:21][sym:Asym:20:] A[lib:one][address:20][sym:Asym:20:] + B[lib:one][address:30][sym:Bsym:30:] D[lib:one][address:50][sym:Dsym:40:] B[lib:one][address:31][sym:Bsym:30:] + D[lib:one][address:51][sym:Dsym:40:] D[lib:one][address:52][sym:Dsym:40:] C[lib:two][address:12][sym:Csym:10:] + D[lib:one][address:51][sym:Dsym:40:] `); const categories = ensureExists( profile.meta.categories, @@ -394,10 +394,10 @@ describe('getAddressTimings for getStackAddressInfoForCallNode', function () { // In this test, we compute the timings for native symbol Bsym. const { profile, funcNamesDictPerThread, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` - A[lib:one][address:0x20][sym:Asym] A[lib:one][address:0x30][sym:Asym] A[lib:one][address:0x20][sym:Asym] A[lib:one][address:0x20][sym:Asym] - B[lib:one][address:0x40][sym:Bsym] B[lib:one][address:0x30][sym:Asym][inl:1] B[lib:one][address:0x45][sym:Bsym] E[lib:one][address:0x31][sym:Esym] - C[lib:one][address:0x40][sym:Bsym][inl:1] C[lib:one][address:0x30][sym:Asym][inl:2] C[lib:one][address:0x45][sym:Bsym] - D[lib:one][address:0x51][sym:Dsym] + A[lib:one][address:20][sym:Asym:20:] A[lib:one][address:30][sym:Asym:20:] A[lib:one][address:20][sym:Asym:20:] A[lib:one][address:20][sym:Asym:20:] + B[lib:one][address:40][sym:Bsym:30:] B[lib:one][address:30][sym:Asym:20:][inl:1] B[lib:one][address:45][sym:Bsym:30:] E[lib:one][address:31][sym:Esym:30:] + C[lib:one][address:40][sym:Bsym:30:][inl:1] C[lib:one][address:30][sym:Asym:20:][inl:2] C[lib:one][address:45][sym:Bsym:30:] + D[lib:one][address:51][sym:Dsym:40:] `); const categories = ensureExists( profile.meta.categories, diff --git a/src/test/unit/profile-data.test.js b/src/test/unit/profile-data.test.js index a046eb8cb2..16f657d8b4 100644 --- a/src/test/unit/profile-data.test.js +++ b/src/test/unit/profile-data.test.js @@ -13,6 +13,7 @@ import { } from '../../profile-logic/process-profile'; import { getCallNodeInfo, + invertCallstack, filterThreadByImplementation, getCallNodePathFromIndex, getSampleIndexClosestToStartTime, @@ -23,6 +24,9 @@ import { getSamplesSelectedStates, extractProfileFilterPageData, findAddressProofForFile, + calculateFunctionSizeLowerBound, + getNativeSymbolsForCallNode, + getNativeSymbolInfo, } from '../../profile-logic/profile-data'; import { resourceTypes } from '../../profile-logic/data-structures'; import { @@ -1099,15 +1103,15 @@ describe('extractProfileFilterPageData', function () { describe('findAddressProofForFile', function () { it('finds a correct address for a file', function () { const { profile } = getProfileFromTextSamples(` - wr_renderer_render[lib:XUL][file:/Users/mstange/code/mozilla/gfx/webrender_bindings/src/bindings.rs][line:622][address:0x49d67a7] - webrender::renderer::Renderer::render[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:1724][address:0x4bcff7b] - webrender::renderer::Renderer::render_impl[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:2002][address:0x4bd0c57] - webrender::renderer::Renderer::draw_frame[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:4701][address:0x4bd8d8b] - webrender::renderer::Renderer::draw_picture_cache_target[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:2808][address:0x4bd8d8b][inl:1] - webrender::renderer::Renderer::draw_alpha_batch_container[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:2980][address:0x4bd4d43] - webrender::renderer::Renderer::draw_picture_cache_target[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:2808][address:0x4bd8d8b][inl:1] - webrender::renderer::Renderer::draw_alpha_batch_container[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:2980][address:0x4bd4d43] - webrender::renderer::shade::LazilyCompiledShader::bind[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/shade.rs][line:150][address:0x4a9f89b] + wr_renderer_render[lib:XUL][file:/Users/mstange/code/mozilla/gfx/webrender_bindings/src/bindings.rs][line:622][address:49d67a7] + webrender::renderer::Renderer::render[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:1724][address:4bcff7b] + webrender::renderer::Renderer::render_impl[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:2002][address:4bd0c57] + webrender::renderer::Renderer::draw_frame[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:4701][address:4bd8d8b] + webrender::renderer::Renderer::draw_picture_cache_target[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:2808][address:4bd8d8b][inl:1] + webrender::renderer::Renderer::draw_alpha_batch_container[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:2980][address:4bd4d43] + webrender::renderer::Renderer::draw_picture_cache_target[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:2808][address:4bd8d8b][inl:1] + webrender::renderer::Renderer::draw_alpha_batch_container[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/mod.rs][line:2980][address:4bd4d43] + webrender::renderer::shade::LazilyCompiledShader::bind[lib:XUL][file:/Users/mstange/code/mozilla/gfx/wr/webrender/src/renderer/shade.rs][line:150][address:4a9f89b] `); const addressProof1 = findAddressProofForFile( @@ -1137,3 +1141,164 @@ describe('findAddressProofForFile', function () { expect(missingAddressProof).toBeNull(); }); }); + +describe('calculateFunctionSizeLowerBound', function () { + it('calculates the correct minimum size', function () { + const { profile, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` + some_function[lib:XUL][file:hello.cpp][line:622][address:1005][sym:symSomeFunc:1000:] + some_function[lib:XUL][file:hello.cpp][line:622][address:1002][sym:symSomeFunc:1000:] + some_function[lib:XUL][file:hello.cpp][line:622][address:1007][sym:symSomeFunc:1000:] + `); + + const thread = profile.threads[0]; + const nativeSymbolsDict = nativeSymbolsDictPerThread[0]; + const nativeSymbolIndex = nativeSymbolsDict.symSomeFunc; + + const functionSizeLowerBound = calculateFunctionSizeLowerBound( + thread.frameTable, + 0x1000, + nativeSymbolIndex + ); + expect(functionSizeLowerBound).toEqual(8); // 0x1007 - 0x1000 + 1 + }); +}); + +describe('getNativeSymbolsForCallNode', function () { + it('finds a single symbol', function () { + const { profile, funcNamesDictPerThread, nativeSymbolsDictPerThread } = + getProfileFromTextSamples(` + funA[lib:XUL][address:1005][sym:symA:1000:] + funB[lib:XUL][address:2007][sym:symB:2000:] + funC[lib:XUL][address:2007][sym:symB:2000:][inl:1] + `); + + const thread = profile.threads[0]; + const { funA, funB, funC } = funcNamesDictPerThread[0]; + const { symB } = nativeSymbolsDictPerThread[0]; + const categories = ensureExists( + profile.meta.categories, + 'Expected to find categories' + ); + const defaultCategory = categories.findIndex((c) => c.name === 'Other'); + const callNodeInfo = getCallNodeInfo( + thread.stackTable, + thread.frameTable, + thread.funcTable, + defaultCategory + ); + const ab = getCallNodeIndexFromPath( + [funA, funB], + callNodeInfo.callNodeTable + ); + expect(ab).not.toBeNull(); + const abc = getCallNodeIndexFromPath( + [funA, funB, funC], + callNodeInfo.callNodeTable + ); + expect(abc).not.toBeNull(); + + // Both the call path [funA, funB] and the call path [funA, funB, funC] end + // up at a call node with native symbol symB. + expect( + getNativeSymbolsForCallNode( + ensureExists(ab), + callNodeInfo, + thread.stackTable, + thread.frameTable + ) + ).toEqual([symB]); + expect( + getNativeSymbolsForCallNode( + ensureExists(abc), + callNodeInfo, + thread.stackTable, + thread.frameTable + ) + ).toEqual([symB]); + }); + + it('finds multiple symbols', function () { + const { profile, funcNamesDictPerThread, nativeSymbolsDictPerThread } = + getProfileFromTextSamples(` + funA[lib:XUL][address:1005][sym:symA:1000:] funA[lib:XUL][address:1005][sym:symA:1000:] + funB[lib:XUL][address:2007][sym:symB:2000:] funD[lib:XUL][address:4007][sym:symD:4000:] + funC[lib:XUL][address:2007][sym:symB:2000:][inl:1] funC[lib:XUL][address:4007][sym:symD:4000:][inl:1] + `); + + const thread = profile.threads[0]; + const { funC } = funcNamesDictPerThread[0]; + const { symB, symD } = nativeSymbolsDictPerThread[0]; + const categories = ensureExists( + profile.meta.categories, + 'Expected to find categories' + ); + const defaultCategory = categories.findIndex((c) => c.name === 'Other'); + const invertedThread = invertCallstack(thread, defaultCategory); + const callNodeInfo = getCallNodeInfo( + invertedThread.stackTable, + invertedThread.frameTable, + invertedThread.funcTable, + defaultCategory + ); + const c = getCallNodeIndexFromPath([funC], callNodeInfo.callNodeTable); + expect(c).not.toBeNull(); + + // The call node for funC in the inverted thread has one sample where funC + // is called by funB and one sample where it's called by funD. The call to + // funC was inlined into each of those functions. So the call node has two + // native symbols, B and D. + expect( + new Set( + getNativeSymbolsForCallNode( + ensureExists(c), + callNodeInfo, + invertedThread.stackTable, + invertedThread.frameTable + ) + ) + ).toEqual(new Set([symB, symD])); + }); +}); + +describe('getNativeSymbolInfo', function () { + it('calculates the correct native symbol info', function () { + const { profile, nativeSymbolsDictPerThread } = getProfileFromTextSamples(` + some_function[lib:XUL][file:hello.cpp][line:622][address:1005][sym:symSomeFunc:1000:] + some_function[lib:XUL][file:hello.cpp][line:622][address:1002][sym:symSomeFunc:1000:] + some_function[lib:XUL][file:hello.cpp][line:622][address:1007][sym:symSomeFunc:1000:] + other_function[lib:XUL][file:hello.cpp][line:622][address:2007][sym:symOtherFunc:2000:1e] + `); + + const thread = profile.threads[0]; + const { symSomeFunc, symOtherFunc } = nativeSymbolsDictPerThread[0]; + + expect( + getNativeSymbolInfo( + symSomeFunc, + thread.nativeSymbols, + thread.frameTable, + thread.stringTable + ) + ).toEqual({ + name: 'symSomeFunc', + address: 0x1000, + functionSize: 8, + functionSizeIsKnown: false, + libIndex: profile.libs.findIndex((l) => l.name === 'XUL'), + }); + expect( + getNativeSymbolInfo( + symOtherFunc, + thread.nativeSymbols, + thread.frameTable, + thread.stringTable + ) + ).toEqual({ + name: 'symOtherFunc', + address: 0x2000, + functionSize: 0x1e, + functionSizeIsKnown: true, + libIndex: profile.libs.findIndex((l) => l.name === 'XUL'), + }); + }); +}); diff --git a/src/types/actions.js b/src/types/actions.js index 17435ba2c8..85e9185cc0 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -13,6 +13,7 @@ import type { Pid, TabID, IndexIntoCategoryList, + IndexIntoLibs, } from './profile'; import type { CallNodePath, @@ -24,6 +25,7 @@ import type { OriginsTimeline, ActiveTabTimeline, ThreadsKey, + NativeSymbolInfo, } from './profile-derived'; import type { FuncToFuncsMap } from '../profile-logic/symbolication'; import type { TemporaryError } from '../utils/errors'; @@ -349,9 +351,20 @@ type ProfileAction = +localTrackOrderByPid: Map, |} | {| - +type: 'OPEN_SOURCE_VIEW', - +file: string, + +type: 'UPDATE_BOTTOM_BOX', + +libIndex: IndexIntoLibs | null, + +sourceFile: string | null, + +nativeSymbol: NativeSymbolInfo | null, + +allNativeSymbolsForInitiatingCallNode: NativeSymbolInfo[], +currentTab: TabSlug, + +shouldOpenBottomBox: boolean, + +shouldOpenAssemblyView: boolean, + |} + | {| + +type: 'OPEN_ASSEMBLY_VIEW', + |} + | {| + +type: 'CLOSE_ASSEMBLY_VIEW', |} | {| +type: 'CLOSE_BOTTOM_BOX_FOR_TAB', diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index d8f413437c..1f31805ef8 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // @flow -import type { Milliseconds, StartEndRange, Address } from './units'; +import type { Milliseconds, StartEndRange, Address, Bytes } from './units'; import type { MarkerPayload } from './markers'; import type { IndexIntoFuncTable, @@ -12,6 +12,7 @@ import type { IndexIntoJsTracerEvents, IndexIntoCategoryList, IndexIntoNativeSymbolTable, + IndexIntoLibs, CounterIndex, InnerWindowID, Page, @@ -531,3 +532,30 @@ export type EventDelayInfo = {| * comma separated thread indexes, e.g. "5,7,10" */ export type ThreadsKey = string | number; + +/** + * A representation of a native symbol which is independent from a thread. + * This is used for storing the global state of the assembly view, which needs + * to be independent from the selected thread. An IndexIntoNativeSymbolTable + * would only be meaningful within a thread. + * This can be removed if the native symbol table ever becomes global. + */ +export type NativeSymbolInfo = {| + name: string, + address: Address, + // The number of bytes belonging to this function, starting at the symbol address. + // If functionSizeIsKnown is false, then this is a minimum size. + functionSize: Bytes, + functionSizeIsKnown: boolean, + libIndex: IndexIntoLibs, +|}; + +/** + * Information about the initiating call node when the bottom box (source view + + * assembly view) is updated. + */ +export type BottomBoxInfo = {| + libIndex: IndexIntoLibs | null, + sourceFile: string | null, + nativeSymbols: NativeSymbolInfo[], +|}; diff --git a/src/types/state.js b/src/types/state.js index 6c423b4984..94bc1f1a19 100644 --- a/src/types/state.js +++ b/src/types/state.js @@ -19,7 +19,13 @@ import type { } from './actions'; import type { TabSlug } from '../app-logic/tabs-handling'; import type { StartEndRange, CssPixels, Milliseconds } from './units'; -import type { Profile, ThreadIndex, Pid, TabID } from './profile'; +import type { + Profile, + ThreadIndex, + Pid, + TabID, + IndexIntoLibs, +} from './profile'; import type { CallNodePath, @@ -30,6 +36,7 @@ import type { ActiveTabTimeline, OriginsTimeline, ThreadsKey, + NativeSymbolInfo, } from './profile-derived'; import type { Attempt } from '../utils/errors'; import type { TransformStacksPerThread } from './transforms'; @@ -240,7 +247,33 @@ export type ZippedProfilesState = { export type SourceViewState = {| activationGeneration: number, - file: string | null, + // Non-null if this source file was opened for a function from native code. + // In theory, multiple different libraries can have source files with the same + // path but different content. + // Null if the source file is not for native code or if the lib is not known, + // for example if the source view was opened via the URL (the source URL param + // currently discards the libIndex). + libIndex: IndexIntoLibs | null, + // The path to the source file. Null if a function without a file path was + // double clicked. + sourceFile: string | null, +|}; + +export type AssemblyViewState = {| + // Whether the assembly view panel is open within the bottom box. This can be + // true even if the bottom box itself is closed. + isOpen: boolean, + // When this is incremented, the assembly view scrolls to the "hotspot" line. + activationGeneration: number, + // The native symbol for which the assembly code is being shown at the moment. + // Null if the initiating call node did not have a native symbol. + nativeSymbol: NativeSymbolInfo | null, + // The set of native symbols which contributed samples to the initiating call + // node. Often, this will just be one element (the same as `nativeSymbol`), + // but it can also be multiple elements, for example when double-clicking a + // function like `Vec::push` in an inverted call tree, if that function has + // been inlined into multiple different callers. + allNativeSymbolsForInitiatingCallNode: NativeSymbolInfo[], |}; export type FileSourceStatus = @@ -318,6 +351,7 @@ export type ProfileSpecificUrlState = {| transforms: TransformStacksPerThread, timelineType: TimelineType, sourceView: SourceViewState, + assemblyView: AssemblyViewState, isBottomBoxOpenPerPanel: IsOpenPerPanelState, full: FullProfileSpecificUrlState, activeTab: ActiveTabSpecificProfileUrlState,