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,31 @@ 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+
107+ _ensureRedirectBuffer ( ) : Int32Array {
108+ return ( this . oldIndexToCanonicalOldIndexPlusOne ??= new Int32Array (
109+ this . oldIndexToNewIndexPlusOne . length
110+ ) ) ;
111+ }
112+
88113 computeIndexTranslation ( ) : void {
89114 let newLength = 0 ;
90115 for ( let i = 0 ; i < this . oldIndexToNewIndexPlusOne . length ; i ++ ) {
@@ -94,6 +119,20 @@ class TableCompactionState {
94119 }
95120 }
96121 this . newLength = newLength ;
122+
123+ if ( this . hasCanonicalRedirects ) {
124+ // Patch redirected (deduped-away) rows so any reference to them
125+ // resolves to their canonical row's new index. For tables that didn't
126+ // dedup, oldIndexToCanonicalOldIndexPlusOne is all zeros and this loop is a no-op.
127+ for ( let i = 0 ; i < this . oldIndexToCanonicalOldIndexPlusOne . length ; i ++ ) {
128+ const canonicalOldIndex =
129+ this . oldIndexToCanonicalOldIndexPlusOne [ i ] - 1 ;
130+ if ( canonicalOldIndex !== - 1 ) {
131+ this . oldIndexToNewIndexPlusOne [ i ] =
132+ this . oldIndexToNewIndexPlusOne [ canonicalOldIndex ] ;
133+ }
134+ }
135+ }
97136 }
98137}
99138
@@ -208,6 +247,7 @@ export function computeCompactedProfile(
208247 tcs . nativeSymbols . computeIndexTranslation ( ) ;
209248 tcs . resourceTable . computeIndexTranslation ( ) ;
210249 tcs . funcTable . computeIndexTranslation ( ) ;
250+ _dedupFrameTable ( shared . frameTable , tcs . frameTable ) ;
211251 tcs . frameTable . computeIndexTranslation ( ) ;
212252 tcs . stackTable . computeIndexTranslation ( ) ;
213253
@@ -372,6 +412,111 @@ function markColumnWithNegOneableFields(
372412 }
373413}
374414
415+ // Collapse identical rows in the frame table. Two rows are identical if every
416+ // column has the same value. Duplicates often arise during profile processing
417+ // because Firefox's gecko profile has a per-thread frame table, and
418+ // process-profile.ts merges those into a single frame table without
419+ // deduplicating (so that profile loading stays fast). Compacting runs in
420+ // contexts where small profile size matters more than latency, so we dedupe
421+ // here.
422+ //
423+ // We sort an array of marked frame indices using a comparator that walks the
424+ // frame columns directly (no per-frame object is constructed). After sorting,
425+ // duplicates are adjacent and a single linear pass picks one canonical frame
426+ // per group, redirects the others to it, and clears their bits in markBuffer
427+ // so they get skipped during the compact phase.
428+ function _dedupFrameTable (
429+ frameTable : FrameTable ,
430+ state : TableCompactionState
431+ ) : void {
432+ const markedFrames = new Array < IndexIntoFrameTable > ( ) ;
433+ const { markBuffer } = state ;
434+ for ( let i = 0 ; i < frameTable . length ; i ++ ) {
435+ if ( checkBit ( markBuffer , i ) ) {
436+ markedFrames . push ( i ) ;
437+ }
438+ }
439+
440+ if ( markedFrames . length === 0 ) {
441+ return ;
442+ }
443+
444+ // Sort, so that we can deduplicate without creating hash strings.
445+ markedFrames . sort ( ( a , b ) => _compareFrames ( frameTable , a , b ) ) ;
446+
447+ // Walk the sorted list. If we find matching subsequent frames,
448+ // redirect the later frames to the first matching frame.
449+ let prevFrame = markedFrames [ 0 ] ;
450+ for ( let i = 1 ; i < markedFrames . length ; i ++ ) {
451+ const frameIndex = markedFrames [ i ] ;
452+ if ( _compareFrames ( frameTable , frameIndex , prevFrame ) === 0 ) {
453+ state . redirectOldIndexToCanonicalOldIndex ( frameIndex , prevFrame ) ;
454+ continue ;
455+ }
456+ prevFrame = frameIndex ;
457+ }
458+ }
459+
460+ function _compareFrames ( frameTable : FrameTable , a : number , b : number ) : number {
461+ let d ;
462+ const funcCol = frameTable . func ;
463+ d = funcCol [ a ] - funcCol [ b ] ;
464+ if ( d !== 0 ) {
465+ return d ;
466+ }
467+ const addressCol = frameTable . address ;
468+ d = addressCol [ a ] - addressCol [ b ] ;
469+ if ( d !== 0 ) {
470+ return d ;
471+ }
472+ const inlineDepthCol = frameTable . inlineDepth ;
473+ d = inlineDepthCol [ a ] - inlineDepthCol [ b ] ;
474+ if ( d !== 0 ) {
475+ return d ;
476+ }
477+ const categoryCol = frameTable . category ;
478+ d = _compareNullableNumber ( categoryCol [ a ] , categoryCol [ b ] ) ;
479+ if ( d !== 0 ) {
480+ return d ;
481+ }
482+ const subcategoryCol = frameTable . subcategory ;
483+ d = _compareNullableNumber ( subcategoryCol [ a ] , subcategoryCol [ b ] ) ;
484+ if ( d !== 0 ) {
485+ return d ;
486+ }
487+ const nativeSymbolCol = frameTable . nativeSymbol ;
488+ d = _compareNullableNumber ( nativeSymbolCol [ a ] , nativeSymbolCol [ b ] ) ;
489+ if ( d !== 0 ) {
490+ return d ;
491+ }
492+ const innerWindowIDCol = frameTable . innerWindowID ;
493+ d = _compareNullableNumber ( innerWindowIDCol [ a ] , innerWindowIDCol [ b ] ) ;
494+ if ( d !== 0 ) {
495+ return d ;
496+ }
497+ const lineCol = frameTable . line ;
498+ d = _compareNullableNumber ( lineCol [ a ] , lineCol [ b ] ) ;
499+ if ( d !== 0 ) {
500+ return d ;
501+ }
502+ const columnCol = frameTable . column ;
503+ d = _compareNullableNumber ( columnCol [ a ] , columnCol [ b ] ) ;
504+ if ( d !== 0 ) {
505+ return d ;
506+ }
507+ return 0 ;
508+ }
509+
510+ function _compareNullableNumber ( a : number | null , b : number | null ) : number {
511+ if ( a === null ) {
512+ return b === null ? 0 : - 1 ;
513+ }
514+ if ( b === null ) {
515+ return 1 ;
516+ }
517+ return a - b ;
518+ }
519+
375520function _gatherReferencesInThread (
376521 thread : RawThread ,
377522 tcs : TableCompactionStates ,
0 commit comments