Skip to content

Commit 68942f5

Browse files
committed
Deduplicate the frameTable during compaction.
1 parent 98b7f57 commit 68942f5

3 files changed

Lines changed: 585 additions & 1335 deletions

File tree

src/profile-logic/profile-compacting.ts

Lines changed: 140 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,25 @@ 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+
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+
375514
function _gatherReferencesInThread(
376515
thread: RawThread,
377516
tcs: TableCompactionStates,

0 commit comments

Comments
 (0)