Skip to content
Permalink
 
 
Cannot retrieve contributors at this time
/* 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
import { oneLine } from 'common-tags';
import { timeCode } from '../utils/time-code';
import {
getSampleIndexToCallNodeIndex,
getOriginAnnotationForFunc,
getCategoryPairLabel,
} from './profile-data';
import { resourceTypes } from './data-structures';
import { UniqueStringArray } from '../utils/unique-string-array';
import type {
CategoryList,
Thread,
FuncTable,
ResourceTable,
IndexIntoFuncTable,
SamplesLikeTable,
WeightType,
CallNodeTable,
IndexIntoCallNodeTable,
CallNodeInfo,
CallNodeData,
CallNodeDisplayData,
Milliseconds,
TracedTiming,
SamplesTable,
} from 'firefox-profiler/types';
import ExtensionIcon from '../../res/img/svg/extension.svg';
import { formatCallNodeNumber, formatPercent } from '../utils/format-numbers';
import { assertExhaustiveCheck, ensureExists } from '../utils/flow';
import * as ProfileData from './profile-data';
import type { CallTreeSummaryStrategy } from '../types/actions';
type CallNodeChildren = IndexIntoCallNodeTable[];
type CallNodeSummary = {
self: Float32Array,
total: Float32Array,
};
export type CallTreeCountsAndSummary = {
callNodeChildCount: Uint32Array,
callNodeSummary: CallNodeSummary,
rootCount: number,
rootTotalSummary: number,
};
function extractFaviconFromLibname(libname: string): string | null {
try {
const url = new URL('/favicon.ico', libname);
if (url.protocol === 'http:') {
// Upgrade http requests.
url.protocol = 'https:';
}
return url.href;
} catch (e) {
console.error(
'Error while extracing the favicon from the libname',
libname
);
return null;
}
}
export class CallTree {
_categories: CategoryList;
_callNodeTable: CallNodeTable;
_callNodeSummary: CallNodeSummary;
_callNodeChildCount: Uint32Array; // A table column matching the callNodeTable
_funcTable: FuncTable;
_resourceTable: ResourceTable;
_stringTable: UniqueStringArray;
_rootTotalSummary: number;
_rootCount: number;
_displayDataByIndex: Map<IndexIntoCallNodeTable, CallNodeDisplayData>;
// _children is indexed by IndexIntoCallNodeTable. Since they are
// integers, using an array directly is faster than going through a Map.
_children: Array<CallNodeChildren>;
_jsOnly: boolean;
_interval: number;
_isHighPrecision: boolean;
_weightType: WeightType;
constructor(
{ funcTable, resourceTable, stringTable }: Thread,
categories: CategoryList,
callNodeTable: CallNodeTable,
callNodeSummary: CallNodeSummary,
callNodeChildCount: Uint32Array,
rootTotalSummary: number,
rootCount: number,
jsOnly: boolean,
interval: number,
isHighPrecision: boolean,
weightType: WeightType
) {
this._categories = categories;
this._callNodeTable = callNodeTable;
this._callNodeSummary = callNodeSummary;
this._callNodeChildCount = callNodeChildCount;
this._funcTable = funcTable;
this._resourceTable = resourceTable;
this._stringTable = stringTable;
this._rootTotalSummary = rootTotalSummary;
this._rootCount = rootCount;
this._displayDataByIndex = new Map();
this._children = [];
this._jsOnly = jsOnly;
this._interval = interval;
this._isHighPrecision = isHighPrecision;
this._weightType = weightType;
}
getRoots() {
return this.getChildren(-1);
}
getChildren(callNodeIndex: IndexIntoCallNodeTable): CallNodeChildren {
let children = this._children[callNodeIndex];
if (children === undefined) {
const childCount =
callNodeIndex === -1
? this._rootCount
: this._callNodeChildCount[callNodeIndex];
children = [];
for (
let childCallNodeIndex = callNodeIndex + 1;
childCallNodeIndex < this._callNodeTable.length &&
children.length < childCount;
childCallNodeIndex++
) {
const childPrefixIndex = this._callNodeTable.prefix[childCallNodeIndex];
const childTotalSummary = this._callNodeSummary.total[
childCallNodeIndex
];
const childChildCount = this._callNodeChildCount[childCallNodeIndex];
if (
childPrefixIndex === callNodeIndex &&
(childTotalSummary !== 0 || childChildCount !== 0)
) {
children.push(childCallNodeIndex);
}
}
children.sort(
(a, b) =>
Math.abs(this._callNodeSummary.total[b]) -
Math.abs(this._callNodeSummary.total[a])
);
this._children[callNodeIndex] = children;
}
return children;
}
hasChildren(callNodeIndex: IndexIntoCallNodeTable): boolean {
return this.getChildren(callNodeIndex).length !== 0;
}
_addDescendantsToSet(
callNodeIndex: IndexIntoCallNodeTable,
set: Set<IndexIntoCallNodeTable>
): void {
for (const child of this.getChildren(callNodeIndex)) {
set.add(child);
this._addDescendantsToSet(child, set);
}
}
getAllDescendants(
callNodeIndex: IndexIntoCallNodeTable
): Set<IndexIntoCallNodeTable> {
const result = new Set();
this._addDescendantsToSet(callNodeIndex, result);
return result;
}
getParent(
callNodeIndex: IndexIntoCallNodeTable
): IndexIntoCallNodeTable | -1 {
return this._callNodeTable.prefix[callNodeIndex];
}
getDepth(callNodeIndex: IndexIntoCallNodeTable): number {
return this._callNodeTable.depth[callNodeIndex];
}
hasSameNodeIds(tree: CallTree): boolean {
return this._callNodeTable === tree._callNodeTable;
}
getNodeData(callNodeIndex: IndexIntoCallNodeTable): CallNodeData {
const funcIndex = this._callNodeTable.func[callNodeIndex];
const funcName = this._stringTable.getString(
this._funcTable.name[funcIndex]
);
const total = this._callNodeSummary.total[callNodeIndex];
const totalRelative = total / this._rootTotalSummary;
const self = this._callNodeSummary.self[callNodeIndex];
const selfRelative = self / this._rootTotalSummary;
return {
funcName,
total,
totalRelative,
self,
selfRelative,
};
}
getDisplayData(callNodeIndex: IndexIntoCallNodeTable): CallNodeDisplayData {
let displayData: CallNodeDisplayData | void = this._displayDataByIndex.get(
callNodeIndex
);
if (displayData === undefined) {
const { funcName, total, totalRelative, self } = this.getNodeData(
callNodeIndex
);
const funcIndex = this._callNodeTable.func[callNodeIndex];
const categoryIndex = this._callNodeTable.category[callNodeIndex];
const subcategoryIndex = this._callNodeTable.subcategory[callNodeIndex];
const resourceIndex = this._funcTable.resource[funcIndex];
const resourceType = this._resourceTable.type[resourceIndex];
const isFrameLabel = resourceIndex === -1;
const libName = this._getOriginAnnotation(funcIndex);
const weightType = this._weightType;
let iconSrc = null;
let icon = null;
if (resourceType === resourceTypes.webhost) {
icon = iconSrc = extractFaviconFromLibname(libName);
} else if (resourceType === resourceTypes.addon) {
iconSrc = ExtensionIcon;
const resourceNameIndex = this._resourceTable.name[resourceIndex];
if (resourceNameIndex !== undefined) {
const iconText = this._stringTable.getString(resourceNameIndex);
icon = iconText;
}
}
const formattedTotal = formatCallNodeNumber(
weightType,
this._isHighPrecision,
total
);
const formattedSelf = formatCallNodeNumber(
weightType,
this._isHighPrecision,
self
);
const totalPercent = `${formatPercent(totalRelative)}`;
let ariaLabel;
let totalWithUnit;
let selfWithUnit;
switch (weightType) {
case 'tracing-ms': {
totalWithUnit = `${formattedTotal}ms`;
selfWithUnit = `${formattedSelf}ms`;
ariaLabel = oneLine`
${funcName},
running time is ${totalWithUnit} (${totalPercent}),
self time is ${selfWithUnit}
`;
break;
}
case 'samples': {
// TODO - L10N pluralization
totalWithUnit =
total === 1
? `${formattedTotal} sample`
: `${formattedTotal} samples`;
selfWithUnit =
self === 1 ? `${formattedSelf} sample` : `${formattedSelf} samples`;
ariaLabel = oneLine`
${funcName},
running count is ${totalWithUnit} (${totalPercent}),
self count is ${selfWithUnit}
`;
break;
}
case 'bytes': {
totalWithUnit = `${formattedTotal} bytes`;
selfWithUnit = `${formattedSelf} bytes`;
ariaLabel = oneLine`
${funcName},
total size is ${totalWithUnit} (${totalPercent}),
self size is ${selfWithUnit}
`;
break;
}
default:
throw assertExhaustiveCheck(weightType, 'Unhandled WeightType.');
}
displayData = {
total: total === 0 ? '—' : formattedTotal,
totalWithUnit: total === 0 ? '—' : totalWithUnit,
self: self === 0 ? '—' : formattedSelf,
selfWithUnit: self === 0 ? '—' : selfWithUnit,
totalPercent,
name: funcName,
lib: libName.slice(0, 1000),
// Dim platform pseudo-stacks.
isFrameLabel,
categoryName: getCategoryPairLabel(
this._categories,
categoryIndex,
subcategoryIndex
),
categoryColor: this._categories[categoryIndex].color,
iconSrc,
icon,
ariaLabel,
};
this._displayDataByIndex.set(callNodeIndex, displayData);
}
return displayData;
}
_getOriginAnnotation(funcIndex: IndexIntoFuncTable): string {
return getOriginAnnotationForFunc(
funcIndex,
this._funcTable,
this._resourceTable,
this._stringTable
);
}
}
function _getInvertedStackSelf(
// The samples could either be a SamplesTable, or a JsAllocationsTable.
samples: SamplesLikeTable,
callNodeTable: CallNodeTable,
sampleIndexToCallNodeIndex: Array<IndexIntoCallNodeTable | null>
): {
// In an inverted profile, all the amount of self unit (time, bytes, count, etc.) is
// accounted to the root nodes. So `callNodeSelf` will be 0 for all non-root nodes.
callNodeSelf: Float32Array,
// This property stores the amount of unit (time, bytes, count, etc.) spent in the
// stacks' leaf nodes. Later these values will make it possible to compute the
// total for all nodes by summing up the values up the tree.
callNodeLeaf: Float32Array,
} {
// Compute an array that maps the callNodeIndex to its root.
const callNodeToRoot = new Int32Array(callNodeTable.length);
for (
let callNodeIndex = 0;
callNodeIndex < callNodeTable.length;
callNodeIndex++
) {
const prefixCallNode = callNodeTable.prefix[callNodeIndex];
if (prefixCallNode === -1) {
// callNodeIndex is a root node
callNodeToRoot[callNodeIndex] = callNodeIndex;
} else {
// The callNodeTable guarantees that a callNode's prefix always comes
// before the callNode; prefix references are always to lower callNode
// indexes and never to higher indexes.
// We are iterating the callNodeTable in forwards direction (starting at
// index 0) so we know that we have already visited the current call
// node's prefix call node and can reuse its stored root node, which
// recursively is the value we're looking for.
callNodeToRoot[callNodeIndex] = callNodeToRoot[prefixCallNode];
}
}
// Calculate the timing information by going through each sample.
const callNodeSelf = new Float32Array(callNodeTable.length);
const callNodeLeaf = new Float32Array(callNodeTable.length);
for (
let sampleIndex = 0;
sampleIndex < sampleIndexToCallNodeIndex.length;
sampleIndex++
) {
const callNodeIndex = sampleIndexToCallNodeIndex[sampleIndex];
if (callNodeIndex !== null) {
const rootIndex = callNodeToRoot[callNodeIndex];
const weight = samples.weight ? samples.weight[sampleIndex] : 1;
callNodeSelf[rootIndex] += weight;
callNodeLeaf[callNodeIndex] += weight;
}
}
return { callNodeSelf, callNodeLeaf };
}
/**
* This is a helper function to get the stack timings for un-inverted call trees.
*/
function _getStackSelf(
samples: SamplesLikeTable,
callNodeTable: CallNodeTable,
sampleIndexToCallNodeIndex: Array<null | IndexIntoCallNodeTable>
): {
callNodeSelf: Float32Array, // Milliseconds[]
callNodeLeaf: Float32Array, // Milliseconds[]
} {
const callNodeSelf = new Float32Array(callNodeTable.length);
for (
let sampleIndex = 0;
sampleIndex < sampleIndexToCallNodeIndex.length;
sampleIndex++
) {
const callNodeIndex = sampleIndexToCallNodeIndex[sampleIndex];
if (callNodeIndex !== null) {
const weight = samples.weight ? samples.weight[sampleIndex] : 1;
callNodeSelf[callNodeIndex] += weight;
}
}
return { callNodeSelf, callNodeLeaf: callNodeSelf };
}
/**
* This computes all of the count and timing information displayed in the calltree.
* It takes into account both the normal tree, and the inverted tree.
*
* Note: The "timionmgs" could have a number of different meanings based on the
* what type of weight is in the SamplesLikeTable. For instance, it could be
* milliseconds, sample counts, or bytes.
*/
export function computeCallTreeCountsAndSummary(
samples: SamplesLikeTable,
{ callNodeTable, stackIndexToCallNodeIndex }: CallNodeInfo,
interval: Milliseconds,
invertCallstack: boolean
): CallTreeCountsAndSummary {
const sampleIndexToCallNodeIndex = getSampleIndexToCallNodeIndex(
samples.stack,
stackIndexToCallNodeIndex
);
// Inverted trees need a different method for computing the timing.
const { callNodeSelf, callNodeLeaf } = invertCallstack
? _getInvertedStackSelf(samples, callNodeTable, sampleIndexToCallNodeIndex)
: _getStackSelf(samples, callNodeTable, sampleIndexToCallNodeIndex);
// Compute the following variables:
const callNodeTotalSummary = new Float32Array(callNodeTable.length);
const callNodeChildCount = new Uint32Array(callNodeTable.length);
let rootTotalSummary = 0;
let rootCount = 0;
// We loop the call node table in reverse, so that we find the children
// before their parents, and the total is known at the time we reach a
// node.
for (
let callNodeIndex = callNodeTable.length - 1;
callNodeIndex >= 0;
callNodeIndex--
) {
callNodeTotalSummary[callNodeIndex] += callNodeLeaf[callNodeIndex];
rootTotalSummary += Math.abs(callNodeLeaf[callNodeIndex]);
const hasChildren = callNodeChildCount[callNodeIndex] !== 0;
const hasTotalValue = callNodeTotalSummary[callNodeIndex] !== 0;
if (!hasChildren && !hasTotalValue) {
continue;
}
const prefixCallNode = callNodeTable.prefix[callNodeIndex];
if (prefixCallNode === -1) {
rootCount++;
} else {
callNodeTotalSummary[prefixCallNode] +=
callNodeTotalSummary[callNodeIndex];
callNodeChildCount[prefixCallNode]++;
}
}
return {
callNodeSummary: {
self: callNodeSelf,
total: callNodeTotalSummary,
},
callNodeChildCount,
rootTotalSummary,
rootCount,
};
}
/**
* An exported interface to get an instance of the CallTree class.
* This handles computing timing information, and passing it all into
* the CallTree constructor.
*/
export function getCallTree(
thread: Thread,
interval: Milliseconds,
callNodeInfo: CallNodeInfo,
categories: CategoryList,
implementationFilter: string,
callTreeCountsAndSummary: CallTreeCountsAndSummary,
weightType: WeightType
): CallTree {
return timeCode('getCallTree', () => {
const {
callNodeSummary,
callNodeChildCount,
rootTotalSummary,
rootCount,
} = callTreeCountsAndSummary;
const jsOnly = implementationFilter === 'js';
// By default add a single decimal value, e.g 13.1, 0.3, 5234.4
return new CallTree(
thread,
categories,
callNodeInfo.callNodeTable,
callNodeSummary,
callNodeChildCount,
rootTotalSummary,
rootCount,
jsOnly,
interval,
Boolean(thread.isJsTracer),
weightType
);
});
}
/**
* This function takes the call tree summary strategy, and finds the appropriate data
* structure. This can then be used by the call tree and other UI to report on the data.
*/
export function extractSamplesLikeTable(
thread: Thread,
strategy: CallTreeSummaryStrategy
): SamplesLikeTable {
switch (strategy) {
case 'timing':
return thread.samples;
case 'js-allocations':
return ensureExists(
thread.jsAllocations,
'Expected the NativeAllocationTable to exist when using a "js-allocation" strategy'
);
case 'native-retained-allocations': {
const nativeAllocations = ensureExists(
thread.nativeAllocations,
'Expected the NativeAllocationTable to exist when using a "native-allocation" strategy'
);
/* istanbul ignore if */
if (!nativeAllocations.memoryAddress) {
throw new Error(
'Attempting to filter by retained allocations data that is missing the memory addresses.'
);
}
return ProfileData.filterToRetainedAllocations(nativeAllocations);
}
case 'native-allocations':
return ProfileData.filterToAllocations(
ensureExists(
thread.nativeAllocations,
'Expected the NativeAllocationTable to exist when using a "native-allocations" strategy'
)
);
case 'native-deallocations-sites':
return ProfileData.filterToDeallocationsSites(
ensureExists(
thread.nativeAllocations,
'Expected the NativeAllocationTable to exist when using a "native-deallocations-sites" strategy'
)
);
case 'native-deallocations-memory': {
const nativeAllocations = ensureExists(
thread.nativeAllocations,
'Expected the NativeAllocationTable to exist when using a "native-deallocations-memory" strategy'
);
/* istanbul ignore if */
if (!nativeAllocations.memoryAddress) {
throw new Error(
'Attempting to filter by retained allocations data that is missing the memory addresses.'
);
}
return ProfileData.filterToDeallocationsMemory(
ensureExists(
nativeAllocations,
'Expected the NativeAllocationTable to exist when using a "js-allocation" strategy'
)
);
}
/* istanbul ignore next */
default:
throw assertExhaustiveCheck(strategy);
}
}
/**
* This function is extremely similar to computeCallTreeCountsAndSummary,
* but is specialized for converting sample counts into traced timing. Samples
* don't have duration information associated with them, it's mostly how long they
* were observed to be running. This function computes the timing the exact same
* way that the stack chart will display the information, so that timing information
* will agree. In the past, timing was computed by samplingInterval * sampleCount.
* This caused confusion when switching to the trace-based views when the numbers
* did not agree. In order to remove confusion, we can show the sample counts,
* plus the traced timing, which is a compromise between correctness, and consistency.
*/
export function computeTracedTiming(
samples: SamplesLikeTable,
{ callNodeTable, stackIndexToCallNodeIndex }: CallNodeInfo,
interval: Milliseconds,
invertCallstack: boolean
): TracedTiming | null {
if (samples.weightType !== 'samples' || samples.weight) {
// Only compute for the samples weight types that have no weights. If a samples
// table has weights then it's a diff profile. Currently, we aren't calculating
// diff profiles, but it could be possible to compute this information twice,
// once for positive weights, and once for negative weights, then sum them
// together. At this time it's not really worth it.
//
// See https://github.com/firefox-devtools/profiler/issues/2615
return null;
}
// Compute the timing duration, which is the time between this sample and the next.
const weight = [];
for (let sampleIndex = 0; sampleIndex < samples.length - 1; sampleIndex++) {
weight.push(samples.time[sampleIndex + 1] - samples.time[sampleIndex]);
}
if (samples.length > 0) {
// Use the sampling interval for the last sample.
weight.push(interval);
}
const samplesWithWeight: SamplesTable = {
...samples,
weight,
};
const sampleIndexToCallNodeIndex = getSampleIndexToCallNodeIndex(
samples.stack,
stackIndexToCallNodeIndex
);
// Inverted trees need a different method for computing the timing.
const { callNodeSelf, callNodeLeaf } = invertCallstack
? _getInvertedStackSelf(
samplesWithWeight,
callNodeTable,
sampleIndexToCallNodeIndex
)
: _getStackSelf(
samplesWithWeight,
callNodeTable,
sampleIndexToCallNodeIndex
);
// Compute the following variables:
const callNodeTotalSummary = new Float32Array(callNodeTable.length);
const callNodeChildCount = new Uint32Array(callNodeTable.length);
// We loop the call node table in reverse, so that we find the children
// before their parents, and the total time is known at the time we reach a
// node.
for (
let callNodeIndex = callNodeTable.length - 1;
callNodeIndex >= 0;
callNodeIndex--
) {
callNodeTotalSummary[callNodeIndex] += callNodeLeaf[callNodeIndex];
const hasChildren = callNodeChildCount[callNodeIndex] !== 0;
const hasTotalValue = callNodeTotalSummary[callNodeIndex] !== 0;
if (!hasChildren && !hasTotalValue) {
continue;
}
const prefixCallNode = callNodeTable.prefix[callNodeIndex];
if (prefixCallNode !== -1) {
callNodeTotalSummary[prefixCallNode] +=
callNodeTotalSummary[callNodeIndex];
callNodeChildCount[prefixCallNode]++;
}
}
return {
self: callNodeSelf,
running: callNodeTotalSummary,
};
}