From 545a904d639cafeb5e799fdf155a226919174650 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 7 Feb 2026 12:13:23 -0500 Subject: [PATCH 1/4] Use a Uint8Array for sample selected states (and rename it from "samples" to "sample"). --- .../shared/thread/ActivityGraph.tsx | 7 ++-- .../shared/thread/ActivityGraphCanvas.tsx | 7 ++-- .../shared/thread/ActivityGraphFills.tsx | 18 ++++----- src/components/shared/thread/CPUGraph.tsx | 7 ++-- src/components/shared/thread/HeightGraph.tsx | 8 ++-- src/components/shared/thread/SampleGraph.tsx | 14 +++---- src/components/shared/thread/StackGraph.tsx | 7 ++-- src/components/timeline/TrackThread.tsx | 17 ++++---- src/profile-logic/profile-data.ts | 40 +++++++++---------- src/selectors/per-thread/stack-sample.ts | 30 +++++++------- src/test/unit/profile-data.test.ts | 28 ++++++++----- 11 files changed, 95 insertions(+), 88 deletions(-) diff --git a/src/components/shared/thread/ActivityGraph.tsx b/src/components/shared/thread/ActivityGraph.tsx index 526faaa727..d089e34617 100644 --- a/src/components/shared/thread/ActivityGraph.tsx +++ b/src/components/shared/thread/ActivityGraph.tsx @@ -21,7 +21,6 @@ import type { CategoryList, ImplementationFilter, IndexIntoSamplesTable, - SelectedState, Milliseconds, CssPixels, TimelineType, @@ -45,7 +44,7 @@ export type Props = { sampleIndex: IndexIntoSamplesTable | null ) => void; readonly categories: CategoryList; - readonly samplesSelectedStates: null | SelectedState[]; + readonly sampleSelectedStates: null | Uint8Array; readonly treeOrderSampleComparator: ( a: IndexIntoSamplesTable, b: IndexIntoSamplesTable @@ -135,7 +134,7 @@ class ThreadActivityGraphImpl extends React.PureComponent { rangeStart, rangeEnd, sampleIndexOffset, - samplesSelectedStates, + sampleSelectedStates, treeOrderSampleComparator, enableCPUUsage, implementationFilter, @@ -164,7 +163,7 @@ class ThreadActivityGraphImpl extends React.PureComponent { rangeStart={rangeStart} rangeEnd={rangeEnd} sampleIndexOffset={sampleIndexOffset} - samplesSelectedStates={samplesSelectedStates} + sampleSelectedStates={sampleSelectedStates} treeOrderSampleComparator={treeOrderSampleComparator} categories={categories} passFillsQuerier={this._setFillsQuerier} diff --git a/src/components/shared/thread/ActivityGraphCanvas.tsx b/src/components/shared/thread/ActivityGraphCanvas.tsx index 405f8a3160..83472a9bf2 100644 --- a/src/components/shared/thread/ActivityGraphCanvas.tsx +++ b/src/components/shared/thread/ActivityGraphCanvas.tsx @@ -15,7 +15,6 @@ import { mapCategoryColorNameToStyles } from 'firefox-profiler/utils/colors'; import type { Thread, Milliseconds, - SelectedState, IndexIntoSamplesTable, CategoryList, } from 'firefox-profiler/types'; @@ -30,7 +29,7 @@ type CanvasProps = { readonly rangeStart: Milliseconds; readonly rangeEnd: Milliseconds; readonly sampleIndexOffset: number; - readonly samplesSelectedStates: null | SelectedState[]; + readonly sampleSelectedStates: null | Uint8Array; readonly treeOrderSampleComparator: ( a: IndexIntoSamplesTable, b: IndexIntoSamplesTable @@ -131,7 +130,7 @@ export class ActivityGraphCanvas extends React.PureComponent { rangeStart, rangeEnd, sampleIndexOffset, - samplesSelectedStates, + sampleSelectedStates, treeOrderSampleComparator, enableCPUUsage, width, @@ -153,7 +152,7 @@ export class ActivityGraphCanvas extends React.PureComponent { rangeStart, rangeEnd, sampleIndexOffset, - samplesSelectedStates, + sampleSelectedStates, enableCPUUsage, xPixelsPerMs: canvasPixelWidth / (rangeEnd - rangeStart), treeOrderSampleComparator, diff --git a/src/components/shared/thread/ActivityGraphFills.tsx b/src/components/shared/thread/ActivityGraphFills.tsx index d015189852..888c1cbdad 100644 --- a/src/components/shared/thread/ActivityGraphFills.tsx +++ b/src/components/shared/thread/ActivityGraphFills.tsx @@ -41,7 +41,7 @@ type RenderedComponentSettings = { readonly treeOrderSampleComparator: | ((a: IndexIntoSamplesTable, b: IndexIntoSamplesTable) => number) | null; - readonly samplesSelectedStates: null | Array; + readonly sampleSelectedStates: null | Uint8Array; readonly categoryDrawStyles: CategoryDrawStyles; }; @@ -338,21 +338,21 @@ export class ActivityGraphFillComputer { percentageBuffers: SelectedPercentageAtPixelBuffers, sampleIndex: IndexIntoSamplesTable ): Float32Array { - const { samplesSelectedStates } = this.renderedComponentSettings; - if (!samplesSelectedStates) { + const { sampleSelectedStates } = this.renderedComponentSettings; + if (!sampleSelectedStates) { return percentageBuffers.selectedPercentageAtPixel; } - switch (samplesSelectedStates[sampleIndex]) { - case SelectedState.FilteredOutByTransform: + switch (sampleSelectedStates[sampleIndex]) { + case SelectedState.FilteredOutByTransform as number: return percentageBuffers.filteredOutByTransformPercentageAtPixel; - case SelectedState.UnselectedOrderedBeforeSelected: + case SelectedState.UnselectedOrderedBeforeSelected as number: return percentageBuffers.beforeSelectedPercentageAtPixel; - case SelectedState.Selected: + case SelectedState.Selected as number: return percentageBuffers.selectedPercentageAtPixel; - case SelectedState.UnselectedOrderedAfterSelected: + case SelectedState.UnselectedOrderedAfterSelected as number: return percentageBuffers.afterSelectedPercentageAtPixel; default: - throw new Error('Unexpected samplesSelectedStates value'); + throw new Error('Unexpected sampleSelectedStates value'); } } } diff --git a/src/components/shared/thread/CPUGraph.tsx b/src/components/shared/thread/CPUGraph.tsx index e9af32b72c..ce20123116 100644 --- a/src/components/shared/thread/CPUGraph.tsx +++ b/src/components/shared/thread/CPUGraph.tsx @@ -11,14 +11,13 @@ import type { CategoryList, IndexIntoSamplesTable, Milliseconds, - SelectedState, } from 'firefox-profiler/types'; import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; type Props = { readonly className: string; readonly thread: Thread; - readonly samplesSelectedStates: null | SelectedState[]; + readonly sampleSelectedStates: null | Uint8Array; readonly interval: Milliseconds; readonly rangeStart: Milliseconds; readonly rangeEnd: Milliseconds; @@ -51,7 +50,7 @@ export class ThreadCPUGraph extends PureComponent { const { className, thread, - samplesSelectedStates, + sampleSelectedStates, interval, rangeStart, rangeEnd, @@ -71,7 +70,7 @@ export class ThreadCPUGraph extends PureComponent { trackName={trackName} interval={interval} thread={thread} - samplesSelectedStates={samplesSelectedStates} + sampleSelectedStates={sampleSelectedStates} rangeStart={rangeStart} rangeEnd={rangeEnd} categories={categories} diff --git a/src/components/shared/thread/HeightGraph.tsx b/src/components/shared/thread/HeightGraph.tsx index b46b51cc65..56b366ffdc 100644 --- a/src/components/shared/thread/HeightGraph.tsx +++ b/src/components/shared/thread/HeightGraph.tsx @@ -23,7 +23,7 @@ type Props = { readonly maxValue: number; readonly className: string; readonly thread: Thread; - readonly samplesSelectedStates: null | SelectedState[]; + readonly sampleSelectedStates: null | Uint8Array; readonly interval: Milliseconds; readonly rangeStart: Milliseconds; readonly rangeEnd: Milliseconds; @@ -64,7 +64,7 @@ export class ThreadHeightGraph extends PureComponent { drawCanvas(canvas: HTMLCanvasElement) { const { thread, - samplesSelectedStates, + sampleSelectedStates, interval, rangeStart, rangeEnd, @@ -137,8 +137,8 @@ export class ThreadHeightGraph extends PureComponent { const xPos = (sampleTime - range[0]) * xPixelsPerMs; let samplesBucket; if ( - samplesSelectedStates !== null && - samplesSelectedStates[i] === SelectedState.Selected + sampleSelectedStates !== null && + sampleSelectedStates[i] === (SelectedState.Selected as number) ) { samplesBucket = highlightedSamples; } else { diff --git a/src/components/shared/thread/SampleGraph.tsx b/src/components/shared/thread/SampleGraph.tsx index f271a2c13c..4b3b1dcf57 100644 --- a/src/components/shared/thread/SampleGraph.tsx +++ b/src/components/shared/thread/SampleGraph.tsx @@ -41,7 +41,7 @@ export type HoveredPixelState = { type Props = { readonly className: string; readonly thread: Thread; - readonly samplesSelectedStates: null | SelectedState[]; + readonly sampleSelectedStates: null | Uint8Array; readonly interval: Milliseconds; readonly rangeStart: Milliseconds; readonly rangeEnd: Milliseconds; @@ -66,7 +66,7 @@ type State = { type CanvasProps = { readonly className: string; readonly thread: Thread; - readonly samplesSelectedStates: null | SelectedState[]; + readonly sampleSelectedStates: null | Uint8Array; readonly interval: Milliseconds; readonly rangeStart: Milliseconds; readonly rangeEnd: Milliseconds; @@ -140,7 +140,7 @@ class ThreadSampleGraphCanvas extends React.PureComponent { interval, rangeStart, rangeEnd, - samplesSelectedStates, + sampleSelectedStates, categories, width, height, @@ -196,8 +196,8 @@ class ThreadSampleGraphCanvas extends React.PureComponent { (sampleTime - rangeStart) * xPixelsPerMs - drawnSampleWidth / 2; let samplesBucket; if ( - samplesSelectedStates !== null && - samplesSelectedStates[i] === SelectedState.Selected + sampleSelectedStates !== null && + sampleSelectedStates[i] === (SelectedState.Selected as number) ) { samplesBucket = highlightedSamples; } else { @@ -350,7 +350,7 @@ export class ThreadSampleGraphImpl extends PureComponent { interval, rangeStart, rangeEnd, - samplesSelectedStates, + sampleSelectedStates, width, height, zeroAt, @@ -372,7 +372,7 @@ export class ThreadSampleGraphImpl extends PureComponent { thread={thread} rangeStart={rangeStart} rangeEnd={rangeEnd} - samplesSelectedStates={samplesSelectedStates} + sampleSelectedStates={sampleSelectedStates} categories={categories} width={width} height={height} diff --git a/src/components/shared/thread/StackGraph.tsx b/src/components/shared/thread/StackGraph.tsx index 3a83052809..2e21c23dbf 100644 --- a/src/components/shared/thread/StackGraph.tsx +++ b/src/components/shared/thread/StackGraph.tsx @@ -11,14 +11,13 @@ import type { IndexIntoSamplesTable, Milliseconds, IndexIntoCallNodeTable, - SelectedState, } from 'firefox-profiler/types'; import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; type Props = { readonly className: string; readonly thread: Thread; - readonly samplesSelectedStates: null | SelectedState[]; + readonly sampleSelectedStates: null | Uint8Array; readonly sampleNonInvertedCallNodes: Array; readonly interval: Milliseconds; readonly rangeStart: Milliseconds; @@ -50,7 +49,7 @@ export class ThreadStackGraph extends PureComponent { const { className, thread, - samplesSelectedStates, + sampleSelectedStates, interval, rangeStart, rangeEnd, @@ -76,7 +75,7 @@ export class ThreadStackGraph extends PureComponent { trackName={trackName} interval={interval} thread={thread} - samplesSelectedStates={samplesSelectedStates} + sampleSelectedStates={sampleSelectedStates} rangeStart={rangeStart} rangeEnd={rangeEnd} categories={categories} diff --git a/src/components/timeline/TrackThread.tsx b/src/components/timeline/TrackThread.tsx index a7d2565a57..d27b04124e 100644 --- a/src/components/timeline/TrackThread.tsx +++ b/src/components/timeline/TrackThread.tsx @@ -53,7 +53,6 @@ import type { StartEndRange, ImplementationFilter, IndexIntoCallNodeTable, - SelectedState, State, ThreadsKey, } from 'firefox-profiler/types'; @@ -81,7 +80,7 @@ type StateProps = { readonly categories: CategoryList; readonly timelineType: TimelineType; readonly hasFileIoMarkers: boolean; - readonly samplesSelectedStates: null | SelectedState[]; + readonly sampleSelectedStates: null | Uint8Array; readonly sampleNonInvertedCallNodes: Array; readonly treeOrderSampleComparator: ( a: IndexIntoSamplesTable, @@ -183,7 +182,7 @@ class TimelineTrackThreadImpl extends PureComponent { hasFileIoMarkers, showMemoryMarkers, sampleNonInvertedCallNodes, - samplesSelectedStates, + sampleSelectedStates, treeOrderSampleComparator, trackType, trackName, @@ -256,7 +255,7 @@ class TimelineTrackThreadImpl extends PureComponent { sampleIndexOffset={sampleIndexOffset} onSampleClick={this._onSampleClick} categories={categories} - samplesSelectedStates={samplesSelectedStates} + sampleSelectedStates={sampleSelectedStates} treeOrderSampleComparator={treeOrderSampleComparator} enableCPUUsage={enableCPUUsage} implementationFilter={implementationFilter} @@ -272,7 +271,7 @@ class TimelineTrackThreadImpl extends PureComponent { thread={filteredThread} rangeStart={rangeStart} rangeEnd={rangeEnd} - samplesSelectedStates={samplesSelectedStates} + sampleSelectedStates={sampleSelectedStates} categories={categories} onSampleClick={this._onSampleClick} timelineType={timelineType} @@ -291,7 +290,7 @@ class TimelineTrackThreadImpl extends PureComponent { rangeStart={rangeStart} rangeEnd={rangeEnd} callNodeInfo={callNodeInfo} - samplesSelectedStates={samplesSelectedStates} + sampleSelectedStates={sampleSelectedStates} categories={categories} onSampleClick={this._onSampleClick} /> @@ -307,7 +306,7 @@ class TimelineTrackThreadImpl extends PureComponent { rangeEnd={rangeEnd} callNodeInfo={callNodeInfo} sampleNonInvertedCallNodes={sampleNonInvertedCallNodes} - samplesSelectedStates={samplesSelectedStates} + sampleSelectedStates={sampleSelectedStates} categories={categories} onSampleClick={this._onSampleClick} /> @@ -358,8 +357,8 @@ export const TimelineTrackThread = explicitConnect< timelineType, hasFileIoMarkers: selectors.getTimelineFileIoMarkerIndexes(state).length !== 0, - samplesSelectedStates: - selectors.getSamplesSelectedStatesInFilteredThread(state), + sampleSelectedStates: + selectors.getSampleSelectedStatesInFilteredThread(state), treeOrderSampleComparator: selectors.getTreeOrderComparatorInFilteredThread(state), selectedThreadIndexes, diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index ae6ffd29ef..b97200fbc2 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -902,13 +902,13 @@ export function getSampleIndexToCallNodeIndex( } /** - * This is an implementation of getSamplesSelectedStates for just the case where + * This is an implementation of getSampleSelectedStates for just the case where * no call node is selected. */ -function _getSamplesSelectedStatesForNoSelection( +function _getSampleSelectedStatesForNoSelection( sampleCallNodes: Array -): SelectedState[] { - const result = new Array(sampleCallNodes.length); +): Uint8Array { + const result = new Uint8Array(sampleCallNodes.length); for ( let sampleIndex = 0; sampleIndex < sampleCallNodes.length; @@ -978,16 +978,16 @@ function _getSamplesSelectedStatesForNoSelection( * In this example, the selected node has index 13 and the "selected index range" * is the range from 13 to 21 (not including 21). */ -function _getSamplesSelectedStatesNonInverted( +function _getSampleSelectedStatesNonInverted( sampleCallNodes: Array, selectedCallNodeIndex: IndexIntoCallNodeTable, callNodeInfo: CallNodeInfo -): SelectedState[] { +): Uint8Array { const callNodeTable = callNodeInfo.getCallNodeTable(); const selectedCallNodeDescendantsEndIndex = callNodeTable.subtreeRangeEnd[selectedCallNodeIndex]; const sampleCount = sampleCallNodes.length; - const samplesSelectedStates = new Array(sampleCount); + const sampleSelectedStates = new Uint8Array(sampleCount); for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) { let sampleSelectedState: SelectedState = SelectedState.Selected; const callNodeIndex = sampleCallNodes[sampleIndex]; @@ -1003,28 +1003,28 @@ function _getSamplesSelectedStatesNonInverted( // This sample was filtered out. sampleSelectedState = SelectedState.FilteredOutByTransform; } - samplesSelectedStates[sampleIndex] = sampleSelectedState; + sampleSelectedStates[sampleIndex] = sampleSelectedState; } - return samplesSelectedStates; + return sampleSelectedStates; } /** - * The implementation of getSamplesSelectedStates for the inverted tree. + * The implementation of getSampleSelectedStates for the inverted tree. * * This uses the suffix order, see the documentation of CallNodeInfoInverted. */ -function _getSamplesSelectedStatesInverted( +function _getSampleSelectedStatesInverted( sampleNonInvertedCallNodes: Array, selectedInvertedCallNodeIndex: IndexIntoCallNodeTable, callNodeInfo: CallNodeInfoInverted -): SelectedState[] { +): Uint8Array { const suffixOrderIndexes = callNodeInfo.getSuffixOrderIndexes(); const [selectedSubtreeRangeStart, selectedSubtreeRangeEnd] = callNodeInfo.getSuffixOrderIndexRangeForCallNode( selectedInvertedCallNodeIndex ); const sampleCount = sampleNonInvertedCallNodes.length; - const samplesSelectedStates = new Array(sampleCount); + const sampleSelectedStates = new Uint8Array(sampleCount); for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) { let sampleSelectedState: SelectedState = SelectedState.Selected; const callNodeIndex = sampleNonInvertedCallNodes[sampleIndex]; @@ -1039,9 +1039,9 @@ function _getSamplesSelectedStatesInverted( // This sample was filtered out. sampleSelectedState = SelectedState.FilteredOutByTransform; } - samplesSelectedStates[sampleIndex] = sampleSelectedState; + sampleSelectedStates[sampleIndex] = sampleSelectedState; } - return samplesSelectedStates; + return sampleSelectedStates; } /** @@ -1051,23 +1051,23 @@ function _getSamplesSelectedStatesInverted( * This is used in the activity graph. The "ordering" is used so that samples * from the same subtree (in the call tree) "clump together" in the graph. */ -export function getSamplesSelectedStates( +export function getSampleSelectedStates( callNodeInfo: CallNodeInfo, sampleNonInvertedCallNodes: Array, selectedCallNodeIndex: IndexIntoCallNodeTable | null -): SelectedState[] { +): Uint8Array { if (selectedCallNodeIndex === null || selectedCallNodeIndex === -1) { - return _getSamplesSelectedStatesForNoSelection(sampleNonInvertedCallNodes); + return _getSampleSelectedStatesForNoSelection(sampleNonInvertedCallNodes); } const callNodeInfoInverted = callNodeInfo.asInverted(); return callNodeInfoInverted !== null - ? _getSamplesSelectedStatesInverted( + ? _getSampleSelectedStatesInverted( sampleNonInvertedCallNodes, selectedCallNodeIndex, callNodeInfoInverted ) - : _getSamplesSelectedStatesNonInverted( + : _getSampleSelectedStatesNonInverted( sampleNonInvertedCallNodes, selectedCallNodeIndex, callNodeInfo diff --git a/src/selectors/per-thread/stack-sample.ts b/src/selectors/per-thread/stack-sample.ts index 81d87d2433..207d2e89ac 100644 --- a/src/selectors/per-thread/stack-sample.ts +++ b/src/selectors/per-thread/stack-sample.ts @@ -34,7 +34,6 @@ import type { AddressTimings, IndexIntoCallNodeTable, IndexIntoNativeSymbolTable, - SelectedState, StartEndRange, Selector, ThreadsKey, @@ -269,20 +268,23 @@ export function getStackAndSampleSelectorsPerThread( ProfileData.getSampleIndexToCallNodeIndex ); - const getSamplesSelectedStatesInFilteredThread: Selector< - null | SelectedState[] - > = createSelector( - getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, - getCallNodeInfo, - getSelectedCallNodeIndex, - (sampleIndexToNonInvertedCallNodeIndex, callNodeInfo, selectedCallNode) => { - return ProfileData.getSamplesSelectedStates( - callNodeInfo, + const getSampleSelectedStatesInFilteredThread: Selector = + createSelector( + getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, + getCallNodeInfo, + getSelectedCallNodeIndex, + ( sampleIndexToNonInvertedCallNodeIndex, + callNodeInfo, selectedCallNode - ); - } - ); + ) => { + return ProfileData.getSampleSelectedStates( + callNodeInfo, + sampleIndexToNonInvertedCallNodeIndex, + selectedCallNode + ); + } + ); const getTreeOrderComparatorInFilteredThread: Selector< ( @@ -502,7 +504,7 @@ export function getStackAndSampleSelectorsPerThread( getExpandedCallNodePaths, getExpandedCallNodeIndexes, getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, - getSamplesSelectedStatesInFilteredThread, + getSampleSelectedStatesInFilteredThread, getTreeOrderComparatorInFilteredThread, getCallTree, getFunctionListTree, diff --git a/src/test/unit/profile-data.test.ts b/src/test/unit/profile-data.test.ts index b642504930..9e074fe2e0 100644 --- a/src/test/unit/profile-data.test.ts +++ b/src/test/unit/profile-data.test.ts @@ -18,7 +18,7 @@ import { getSampleIndexClosestToStartTime, getSampleIndexToCallNodeIndex, getTreeOrderComparator, - getSamplesSelectedStates, + getSampleSelectedStates, extractProfileFilterPageData, findAddressProofForFile, calculateFunctionSizeLowerBound, @@ -999,7 +999,7 @@ describe('funcHasDirectRecursiveCall and funcHasRecursiveCall', function () { }); }); -describe('getSamplesSelectedStates', function () { +describe('getSampleSelectedStates', function () { function setup(textSamples: string) { const { derivedThreads, @@ -1049,7 +1049,7 @@ describe('getSamplesSelectedStates', function () { it('determines the selection status of all the samples', function () { expect( - getSamplesSelectedStates(callNodeInfo, sampleCallNodes, A_B) + Array.from(getSampleSelectedStates(callNodeInfo, sampleCallNodes, A_B)) ).toEqual([ SelectedState.Selected, SelectedState.UnselectedOrderedAfterSelected, @@ -1058,7 +1058,7 @@ describe('getSamplesSelectedStates', function () { SelectedState.UnselectedOrderedAfterSelected, ]); expect( - getSamplesSelectedStates(callNodeInfo, sampleCallNodes, A_D) + Array.from(getSampleSelectedStates(callNodeInfo, sampleCallNodes, A_D)) ).toEqual([ SelectedState.UnselectedOrderedBeforeSelected, SelectedState.Selected, @@ -1067,7 +1067,9 @@ describe('getSamplesSelectedStates', function () { SelectedState.Selected, ]); expect( - getSamplesSelectedStates(callNodeInfo, sampleCallNodes, A_B_F) + Array.from( + getSampleSelectedStates(callNodeInfo, sampleCallNodes, A_B_F) + ) ).toEqual([ SelectedState.UnselectedOrderedBeforeSelected, SelectedState.UnselectedOrderedAfterSelected, @@ -1076,7 +1078,9 @@ describe('getSamplesSelectedStates', function () { SelectedState.UnselectedOrderedAfterSelected, ]); expect( - getSamplesSelectedStates(callNodeInfo, sampleCallNodes, A_D_E) + Array.from( + getSampleSelectedStates(callNodeInfo, sampleCallNodes, A_D_E) + ) ).toEqual([ SelectedState.UnselectedOrderedBeforeSelected, SelectedState.Selected, @@ -1145,7 +1149,9 @@ describe('getSamplesSelectedStates', function () { // Test B <- A <- ... // Only samples 2 and 6 have stacks ending in ... -> A -> B expect( - getSamplesSelectedStates(callNodeInfoInverted, sampleCallNodes, inBA) + Array.from( + getSampleSelectedStates(callNodeInfoInverted, sampleCallNodes, inBA) + ) ).toEqual([ SelectedState.UnselectedOrderedBeforeSelected, SelectedState.UnselectedOrderedBeforeSelected, @@ -1158,7 +1164,9 @@ describe('getSamplesSelectedStates', function () { // Test C <- B <- A <- ... // Only sample 5 has a stack ending in ... -> A -> B -> C expect( - getSamplesSelectedStates(callNodeInfoInverted, sampleCallNodes, inCBA) + Array.from( + getSampleSelectedStates(callNodeInfoInverted, sampleCallNodes, inCBA) + ) ).toEqual([ SelectedState.UnselectedOrderedBeforeSelected, SelectedState.UnselectedOrderedBeforeSelected, @@ -1171,7 +1179,9 @@ describe('getSamplesSelectedStates', function () { // Test B <- ... // Only samples 2 and 6 have stacks ending in ... -> B expect( - getSamplesSelectedStates(callNodeInfoInverted, sampleCallNodes, inB) + Array.from( + getSampleSelectedStates(callNodeInfoInverted, sampleCallNodes, inB) + ) ).toEqual([ SelectedState.UnselectedOrderedBeforeSelected, SelectedState.UnselectedOrderedBeforeSelected, From 5437d83243736ded4df5d78df14999c1d27bbd6e Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sun, 8 Feb 2026 13:13:48 -0500 Subject: [PATCH 2/4] Remove unnecessary null check. --- src/components/shared/thread/ActivityGraph.tsx | 2 +- src/components/shared/thread/ActivityGraphCanvas.tsx | 2 +- src/components/shared/thread/ActivityGraphFills.tsx | 5 +---- src/components/timeline/TrackThread.tsx | 2 +- src/selectors/per-thread/stack-sample.ts | 2 +- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/shared/thread/ActivityGraph.tsx b/src/components/shared/thread/ActivityGraph.tsx index d089e34617..60dcefc65b 100644 --- a/src/components/shared/thread/ActivityGraph.tsx +++ b/src/components/shared/thread/ActivityGraph.tsx @@ -44,7 +44,7 @@ export type Props = { sampleIndex: IndexIntoSamplesTable | null ) => void; readonly categories: CategoryList; - readonly sampleSelectedStates: null | Uint8Array; + readonly sampleSelectedStates: Uint8Array; readonly treeOrderSampleComparator: ( a: IndexIntoSamplesTable, b: IndexIntoSamplesTable diff --git a/src/components/shared/thread/ActivityGraphCanvas.tsx b/src/components/shared/thread/ActivityGraphCanvas.tsx index 83472a9bf2..640482ce8e 100644 --- a/src/components/shared/thread/ActivityGraphCanvas.tsx +++ b/src/components/shared/thread/ActivityGraphCanvas.tsx @@ -29,7 +29,7 @@ type CanvasProps = { readonly rangeStart: Milliseconds; readonly rangeEnd: Milliseconds; readonly sampleIndexOffset: number; - readonly sampleSelectedStates: null | Uint8Array; + readonly sampleSelectedStates: Uint8Array; readonly treeOrderSampleComparator: ( a: IndexIntoSamplesTable, b: IndexIntoSamplesTable diff --git a/src/components/shared/thread/ActivityGraphFills.tsx b/src/components/shared/thread/ActivityGraphFills.tsx index 888c1cbdad..cbc6804fd1 100644 --- a/src/components/shared/thread/ActivityGraphFills.tsx +++ b/src/components/shared/thread/ActivityGraphFills.tsx @@ -41,7 +41,7 @@ type RenderedComponentSettings = { readonly treeOrderSampleComparator: | ((a: IndexIntoSamplesTable, b: IndexIntoSamplesTable) => number) | null; - readonly sampleSelectedStates: null | Uint8Array; + readonly sampleSelectedStates: Uint8Array; readonly categoryDrawStyles: CategoryDrawStyles; }; @@ -339,9 +339,6 @@ export class ActivityGraphFillComputer { sampleIndex: IndexIntoSamplesTable ): Float32Array { const { sampleSelectedStates } = this.renderedComponentSettings; - if (!sampleSelectedStates) { - return percentageBuffers.selectedPercentageAtPixel; - } switch (sampleSelectedStates[sampleIndex]) { case SelectedState.FilteredOutByTransform as number: return percentageBuffers.filteredOutByTransformPercentageAtPixel; diff --git a/src/components/timeline/TrackThread.tsx b/src/components/timeline/TrackThread.tsx index d27b04124e..8ee87d03d1 100644 --- a/src/components/timeline/TrackThread.tsx +++ b/src/components/timeline/TrackThread.tsx @@ -80,7 +80,7 @@ type StateProps = { readonly categories: CategoryList; readonly timelineType: TimelineType; readonly hasFileIoMarkers: boolean; - readonly sampleSelectedStates: null | Uint8Array; + readonly sampleSelectedStates: Uint8Array; readonly sampleNonInvertedCallNodes: Array; readonly treeOrderSampleComparator: ( a: IndexIntoSamplesTable, diff --git a/src/selectors/per-thread/stack-sample.ts b/src/selectors/per-thread/stack-sample.ts index 207d2e89ac..11f1a282a5 100644 --- a/src/selectors/per-thread/stack-sample.ts +++ b/src/selectors/per-thread/stack-sample.ts @@ -268,7 +268,7 @@ export function getStackAndSampleSelectorsPerThread( ProfileData.getSampleIndexToCallNodeIndex ); - const getSampleSelectedStatesInFilteredThread: Selector = + const getSampleSelectedStatesInFilteredThread: Selector = createSelector( getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, getCallNodeInfo, From 4caf66fdd97e878407f23787a92bd9cedebc5581 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sun, 8 Feb 2026 13:09:50 -0500 Subject: [PATCH 3/4] Use direct access by index to pick the right percentage buffer. --- .../shared/thread/ActivityGraphFills.tsx | 87 ++++++------------- 1 file changed, 27 insertions(+), 60 deletions(-) diff --git a/src/components/shared/thread/ActivityGraphFills.tsx b/src/components/shared/thread/ActivityGraphFills.tsx index cbc6804fd1..37a90c53b3 100644 --- a/src/components/shared/thread/ActivityGraphFills.tsx +++ b/src/components/shared/thread/ActivityGraphFills.tsx @@ -75,20 +75,17 @@ export type CategoryDrawStyles = ReadonlyArray<{ readonly filteredOutByTransformFillStyle: CanvasPattern | string; }>; -type SelectedPercentageAtPixelBuffers = { - // These Float32Arrays are mutated in place during the computation step. - readonly beforeSelectedPercentageAtPixel: Float32Array; - readonly selectedPercentageAtPixel: Float32Array; - readonly afterSelectedPercentageAtPixel: Float32Array; - readonly filteredOutByTransformPercentageAtPixel: Float32Array; - readonly filteredOutByTabPercentageAtPixel: Float32Array; -}; +// These Float32Arrays are mutated in place during the computation step. +// buffers[selectedState] is the buffer for the given SelectedState. +type SelectedPercentageAtPixelBuffers = Float32Array[]; export type CpuRatioInTimeRange = { readonly cpuRatio: number; readonly timeRange: Milliseconds; }; +const SELECTED_STATE_BUFFER_COUNT = 4; + const BOX_BLUR_RADII = [3, 2, 2]; const SMOOTHING_RADIUS = 3 + 2 + 2; const SMOOTHING_KERNEL: Float32Array = _getSmoothingKernel( @@ -312,12 +309,10 @@ export class ActivityGraphFillComputer { beforeSampleCpuRatio: number, afterSampleCpuRatio: number ) { - const { rangeStart } = this.renderedComponentSettings; + const { rangeStart, sampleSelectedStates } = this.renderedComponentSettings; const percentageBuffers = this.mutablePercentageBuffers[category]; - const percentageBuffer = this._pickPercentageBuffer( - percentageBuffers, - sampleIndex - ); + const selectedState = sampleSelectedStates[sampleIndex]; + const percentageBuffer = percentageBuffers[selectedState]; _accumulateInBuffer( percentageBuffer, @@ -330,28 +325,6 @@ export class ActivityGraphFillComputer { rangeStart ); } - - /** - * Pick the correct percentage buffer based on the sample state. - */ - _pickPercentageBuffer( - percentageBuffers: SelectedPercentageAtPixelBuffers, - sampleIndex: IndexIntoSamplesTable - ): Float32Array { - const { sampleSelectedStates } = this.renderedComponentSettings; - switch (sampleSelectedStates[sampleIndex]) { - case SelectedState.FilteredOutByTransform as number: - return percentageBuffers.filteredOutByTransformPercentageAtPixel; - case SelectedState.UnselectedOrderedBeforeSelected as number: - return percentageBuffers.beforeSelectedPercentageAtPixel; - case SelectedState.Selected as number: - return percentageBuffers.selectedPercentageAtPixel; - case SelectedState.UnselectedOrderedAfterSelected as number: - return percentageBuffers.afterSelectedPercentageAtPixel; - default: - throw new Error('Unexpected sampleSelectedStates value'); - } - } } /** @@ -736,15 +709,13 @@ function _createSelectedPercentageAtPixelBuffers({ categoryDrawStyles: CategoryDrawStyles; canvasPixelWidth: number; }): SelectedPercentageAtPixelBuffers[] { - return categoryDrawStyles.map(() => ({ - beforeSelectedPercentageAtPixel: new Float32Array(canvasPixelWidth), - selectedPercentageAtPixel: new Float32Array(canvasPixelWidth), - afterSelectedPercentageAtPixel: new Float32Array(canvasPixelWidth), - filteredOutByTransformPercentageAtPixel: new Float32Array(canvasPixelWidth), - // Unlike other fields, we do not mutate that array and we keep that zero - // array to indicate that we don't want to draw anything for this case. - filteredOutByTabPercentageAtPixel: new Float32Array(canvasPixelWidth), - })); + return categoryDrawStyles.map(() => { + const percentageBuffers = []; + for (let i = 0; i < SELECTED_STATE_BUFFER_COUNT; i++) { + percentageBuffers[i] = new Float32Array(canvasPixelWidth); + } + return percentageBuffers; + }); } /** @@ -771,39 +742,35 @@ function _getCategoryFills( (categoryIndex) => { const categoryDrawStyle = categoryDrawStyles[categoryIndex]; const buffer = percentageBuffers[categoryIndex]; + const canvasPixelWidth = + buffer[SelectedState.UnselectedOrderedBeforeSelected].length; // For every category we draw four fills, for the four selection kinds: return [ { category: categoryDrawStyle.category, fillStyle: categoryDrawStyle.getUnselectedFillStyle(), - perPixelContribution: buffer.beforeSelectedPercentageAtPixel, - accumulatedUpperEdge: new Float32Array( - buffer.beforeSelectedPercentageAtPixel.length - ), + perPixelContribution: + buffer[SelectedState.UnselectedOrderedBeforeSelected], + accumulatedUpperEdge: new Float32Array(canvasPixelWidth), }, { category: categoryDrawStyle.category, fillStyle: categoryDrawStyle.getSelectedFillStyle(), - perPixelContribution: buffer.selectedPercentageAtPixel, - accumulatedUpperEdge: new Float32Array( - buffer.beforeSelectedPercentageAtPixel.length - ), + perPixelContribution: buffer[SelectedState.Selected], + accumulatedUpperEdge: new Float32Array(canvasPixelWidth), }, { category: categoryDrawStyle.category, fillStyle: categoryDrawStyle.getUnselectedFillStyle(), - perPixelContribution: buffer.afterSelectedPercentageAtPixel, - accumulatedUpperEdge: new Float32Array( - buffer.beforeSelectedPercentageAtPixel.length - ), + perPixelContribution: + buffer[SelectedState.UnselectedOrderedAfterSelected], + accumulatedUpperEdge: new Float32Array(canvasPixelWidth), }, { category: categoryDrawStyle.category, fillStyle: categoryDrawStyle.filteredOutByTransformFillStyle, - perPixelContribution: buffer.filteredOutByTransformPercentageAtPixel, - accumulatedUpperEdge: new Float32Array( - buffer.beforeSelectedPercentageAtPixel.length - ), + perPixelContribution: buffer[SelectedState.FilteredOutByTransform], + accumulatedUpperEdge: new Float32Array(canvasPixelWidth), }, ]; } From f90c13d0fcd84928bb8c70d37782f3de88e5a67f Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sun, 8 Feb 2026 13:47:05 -0500 Subject: [PATCH 4/4] Inline-away _accumulateInCategory. --- .../shared/thread/ActivityGraphFills.tsx | 45 ++++++------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/src/components/shared/thread/ActivityGraphFills.tsx b/src/components/shared/thread/ActivityGraphFills.tsx index 37a90c53b3..c31f817795 100644 --- a/src/components/shared/thread/ActivityGraphFills.tsx +++ b/src/components/shared/thread/ActivityGraphFills.tsx @@ -215,6 +215,8 @@ export class ActivityGraphFillComputer { interval, enableCPUUsage, sampleIndexOffset, + rangeStart, + sampleSelectedStates, } = this.renderedComponentSettings; if (samples.length === 0) { @@ -246,15 +248,19 @@ export class ActivityGraphFillComputer { afterSampleCpuRatio = threadCPURatio[i + 1]; } - // Mutate the percentage buffers. - this._accumulateInCategory( - category, - i, + const percentageBuffers = this.mutablePercentageBuffers[category]; + const selectedState = sampleSelectedStates[i]; + const percentageBuffer = percentageBuffers[selectedState]; + + _accumulateInBuffer( + percentageBuffer, + this.renderedComponentSettings, prevSampleTime, sampleTime, nextSampleTime, beforeSampleCpuRatio, - afterSampleCpuRatio + afterSampleCpuRatio, + rangeStart ); prevSampleTime = sampleTime; @@ -285,33 +291,10 @@ export class ActivityGraphFillComputer { } } - this._accumulateInCategory( - lastSampleCategory, - samples.length - 1, - prevSampleTime, - sampleTime, - sampleTime + interval, - beforeSampleCpuRatio, - afterSampleCpuRatio - ); - } + const nextSampleTime = sampleTime + interval; + const percentageBuffers = this.mutablePercentageBuffers[lastSampleCategory]; - /** - * Mutate the percentage buffers, by taking this category, and accumulating its - * percentage into the buffer. - */ - _accumulateInCategory( - category: IndexIntoCategoryList, - sampleIndex: IndexIntoSamplesTable, - prevSampleTime: Milliseconds, - sampleTime: Milliseconds, - nextSampleTime: Milliseconds, - beforeSampleCpuRatio: number, - afterSampleCpuRatio: number - ) { - const { rangeStart, sampleSelectedStates } = this.renderedComponentSettings; - const percentageBuffers = this.mutablePercentageBuffers[category]; - const selectedState = sampleSelectedStates[sampleIndex]; + const selectedState = sampleSelectedStates[lastIdx]; const percentageBuffer = percentageBuffers[selectedState]; _accumulateInBuffer(