From 871fda3a4bf22fa1d2dc01d80d08f15494a6312c Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 4 Sep 2025 15:00:03 -0400 Subject: [PATCH 1/3] Add a test for call tree search filtering. These tests would have caught #5598. We didn't really have tests for this call tree search filtering! I was quite surprised to learn this. --- src/test/store/profile-view.test.ts | 170 ++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/src/test/store/profile-view.test.ts b/src/test/store/profile-view.test.ts index bb3651313c..e69d5d8acc 100644 --- a/src/test/store/profile-view.test.ts +++ b/src/test/store/profile-view.test.ts @@ -27,6 +27,7 @@ import { withAnalyticsMock } from '../fixtures/mocks/analytics'; import { getProfileWithNiceTracks } from '../fixtures/profiles/tracks'; import { blankStore, storeWithProfile } from '../fixtures/stores'; import { assertSetContainsOnly } from '../fixtures/custom-assertions'; +import { formatTree } from '../fixtures/utils'; import * as App from '../../actions/app'; import * as ProfileView from '../../actions/profile-view'; @@ -750,6 +751,175 @@ describe('actions/ProfileView', function () { }); }); }); + + function setup() { + // Create a profile which has more than 32 stacks, so that, if any parts of the implementation + // use a BitSet to keep track of something that's per-stack (such as whether a stack matches + // the search filter), the BitSet needs at least two 32-bit slots. + const { profile } = getProfileFromTextSamples(` + A[lib:K][file:S] A[lib:K][file:S] A[lib:K][file:S] D[lib:nNn][file:uV] C[lib:m][file:t] + B[lib:L][file:t] B[lib:L][file:t] E[lib:O][file:Pq] + A[lib:K][file:S] C[lib:m][file:t] + B[lib:L][file:t] D[lib:n][file:uV] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + B[lib:L][file:t] + `); + + const { dispatch, getState } = storeWithProfile(profile); + return { dispatch, getState, profile }; + } + + it('starts as an unfiltered call tree', function () { + const { getState } = setup(); + const originalCallTree = selectedThreadSelectors.getCallTree(getState()); + expect(formatTree(originalCallTree)).toEqual([ + '- A (total: 3, self: —)', + ' - B (total: 2, self: —)', + ' - A (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - B (total: 1, self: 1)', + ' - C (total: 1, self: —)', + ' - D (total: 1, self: 1)', + ' - E (total: 1, self: 1)', + '- D (total: 1, self: 1)', + '- C (total: 1, self: 1)', + ]); + }); + + it('filters out all samples if there is no match', function () { + const { dispatch, getState } = setup(); + dispatch(ProfileView.changeCallTreeSearchString('F')); + const callTree = selectedThreadSelectors.getCallTree(getState()); + expect(formatTree(callTree)).toEqual([]); + }); + + it('filters based on function names', function () { + const { dispatch, getState } = setup(); + dispatch(ProfileView.changeCallTreeSearchString('c')); + const callTree = selectedThreadSelectors.getCallTree(getState()); + // Keep all stacks which include function C + expect(formatTree(callTree)).toEqual([ + '- A (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - C (total: 1, self: —)', + ' - D (total: 1, self: 1)', + '- C (total: 1, self: 1)', + ]); + + // Also test with uppercase 'C' + dispatch(ProfileView.changeCallTreeSearchString('C')); + const callTree2 = selectedThreadSelectors.getCallTree(getState()); + // Keep all stacks which include function C + expect(formatTree(callTree2)).toEqual([ + '- A (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - C (total: 1, self: —)', + ' - D (total: 1, self: 1)', + '- C (total: 1, self: 1)', + ]); + }); + + it('filters based on filenames', function () { + const { dispatch, getState } = setup(); + dispatch(ProfileView.changeCallTreeSearchString('u')); + const callTree_u = selectedThreadSelectors.getCallTree(getState()); + // Keep all stacks which include function D, which has filename uV + expect(formatTree(callTree_u)).toEqual([ + '- A (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - C (total: 1, self: —)', + ' - D (total: 1, self: 1)', + '- D (total: 1, self: 1)', + ]); + dispatch(ProfileView.changeCallTreeSearchString('pQ')); + const callTree_pQ = selectedThreadSelectors.getCallTree(getState()); + // Keep all stacks which include function E, which has filename Pq + expect(formatTree(callTree_pQ)).toEqual([ + '- A (total: 1, self: —)', + ' - E (total: 1, self: 1)', + ]); + }); + + it('filters based on library names', function () { + const { dispatch, getState } = setup(); + dispatch(ProfileView.changeCallTreeSearchString('M')); + const callTree_M = selectedThreadSelectors.getCallTree(getState()); + // Keep all stacks which include function C, which has lib name m + expect(formatTree(callTree_M)).toEqual([ + '- A (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - C (total: 1, self: —)', + ' - D (total: 1, self: 1)', + '- C (total: 1, self: 1)', + ]); + dispatch(ProfileView.changeCallTreeSearchString('NN')); + const callTree_NN = selectedThreadSelectors.getCallTree(getState()); + // Keep all stacks which include function D, which has filename nNn + expect(formatTree(callTree_NN)).toEqual([ + '- A (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - C (total: 1, self: —)', + ' - D (total: 1, self: 1)', + '- D (total: 1, self: 1)', + ]); + }); }); /** From 999deddcc4a3ca23c4156ea9baac91f8e04a9888 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 4 Sep 2025 15:00:03 -0400 Subject: [PATCH 2/3] Make bit set functions check for out-of-bounds indexes. This is imprecise because bitsets don't remember the total bitcount, only the slot count, and each slot can store 32 bits. --- src/utils/bitset.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/utils/bitset.ts b/src/utils/bitset.ts index 87043985fa..7428ea178f 100644 --- a/src/utils/bitset.ts +++ b/src/utils/bitset.ts @@ -21,17 +21,35 @@ export function makeBitSet(length: number): BitSet { export function setBit(bitSet: BitSet, bitIndex: number) { const q = bitIndex >> 5; const r = bitIndex & 0b11111; + if (q >= bitSet.length) { + throw new BitSetOutOfBoundsError(bitIndex); + } bitSet[q] |= 1 << r; } export function clearBit(bitSet: BitSet, bitIndex: number) { const q = bitIndex >> 5; const r = bitIndex & 0b11111; + if (q >= bitSet.length) { + throw new BitSetOutOfBoundsError(bitIndex); + } bitSet[q] &= ~(1 << r); } export function checkBit(bitSet: BitSet, bitIndex: number): boolean { const q = bitIndex >> 5; const r = bitIndex & 0b11111; + if (q >= bitSet.length) { + throw new BitSetOutOfBoundsError(bitIndex); + } return (bitSet[q] & (1 << r)) !== 0; } + +export class BitSetOutOfBoundsError extends Error { + override name = 'BitSetOutOfBoundsError'; + bitIndex: number; + constructor(bitIndex: number) { + super(`Bit index ${bitIndex} is out of bounds`); + this.bitIndex = bitIndex; + } +} From 9be394d06001eedfd81a02f184712ae8870336d8 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 4 Sep 2025 15:00:03 -0400 Subject: [PATCH 3/3] Pass the right length when creating the stackMatchesSearch bit set. Fixes #5598. --- src/profile-logic/profile-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 282acdfeb3..06b0c89c44 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -1538,7 +1538,7 @@ export function filterThreadToSearchString( } } - const stackMatchesSearch = makeBitSet(funcTable.length); + const stackMatchesSearch = makeBitSet(stackTable.length); for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { const prefix = stackTable.prefix[stackIndex]; if (prefix !== null && checkBit(stackMatchesSearch, prefix)) {