33 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44
55import { computeStringIndexMarkerFieldsByDataType } from './marker-schema' ;
6- import { type BitSet , makeBitSet , setBit , checkBit } from '../utils/bitset' ;
6+ import {
7+ type BitSet ,
8+ makeBitSet ,
9+ setBit ,
10+ clearBit ,
11+ checkBit ,
12+ } from '../utils/bitset' ;
713
814import type {
915 Profile ,
@@ -18,6 +24,7 @@ import type {
1824 NativeSymbolTable ,
1925 Lib ,
2026 SourceTable ,
27+ IndexIntoFrameTable ,
2128} from 'firefox-profiler/types' ;
2229import {
2330 assertExhaustiveCheck ,
@@ -78,13 +85,25 @@ const ColDesc = {
7885class TableCompactionState {
7986 markBuffer : BitSet ;
8087 oldIndexToNewIndexPlusOne : Int32Array ;
88+ oldIndexToCanonicalOldIndexPlusOne : Int32Array ;
89+ hasCanonicalRedirects : boolean = false ; // whether oldIndexToCanonicalOldIndexPlusOne has any non-zero values
8190 newLength : number | null = null ;
8291
8392 constructor ( itemCount : number ) {
8493 this . markBuffer = makeBitSet ( itemCount ) ;
94+ this . oldIndexToCanonicalOldIndexPlusOne = new Int32Array ( itemCount ) ;
8595 this . oldIndexToNewIndexPlusOne = new Int32Array ( itemCount ) ;
8696 }
8797
98+ redirectOldIndexToCanonicalOldIndex (
99+ redirected : number ,
100+ canonical : number
101+ ) : void {
102+ clearBit ( this . markBuffer , redirected ) ;
103+ this . oldIndexToCanonicalOldIndexPlusOne [ redirected ] = canonical + 1 ;
104+ this . hasCanonicalRedirects = true ;
105+ }
106+
88107 computeIndexTranslation ( ) : void {
89108 let newLength = 0 ;
90109 for ( let i = 0 ; i < this . oldIndexToNewIndexPlusOne . length ; i ++ ) {
@@ -94,6 +113,20 @@ class TableCompactionState {
94113 }
95114 }
96115 this . newLength = newLength ;
116+
117+ if ( this . hasCanonicalRedirects ) {
118+ // Patch redirected (deduped-away) rows so any reference to them
119+ // resolves to their canonical row's new index. For tables that didn't
120+ // dedup, oldIndexToCanonicalOldIndexPlusOne is all zeros and this loop is a no-op.
121+ for ( let i = 0 ; i < this . oldIndexToCanonicalOldIndexPlusOne . length ; i ++ ) {
122+ const canonicalOldIndex =
123+ this . oldIndexToCanonicalOldIndexPlusOne [ i ] - 1 ;
124+ if ( canonicalOldIndex !== - 1 ) {
125+ this . oldIndexToNewIndexPlusOne [ i ] =
126+ this . oldIndexToNewIndexPlusOne [ canonicalOldIndex ] ;
127+ }
128+ }
129+ }
97130 }
98131}
99132
@@ -208,6 +241,7 @@ export function computeCompactedProfile(
208241 tcs . nativeSymbols . computeIndexTranslation ( ) ;
209242 tcs . resourceTable . computeIndexTranslation ( ) ;
210243 tcs . funcTable . computeIndexTranslation ( ) ;
244+ _dedupFrameTable ( shared . frameTable , tcs . frameTable ) ;
211245 tcs . frameTable . computeIndexTranslation ( ) ;
212246 tcs . stackTable . computeIndexTranslation ( ) ;
213247
@@ -372,6 +406,111 @@ function markColumnWithNegOneableFields(
372406 }
373407}
374408
409+ // Collapse identical rows in the frame table. Two rows are identical if every
410+ // column has the same value. Duplicates often arise during profile processing
411+ // because Firefox's gecko profile has a per-thread frame table, and
412+ // process-profile.ts merges those into a single frame table without
413+ // deduplicating (so that profile loading stays fast). Compacting runs in
414+ // contexts where small profile size matters more than latency, so we dedupe
415+ // here.
416+ //
417+ // We sort an array of marked frame indices using a comparator that walks the
418+ // frame columns directly (no per-frame object is constructed). After sorting,
419+ // duplicates are adjacent and a single linear pass picks one canonical frame
420+ // per group, redirects the others to it, and clears their bits in markBuffer
421+ // so they get skipped during the compact phase.
422+ function _dedupFrameTable (
423+ frameTable : FrameTable ,
424+ state : TableCompactionState
425+ ) : void {
426+ const markedFrames = new Array < IndexIntoFrameTable > ( ) ;
427+ const { markBuffer } = state ;
428+ for ( let i = 0 ; i < frameTable . length ; i ++ ) {
429+ if ( checkBit ( markBuffer , i ) ) {
430+ markedFrames . push ( i ) ;
431+ }
432+ }
433+
434+ if ( markedFrames . length === 0 ) {
435+ return ;
436+ }
437+
438+ // Sort, so that we can deduplicate without creating hash strings.
439+ markedFrames . sort ( ( a , b ) => _compareFrames ( frameTable , a , b ) ) ;
440+
441+ // Walk the sorted list. If we find matching subsequent frames,
442+ // redirect the later frames to the first matching frame.
443+ let prevFrame = markedFrames [ 0 ] ;
444+ for ( let i = 1 ; i < markedFrames . length ; i ++ ) {
445+ const frameIndex = markedFrames [ i ] ;
446+ if ( _compareFrames ( frameTable , frameIndex , prevFrame ) === 0 ) {
447+ state . redirectOldIndexToCanonicalOldIndex ( frameIndex , prevFrame ) ;
448+ continue ;
449+ }
450+ prevFrame = frameIndex ;
451+ }
452+ }
453+
454+ function _compareFrames ( frameTable : FrameTable , a : number , b : number ) : number {
455+ let d ;
456+ const funcCol = frameTable . func ;
457+ d = funcCol [ a ] - funcCol [ b ] ;
458+ if ( d !== 0 ) {
459+ return d ;
460+ }
461+ const addressCol = frameTable . address ;
462+ d = addressCol [ a ] - addressCol [ b ] ;
463+ if ( d !== 0 ) {
464+ return d ;
465+ }
466+ const inlineDepthCol = frameTable . inlineDepth ;
467+ d = inlineDepthCol [ a ] - inlineDepthCol [ b ] ;
468+ if ( d !== 0 ) {
469+ return d ;
470+ }
471+ const categoryCol = frameTable . category ;
472+ d = _compareNullableNumber ( categoryCol [ a ] , categoryCol [ b ] ) ;
473+ if ( d !== 0 ) {
474+ return d ;
475+ }
476+ const subcategoryCol = frameTable . subcategory ;
477+ d = _compareNullableNumber ( subcategoryCol [ a ] , subcategoryCol [ b ] ) ;
478+ if ( d !== 0 ) {
479+ return d ;
480+ }
481+ const nativeSymbolCol = frameTable . nativeSymbol ;
482+ d = _compareNullableNumber ( nativeSymbolCol [ a ] , nativeSymbolCol [ b ] ) ;
483+ if ( d !== 0 ) {
484+ return d ;
485+ }
486+ const innerWindowIDCol = frameTable . innerWindowID ;
487+ d = _compareNullableNumber ( innerWindowIDCol [ a ] , innerWindowIDCol [ b ] ) ;
488+ if ( d !== 0 ) {
489+ return d ;
490+ }
491+ const lineCol = frameTable . line ;
492+ d = _compareNullableNumber ( lineCol [ a ] , lineCol [ b ] ) ;
493+ if ( d !== 0 ) {
494+ return d ;
495+ }
496+ const columnCol = frameTable . column ;
497+ d = _compareNullableNumber ( columnCol [ a ] , columnCol [ b ] ) ;
498+ if ( d !== 0 ) {
499+ return d ;
500+ }
501+ return 0 ;
502+ }
503+
504+ function _compareNullableNumber ( a : number | null , b : number | null ) : number {
505+ if ( a === null ) {
506+ return b === null ? 0 : - 1 ;
507+ }
508+ if ( b === null ) {
509+ return 1 ;
510+ }
511+ return a - b ;
512+ }
513+
375514function _gatherReferencesInThread (
376515 thread : RawThread ,
377516 tcs : TableCompactionStates ,
0 commit comments