Skip to content

Commit 2bff313

Browse files
committed
Deduplicate the frameTable during compaction.
1 parent 98b7f57 commit 2bff313

3 files changed

Lines changed: 641 additions & 1335 deletions

File tree

src/profile-logic/profile-compacting.ts

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
import { 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

814
import type {
915
Profile,
@@ -18,6 +24,7 @@ import type {
1824
NativeSymbolTable,
1925
Lib,
2026
SourceTable,
27+
IndexIntoFrameTable,
2128
} from 'firefox-profiler/types';
2229
import {
2330
assertExhaustiveCheck,
@@ -78,13 +85,31 @@ const ColDesc = {
7885
class 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+
375520
function _gatherReferencesInThread(
376521
thread: RawThread,
377522
tcs: TableCompactionStates,

0 commit comments

Comments
 (0)