Skip to content

Commit d446334

Browse files
committed
Reduce allocations for StackLineInfo and StackAddressInfo.
Before: https://share.firefox.dev/3NnthpO After: https://share.firefox.dev/45dMNv1 getStackLineInfo now allocates 46MB instead of 220MB (4.8x reduction), and takes 146ms instead of 338ms (2.3x reduction), when opening the source view for mozjemalloc.cpp on https://share.firefox.dev/3NnzEcF
1 parent c21a3af commit d446334

File tree

6 files changed

+343
-132
lines changed

6 files changed

+343
-132
lines changed

src/profile-logic/address-timings.ts

Lines changed: 64 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ import type {
7777
StackAddressInfo,
7878
AddressTimings,
7979
Address,
80+
IndexIntoAddressSetTable,
8081
} from 'firefox-profiler/types';
82+
import { IntSetTableBuilder } from 'firefox-profiler/utils/intset-table';
8183

8284
import { getMatchingAncestorStackForInvertedCallNode } from './profile-data';
8385
import type { CallNodeInfo, CallNodeInfoInverted } from './call-node-info';
@@ -135,48 +137,32 @@ export function getStackAddressInfo(
135137
_funcTable: FuncTable,
136138
nativeSymbol: IndexIntoNativeSymbolTable
137139
): StackAddressInfo {
138-
// "self address" == "the address which a stack's self time is contributed to"
139-
const selfAddressForAllStacks = [];
140-
// "total addresses" == "the set of addresses whose total time this stack contributes to"
141-
const totalAddressesForAllStacks: Array<Set<Address> | null> = [];
140+
const builder = new IntSetTableBuilder();
141+
const stackIndexToAddressSetIndex = new Int32Array(stackTable.length);
142142

143-
// This loop takes advantage of the fact that the stack table is topologically ordered:
144-
// Prefix stacks are always visited before their descendants.
145-
// Each stack inherits the "total" addresses from its parent stack, and then adds its
146-
// self address to that set. If the stack doesn't have a self address in the library, we just
147-
// re-use the prefix's set object without copying it.
148143
for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) {
149-
const frame = stackTable.frame[stackIndex];
150144
const prefixStack = stackTable.prefix[stackIndex];
151-
const nativeSymbolOfThisStack = frameTable.nativeSymbol[frame];
145+
const prefixAddressSet: IndexIntoAddressSetTable | -1 =
146+
prefixStack !== null ? stackIndexToAddressSetIndex[prefixStack] : -1;
152147

153-
let selfAddress: Address | null = null;
154-
let totalAddresses: Set<Address> | null =
155-
prefixStack !== null ? totalAddressesForAllStacks[prefixStack] : null;
148+
const frame = stackTable.frame[stackIndex];
149+
const nativeSymbolOfThisStack = frameTable.nativeSymbol[frame];
150+
const matchesNativeSymbol = nativeSymbolOfThisStack === nativeSymbol;
151+
if (prefixAddressSet === -1 && !matchesNativeSymbol) {
152+
stackIndexToAddressSetIndex[stackIndex] = -1;
153+
} else {
154+
const selfAddress = matchesNativeSymbol ? frameTable.address[frame] : -1;
156155

157-
if (nativeSymbolOfThisStack === nativeSymbol) {
158-
selfAddress = frameTable.address[frame];
159-
if (selfAddress !== -1) {
160-
// Add this stack's address to this stack's totalAddresses. The rest of this stack's
161-
// totalAddresses is the same as for the parent stack.
162-
// We avoid creating new Set objects unless the new set is actually
163-
// different.
164-
if (totalAddresses === null) {
165-
// None of the ancestor stack nodes have hit a address in the given library.
166-
totalAddresses = new Set([selfAddress]);
167-
} else if (!totalAddresses.has(selfAddress)) {
168-
totalAddresses = new Set(totalAddresses);
169-
totalAddresses.add(selfAddress);
170-
}
171-
}
156+
stackIndexToAddressSetIndex[stackIndex] =
157+
builder.indexForSetWithParentAndSelf(
158+
prefixAddressSet !== -1 ? prefixAddressSet : null,
159+
selfAddress
160+
);
172161
}
173-
174-
selfAddressForAllStacks.push(selfAddress);
175-
totalAddressesForAllStacks.push(totalAddresses);
176162
}
177163
return {
178-
selfAddress: selfAddressForAllStacks,
179-
stackAddresses: totalAddressesForAllStacks,
164+
stackIndexToAddressSetIndex,
165+
addressSetTable: builder.finish(),
180166
};
181167
}
182168

@@ -502,30 +488,58 @@ export function getAddressTimings(
502488
if (stackAddressInfo === null) {
503489
return emptyAddressTimings;
504490
}
505-
const { selfAddress, stackAddresses } = stackAddressInfo;
506-
const totalAddressHits: Map<Address, number> = new Map();
507-
const selfAddressHits: Map<Address, number> = new Map();
508491

509-
// Iterate over all the samples, and aggregate the sample's weight into the
510-
// addresses which are hit by the sample's stack.
511-
// TODO: Maybe aggregate sample count per stack first, and then visit each stack only once?
492+
const { stackIndexToAddressSetIndex, addressSetTable } = stackAddressInfo;
493+
// Iterate over all the samples, and aggregate the sample's weight into
494+
// selfPerAddressSet.
495+
const selfPerAddressSet = new Float64Array(addressSetTable.length);
512496
for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) {
513497
const stackIndex = samples.stack[sampleIndex];
514498
if (stackIndex === null) {
515499
continue;
516500
}
517-
const weight = samples.weight ? samples.weight[sampleIndex] : 1;
518-
const setOfHitAddresses = stackAddresses[stackIndex];
519-
if (setOfHitAddresses !== null) {
520-
for (const address of setOfHitAddresses) {
521-
const oldHitCount = totalAddressHits.get(address) ?? 0;
522-
totalAddressHits.set(address, oldHitCount + weight);
501+
const addressSetIndex = stackIndexToAddressSetIndex[stackIndex];
502+
if (addressSetIndex !== -1) {
503+
const weight = samples.weight ? samples.weight[sampleIndex] : 1;
504+
selfPerAddressSet[addressSetIndex] += weight;
505+
}
506+
}
507+
508+
const totalAddressHits: Map<Address, number> = new Map();
509+
const selfAddressHits: Map<Address, number> = new Map();
510+
const selfSumOfAddressSetDescendants = new Float64Array(
511+
addressSetTable.length
512+
);
513+
514+
for (
515+
let addressSetIndex = addressSetTable.length - 1;
516+
addressSetIndex >= 0;
517+
addressSetIndex--
518+
) {
519+
const selfWeight = selfPerAddressSet[addressSetIndex];
520+
if (selfWeight !== 0) {
521+
const selfAddress = addressSetTable.self[addressSetIndex];
522+
if (selfAddress !== -1) {
523+
const oldHitCount = selfAddressHits.get(selfAddress) ?? 0;
524+
selfAddressHits.set(selfAddress, oldHitCount + selfWeight);
523525
}
524526
}
525-
const address = selfAddress[stackIndex];
526-
if (address !== null) {
527-
const oldHitCount = selfAddressHits.get(address) ?? 0;
528-
selfAddressHits.set(address, oldHitCount + weight);
527+
528+
const selfSumOfThisAddressSetDescendants =
529+
selfSumOfAddressSetDescendants[addressSetIndex];
530+
const thisAddressSetWeight =
531+
selfWeight + selfSumOfThisAddressSetDescendants;
532+
const AddressSetPrefix = addressSetTable.prefix[addressSetIndex];
533+
if (AddressSetPrefix !== null) {
534+
selfSumOfAddressSetDescendants[AddressSetPrefix] += thisAddressSetWeight;
535+
}
536+
537+
if (thisAddressSetWeight !== 0) {
538+
const address = addressSetTable.value[addressSetIndex];
539+
if (address !== -1) {
540+
const oldHitCount = totalAddressHits.get(address) ?? 0;
541+
totalAddressHits.set(address, oldHitCount + thisAddressSetWeight);
542+
}
529543
}
530544
}
531545
return { totalAddressHits, selfAddressHits };

src/profile-logic/line-timings.ts

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import type {
1212
LineTimings,
1313
LineNumber,
1414
IndexIntoSourceTable,
15+
IndexIntoLineSetTable,
1516
} from 'firefox-profiler/types';
17+
import { IntSetTableBuilder } from 'firefox-profiler/utils/intset-table';
1618

1719
import { getMatchingAncestorStackForInvertedCallNode } from './profile-data';
1820
import type { CallNodeInfo, CallNodeInfoInverted } from './call-node-info';
@@ -60,53 +62,35 @@ export function getStackLineInfo(
6062
funcTable: FuncTable,
6163
sourceViewSourceIndex: IndexIntoSourceTable
6264
): StackLineInfo {
63-
// "self line" == "the line which a stack's self time is contributed to"
64-
const selfLineForAllStacks = [];
65-
// "total lines" == "the set of lines whose total time this stack contributes to"
66-
const totalLinesForAllStacks: Array<Set<LineNumber> | null> = [];
65+
const builder = new IntSetTableBuilder();
66+
const stackIndexToLineSetIndex = new Int32Array(stackTable.length);
6767

68-
// This loop takes advantage of the fact that the stack table is topologically ordered:
69-
// Prefix stacks are always visited before their descendants.
70-
// Each stack inherits the "total" lines from its parent stack, and then adds its
71-
// self line to that set. If the stack doesn't have a self line in the file, we just
72-
// re-use the prefix's set object without copying it.
7368
for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) {
74-
const frame = stackTable.frame[stackIndex];
7569
const prefixStack = stackTable.prefix[stackIndex];
70+
const prefixLineSet: IndexIntoLineSetTable | -1 =
71+
prefixStack !== null ? stackIndexToLineSetIndex[prefixStack] : -1;
72+
73+
const frame = stackTable.frame[stackIndex];
7674
const func = frameTable.func[frame];
7775
const sourceIndexOfThisStack = funcTable.source[func];
76+
const matchesSource = sourceIndexOfThisStack === sourceViewSourceIndex;
77+
if (prefixLineSet === -1 && !matchesSource) {
78+
stackIndexToLineSetIndex[stackIndex] = -1;
79+
} else {
80+
const selfLineOrNull = matchesSource
81+
? (frameTable.line[frame] ?? funcTable.lineNumber[func])
82+
: null;
7883

79-
let selfLine: LineNumber | null = null;
80-
let totalLines: Set<LineNumber> | null =
81-
prefixStack !== null ? totalLinesForAllStacks[prefixStack] : null;
82-
83-
if (sourceIndexOfThisStack === sourceViewSourceIndex) {
84-
selfLine = frameTable.line[frame];
85-
// Fallback to func line info if frame line info is not available
86-
if (selfLine === null) {
87-
selfLine = funcTable.lineNumber[func];
88-
}
89-
if (selfLine !== null) {
90-
// Add this stack's line to this stack's totalLines. The rest of this stack's
91-
// totalLines is the same as for the parent stack.
92-
// We avoid creating new Set objects unless the new set is actually
93-
// different.
94-
if (totalLines === null) {
95-
// None of the ancestor stack nodes have hit a line in the given file.
96-
totalLines = new Set([selfLine]);
97-
} else if (!totalLines.has(selfLine)) {
98-
totalLines = new Set(totalLines);
99-
totalLines.add(selfLine);
100-
}
101-
}
84+
stackIndexToLineSetIndex[stackIndex] =
85+
builder.indexForSetWithParentAndSelf(
86+
prefixLineSet !== -1 ? prefixLineSet : null,
87+
selfLineOrNull !== null ? selfLineOrNull : -1
88+
);
10289
}
103-
104-
selfLineForAllStacks.push(selfLine);
105-
totalLinesForAllStacks.push(totalLines);
10690
}
10791
return {
108-
selfLine: selfLineForAllStacks,
109-
stackLines: totalLinesForAllStacks,
92+
stackIndexToLineSetIndex,
93+
lineSetTable: builder.finish(),
11094
};
11195
}
11296

@@ -376,30 +360,57 @@ export function getLineTimings(
376360
if (stackLineInfo === null) {
377361
return emptyLineTimings;
378362
}
379-
const { selfLine, stackLines } = stackLineInfo;
380-
const totalLineHits: Map<LineNumber, number> = new Map();
381-
const selfLineHits: Map<LineNumber, number> = new Map();
363+
const { stackIndexToLineSetIndex, lineSetTable } = stackLineInfo;
364+
365+
console.log('lineSetTable.length:', lineSetTable.length);
382366

383-
// Iterate over all the samples, and aggregate the sample's weight into the
384-
// lines which are hit by the sample's stack.
385-
// TODO: Maybe aggregate sample count per stack first, and then visit each stack only once?
367+
// Iterate over all the samples, and aggregate the sample's weight into
368+
// selfPerLineSet.
369+
const selfPerLineSet = new Float64Array(lineSetTable.length);
386370
for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) {
387371
const stackIndex = samples.stack[sampleIndex];
388372
if (stackIndex === null) {
389373
continue;
390374
}
391-
const weight = samples.weight ? samples.weight[sampleIndex] : 1;
392-
const setOfHitLines = stackLines[stackIndex];
393-
if (setOfHitLines !== null) {
394-
for (const line of setOfHitLines) {
395-
const oldHitCount = totalLineHits.get(line) ?? 0;
396-
totalLineHits.set(line, oldHitCount + weight);
375+
const lineSetIndex = stackIndexToLineSetIndex[stackIndex];
376+
if (lineSetIndex !== -1) {
377+
const weight = samples.weight ? samples.weight[sampleIndex] : 1;
378+
selfPerLineSet[lineSetIndex] += weight;
379+
}
380+
}
381+
382+
const totalLineHits: Map<LineNumber, number> = new Map();
383+
const selfLineHits: Map<LineNumber, number> = new Map();
384+
const selfSumOfLineSetDescendants = new Float64Array(lineSetTable.length);
385+
386+
for (
387+
let lineSetIndex = lineSetTable.length - 1;
388+
lineSetIndex >= 0;
389+
lineSetIndex--
390+
) {
391+
const selfWeight = selfPerLineSet[lineSetIndex];
392+
if (selfWeight !== 0) {
393+
const selfLine = lineSetTable.self[lineSetIndex];
394+
if (selfLine !== -1) {
395+
const oldHitCount = selfLineHits.get(selfLine) ?? 0;
396+
selfLineHits.set(selfLine, oldHitCount + selfWeight);
397397
}
398398
}
399-
const line = selfLine[stackIndex];
400-
if (line !== null) {
401-
const oldHitCount = selfLineHits.get(line) ?? 0;
402-
selfLineHits.set(line, oldHitCount + weight);
399+
400+
const selfSumOfThisLineSetDescendants =
401+
selfSumOfLineSetDescendants[lineSetIndex];
402+
const thisLineSetWeight = selfWeight + selfSumOfThisLineSetDescendants;
403+
const lineSetPrefix = lineSetTable.prefix[lineSetIndex];
404+
if (lineSetPrefix !== null) {
405+
selfSumOfLineSetDescendants[lineSetPrefix] += thisLineSetWeight;
406+
}
407+
408+
if (thisLineSetWeight !== 0) {
409+
const line = lineSetTable.value[lineSetIndex];
410+
if (line !== -1) {
411+
const oldHitCount = totalLineHits.get(line) ?? 0;
412+
totalLineHits.set(line, oldHitCount + thisLineSetWeight);
413+
}
403414
}
404415
}
405416
return { totalLineHits, selfLineHits };

src/test/unit/address-timings.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ describe('getStackAddressInfo', function () {
4242

4343
// Expect the returned arrays to have the same length as the stackTable.
4444
expect(stackTable.length).toBe(9);
45-
expect(stackLineInfoOne.selfAddress.length).toBe(9);
46-
expect(stackLineInfoOne.stackAddresses.length).toBe(9);
45+
expect(stackLineInfoOne.stackIndexToAddressSetIndex.length).toBe(9);
46+
expect(stackLineInfoOne.addressSetTable.length).toBe(4);
4747
});
4848
});
4949

src/test/unit/line-timings.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ describe('getStackLineInfo', function () {
4242

4343
// Expect the returned arrays to have the same length as the stackTable.
4444
expect(stackTable.length).toBe(9);
45-
expect(stackLineInfoOne.selfLine.length).toBe(9);
46-
expect(stackLineInfoOne.stackLines.length).toBe(9);
45+
expect(stackLineInfoOne.lineSetTable.length).toBe(7);
46+
// expect(stackLineInfoOne.stackLines.length).toBe(9);
4747
});
4848
});
4949

src/types/profile-derived.ts

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import type { IndexedArray } from './utils';
3939
import type { BitSet } from '../utils/bitset';
4040
import type { StackTiming } from '../profile-logic/stack-timing';
4141
import type { StringTable } from '../utils/string-table';
42+
import type { IndexIntoIntSetTable, IntSetTable } from 'firefox-profiler/utils/intset-table';
4243
export type IndexIntoCallNodeTable = number;
4344

4445
/**
@@ -337,20 +338,12 @@ export type LineNumber = number;
337338
// stackLine may be null. This is fine because their values are not accessed
338339
// during the LineTimings computation.
339340
export type StackLineInfo = {
340-
// An array that contains, for each "self" stack, the line number that this stack
341-
// spends its self time in, in this file, or null if the self time of the
342-
// stack is in a different file or if the line number is not known.
343-
// For non-"self" stacks, i.e. stacks which are only used as prefix stacks and
344-
// never referred to from a SamplesLikeTable, the value may be null.
345-
selfLine: Array<LineNumber | null>;
346-
// An array that contains, for each "self" stack, all the lines that the frames in
347-
// this stack hit in this file, or null if this stack does not hit any line
348-
// in the given file.
349-
// For non-"self" stacks, i.e. stacks which are only used as prefix stacks and
350-
// never referred to from a SamplesLikeTable, the value may be null.
351-
stackLines: Array<Set<LineNumber> | null>;
341+
stackIndexToLineSetIndex: Int32Array;
342+
lineSetTable: IntSetTable;
352343
};
353344

345+
export type IndexIntoLineSetTable = IndexIntoIntSetTable;
346+
354347
// Stores, for all lines of one specific file, how many times each line is hit
355348
// by samples in a thread. The maps only contain non-zero values.
356349
// So map.get(line) === undefined should be treated as zero.
@@ -377,20 +370,12 @@ export type LineTimings = {
377370
// stackAddress may be null. This is fine because their values are not accessed
378371
// during the AddressTimings computation.
379372
export type StackAddressInfo = {
380-
// An array that contains, for each "self" stack, the address that this stack
381-
// spends its self time in, in this native symbol, or null if the self time of
382-
// the stack is in a different native symbol or if the address is not known.
383-
// For non-"self" stacks, i.e. stacks which are only used as prefix stacks and
384-
// never referred to from a SamplesLikeTable, the value may be null.
385-
selfAddress: Array<Address | null>;
386-
// An array that contains, for each "self" stack, all the addresses that the
387-
// frames in this stack hit in this native symbol, or null if this stack does
388-
// not hit any address in the given native symbol.
389-
// For non-"self" stacks, i.e. stacks which are only used as prefix stacks and
390-
// never referred to from a SamplesLikeTable, the value may be null.
391-
stackAddresses: Array<Set<Address> | null>;
373+
stackIndexToAddressSetIndex: Int32Array;
374+
addressSetTable: IntSetTable;
392375
};
393376

377+
export type IndexIntoAddressSetTable = IndexIntoIntSetTable;
378+
394379
// Stores, for all addresses of one specific library, how many times each
395380
// address is hit by samples in a thread. The maps only contain non-zero values.
396381
// So map.get(address) === undefined should be treated as zero.

0 commit comments

Comments
 (0)