Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs-developer/CHANGELOG-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ Note that this is not an exhaustive list. Processed profile format upgraders can

## Processed profile format

### Version 75

The func table (`profile.shared.funcTable`) representation changed, mirroring the v71 frame table change:

- A new `flags` bitfield column was added. It can be stored as a plain array of numbers or as a `Uint8Array` (for profiles loaded from [JsonSlabs](https://github.com/mstange/json-slabs/) files). The bits are:
- `1 << 0` — `IsJS`: this func is a JavaScript function.
- `1 << 1` — `RelevantForJS`: this func should be treated as "relevant for JS" (e.g. DOM API label funcs).
- `1 << 2` — `HasResource`: `resource[i]` is meaningful.
- `1 << 3` — `HasSource`: `source[i]` is meaningful.
- `1 << 4` — `HasLine`: `lineNumber[i]` is meaningful.
- `1 << 5` — `HasColumn`: `columnNumber[i]` is meaningful.
- `1 << 6` — `HasOriginalLocation`: `originalLocation[i]` is meaningful.
- The `isJS` and `relevantForJS` boolean columns were removed; the `IsJS` and `RelevantForJS` flag bits carry that information.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

- The `resource`, `source`, `lineNumber`, `columnNumber`, and `originalLocation` columns are no longer nullable in-band. Each may still be stored as a plain array, but the values are always numbers, and when the corresponding "Has..." flag is unset, the value in the column is ignored (producers typically write `0`).
- The following columns can now optionally be stored as typed arrays:
- `name` (`Int32Array`)
- `resource` (`Int32Array`)
- `source` (`Int32Array`)
- `lineNumber` (`Int32Array`)
- `columnNumber` (`Int32Array`)
- `originalLocation` (`Int32Array`)

### Version 74

The columns of the native symbol table (`profile.shared.nativeSymbols`) can now optionally be stored as typed arrays, for profiles loaded from [JsonSlabs](https://github.com/mstange/json-slabs/) files (.jslb, .jslb.gz). Regular JS / JSON arrays are still accepted.
Expand Down
8 changes: 6 additions & 2 deletions src/actions/profile-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
getTrackReferenceFromThreadIndex,
} from 'firefox-profiler/profile-logic/tracks';

import { FuncFlag } from 'firefox-profiler/types';
import type {
PreviewSelection,
ImplementationFilter,
Expand Down Expand Up @@ -2096,11 +2097,14 @@ export function handleCallNodeTransformShortcut(
break;
case 'C': {
const { funcTable } = unfilteredThread;
const resourceIndex = funcTable.resource[funcIndex];
if ((funcTable.flags[funcIndex] & FuncFlag.HasResource) === 0) {
// This func has no resource, so there is nothing to collapse.
return;
}
dispatch(
addCollapseResourceTransformToStack(
threadsKey,
resourceIndex,
funcTable.resource[funcIndex],
implementation
)
);
Expand Down
2 changes: 1 addition & 1 deletion src/app-logic/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const GECKO_PROFILE_VERSION = 36;
// The current version of the "processed" profile format.
// Please don't forget to update the processed profile format changelog in
// `docs-developer/CHANGELOG-formats.md`.
export const PROCESSED_PROFILE_VERSION = 74;
export const PROCESSED_PROFILE_VERSION = 75;

// The following are the margin sizes for the left and right of the timeline. Independent
// components need to share these values.
Expand Down
9 changes: 7 additions & 2 deletions src/app-logic/url-handling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type {
MarkerIndex,
SelectedMarkersPerThread,
} from 'firefox-profiler/types';
import { FuncFlag } from 'firefox-profiler/types';
import {
decodeUintArrayFromUrlComponent,
encodeUintArrayForUrlComponent,
Expand Down Expand Up @@ -1719,7 +1720,7 @@ function getStackIndexFromVersion3JSCallNodePath(
const prefix = offset === 0 ? null : stackIndex - offset;
const frameIndex = stackTable.frame[stackIndex];
const funcIndex = frameTable.func[frameIndex];
const isJS = funcTable.isJS[funcIndex];
const isJS = (funcTable.flags[funcIndex] & FuncFlag.IsJS) !== 0;
// We know that at this point stack table is sorted and the following
// condition holds:
// assert(prefix === null || prefix < stackIndex);
Expand Down Expand Up @@ -1762,7 +1763,11 @@ function getVersion4JSCallNodePathFromStackIndex(
while (nextStackIndex !== null) {
const frameIndex: IndexIntoFrameTable = stackTable.frame[nextStackIndex];
const funcIndex = frameTable.func[frameIndex];
if (funcTable.isJS[funcIndex] || funcTable.relevantForJS[funcIndex]) {
if (
(funcTable.flags[funcIndex] &
(FuncFlag.IsJS | FuncFlag.RelevantForJS)) !==
Comment on lines +1767 to +1768

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but now that we are having more bit flags and tests like this, I wonder if it makes sense to create a generic function for testing these flags to make things easier to read. Especially it's a footgun to forget the parenthesis before !== 0 or == 0 due to them being higher precedence (especially if people are not familiar with it).

But I'm also not sure about the perf overhead of it in hot loops in general. So I think this is perfectly fine.

0
) {
callNodePath.unshift(funcIndex);
}
const offset: number = stackTable.prefixOffset[nextStackIndex];
Expand Down
31 changes: 20 additions & 11 deletions src/components/shared/CallNodeContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import type {
Page,
SamplesLikeTable,
} from 'firefox-profiler/types';
import { FuncFlag } from 'firefox-profiler/types';

import type { TabSlug } from 'firefox-profiler/app-logic/tabs-handling';
import type { ConnectedProps } from 'firefox-profiler/utils/connect';
Expand Down Expand Up @@ -147,7 +148,7 @@ class CallNodeContextMenuImpl extends React.PureComponent<Props> {
} = rightClickedCallNodeInfo;

const funcIndex = callNodeInfo.funcForNode(callNodeIndex);
const isJS = funcTable.isJS[funcIndex];
const isJS = (funcTable.flags[funcIndex] & FuncFlag.IsJS) !== 0;
const stringIndex = funcTable.name[funcIndex];
const functionCall = stringTable.getString(stringIndex);
const name = isJS ? functionCall : getFunctionName(functionCall);
Expand Down Expand Up @@ -184,10 +185,10 @@ class CallNodeContextMenuImpl extends React.PureComponent<Props> {
} = rightClickedCallNodeInfo;

const funcIndex = callNodeInfo.funcForNode(callNodeIndex);
const sourceIndex = funcTable.source[funcIndex];
if (sourceIndex === null) {
if ((funcTable.flags[funcIndex] & FuncFlag.HasSource) === 0) {
return null;
}
const sourceIndex = funcTable.source[funcIndex];
const stringIndex = sources.filename[sourceIndex];
return stringTable.getString(stringIndex);
}
Expand All @@ -208,8 +209,15 @@ class CallNodeContextMenuImpl extends React.PureComponent<Props> {
} = rightClickedCallNodeInfo;

const funcIndex = callNodeInfo.funcForNode(callNodeIndex);
const line = funcTable.lineNumber[funcIndex];
const column = funcTable.columnNumber[funcIndex];
const funcFlags = funcTable.flags[funcIndex];
const line =
(funcFlags & FuncFlag.HasLine) !== 0
? funcTable.lineNumber[funcIndex]
: null;
const column =
(funcFlags & FuncFlag.HasColumn) !== 0
? funcTable.columnNumber[funcIndex]
: null;
return { line, column };
}

Expand Down Expand Up @@ -530,21 +538,22 @@ class CallNodeContextMenuImpl extends React.PureComponent<Props> {
if (funcIndex === undefined) {
return null;
}
const isJS = funcTable.isJS[funcIndex];
const funcFlags = funcTable.flags[funcIndex];
const isJS = (funcFlags & FuncFlag.IsJS) !== 0;

if (isJS) {
const sourceIndex = funcTable.source[funcIndex];
if (sourceIndex === null) {
if ((funcFlags & FuncFlag.HasSource) === 0) {
return null;
}
const sourceIndex = funcTable.source[funcIndex];

const fileNameIndex = sources.filename[sourceIndex];
return stringTable.getString(fileNameIndex);
}
const resourceIndex = funcTable.resource[funcIndex];
if (resourceIndex === -1) {
if ((funcFlags & FuncFlag.HasResource) === 0) {
return null;
}
const resourceIndex = funcTable.resource[funcIndex];
const resNameStringIndex = resourceTable.name[resourceIndex];
return stringTable.getString(resNameStringIndex);
}
Expand Down Expand Up @@ -608,7 +617,7 @@ class CallNodeContextMenuImpl extends React.PureComponent<Props> {

const categoryIndex = callNodeInfo.categoryForNode(callNodeIndex);
const funcIndex = callNodeInfo.funcForNode(callNodeIndex);
const isJS = funcTable.isJS[funcIndex];
const isJS = (funcTable.flags[funcIndex] & FuncFlag.IsJS) !== 0;
const hasCategory = categoryIndex !== -1;
// This could be the C++ library, or the JS filename.
const nameForResource = this.getNameForSelectedResource();
Expand Down
15 changes: 9 additions & 6 deletions src/components/tooltip/CallNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
IndexIntoCategoryList,
IndexIntoSubcategoryListForCategory,
} from 'firefox-profiler/types';
import { FuncFlag } from 'firefox-profiler/types';

import type {
TimingsForPath,
Expand Down Expand Up @@ -424,9 +425,10 @@ export class TooltipCallNode extends React.PureComponent<Props> {
}

let resource = null;
const resourceIndex = thread.funcTable.resource[funcIndex];

if (resourceIndex !== -1) {
const hasResource =
(thread.funcTable.flags[funcIndex] & FuncFlag.HasResource) !== 0;
if (hasResource) {
const resourceIndex = thread.funcTable.resource[funcIndex];
const resourceNameIndex = thread.resourceTable.name[resourceIndex];
// Because of our use of Grid Layout, all our elements need to be direct
// children of the grid parent. That's why we use arrays here, to add
Expand Down Expand Up @@ -519,9 +521,10 @@ export class TooltipCallNode extends React.PureComponent<Props> {
stackTypeLabel = 'JavaScript';
break;
case 'unsymbolicated':
stackTypeLabel = thread.funcTable.isJS[funcIndex]
? 'Unsymbolicated native'
: 'Unsymbolicated or generated JIT instructions';
stackTypeLabel =
(thread.funcTable.flags[funcIndex] & FuncFlag.IsJS) !== 0
? 'Unsymbolicated native'
: 'Unsymbolicated or generated JIT instructions';
break;
default:
throw new Error(`Unknown stack type case "${stackType}".`);
Expand Down
5 changes: 3 additions & 2 deletions src/node-tools/profiler-edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
type WasmSymbolicationSpec,
} from 'firefox-profiler/profile-logic/wasm-symbolication';
import { getThreadsWithMarkersMatchingSearchFilter } from 'firefox-profiler/profile-logic/marker-data';
import { FuncFlag } from 'firefox-profiler/types/profile';
import type {
Profile,
RawThread,
Expand Down Expand Up @@ -124,8 +125,8 @@ export function collectFuncNames(profile: Profile): string[] {
const result: string[] = [];
for (let i = 0; i < funcTable.length; i++) {
let name = stringArray[funcTable.name[i]];
const sourceIndex = funcTable.source[i];
if (sourceIndex !== null) {
if ((funcTable.flags[i] & FuncFlag.HasSource) !== 0) {
const sourceIndex = funcTable.source[i];
const filename = stringArray[sources.filename[sourceIndex]];
name += ` (${filename})`;
}
Expand Down
13 changes: 9 additions & 4 deletions src/profile-logic/call-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type {
SampleCategoriesAndSubcategories,
IndexIntoCategoryList,
} from 'firefox-profiler/types';
import { ResourceType } from 'firefox-profiler/types';
import { ResourceType, FuncFlag } from 'firefox-profiler/types';

import ExtensionIcon from '../../res/img/svg/extension.svg';
import { formatCallNodeNumber, formatPercent } from '../utils/format-numbers';
Expand Down Expand Up @@ -522,9 +522,14 @@ export class CallTree {
const subcategoryIndex =
this._callNodeInfo.subcategoryForNode(callNodeIndex);
const badge = this._getInliningBadge(callNodeIndex, funcName);
const resourceIndex = this._thread.funcTable.resource[funcIndex];
const resourceType = this._thread.resourceTable.type[resourceIndex];
const isFrameLabel = resourceIndex === -1;
const funcFlags = this._thread.funcTable.flags[funcIndex];
const isFrameLabel = (funcFlags & FuncFlag.HasResource) === 0;
const resourceIndex = isFrameLabel
? -1
: this._thread.funcTable.resource[funcIndex];
const resourceType = isFrameLabel
? -1
: this._thread.resourceTable.type[resourceIndex];
const libName = this._getOriginAnnotation(funcIndex);
const weightType = this._weightType;

Expand Down
51 changes: 37 additions & 14 deletions src/profile-logic/data-structures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type {
RawJsAllocationsTable,
RawUnbalancedNativeAllocationsTable,
RawBalancedNativeAllocationsTable,
FuncTable,
RawFuncTable,
RawMarkerTable,
ResourceTable,
RawNativeSymbolTable,
Expand All @@ -35,6 +35,8 @@ import type {
IndexIntoFrameTable,
IndexIntoFuncTable,
IndexIntoLibs,
IndexIntoResourceTable,
IndexIntoSourceTable,
IndexIntoStackTable,
IndexIntoStringTable,
IndexIntoCategoryList,
Expand Down Expand Up @@ -385,14 +387,24 @@ export function finishRawFrameTableBuilder(
};
}

export function getEmptyFuncTable(): FuncTable {
export type RawFuncTableBuilder = {
flags: number[];
name: IndexIntoStringTable[];
resource: IndexIntoResourceTable[];
source: IndexIntoSourceTable[];
lineNumber: number[];
columnNumber: number[];
originalLocation: IndexIntoSourceLocationTable[];
length: number;
};

export function getRawFuncTableBuilder(): RawFuncTableBuilder {
return {
// Important!
// If modifying this structure, please update all callers of this function to ensure
// that they are pushing on correctly to the data structure. These pushes may not
// be caught by the type system.
isJS: [],
relevantForJS: [],
flags: [],
name: [],
resource: [],
source: [],
Expand All @@ -403,24 +415,35 @@ export function getEmptyFuncTable(): FuncTable {
};
}

export function shallowCloneFuncTable(funcTable: FuncTable): FuncTable {
export function getRawFuncTableBuilderWithExistingContents(
funcTable: RawFuncTable
): RawFuncTableBuilder {
return {
// Important!
// If modifying this structure, please update all callers of this function to ensure
// that they are pushing on correctly to the data structure. These pushes may not
// be caught by the type system.
isJS: funcTable.isJS.slice(),
relevantForJS: funcTable.relevantForJS.slice(),
name: funcTable.name.slice(),
resource: funcTable.resource.slice(),
source: funcTable.source.slice(),
lineNumber: funcTable.lineNumber.slice(),
columnNumber: funcTable.columnNumber.slice(),
originalLocation: funcTable.originalLocation.slice(),
flags: Array.from(funcTable.flags),
name: Array.from(funcTable.name),
resource: Array.from(funcTable.resource),
source: Array.from(funcTable.source),
lineNumber: Array.from(funcTable.lineNumber),
columnNumber: Array.from(funcTable.columnNumber),
originalLocation: Array.from(funcTable.originalLocation),
length: funcTable.length,
};
}

export function finishRawFuncTableBuilder(
builder: RawFuncTableBuilder
): RawFuncTable {
return { ...builder };
}

export function getEmptyRawFuncTable(): RawFuncTable {
return finishRawFuncTableBuilder(getRawFuncTableBuilder());
}

export function getEmptySourceLocationTable(): SourceLocationTable {
return {
source: [],
Expand Down Expand Up @@ -676,7 +699,7 @@ export function getEmptySharedData(): RawProfileSharedData {
return {
stackTable: finishRawStackTableBuilder(getRawStackTableBuilder()),
frameTable: finishRawFrameTableBuilder(getRawFrameTableBuilder()),
funcTable: getEmptyFuncTable(),
funcTable: getEmptyRawFuncTable(),
resourceTable: getEmptyResourceTable(),
nativeSymbols: finishRawNativeSymbolTableBuilder(
getRawNativeSymbolTableBuilder()
Expand Down
Loading
Loading