From 993124733e5ef4c4f04b025552178d5de61ac1d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 2 Apr 2026 14:01:13 +0200 Subject: [PATCH 1/9] Convert stackContainsFunc to use BitSets --- src/profile-logic/transforms.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/profile-logic/transforms.ts b/src/profile-logic/transforms.ts index 604b7f1179..94c23e4ee8 100644 --- a/src/profile-logic/transforms.ts +++ b/src/profile-logic/transforms.ts @@ -54,6 +54,7 @@ import { translateFuncIndex, translateResourceIndex, } from './index-translation'; +import { checkBit, makeBitSet, setBit } from 'firefox-profiler/utils/bitset'; /** * This file contains the functions and logic for working with and applying transforms @@ -926,9 +927,7 @@ export function dropFunction( const { stackTable, frameTable } = thread; // Go through each stack, and label it as containing the function or not. - // stackContainsFunc is a stackIndex => bool map, implemented as a U8 typed - // array for better performance. 0 means false, 1 means true. - const stackContainsFunc = new Uint8Array(stackTable.length); + const stackContainsFunc = makeBitSet(stackTable.length); for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { const prefix = stackTable.prefix[stackIndex]; const frameIndex = stackTable.frame[stackIndex]; @@ -937,15 +936,15 @@ export function dropFunction( // This is the function we want to remove. funcIndex === funcIndexToDrop || // The parent of this stack contained the function. - (prefix !== null && stackContainsFunc[prefix] === 1) + (prefix !== null && checkBit(stackContainsFunc, prefix)) ) { - stackContainsFunc[stackIndex] = 1; + setBit(stackContainsFunc, stackIndex); } } return updateThreadStacks(thread, stackTable, (stack) => // Drop the stacks that contain that function. - stack !== null && stackContainsFunc[stack] === 1 ? null : stack + stack !== null && checkBit(stackContainsFunc, stack) ? null : stack ); } From 62d69a74a9ee7a8915b61f2e375114fd74fb16b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 2 Apr 2026 14:01:58 +0200 Subject: [PATCH 2/9] Convert isInCollapsedSubtree to use BitSets --- src/profile-logic/transforms.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/profile-logic/transforms.ts b/src/profile-logic/transforms.ts index 94c23e4ee8..94052d6b43 100644 --- a/src/profile-logic/transforms.ts +++ b/src/profile-logic/transforms.ts @@ -1213,19 +1213,19 @@ export function collapseFunctionSubtree( ): Thread { const { stackTable, frameTable } = thread; const oldStackToNewStack = new Int32Array(stackTable.length); - const isInCollapsedSubtree = new Uint8Array(stackTable.length); + const isInCollapsedSubtree = makeBitSet(stackTable.length); for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { const prefix = stackTable.prefix[stackIndex]; - if (prefix !== null && isInCollapsedSubtree[prefix] !== 0) { + if (prefix !== null && checkBit(isInCollapsedSubtree, prefix)) { oldStackToNewStack[stackIndex] = oldStackToNewStack[prefix]; - isInCollapsedSubtree[stackIndex] = 1; + setBit(isInCollapsedSubtree, stackIndex); } else { oldStackToNewStack[stackIndex] = stackIndex; const frameIndex = stackTable.frame[stackIndex]; const funcIndex = frameTable.func[frameIndex]; if (funcToCollapse === funcIndex) { - isInCollapsedSubtree[stackIndex] = 1; + setBit(isInCollapsedSubtree, stackIndex); } } } From 2a4e38cf3cff26f052d75bd8911c6f8dec04b126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 2 Apr 2026 14:09:23 +0200 Subject: [PATCH 3/9] Convert ancestorOfCallNodeContainsFuncToCheck to use BitSets --- src/profile-logic/transforms.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/profile-logic/transforms.ts b/src/profile-logic/transforms.ts index 94052d6b43..771991998e 100644 --- a/src/profile-logic/transforms.ts +++ b/src/profile-logic/transforms.ts @@ -1739,7 +1739,7 @@ export function funcHasRecursiveCall( funcToCheck: IndexIntoFuncTable ) { // Set of stack indices that are funcToCheck or have a funcToCheck ancestor. - const ancestorOfCallNodeContainsFuncToCheck = new Uint8Array( + const ancestorOfCallNodeContainsFuncToCheck = makeBitSet( callNodeTable.length ); @@ -1747,16 +1747,16 @@ export function funcHasRecursiveCall( const prefix = callNodeTable.prefix[i]; const funcIndex = callNodeTable.func[i]; const recursivePrefix = - prefix !== -1 && ancestorOfCallNodeContainsFuncToCheck[prefix] !== 0; + prefix !== -1 && checkBit(ancestorOfCallNodeContainsFuncToCheck, prefix); if (funcToCheck === funcIndex) { if (recursivePrefix) { // This function matches and so did one of its ancestors. return true; } - ancestorOfCallNodeContainsFuncToCheck[i] = 1; + setBit(ancestorOfCallNodeContainsFuncToCheck, i); } else if (recursivePrefix) { - ancestorOfCallNodeContainsFuncToCheck[i] = 1; + setBit(ancestorOfCallNodeContainsFuncToCheck, i); } } return false; From 285628eb769cae5dbab8699d6c4ae64c53a25107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 2 Apr 2026 14:23:06 +0200 Subject: [PATCH 4/9] Convert hasChildrenPerRootFunc to use BitSets --- src/profile-logic/call-tree.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/profile-logic/call-tree.ts b/src/profile-logic/call-tree.ts index 57495bec52..3a89838712 100644 --- a/src/profile-logic/call-tree.ts +++ b/src/profile-logic/call-tree.ts @@ -34,7 +34,7 @@ import { ResourceType } from 'firefox-profiler/types'; import ExtensionIcon from '../../res/img/svg/extension.svg'; import { formatCallNodeNumber, formatPercent } from '../utils/format-numbers'; import { assertExhaustiveCheck, ensureExists } from '../utils/types'; -import { checkBit } from '../utils/bitset'; +import { type BitSet, checkBit, makeBitSet, setBit } from '../utils/bitset'; import * as ProfileData from './profile-data'; import type { CallTreeSummaryStrategy } from '../types/actions'; import type { CallNodeInfo, CallNodeInfoInverted } from './call-node-info'; @@ -61,7 +61,7 @@ export type CallTreeTimingsInverted = { rootTotalSummary: number; sortedRoots: IndexIntoFuncTable[]; totalPerRootFunc: Float64Array; - hasChildrenPerRootFunc: Uint8Array; + hasChildrenPerRootFunc: BitSet; }; export type CallTreeTimingsFunctionList = { @@ -242,7 +242,7 @@ class CallTreeInternalInverted implements CallTreeInternal { _callNodeSelf: Float64Array; _rootNodes: IndexIntoCallNodeTable[]; _totalPerRootFunc: Float64Array; - _hasChildrenPerRootFunc: Uint8Array; + _hasChildrenPerRootFunc: BitSet; _totalAndHasChildrenPerNonRootNode: Map< IndexIntoCallNodeTable, TotalAndHasChildren @@ -268,7 +268,7 @@ class CallTreeInternalInverted implements CallTreeInternal { hasChildren(callNodeIndex: IndexIntoCallNodeTable): boolean { if (this._callNodeInfo.isRoot(callNodeIndex)) { - return this._hasChildrenPerRootFunc[callNodeIndex] !== 0; + return checkBit(this._hasChildrenPerRootFunc, callNodeIndex); } return this._getTotalAndHasChildren(callNodeIndex).hasChildren; } @@ -790,7 +790,7 @@ export function computeCallTreeTimingsInverted( const callNodeTableFuncCol = callNodeTable.func; const callNodeTableDepthCol = callNodeTable.depth; const totalPerRootFunc = new Float64Array(funcCount); - const hasChildrenPerRootFunc = new Uint8Array(funcCount); + const hasChildrenPerRootFunc = makeBitSet(funcCount); const seenPerRootFunc = new Uint8Array(funcCount); const sortedRoots = []; for (let i = 0; i < callNodeSelf.length; i++) { @@ -810,7 +810,7 @@ export function computeCallTreeTimingsInverted( sortedRoots.push(func); } if (callNodeTableDepthCol[i] !== 0) { - hasChildrenPerRootFunc[func] = 1; + setBit(hasChildrenPerRootFunc, func); } } sortedRoots.sort( From 95c406e8c825ed0f960b613b250c2512b215e691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 2 Apr 2026 14:25:26 +0200 Subject: [PATCH 5/9] Convert seenPerRootFunc to use BitSets --- src/profile-logic/call-tree.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/profile-logic/call-tree.ts b/src/profile-logic/call-tree.ts index 3a89838712..0c8dd30cc5 100644 --- a/src/profile-logic/call-tree.ts +++ b/src/profile-logic/call-tree.ts @@ -791,7 +791,7 @@ export function computeCallTreeTimingsInverted( const callNodeTableDepthCol = callNodeTable.depth; const totalPerRootFunc = new Float64Array(funcCount); const hasChildrenPerRootFunc = makeBitSet(funcCount); - const seenPerRootFunc = new Uint8Array(funcCount); + const seenPerRootFunc = makeBitSet(funcCount); const sortedRoots = []; for (let i = 0; i < callNodeSelf.length; i++) { const self = callNodeSelf[i]; @@ -805,8 +805,8 @@ export function computeCallTreeTimingsInverted( const func = callNodeTableFuncCol[i]; totalPerRootFunc[func] += self; - if (seenPerRootFunc[func] === 0) { - seenPerRootFunc[func] = 1; + if (!checkBit(seenPerRootFunc, func)) { + setBit(seenPerRootFunc, func); sortedRoots.push(func); } if (callNodeTableDepthCol[i] !== 0) { From 4bda5a64a4c83380f2c23ae909cad13c3b69d550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 2 Apr 2026 14:26:18 +0200 Subject: [PATCH 6/9] Convert callNodeHasChildren to use BitSets --- src/profile-logic/call-tree.ts | 19 ++++++++------- .../__snapshots__/profile-view.test.ts.snap | 24 ++++--------------- src/test/unit/profile-tree.test.ts | 8 ++++++- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/profile-logic/call-tree.ts b/src/profile-logic/call-tree.ts index 0c8dd30cc5..6d97c9333b 100644 --- a/src/profile-logic/call-tree.ts +++ b/src/profile-logic/call-tree.ts @@ -43,7 +43,7 @@ import { getBottomBoxInfoForCallNode } from './bottom-box'; type CallNodeChildren = IndexIntoCallNodeTable[]; export type CallTreeTimingsNonInverted = { - callNodeHasChildren: Uint8Array; + callNodeHasChildren: BitSet; self: Float64Array; total: Float64Array; rootTotalSummary: number; // sum of absolute values, this is used for computing percentages @@ -119,7 +119,7 @@ export class CallTreeInternalNonInverted implements CallTreeInternal { _callNodeInfo: CallNodeInfo; _callNodeTable: CallNodeTable; _callTreeTimings: CallTreeTimingsNonInverted; - _callNodeHasChildren: Uint8Array; // A table column matching the callNodeTable + _callNodeHasChildren: BitSet; // A table column matching the callNodeTable constructor( callNodeInfo: CallNodeInfo, @@ -157,9 +157,12 @@ export class CallTreeInternalNonInverted implements CallTreeInternal { childCallNodeIndex = this._callNodeTable.nextSibling[childCallNodeIndex] ) { const childTotalSummary = this._callTreeTimings.total[childCallNodeIndex]; - const childHasChildren = this._callNodeHasChildren[childCallNodeIndex]; + const childHasChildren = checkBit( + this._callNodeHasChildren, + childCallNodeIndex + ); - if (childTotalSummary !== 0 || childHasChildren !== 0) { + if (childTotalSummary !== 0 || childHasChildren) { children.push(childCallNodeIndex); } } @@ -172,7 +175,7 @@ export class CallTreeInternalNonInverted implements CallTreeInternal { } hasChildren(callNodeIndex: IndexIntoCallNodeTable): boolean { - return this._callNodeHasChildren[callNodeIndex] !== 0; + return checkBit(this._callNodeHasChildren, callNodeIndex); } getSelfAndTotal(callNodeIndex: IndexIntoCallNodeTable): SelfAndTotal { @@ -861,7 +864,7 @@ export function computeCallTreeTimingsNonInverted( // Compute the following variables: const callNodeTotal = new Float64Array(callNodeTable.length); - const callNodeHasChildren = new Uint8Array(callNodeTable.length); + const callNodeHasChildren = makeBitSet(callNodeTable.length); // 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 @@ -874,7 +877,7 @@ export function computeCallTreeTimingsNonInverted( // callNodeTotal[callNodeIndex] is the sum of our children's totals. // Compute this node's total by adding this node's self. const total = callNodeTotal[callNodeIndex] + callNodeSelf[callNodeIndex]; - const hasChildren = callNodeHasChildren[callNodeIndex] !== 0; + const hasChildren = checkBit(callNodeHasChildren, callNodeIndex); const hasTotalValue = total !== 0; callNodeTotal[callNodeIndex] = total; @@ -886,7 +889,7 @@ export function computeCallTreeTimingsNonInverted( const prefixCallNode = callNodeTable.prefix[callNodeIndex]; if (prefixCallNode !== -1) { callNodeTotal[prefixCallNode] += total; - callNodeHasChildren[prefixCallNode] = 1; + setBit(callNodeHasChildren, prefixCallNode); } } diff --git a/src/test/store/__snapshots__/profile-view.test.ts.snap b/src/test/store/__snapshots__/profile-view.test.ts.snap index f04baf9582..179d6e9125 100644 --- a/src/test/store/__snapshots__/profile-view.test.ts.snap +++ b/src/test/store/__snapshots__/profile-view.test.ts.snap @@ -2069,16 +2069,8 @@ CallTree { "_children": Array [], "_displayDataByIndex": Map {}, "_internal": CallTreeInternalNonInverted { - "_callNodeHasChildren": Uint8Array [ - 1, - 1, - 0, - 0, - 0, - 1, - 0, - 0, - 0, + "_callNodeHasChildren": Int32Array [ + 35, ], "_callNodeInfo": CallNodeInfoNonInverted { "_cache": Map {}, @@ -2301,16 +2293,8 @@ CallTree { ], }, "_callTreeTimings": Object { - "callNodeHasChildren": Uint8Array [ - 1, - 1, - 0, - 0, - 0, - 1, - 0, - 0, - 0, + "callNodeHasChildren": Int32Array [ + 35, ], "rootTotalSummary": 2, "self": Float64Array [ diff --git a/src/test/unit/profile-tree.test.ts b/src/test/unit/profile-tree.test.ts index f50cd99fe8..f71d94784d 100644 --- a/src/test/unit/profile-tree.test.ts +++ b/src/test/unit/profile-tree.test.ts @@ -20,6 +20,7 @@ import { getSampleIndexToCallNodeIndex, } from '../../profile-logic/profile-data'; import { ResourceType } from 'firefox-profiler/types'; +import { makeBitSet, setBit } from '../../utils/bitset'; import { callTreeFromProfile, functionListTreeFromProfile, @@ -79,11 +80,16 @@ describe('unfiltered call tree', function () { callNodeInfo.getCallNodeTable().length ) ); + const expectedHasChildren = makeBitSet(9); + // Nodes 0, 1, 2, 3, 5, 7 have children. + for (const i of [0, 1, 2, 3, 5, 7]) { + setBit(expectedHasChildren, i); + } expect(callTreeTimings).toEqual({ type: 'NON_INVERTED', timings: { rootTotalSummary: 3, - callNodeHasChildren: new Uint8Array([1, 1, 1, 1, 0, 1, 0, 1, 0]), + callNodeHasChildren: expectedHasChildren, self: new Float64Array([0, 0, 0, 0, 1, 0, 1, 0, 1]), total: new Float64Array([3, 3, 2, 1, 1, 1, 1, 1, 1]), }, From 01925bba0cb3c42773228f6990e09b8d669320c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 2 Apr 2026 15:20:55 +0200 Subject: [PATCH 7/9] Convert seenperFunc to use BitSets --- src/profile-logic/call-tree.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/profile-logic/call-tree.ts b/src/profile-logic/call-tree.ts index 6d97c9333b..75edb0346b 100644 --- a/src/profile-logic/call-tree.ts +++ b/src/profile-logic/call-tree.ts @@ -943,7 +943,7 @@ function _computeFuncTotal( // The set of "functions with potentially non-zero totals", stored as an array. const seenFuncs = []; // seenPerFunc[func] stores whether seenFuncs.includes(func). - const seenPerFunc = new Uint8Array(funcCount); + const seenPerFunc = makeBitSet(funcCount); // 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 @@ -982,8 +982,8 @@ function _computeFuncTotal( funcTotal[func] += total; // Add this func to the set of funcs with potentially non-zero totals. - if (seenPerFunc[func] === 0) { - seenPerFunc[func] = 1; + if (!checkBit(seenPerFunc, func)) { + setBit(seenPerFunc, func); seenFuncs.push(func); } } From 002693bdbf209df25476e0fdb9e7a9b6b96c5c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 2 Apr 2026 15:22:04 +0200 Subject: [PATCH 8/9] Convert haveFilled to use BitSets --- src/profile-logic/profile-data.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 49a521c9f1..51edacc88a 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -520,7 +520,7 @@ function _computeCallNodeTableExtraColumns( const innerWindowIDCol = new Float64Array(callNodeCount); const inlinedIntoCol = new Int32Array(callNodeCount); - const haveFilled = new Uint8Array(callNodeCount); + const haveFilled = makeBitSet(callNodeCount); for (let stackIndex = 0; stackIndex < stackCount; stackIndex++) { const category = stackTableCategoryCol[stackIndex]; @@ -530,7 +530,7 @@ function _computeCallNodeTableExtraColumns( const callNodeIndex = stackIndexToCallNodeIndex[stackIndex]; - if (haveFilled[callNodeIndex] === 0) { + if (!checkBit(haveFilled, callNodeIndex)) { funcCol[callNodeIndex] = frameTableFuncCol[frameIndex]; categoryCol[callNodeIndex] = category; @@ -544,7 +544,7 @@ function _computeCallNodeTableExtraColumns( innerWindowIDCol[callNodeIndex] = innerWindowID; } - haveFilled[callNodeIndex] = 1; + setBit(haveFilled, callNodeIndex); } else { // Resolve category conflicts, by resetting a conflicting subcategory or // category to the default category. From 35d88d025a342978e87a98e33e4ee7e8c85395c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 2 Apr 2026 15:22:58 +0200 Subject: [PATCH 9/9] Convert shouldStacksWithThisFrameBeRemoved to use BitSets --- src/profile-logic/symbolication.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/profile-logic/symbolication.ts b/src/profile-logic/symbolication.ts index 3186fb7b3a..ae423e317e 100644 --- a/src/profile-logic/symbolication.ts +++ b/src/profile-logic/symbolication.ts @@ -32,6 +32,7 @@ import type { import { PathSet } from '../utils/path'; import { StringTable } from '../utils/string-table'; import { updateRawThreadStacks } from './profile-data'; +import { type BitSet, makeBitSet, setBit, checkBit } from '../utils/bitset'; // Contains functions to symbolicate a profile. @@ -411,7 +412,7 @@ function finishSymbolicationForLib( // - stack F with frame 5 function _computeStackTableWithAddedExpansionStacks( stackTable: RawStackTable, - shouldStacksWithThisOldFrameBeRemoved: Uint8Array, + shouldStacksWithThisOldFrameBeRemoved: BitSet, frameIndexToInlineExpansionFrames: Map< IndexIntoFrameTable, IndexIntoFrameTable[] @@ -427,7 +428,7 @@ function _computeStackTableWithAddedExpansionStacks( const oldPrefix = stackTable.prefix[stack]; const newPrefixOrMinusOne = oldPrefix === null ? -1 : oldStackToNewStack[oldPrefix]; - if (shouldStacksWithThisOldFrameBeRemoved[oldFrame] !== 0) { + if (checkBit(shouldStacksWithThisOldFrameBeRemoved, oldFrame)) { // Don't add this stack node to the new stack table. Instead, make it // so that this node's children use our prefix as their prefix. oldStackToNewStack[stack] = newPrefixOrMinusOne; @@ -470,7 +471,7 @@ export function applySymbolicationSteps( } { const oldFuncToNewFuncsMap: FuncToFuncsMap = new Map(); const frameCount = oldShared.frameTable.length; - const shouldStacksWithThisFrameBeRemoved = new Uint8Array(frameCount); + const shouldStacksWithThisFrameBeRemoved = makeBitSet(frameCount); const frameIndexToInlineExpansionFrames = new Map< IndexIntoFrameTable, IndexIntoFrameTable[] @@ -541,7 +542,7 @@ function _partiallyApplySymbolicationStep( shared: RawProfileSharedData, symbolicationStepInfo: SymbolicationStepInfo, oldFuncToNewFuncsMap: FuncToFuncsMap, - shouldStacksWithThisFrameBeRemoved: Uint8Array, + shouldStacksWithThisFrameBeRemoved: BitSet, frameIndexToInlineExpansionFrames: Map< IndexIntoFrameTable, IndexIntoFrameTable[] @@ -585,7 +586,7 @@ function _partiallyApplySymbolicationStep( for (const frameIndex of allFramesForThisLib) { if (oldFrameTable.inlineDepth[frameIndex] > 0) { inlinedFrames.push(frameIndex); - shouldStacksWithThisFrameBeRemoved[frameIndex] = 1; + setBit(shouldStacksWithThisFrameBeRemoved, frameIndex); } else { nonInlinedFrames.push(frameIndex); }