{
+ // This compute the sum of the power in the range. This returns a value in Wh.
+ _computePowerSumForRange(start: Milliseconds, end: Milliseconds): number {
+ const { counter } = this.props;
+ const samples = counter.sampleGroups[0].samples;
+ const [beginIndex, endIndex] = getSampleIndexRangeForSelection(
+ samples,
+ start,
+ end
+ );
+
+ let sum = 0;
+ for (
+ let counterSampleIndex = beginIndex;
+ counterSampleIndex < endIndex;
+ counterSampleIndex++
+ ) {
+ sum += samples.count[counterSampleIndex]; // picowatt-hour;
+ }
+ return sum * 1e-12;
+ }
+
+ _computePowerSumForCommittedRange = memoize(
+ ({ start, end }: StartEndRange): number =>
+ this._computePowerSumForRange(start, end)
+ );
+
+ _computePowerSumForPreviewRange = memoize(
+ ({
+ selectionStart,
+ selectionEnd,
+ }: {
+ +hasSelection: true,
+ +selectionStart: number,
+ +selectionEnd: number,
+ }): number => this._computePowerSumForRange(selectionStart, selectionEnd)
+ );
+
+ _formatPowerValue(
+ power: number,
+ l10nIdUnit,
+ l10nIdMilliUnit,
+ l10nIdMicroUnit
+ ): Localized {
+ let value, l10nId;
+ if (power > 1) {
+ value = formatNumber(power, 3);
+ l10nId = l10nIdUnit;
+ } else if (power === 0) {
+ value = 0;
+ l10nId = l10nIdUnit;
+ } else if (power < 0.001 && l10nIdMicroUnit) {
+ value = formatNumber(power * 1000000);
+ l10nId = l10nIdMicroUnit;
+ } else {
+ value = formatNumber(power * 1000);
+ l10nId = l10nIdMilliUnit;
+ }
+
+ return (
+
+ {value}
+
+ );
+ }
+
+ render() {
+ const {
+ counter,
+ counterSampleIndex,
+ interval,
+ committedRange,
+ previewSelection,
+ } = this.props;
+ const samples = counter.sampleGroups[0].samples;
+
+ const powerUsageInPwh = samples.count[counterSampleIndex]; // picowatt-hour
+ const sampleTimeDeltaInMs =
+ counterSampleIndex === 0
+ ? interval
+ : samples.time[counterSampleIndex] -
+ samples.time[counterSampleIndex - 1];
+ const power =
+ ((powerUsageInPwh * 1e-12) /* pWh->Wh */ / sampleTimeDeltaInMs) *
+ 1000 * // ms->s
+ 3600; // s->h
+
+ return (
+
+
+ {this._formatPowerValue(
+ power,
+ 'TrackPower--tooltip-power-watt',
+ 'TrackPower--tooltip-power-milliwatt'
+ )}
+ {previewSelection.hasSelection
+ ? this._formatPowerValue(
+ this._computePowerSumForPreviewRange(previewSelection),
+ 'TrackPower--tooltip-energy-used-in-preview-watthour',
+ 'TrackPower--tooltip-energy-used-in-preview-milliwatthour',
+ 'TrackPower--tooltip-energy-used-in-preview-microwatthour'
+ )
+ : null}
+ {this._formatPowerValue(
+ this._computePowerSumForCommittedRange(committedRange),
+ 'TrackPower--tooltip-energy-used-in-range-watthour',
+ 'TrackPower--tooltip-energy-used-in-range-milliwatthour',
+ 'TrackPower--tooltip-energy-used-in-range-microwatthour'
+ )}
+
+
+ );
+ }
+}
+
+export const TooltipTrackPower = explicitConnect({
+ mapStateToProps: (state) => ({
+ interval: getProfileInterval(state),
+ committedRange: getCommittedRange(state),
+ previewSelection: getPreviewSelection(state),
+ }),
+ component: TooltipTrackPowerImpl,
+});
diff --git a/src/profile-logic/address-timings.js b/src/profile-logic/address-timings.js
new file mode 100644
index 0000000000..f456d771ce
--- /dev/null
+++ b/src/profile-logic/address-timings.js
@@ -0,0 +1,646 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// @flow
+
+/**
+ * In this file, "address" always means "instruction address", expressed as a
+ * byte offset into a given library ("relative address").
+ *
+ * The functions in this file (address-timings.js) behave very similarly to the
+ * ones in line-timings.js.
+ * line-timings.js is for the source view, and address-timings.js is for the
+ * assembly view.
+ *
+ * The assembly view displays the instructions for one "native symbol", i.e. for
+ * a function that the compiler created, and which the compiler didn't inline
+ * away entirely. Every such function has a start address (the symbol address)
+ * and a size in bytes. This defines an address range.
+ *
+ * Since the assembly view only displays the assembly code for a single function
+ * at a time, address-timings.js always computes information only for a single
+ * native symbol. It would also be reasonable to compute information for a
+ * single library; we'll see over time what makes more sense. The computed
+ * result for a single native symbol is small, but needs to be recomputed any
+ * time a different native symbol is selected. The computed result for an entire
+ * library would be quite large (e.g. all address hits for libxul.so), but it
+ * would not need to be recomputed when a different function is selected.
+ */
+
+/**
+ * Quick recap of the relationship between addresses, frames, funcs, and native
+ * symbols for native code:
+ * - There is one native symbol per "outer" (i.e. non-inlined) function.
+ * - There is one func per function name + file name pair. Funcs are used for
+ * both inlined and non-inlined calls.
+ * - Each frame has at least the following properties:
+ * address, nativeSymbol, func, inlineDepth
+ * - As a result, there is a different frame for each sampled instruction address.
+ * - Multiple frames can share the same func.
+ * - Multiple frames can share the same native symbol.
+ *
+ * When there's inlining at a given address, then we create a multiple frames
+ * for that address with different func and inlineDepth values, and all these
+ * frames share the same address and native symbol.
+ *
+ * Here's an example "stack" tree.
+ *
+ * Before symbolication:
+ *
+ * - address:0x123
+ * - address:0x250
+ * - address:0x307
+ * - address:0x427
+ * - address:0x129
+ * - address:0x435
+ *
+ * After symbolication:
+ *
+ * - func:A nativeSymbol:A address:0x123
+ * - func:B nativeSymbol:B address:0x250
+ * - func:C nativeSymbol:B address:0x250 (inlineDepth:1)
+ * - func:D nativeSymbol:B address:0x250 (inlineDepth:2)
+ * - func:E nativeSymbol:E address:0x307
+ * - func:F nativeSymbol:F address:0x427
+ * - func:A nativeSymbol:A address:0x129
+ * - func:G nativeSymbol:A address:0x129 (inlineDepth:1)
+ * - func:F nativeSymbol:F address:0x435
+ * - func:D nativeSymbol:F address:0x435 (inlineDepth:1)
+ */
+
+import type {
+ FrameTable,
+ FuncTable,
+ StackTable,
+ SamplesLikeTable,
+ CallNodeInfo,
+ IndexIntoCallNodeTable,
+ IndexIntoNativeSymbolTable,
+ StackAddressInfo,
+ AddressTimings,
+ Address,
+} from 'firefox-profiler/types';
+
+/**
+ * For each stack in `stackTable`, and one specific native symbol, compute the
+ * sets of addresses for frames belonging to that native symbol that are hit by
+ * the stack.
+ *
+ * For each stack we answer the following question:
+ * - "Does this stack contribute to address X's self time?"
+ * Answer: result.selfAddress[stack] === X
+ * - "Does this stack contribute to address X's total time?"
+ * Answer: result.stackAddresses[stack].has(X)
+ */
+export function getStackAddressInfo(
+ stackTable: StackTable,
+ frameTable: FrameTable,
+ funcTable: FuncTable,
+ nativeSymbol: IndexIntoNativeSymbolTable,
+ isInvertedTree: boolean
+): StackAddressInfo {
+ return isInvertedTree
+ ? getStackAddressInfoInverted(
+ stackTable,
+ frameTable,
+ funcTable,
+ nativeSymbol
+ )
+ : getStackAddressInfoNonInverted(
+ stackTable,
+ frameTable,
+ funcTable,
+ nativeSymbol
+ );
+}
+
+/**
+ * This function handles the non-inverted case of getStackAddressInfo.
+ *
+ * Compute the sets of instruction addresses for the given native symbol that
+ * are hit by each stack.
+ * For each stack in the stack table and each address for the native symbol, we
+ * answer the questions "Does this stack contribute to address X's self time?
+ * Does it contribute to address X's total time?"
+ * Each stack can only contribute to one address's self time: the address of the
+ * stack's own frame.
+ * But each stack can contribute to the total time of multiple addresses for a
+ * single native symbol, if there's recursion and the same native symbol (outer
+ * function) is present in multiple places in the stack.
+ * E.g if function A calls into B which calls into A, the call path [A, B, A]
+ * will contribute to the total time of 2 addresses:
+ * 1. The address in function A which has the call instruction to B,
+ * 2. The address in function A that is being executed at that stack (stack.frame.address).
+ * And if the call to B has been inlined into A, then it'll still just be two
+ * addresses, because the inlined frame has the same address as its parent frame.
+ * But with more complicated recursion you could have more than two addresses
+ * from the same native symbol in the same stack.
+ *
+ * This last address in a stack is the stack's "self address".
+ * If there is recursion, and the same address is present in multiple frames in
+ * the same stack, the address is only counted once - the addresses are stored
+ * in a set.
+ *
+ * The returned StackAddressInfo is computed as follows:
+ * selfAddress[stack]:
+ * For stacks whose stack.frame.nativeSymbol is the given native symbol,
+ * this is stack.frame.address.
+ * For all other stacks this is null.
+ * stackAddresses[stack]:
+ * For stacks whose stack.frame.nativeSymbol is the given native symbol,
+ * this is the stackAddresses of its prefix stack, plus stack.frame.address
+ * added to the set.
+ * For all other stacks this is the same as the stackAddresses set of the
+ * stack's prefix.
+ */
+export function getStackAddressInfoNonInverted(
+ stackTable: StackTable,
+ frameTable: FrameTable,
+ funcTable: FuncTable,
+ nativeSymbol: IndexIntoNativeSymbolTable
+): StackAddressInfo {
+ // "self address" == "the address which a stack's self time is contributed to"
+ const selfAddressForAllStacks = [];
+ // "total addresses" == "the set of addresses whose total time this stack contributes to"
+ const totalAddressesForAllStacks = [];
+
+ // This loop takes advantage of the fact that the stack table is topologically ordered:
+ // Prefix stacks are always visited before their descendants.
+ // Each stack inherits the "total" addresses from its parent stack, and then adds its
+ // self address to that set. If the stack doesn't have a self address in the library, we just
+ // re-use the prefix's set object without copying it.
+ for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) {
+ const frame = stackTable.frame[stackIndex];
+ const prefixStack = stackTable.prefix[stackIndex];
+ const nativeSymbolOfThisStack = frameTable.nativeSymbol[frame];
+
+ let selfAddress: Address | null = null;
+ let totalAddresses: Set | null =
+ prefixStack !== null ? totalAddressesForAllStacks[prefixStack] : null;
+
+ if (nativeSymbolOfThisStack === nativeSymbol) {
+ selfAddress = frameTable.address[frame];
+ if (selfAddress !== -1) {
+ // Add this stack's address to this stack's totalAddresses. The rest of this stack's
+ // totalAddresses is the same as for the parent stack.
+ // We avoid creating new Set objects unless the new set is actually
+ // different.
+ if (totalAddresses === null) {
+ // None of the ancestor stack nodes have hit a address in the given library.
+ totalAddresses = new Set([selfAddress]);
+ } else if (!totalAddresses.has(selfAddress)) {
+ totalAddresses = new Set(totalAddresses);
+ totalAddresses.add(selfAddress);
+ }
+ }
+ }
+
+ selfAddressForAllStacks.push(selfAddress);
+ totalAddressesForAllStacks.push(totalAddresses);
+ }
+ return {
+ selfAddress: selfAddressForAllStacks,
+ stackAddresses: totalAddressesForAllStacks,
+ };
+}
+
+/**
+ * This function handles the inverted case of getStackAddressInfo.
+ *
+ * The return value should exactly match what you'd get if you called
+ * `getStackAddressInfo` on the corresponding non-inverted thread.
+ * This function can probably be removed once we handle call tree inversion
+ * differently.
+ *
+ * Reminder about inverted threads: The self time is in the *root* nodes. Example:
+ *
+ * Stack node A, address 0x20
+ * (called by) Stack node B, address 0x30
+ *
+ * The inverted stack [A, B] contributes to the self time of address 0x20.
+ *
+ * The returned StackAddressInfo is computed as follows:
+ * selfAddress[stack]:
+ * For (inverted thread) root stack nodes whose stack.frame.nativeSymbol is
+ * the given native symbol, this is stack.frame.address.
+ * For (inverted thread) root stack nodes whose frame is in a different
+ * native symbol, this is null.
+ * For (inverted thread) *non-root* stack nodes, this is the same as the
+ * selfAddress of the stack's prefix. This way, the selfAddress is always
+ * inherited from the subtree root.
+ * stackAddresses[stack]:
+ * For stacks whose stack.frame.nativeSymbol is the given native symbol,
+ * this is the stackAddresses of its (inverted thread) prefix stack, plus
+ * stack.frame.address added to the set.
+ * For all other stacks this is the same as the stackAddresses set of the
+ * stack's prefix.
+ */
+export function getStackAddressInfoInverted(
+ stackTable: StackTable,
+ frameTable: FrameTable,
+ funcTable: FuncTable,
+ nativeSymbol: IndexIntoNativeSymbolTable
+): StackAddressInfo {
+ // "self address" == "the address which a stack's self time is contributed to"
+ const selfAddressForAllStacks = [];
+ // "total addresses" == "the set of addresses whose total time this stack contributes to"
+ const totalAddressesForAllStacks = [];
+
+ // This loop takes advantage of the fact that the stack table is topologically ordered:
+ // Prefix stacks are always visited before their descendants.
+ for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) {
+ const frame = stackTable.frame[stackIndex];
+ const prefixStack = stackTable.prefix[stackIndex];
+ const nativeSymbolOfThisStack = frameTable.nativeSymbol[frame];
+
+ let selfAddress: Address | null = null;
+ let totalAddresses: Set | null = null;
+
+ if (prefixStack === null) {
+ // This stack node is a root of the inverted tree. That means that this stack's
+ // frame's address is where the self time is assigned, for the entire subtree of
+ // the inverted stack tree at this root.
+ if (nativeSymbolOfThisStack === nativeSymbol) {
+ selfAddress = frameTable.address[frame];
+ if (selfAddress !== -1) {
+ totalAddresses = new Set([selfAddress]);
+ }
+ }
+ } else {
+ // This stack node has a prefix, which, in inverted mode, means that *this node
+ // calls someone else, and that's where the time is spent*. The prefix is the callee.
+ // So this stack node contributes its time to its root node's address.
+ // We inherit the prefix's self address.
+ selfAddress = selfAddressForAllStacks[prefixStack];
+
+ // Add this stack's address to the totalAddresses set.
+ totalAddresses = totalAddressesForAllStacks[prefixStack];
+ if (nativeSymbolOfThisStack === nativeSymbol) {
+ const thisStackAddress = frameTable.address[frame];
+ if (thisStackAddress !== -1) {
+ if (totalAddresses === null) {
+ totalAddresses = new Set([thisStackAddress]);
+ } else if (!totalAddresses.has(thisStackAddress)) {
+ totalAddresses = new Set(totalAddresses);
+ totalAddresses.add(thisStackAddress);
+ }
+ }
+ }
+ }
+
+ selfAddressForAllStacks.push(selfAddress);
+ totalAddressesForAllStacks.push(totalAddresses);
+ }
+ return {
+ selfAddress: selfAddressForAllStacks,
+ stackAddresses: totalAddressesForAllStacks,
+ };
+}
+
+/**
+ * Gathers the addresses which are hit by a given call node.
+ * This is different from `getStackAddressInfo`: `getStackAddressInfo` counts
+ * address hits anywhere in the stack, and this function only counts hits *in
+ * the given call node*.
+ *
+ * This is useful when opening the assembly view from a call node: We can
+ * directly jump to the place in the assembly where *this particular call node*
+ * spends its time.
+ *
+ * Returns a StackAddressInfo object for the given stackTable and for the library
+ * which contains the call node's func.
+ */
+export function getStackAddressInfoForCallNode(
+ stackTable: StackTable,
+ frameTable: FrameTable,
+ callNodeIndex: IndexIntoCallNodeTable,
+ callNodeInfo: CallNodeInfo,
+ nativeSymbol: IndexIntoNativeSymbolTable,
+ isInvertedTree: boolean
+): StackAddressInfo {
+ return isInvertedTree
+ ? getStackAddressInfoForCallNodeInverted(
+ stackTable,
+ frameTable,
+ callNodeIndex,
+ callNodeInfo,
+ nativeSymbol
+ )
+ : getStackAddressInfoForCallNodeNonInverted(
+ stackTable,
+ frameTable,
+ callNodeIndex,
+ callNodeInfo,
+ nativeSymbol
+ );
+}
+
+/**
+ * This function handles the non-inverted case of getStackAddressInfoForCallNode.
+ *
+ * Gathers the addresses which are hit by a given call node in a given native
+ * symbol.
+ *
+ * This is best explained with an example. We first start with a case that does
+ * not have any inlining, because this is already complicated enough.
+ *
+ * Let the call node be the node for the call path [A, B, C].
+ * Let the native symbol be C.
+ * Let every frame have inlineDepth:0.
+ * Let there be a native symbol for every func, with the same name as the func.
+ * Let this be the stack tree:
+ *
+ * - stack 1, func A
+ * - stack 2, func B
+ * - stack 3, func C, address 0x30
+ * - stack 4, func C, address 0x40
+ * - stack 5, func B
+ * - stack 6, func C, address 0x60
+ * - stack 7, func C, address 0x70
+ * - stack 8, func D
+ * - stack 9, func E
+ * - stack 10, func F
+ *
+ * This maps to the following call tree:
+ *
+ * - call node 1, func A
+ * - call node 2, func B
+ * - call node 3, func C
+ * - call node 4, func D
+ * - call node 5, func E
+ * - call node 6, func F
+ *
+ * The call path [A, B, C] uniquely identifies call node 3.
+ * The following stacks all "collapse into" ("map to") call node 3:
+ * stack 3, 4, 6 and 7.
+ * Stack 8 maps to call node 4, which is a child of call node 3.
+ * Stacks 1, 2, 5, 9 and 10 are outside the call path [A, B, C].
+ *
+ * In this function, we only compute "address hits" that are contributed to
+ * the given call node.
+ * Stacks 3, 4, 6 and 7 all contribute their time both as "self time"
+ * and as "total time" to call node 3, at the addresses 0x30, 0x40, 0x60,
+ * and 0x70, respectively.
+ * Stack 8 also hits call node 3 at address 0x70, but does not contribute to
+ * call node 3's "self time", it only contributes to its "total time".
+ * Stacks 1, 2, 5, 9 and 10 don't contribute to call node 3's self or total time.
+ *
+ * Now here's an example *with* inlining.
+ *
+ * Let the call node be the node for the call path [A, B, C].
+ * Let the native symbol be B.
+ * Let this be the stack tree:
+ *
+ * - stack 1, func A, nativeSymbol A
+ * - stack 2, func B, nativeSymbol B, address 0x40
+ * - stack 3, func C, nativeSymbol B, address 0x40, inlineDepth 1
+ * - stack 4, func B, nativeSymbol B, address 0x45
+ * - stack 5, func C, nativeSymbol B, address 0x45, inlineDepth 1
+ * - stack 6, func D, nativeSymbol D
+ * - stack 7, func E, nativeSymbol E
+ * - stack 8, func A, nativeSymbol A, address 0x30
+ * - stack 9, func B, nativeSymbol A, address 0x30, inlineDepth 1
+ * - stack 10, func C, nativeSymbol A, address 0x30, inlineDepth 2
+ *
+ * This maps to the following call tree:
+ *
+ * - call node 1, func A
+ * - call node 2, func B
+ * - call node 3, func C
+ * - call node 4, func D
+ * - call node 5, func E
+ *
+ * The funky part here is that call node 3 has frames from two different native
+ * symbols: Two from native symbol B, and one from native symbol A. That's
+ * because B is present both as its own native symbol (separate outer function)
+ * and as an inlined call from A. In other words, C has been inlined both into
+ * a standalone B and also into another copy of B which was inlined into A.
+ *
+ * This means that, if the user double clicks call node 3, there are two
+ * different symbols for which we may want to display the assembly code. And
+ * depending on whether the assembly for A or for B is displayed, we want to
+ * call this function for a different native symbol.
+ *
+ * In this example, we call this function for native symbol B.
+ *
+ * The call path [A, B, C] uniquely identifies call node 3.
+ * The following stacks all "collapse into" ("map to") call node 3:
+ * stack 3, 5 and 10. However, only stacks 3 and 5 belong to native symbol B;
+ * stack 10 belongs to native symbol A.
+ * Stack 6 maps to call node 4, which is a child of call node 3.
+ * Stacks 1, 2, 4, 7, 8 and 9 are outside the call path [A, B, C].
+ *
+ * Stacks 3 and 5 both contribute their time both as "self time" and as "total
+ * time" to call node 3 and native symbol B, at the addresses 0x40 and 0x45,
+ * respectively. Stack 10 has the right call node but the wrong native symbol,
+ * so it contributes to neither self nor total time.
+ * Stack 6 also hits call node 3 at address 0x45, but does not contribute to
+ * call node 3's "self time", it only contributes to its "total time".
+ * Stacks 1, 2, 4, 7, 8 and 9 don't contribute to call node 3's self or total time.
+ *
+ * ---
+ *
+ * All stacks can contribute no more than one address in the given call node.
+ * This is different from the getStackAddressInfo function above, where each
+ * stack can hit many addreses in the same library, because all of the ancestor
+ * stacks are taken into account, rather than just one of them. Concretely,
+ * this means that in the returned StackAddressInfo, each stackAddresses[stack]
+ * set will only contain at most one element.
+ *
+ * The returned StackAddressInfo is computed as follows:
+ * selfAddress[stack]:
+ * For stacks that map to the given call node and whose nativeSymbol is the
+ * given native symbol, this is stack.frame.address.
+ * For all other stacks this is null.
+ * stackAddresses[stack]:
+ * For stacks that map to the given call node or one of its descendant
+ * call nodes, and whose nativeSymbol is the given native symbol, this is a
+ * set containing one element, which is ancestorStack.frame.address, where
+ * ancestorStack maps to the given call node.
+ * For all other stacks, this is null.
+ */
+export function getStackAddressInfoForCallNodeNonInverted(
+ stackTable: StackTable,
+ frameTable: FrameTable,
+ callNodeIndex: IndexIntoCallNodeTable,
+ { stackIndexToCallNodeIndex }: CallNodeInfo,
+ nativeSymbol: IndexIntoNativeSymbolTable
+): StackAddressInfo {
+ // "self address" == "the address which a stack's self time is contributed to"
+ const callNodeSelfAddressForAllStacks = [];
+ // "total addresses" == "the set of addresses whose total time this stack contributes to"
+ // Either null or a single-element set.
+ const callNodeTotalAddressesForAllStacks = [];
+
+ // This loop takes advantage of the fact that the stack table is topologically ordered:
+ // Prefix stacks are always visited before their descendants.
+ for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) {
+ let selfAddress: Address | null = null;
+ let totalAddresses: Set | null = null;
+ const frame = stackTable.frame[stackIndex];
+
+ if (
+ stackIndexToCallNodeIndex[stackIndex] === callNodeIndex &&
+ frameTable.nativeSymbol[frame] === nativeSymbol
+ ) {
+ // This stack contributes to the call node's self time for the right
+ // native symbol. We needed to check both, because multiple stacks for the
+ // same call node can have different native symbols.
+ selfAddress = frameTable.address[frame];
+ if (selfAddress !== -1) {
+ totalAddresses = new Set([selfAddress]);
+ }
+ } else {
+ // This stack does not map to the given call node or has the wrong native
+ // symbol. So this stack contributes no self time to the call node for the
+ // requested native symbol, and we leave selfAddress at null.
+ // As for totalTime, this stack contributes to the same address's totalTime
+ // as its parent stack: If it is a descendant of a stack X which maps to
+ // the given call node, then it contributes to stack X's address's totalTime,
+ // otherwise it contributes to no address's totalTime.
+ // In the example above, this is how stack 8 contributes to call node 3's
+ // totalTime.
+ const prefixStack = stackTable.prefix[stackIndex];
+ totalAddresses =
+ prefixStack !== null
+ ? callNodeTotalAddressesForAllStacks[prefixStack]
+ : null;
+ }
+
+ callNodeSelfAddressForAllStacks.push(selfAddress);
+ callNodeTotalAddressesForAllStacks.push(totalAddresses);
+ }
+ return {
+ selfAddress: callNodeSelfAddressForAllStacks,
+ stackAddresses: callNodeTotalAddressesForAllStacks,
+ };
+}
+
+/**
+ * This handles the inverted case of getStackAddressInfoForCallNode.
+ *
+ * The returned StackAddressInfo is computed as follows:
+ * selfAddress[stack]:
+ * For (inverted thread) root stack nodes that map to the given call node
+ * and whose stack.frame.nativeSymbol is the given library, this is stack.frame.address.
+ * For (inverted thread) root stack nodes whose frame is in a different library,
+ * or which don't map to the given call node, this is null.
+ * For (inverted thread) *non-root* stack nodes, this is the same as the selfAddress
+ * of the stack's prefix. This way, the selfAddress is always inherited from the
+ * subtree root.
+ * stackAddresses[stack]:
+ * For stacks that map to the given call node or one of its (inverted tree)
+ * descendant call nodes, this is a set containing one element, which is
+ * ancestorStack.frame.address, where ancestorStack maps to the given call
+ * node.
+ * For all other stacks, this is null.
+ */
+export function getStackAddressInfoForCallNodeInverted(
+ stackTable: StackTable,
+ frameTable: FrameTable,
+ callNodeIndex: IndexIntoCallNodeTable,
+ { stackIndexToCallNodeIndex }: CallNodeInfo,
+ nativeSymbol: IndexIntoNativeSymbolTable
+): StackAddressInfo {
+ // "self address" == "the address which a stack's self time is contributed to"
+ const callNodeSelfAddressForAllStacks = [];
+ // "total addresses" == "the set of addresses whose total time this stack contributes to"
+ // Either null or a single-element set.
+ const callNodeTotalAddressesForAllStacks = [];
+
+ // This loop takes advantage of the fact that the stack table is topologically ordered:
+ // Prefix stacks are always visited before their descendants.
+ for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) {
+ let selfAddress: Address | null = null;
+ let totalAddresses: Set | null = null;
+
+ const prefixStack = stackTable.prefix[stackIndex];
+ if (
+ stackIndexToCallNodeIndex[stackIndex] === callNodeIndex &&
+ frameTable.nativeSymbol[stackTable.frame[stackIndex]] === nativeSymbol
+ ) {
+ // This stack contributes to the call node's self time for the right
+ // native symbol. We needed to check both, because multiple stacks for the
+ // same call node can have different native symbols.
+ const frame = stackTable.frame[stackIndex];
+ const address = frameTable.address[frame];
+ if (address !== -1) {
+ totalAddresses = new Set([address]);
+ if (prefixStack === null) {
+ // This is a root of the inverted tree, and it is the given
+ // call node. That means that we have a self address.
+ selfAddress = address;
+ } else {
+ // This is not a root stack node, so no self time is spent
+ // in the given call node for this stack node.
+ }
+ }
+ } else {
+ if (prefixStack === null) {
+ // This is a root of the inverted tree, but it doesn't map
+ // to the given call node. It doesn't contribute to the call node's
+ // self time or total time.
+ } else {
+ // This is not a root stack node. Samples that hit this stack node
+ // spend their time in the root node of our subtree. If that root
+ // maps to the given call node, we may have self time.
+ // Inherit both self and total time contribution from the parent stack.
+ selfAddress = callNodeSelfAddressForAllStacks[prefixStack];
+ totalAddresses = callNodeTotalAddressesForAllStacks[prefixStack];
+ }
+ }
+
+ callNodeSelfAddressForAllStacks.push(selfAddress);
+ callNodeTotalAddressesForAllStacks.push(totalAddresses);
+ }
+ return {
+ selfAddress: callNodeSelfAddressForAllStacks,
+ stackAddresses: callNodeTotalAddressesForAllStacks,
+ };
+}
+
+// An AddressTimings instance without any hits.
+export const emptyAddressTimings: AddressTimings = {
+ totalAddressHits: new Map(),
+ selfAddressHits: new Map(),
+};
+
+// Compute the AddressTimings for the supplied samples with the help of StackAddressInfo.
+// This is fast and can be done whenever the preview selection changes.
+// The slow part was the computation of the StackAddressInfo, which is already done.
+export function getAddressTimings(
+ stackAddressInfo: StackAddressInfo | null,
+ samples: SamplesLikeTable
+): AddressTimings {
+ if (stackAddressInfo === null) {
+ return emptyAddressTimings;
+ }
+ const { selfAddress, stackAddresses } = stackAddressInfo;
+ const totalAddressHits: Map = new Map();
+ const selfAddressHits: Map = new Map();
+
+ // Iterate over all the samples, and aggregate the sample's weight into the
+ // addresses which are hit by the sample's stack.
+ // TODO: Maybe aggregate sample count per stack first, and then visit each stack only once?
+ for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) {
+ const stackIndex = samples.stack[sampleIndex];
+ if (stackIndex === null) {
+ continue;
+ }
+ const weight = samples.weight ? samples.weight[sampleIndex] : 1;
+ const setOfHitAddresses = stackAddresses[stackIndex];
+ if (setOfHitAddresses !== null) {
+ for (const address of setOfHitAddresses) {
+ const oldHitCount = totalAddressHits.get(address) ?? 0;
+ totalAddressHits.set(address, oldHitCount + weight);
+ }
+ }
+ const address = selfAddress[stackIndex];
+ if (address !== null) {
+ const oldHitCount = selfAddressHits.get(address) ?? 0;
+ selfAddressHits.set(address, oldHitCount + weight);
+ }
+ }
+ return { totalAddressHits, selfAddressHits };
+}
diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js
index 89fd02beb1..bdd31091dc 100644
--- a/src/profile-logic/call-tree.js
+++ b/src/profile-logic/call-tree.js
@@ -353,6 +353,8 @@ export class CallTree {
iconSrc,
icon,
ariaLabel,
+ rawTotal: total,
+ rawSelf: self,
};
this._displayDataByIndex.set(callNodeIndex, displayData);
}
diff --git a/src/profile-logic/data-structures.js b/src/profile-logic/data-structures.js
index 8ac0b3dfe4..4ae4914e17 100644
--- a/src/profile-logic/data-structures.js
+++ b/src/profile-logic/data-structures.js
@@ -172,6 +172,7 @@ export function shallowCloneNativeSymbolTable(
libIndex: nativeSymbols.libIndex.slice(),
address: nativeSymbols.address.slice(),
name: nativeSymbols.name.slice(),
+ functionSize: nativeSymbols.functionSize.slice(),
length: nativeSymbols.length,
};
}
@@ -199,6 +200,7 @@ export function getEmptyNativeSymbolTable(): NativeSymbolTable {
libIndex: [],
address: [],
name: [],
+ functionSize: [],
length: 0,
};
}
diff --git a/src/profile-logic/import/chrome.js b/src/profile-logic/import/chrome.js
index 22148e63d1..f9bcc6e735 100644
--- a/src/profile-logic/import/chrome.js
+++ b/src/profile-logic/import/chrome.js
@@ -424,13 +424,13 @@ type FunctionInfo = {
function makeFunctionInfoFinder(categories) {
const jsCat = categories.findIndex((c) => c.name === 'JavaScript');
const gcCat = categories.findIndex((c) => c.name === 'GC / CC');
- const domCat = categories.findIndex((c) => c.name === 'DOM');
+ const nativeCat = categories.findIndex((c) => c.name === 'Native');
const otherCat = categories.findIndex((c) => c.name === 'Other');
const idleCat = categories.findIndex((c) => c.name === 'Idle');
if (
jsCat === -1 ||
gcCat === -1 ||
- domCat === -1 ||
+ nativeCat === -1 ||
otherCat === -1 ||
idleCat === -1
) {
@@ -460,7 +460,7 @@ function makeFunctionInfoFinder(categories) {
functionName !== '' &&
functionName !== '(unresolved function)'
) {
- return { category: domCat, isJS: false, relevantForJS: true };
+ return { category: nativeCat, isJS: false, relevantForJS: true };
}
return { category: jsCat, isJS: true, relevantForJS: false };
}
@@ -471,6 +471,14 @@ async function processTracingEvents(
eventsByName: Map
): Promise {
const profile = getEmptyProfile();
+ profile.meta.categories = [
+ { name: 'Other', color: 'grey', subcategories: ['Other'] },
+ { name: 'Idle', color: 'transparent', subcategories: ['Other'] },
+ { name: 'JavaScript', color: 'yellow', subcategories: ['Other'] },
+ { name: 'GC / CC', color: 'orange', subcategories: ['Other'] },
+ { name: 'Graphics', color: 'green', subcategories: ['Other'] },
+ { name: 'Native', color: 'blue', subcategories: ['Other'] },
+ ];
profile.meta.product = 'Chrome Trace';
profile.meta.importedFrom = 'Chrome Trace';
diff --git a/src/profile-logic/line-timings.js b/src/profile-logic/line-timings.js
index ef9539f657..e412fbd838 100644
--- a/src/profile-logic/line-timings.js
+++ b/src/profile-logic/line-timings.js
@@ -289,7 +289,7 @@ export function getStackLineInfoForCallNode(
* The following stacks all "collapse into" ("map to") call node 3:
* stack 3, 4, 6 and 7.
* Stack 8 maps to call node 4, which is a child of call node 3.
- * All other stacks are outside the call path [A, B, C].
+ * Stacks 1, 2, 5, 9 and 10 are outside the call path [A, B, C].
*
* In this function, we only compute "line hits" that are contributed to
* the given call node.
@@ -298,7 +298,7 @@ export function getStackLineInfoForCallNode(
* and 70, respectively.
* Stack 8 also hits call node 3 at line 70, but does not contribute to
* call node 3's "self time", it only contributes to its "total time".
- * All other stacks don't contribute to call node 3's self or total time.
+ * Stacks 1, 2, 5, 9 and 10 don't contribute to call node 3's self or total time.
*
* All stacks can contribute no more than one line in the given call node.
* This is different from the getStackLineInfo function above, where each
diff --git a/src/profile-logic/marker-schema.js b/src/profile-logic/marker-schema.js
index 3c28eaa4d0..9235ee5d3a 100644
--- a/src/profile-logic/marker-schema.js
+++ b/src/profile-logic/marker-schema.js
@@ -2,6 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// @flow
+import * as React from 'react';
import { oneLine } from 'common-tags';
import {
formatNumber,
@@ -359,19 +360,64 @@ export function getLabelGetter(
};
}
+/**
+ * This function formats a string from a marker type and a value.
+ * If you wish to get markup instead, have a look at
+ * formatMarkupFromMarkerSchema below.
+ */
export function formatFromMarkerSchema(
markerType: string,
format: MarkerFormatType,
value: any
): string {
+ if (value === undefined || value === null) {
+ console.warn(
+ `Formatting ${value} for ${markerType} with format ${JSON.stringify(
+ format
+ )}`
+ );
+ return '(empty)';
+ }
+ if (typeof format === 'object') {
+ switch (format.type) {
+ case 'table': {
+ const { columns } = format;
+ if (!(value instanceof Array)) {
+ throw new Error('Expected an array for table type');
+ }
+ const hasHeader = columns.some((column) => column.label);
+ const rows = hasHeader
+ ? [columns.map((x) => x.label || '(empty)')]
+ : [];
+ const cellRows = value.map((row, i) => {
+ if (!(row instanceof Array)) {
+ throw new Error('Expected an array for table row');
+ }
+
+ if (row.length !== columns.length) {
+ throw new Error(
+ `Row ${i} length doesn't match column count (row: ${row.length}, cols: ${columns.length})`
+ );
+ }
+ return row.map((cell, j) => {
+ const { format } = columns[j];
+ return formatFromMarkerSchema(markerType, format || 'string', cell);
+ });
+ });
+ rows.push(...cellRows);
+ return rows.map((row) => `(${row.join(', ')})`).join(',');
+ }
+ default:
+ throw new Error(
+ `Unknown format type ${JSON.stringify((format.type: empty))}`
+ );
+ }
+ }
switch (format) {
case 'url':
case 'file-path':
case 'string':
// Make sure a non-empty string is returned here.
- if (value === undefined || value === null) {
- return '(empty)';
- }
return String(value) || '(empty)';
case 'duration':
case 'time':
@@ -392,10 +438,129 @@ export function formatFromMarkerSchema(
return formatNumber(value);
case 'percentage':
return formatPercent(value);
+ case 'list':
+ if (!(value instanceof Array)) {
+ throw new Error('Expected an array for list format');
+ }
+ return value
+ .map((v) => formatFromMarkerSchema(markerType, 'string', v))
+ .join(', ');
default:
- console.error(
- `A marker schema of type "${markerType}" had an unknown format "${(format: empty)}"`
+ console.warn(
+ `A marker schema of type "${markerType}" had an unknown format ${JSON.stringify(
+ (format: empty)
+ )}`
);
return value;
}
}
+
+// This regexp is used to test for URLs and remove their scheme for display.
+const URL_SCHEME_REGEXP = /^http(s?):\/\//;
+
+/**
+ * This function may return structured markup for some types suchs as table,
+ * list, or urls. For other types this falls back to formatFromMarkerSchema
+ * above.
+ */
+export function formatMarkupFromMarkerSchema(
+ markerType: string,
+ format: MarkerFormatType,
+ value: any
+): React.Element | string {
+ if (value === undefined || value === null) {
+ console.warn(`Formatting ${value} for ${JSON.stringify(markerType)}`);
+ return '(empty)';
+ }
+ if (format !== 'url' && typeof format !== 'object' && format !== 'list') {
+ return formatFromMarkerSchema(markerType, format, value);
+ }
+ if (typeof format === 'object') {
+ switch (format.type) {
+ case 'table': {
+ const { columns } = format;
+ if (!(value instanceof Array)) {
+ throw new Error('Expected an array for table type');
+ }
+ const hasHeader = columns.some((column) => column.label);
+ return (
+
+ {hasHeader ? (
+
+
+ {columns.map((col, i) => (
+ | {col.label || ''} |
+ ))}
+
+
+ ) : null}
+
+ {value.map((row, i) => {
+ if (!(row instanceof Array)) {
+ throw new Error('Expected an array for table row');
+ }
+
+ if (row.length !== columns.length) {
+ throw new Error(
+ `Row ${i} length doesn't match column count (row: ${row.length}, cols: ${columns.length})`
+ );
+ }
+ return (
+
+ {row.map((cell, i) => {
+ return (
+ |
+ {formatMarkupFromMarkerSchema(
+ markerType,
+ columns[i].type || 'string',
+ cell
+ )}
+ |
+ );
+ })}
+
+ );
+ })}
+
+
+ );
+ }
+ default:
+ throw new Error(
+ `Unknown format type ${JSON.stringify((format: empty))}`
+ );
+ }
+ }
+ switch (format) {
+ case 'list':
+ if (!(value instanceof Array)) {
+ throw new Error('Expected an array for list format');
+ }
+ return (
+
+ {value.map((entry, i) => (
+ -
+ {formatFromMarkerSchema(markerType, 'string', value[i])}
+
+ ))}
+
+ );
+ case 'url': {
+ if (!URL_SCHEME_REGEXP.test(value)) {
+ return value;
+ }
+ return (
+
+ {value.replace(URL_SCHEME_REGEXP, '')}
+
+ );
+ }
+ default:
+ throw new Error(`Unknown format type ${JSON.stringify((format: empty))}`);
+ }
+}
diff --git a/src/profile-logic/merge-compare.js b/src/profile-logic/merge-compare.js
index eeb0d0c720..b27f173638 100644
--- a/src/profile-logic/merge-compare.js
+++ b/src/profile-logic/merge-compare.js
@@ -561,6 +561,7 @@ function combineNativeSymbolTables(
const nameIndex = nativeSymbols.name[i];
const newName = stringTable.getString(nameIndex);
const address = nativeSymbols.address[i];
+ const functionSize = nativeSymbols.functionSize[i];
// Duplicate search.
const nativeSymbolKey = [newName, address].join('#');
@@ -577,6 +578,7 @@ function combineNativeSymbolTables(
newNativeSymbols.libIndex.push(libIndex);
newNativeSymbols.name.push(newStringTable.indexForString(newName));
newNativeSymbols.address.push(address);
+ newNativeSymbols.functionSize.push(functionSize);
newNativeSymbols.length++;
}
diff --git a/src/profile-logic/mozilla-symbolication-api.js b/src/profile-logic/mozilla-symbolication-api.js
index b9f64829b8..8ae096bfc1 100644
--- a/src/profile-logic/mozilla-symbolication-api.js
+++ b/src/profile-logic/mozilla-symbolication-api.js
@@ -61,6 +61,9 @@ type APIFrameInfoV5 = {
// The hex offset between the requested address and the start of the function,
// e.g. "0x3c".
function_offset?: string,
+ // An optional size, in bytes, of the machine code of the outer function that
+ // this address belongs to, as a hex string, e.g. "0x270".
+ function_size?: string,
// The path of the file that contains the function this frame was in, optional.
// As of June 2021, this is only supported on the staging symbolication server
// ("Eliot") but not on the implementation that's currently in production ("Tecken").
@@ -140,6 +143,22 @@ function _ensureIsAPIResultV5(result: MixedObject): APIResultV5 {
if ('line' in frameInfo && typeof frameInfo.line !== 'number') {
throw new Error('Expected frameInfo.line to be a number, if present');
}
+ if (
+ 'function_offset' in frameInfo &&
+ typeof frameInfo.function_offset !== 'string'
+ ) {
+ throw new Error(
+ 'Expected frameInfo.function_offset to be a string, if present'
+ );
+ }
+ if (
+ 'function_size' in frameInfo &&
+ typeof frameInfo.function_size !== 'string'
+ ) {
+ throw new Error(
+ 'Expected frameInfo.function_size to be a string, if present'
+ );
+ }
if ('inlines' in frameInfo) {
const inlines = frameInfo.inlines;
if (!Array.isArray(inlines)) {
@@ -206,12 +225,18 @@ function getV5ResultForLibRequest(
});
}
+ let functionSize;
+ if (info.function_size !== undefined) {
+ functionSize = parseInt(info.function_size.substr(2), 16);
+ }
+
addressResult = {
name,
symbolAddress: address - functionOffset,
file: info.file,
line: info.line,
inlines,
+ functionSize,
};
} else {
// This can happen if the address is between functions, or before the first
diff --git a/src/profile-logic/processed-profile-versioning.js b/src/profile-logic/processed-profile-versioning.js
index 594785604f..3e115d7491 100644
--- a/src/profile-logic/processed-profile-versioning.js
+++ b/src/profile-logic/processed-profile-versioning.js
@@ -2130,5 +2130,13 @@ const _upgraders = {
}
profile.libs = libs;
},
+ [42]: (profile) => {
+ // The nativeSymbols table now has a new column: functionSize.
+ // Its values can be null.
+ for (const thread of profile.threads) {
+ const { nativeSymbols } = thread;
+ nativeSymbols.functionSize = Array(nativeSymbols.length).fill(null);
+ }
+ },
};
/* eslint-enable no-useless-computed-key */
diff --git a/src/profile-logic/symbol-store.js b/src/profile-logic/symbol-store.js
index 3607baeb1c..f7320fb29a 100644
--- a/src/profile-logic/symbol-store.js
+++ b/src/profile-logic/symbol-store.js
@@ -49,6 +49,9 @@ export type AddressResult = {|
// addressResult.name calls addressResult.inlines[inlines.length - 1].function, which
// calls addressResult.inlines[inlines.length - 2].function etc.
inlines?: Array,
+ // An optional size, in bytes, of the machine code of the outer function that
+ // this address belongs to.
+ functionSize?: number,
|};
export type AddressInlineFrame = {|
@@ -106,6 +109,7 @@ export function readSymbolsFromSymbolTable(
const results = new Map();
let currentSymbolIndex = undefined;
let currentSymbol = '';
+ let currentSymbolFunctionSize = undefined;
for (let i = 0; i < addressArray.length; i++) {
const address = addressArray[i];
@@ -131,11 +135,16 @@ export function readSymbolsFromSymbolTable(
// C++ or rust symbols in the symbol table may have mangled names.
// Demangle them here.
currentSymbol = demangleCallback(decoder.decode(subarray));
+ currentSymbolFunctionSize =
+ symbolIndex < symbolTableAddrs.length - 1
+ ? symbolTableAddrs[symbolIndex + 1] - symbolTableAddrs[symbolIndex]
+ : undefined;
currentSymbolIndex = symbolIndex;
}
results.set(address, {
symbolAddress: symbolTableAddrs[symbolIndex],
name: currentSymbol,
+ functionSize: currentSymbolFunctionSize,
});
} else {
results.set(address, {
diff --git a/src/profile-logic/symbolication.js b/src/profile-logic/symbolication.js
index ede47499f6..49583bd44a 100644
--- a/src/profile-logic/symbolication.js
+++ b/src/profile-logic/symbolication.js
@@ -546,7 +546,7 @@ export function applySymbolicationStep(
allNativeSymbolsForThisLib
);
const frameToSymbolAddressMap: Map = new Map();
- const symbolAddressToNameMap: Map = new Map();
+ const symbolAddressToInfoMap: Map = new Map();
const symbolAddressToCanonicalSymbolIndexMap: Map<
Address,
IndexIntoNativeSymbolTable
@@ -607,7 +607,7 @@ export function applySymbolicationStep(
// offset.
const symbolAddress = addressResult.symbolAddress;
frameToSymbolAddressMap.set(frameIndex, symbolAddress);
- symbolAddressToNameMap.set(symbolAddress, addressResult.name);
+ symbolAddressToInfoMap.set(symbolAddress, addressResult);
if (oldFrameSymbol !== null) {
// Opportunistically match up symbolAddress with oldFrameSymbol.
@@ -653,8 +653,8 @@ export function applySymbolicationStep(
// and give the canonical symbol the right address and symbol.
const availableNativeSymbolIterator = availableNativeSymbols.values();
const nativeSymbols = shallowCloneNativeSymbolTable(oldNativeSymbols);
- for (const [symbolAddress, symbolName] of symbolAddressToNameMap) {
- const symbolStringIndex = stringTable.indexForString(symbolName);
+ for (const [symbolAddress, addressResult] of symbolAddressToInfoMap) {
+ const symbolStringIndex = stringTable.indexForString(addressResult.name);
let symbolIndex = symbolAddressToCanonicalSymbolIndexMap.get(symbolAddress);
if (symbolIndex === undefined) {
// Repurpose a symbol from availableNativeSymbols as the canonical symbol for this
@@ -672,6 +672,8 @@ export function applySymbolicationStep(
// Update the symbol properties.
nativeSymbols.address[symbolIndex] = symbolAddress;
nativeSymbols.name[symbolIndex] = symbolStringIndex;
+ nativeSymbols.functionSize[symbolIndex] =
+ addressResult.functionSize ?? null;
}
// Now we have a canonical symbol for every symbolAddress.
diff --git a/src/reducers/app.js b/src/reducers/app.js
index 305f11e31a..5b1bc8a1ab 100644
--- a/src/reducers/app.js
+++ b/src/reducers/app.js
@@ -327,6 +327,33 @@ const browserConnectionStatus: Reducer = (
}
};
+/**
+ * Signals which categories are opened by default in the sidebar per type
+ */
+const sidebarOpenCategories: Reducer