Skip to content

Commit a2f2f13

Browse files
authored
Add an insertStackLabels helper. (#6076)
This takes an existing profile and creates label frames based on function name prefix matching. The label frames are inserted as parent stack nodes of the matched stack node. This lets us turn native profiles from e.g. samply into profiles where the JS-only view shows DOM calls.
2 parents 7a46d0e + a99497b commit a2f2f13

2 files changed

Lines changed: 447 additions & 0 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import type {
6+
IndexIntoFrameTable,
7+
IndexIntoStackTable,
8+
RawStackTable,
9+
IndexIntoFuncTable,
10+
Profile,
11+
Category,
12+
} from '../types/profile';
13+
import {
14+
shallowCloneFrameTable,
15+
shallowCloneFuncTable,
16+
} from 'firefox-profiler/profile-logic/data-structures';
17+
import { StringTable } from 'firefox-profiler/utils/string-table';
18+
import { updateRawThreadStacks } from 'firefox-profiler/profile-logic/profile-data';
19+
20+
export type LabelDescription = {
21+
name: string;
22+
funcPrefixes: string[];
23+
};
24+
25+
/**
26+
* Takes a profile and creates one which contains "stack labels".
27+
*
28+
* ## Example
29+
*
30+
* Before:
31+
*
32+
* ```
33+
* - BaselineIC: Call.CallNative
34+
* - mozilla::dom::Element_Binding::getBoundingClientRect <- matches a funcPrefix
35+
* - nsIContent::GetPrimaryFrame
36+
* - mozilla::PresShell::DoFlushLayout
37+
* - mozilla::PresShell::ProcessReflowCommands <- matches a funcPrefix
38+
* ```
39+
*
40+
* After:
41+
*
42+
* ```
43+
* - BaselineIC: Call.CallNative
44+
* - Element.getBoundingClientRect <== NEW
45+
* - mozilla::dom::Element_Binding::getBoundingClientRect
46+
* - nsIContent::GetPrimaryFrame
47+
* - mozilla::PresShell::DoFlushLayout
48+
* - Update layout <== NEW
49+
* - mozilla::PresShell::ProcessReflowCommands
50+
* ```
51+
*
52+
* The label frames are inserted as new parent stack nodes for the matched
53+
* stack node. The caller supplies the list of labels and their matchers
54+
* (as function name prefixes).
55+
*
56+
* ## No "duplicate labels"
57+
*
58+
* This implementation avoids duplicate labels. This is best explained with
59+
* an example. Let "Label A" apply to prefix "a" and "Label B" apply to prefix "b".
60+
*
61+
* Input:
62+
*
63+
* ```
64+
* - a1
65+
* - a2
66+
* - b1
67+
* - a2
68+
* ```
69+
*
70+
* Then we get:
71+
*
72+
* ```
73+
* - Label A
74+
* - a1
75+
* - a2
76+
* - Label B
77+
* - b1
78+
* - Label A
79+
* - a2
80+
* ```
81+
*
82+
* Notably there is no extra "Label A" frame between a1 and a2, even though a2
83+
* also matches. We avoid it in order to keep the tree simple; and in the JS-only
84+
* call tree, the samples at a1,a2 are already accounted to the Label A node which
85+
* is all we wanted to achieve.
86+
*/
87+
export function insertStackLabels(
88+
profile: Profile,
89+
labelDescriptions: LabelDescription[]
90+
): Profile {
91+
const labelCategoryIndex = profile.meta.categories!.length;
92+
93+
const newCategories: Category[] = [
94+
...profile.meta.categories!,
95+
{
96+
name: 'Label',
97+
color: 'blue',
98+
subcategories: ['Other'],
99+
},
100+
];
101+
102+
const {
103+
funcTable: oldFuncTable,
104+
frameTable: oldFrameTable,
105+
stackTable: oldStackTable,
106+
sources,
107+
stringArray,
108+
} = profile.shared;
109+
const frameTable = shallowCloneFrameTable(oldFrameTable);
110+
const funcTable = shallowCloneFuncTable(oldFuncTable);
111+
const stringTable = StringTable.withBackingArray(stringArray);
112+
113+
const rootLabelName = 'Root (unaccounted / catch-all)';
114+
const rootLabelFrameIndex = frameTable.length;
115+
116+
const labelFramesStartIndex = rootLabelFrameIndex + 1;
117+
const allLabelNames = [
118+
rootLabelName,
119+
...labelDescriptions.map((label) => label.name),
120+
];
121+
122+
// First, add the label frames and funcs to the frameTable + funcTable.
123+
for (let i = 0; i < allLabelNames.length; i++) {
124+
const labelName = allLabelNames[i];
125+
const funcIndex = funcTable.length++;
126+
funcTable.name[funcIndex] = stringTable.indexForString(labelName);
127+
funcTable.resource[funcIndex] = -1;
128+
funcTable.source[funcIndex] = null;
129+
funcTable.lineNumber[funcIndex] = null;
130+
funcTable.columnNumber[funcIndex] = null;
131+
funcTable.isJS[funcIndex] = false;
132+
funcTable.relevantForJS[funcIndex] = true;
133+
134+
const frameIndex = frameTable.length++;
135+
frameTable.func[frameIndex] = funcIndex;
136+
frameTable.category[frameIndex] = labelCategoryIndex;
137+
frameTable.subcategory[frameIndex] = 0;
138+
frameTable.nativeSymbol[frameIndex] = null;
139+
frameTable.address[frameIndex] = 0;
140+
frameTable.inlineDepth[frameIndex] = 0;
141+
frameTable.line[frameIndex] = null;
142+
frameTable.column[frameIndex] = null;
143+
frameTable.innerWindowID[frameIndex] = null;
144+
}
145+
146+
// Run the function name against the substring matchers and return the first
147+
// match.
148+
function getLabelIndexForFunc(funcIndex: IndexIntoFuncTable): number | null {
149+
let nameString = stringArray[funcTable.name[funcIndex]];
150+
151+
// Include the filename (in brackets), if present. This allows matchers
152+
// like `onStateChange (chrome://browser/content/tabbrowser/`
153+
const sourceIndex = funcTable.source[funcIndex];
154+
if (sourceIndex !== null) {
155+
const filenameString = stringArray[sources.filename[sourceIndex]];
156+
nameString += ` (${filenameString})`;
157+
}
158+
159+
// Check against every funcPrefix of every label. Return the first match.
160+
for (
161+
let labelIndex = 0;
162+
labelIndex < labelDescriptions.length;
163+
labelIndex++
164+
) {
165+
const labelDescription = labelDescriptions[labelIndex];
166+
for (
167+
let prefixIndex = 0;
168+
prefixIndex < labelDescription.funcPrefixes.length;
169+
prefixIndex++
170+
) {
171+
const funcNamePrefix = labelDescription.funcPrefixes[prefixIndex];
172+
if (nameString.startsWith(funcNamePrefix)) {
173+
return labelIndex;
174+
}
175+
}
176+
}
177+
return null;
178+
}
179+
180+
// Compute the label frame index (if any) for every func.
181+
const funcIndexToLabelFrameIndex = new Array<number | null>(funcTable.length);
182+
for (let funcIndex = 0; funcIndex < funcTable.length; funcIndex++) {
183+
const labelIndex = getLabelIndexForFunc(funcIndex);
184+
const labelFrameIndex =
185+
labelIndex !== null ? labelFramesStartIndex + labelIndex : null;
186+
funcIndexToLabelFrameIndex[funcIndex] = labelFrameIndex;
187+
}
188+
189+
// Now compute where in the stack table we need labels.
190+
const labelFrameIndexToInsertAtStack = new Array<number | null>(
191+
oldStackTable.length
192+
);
193+
const inheritedLabelFrameIndexAtStack = new Array<IndexIntoFrameTable | null>(
194+
oldStackTable.length
195+
);
196+
let stacksToInsertCount = 0;
197+
for (let stackIndex = 0; stackIndex < oldStackTable.length; stackIndex++) {
198+
const parentStackIndex = oldStackTable.prefix[stackIndex];
199+
const inheritedLabelFrameIndex =
200+
parentStackIndex !== null
201+
? inheritedLabelFrameIndexAtStack[parentStackIndex]
202+
: null;
203+
const frameIndex = oldStackTable.frame[stackIndex];
204+
const funcIndex = oldFrameTable.func[frameIndex];
205+
const labelFrameIndex = funcIndexToLabelFrameIndex[funcIndex];
206+
if (
207+
labelFrameIndex !== null &&
208+
labelFrameIndex !== inheritedLabelFrameIndex
209+
) {
210+
labelFrameIndexToInsertAtStack[stackIndex] = labelFrameIndex;
211+
inheritedLabelFrameIndexAtStack[stackIndex] = labelFrameIndex;
212+
stacksToInsertCount++;
213+
} else if (
214+
funcTable.isJS[funcIndex] ||
215+
funcTable.relevantForJS[funcIndex]
216+
) {
217+
labelFrameIndexToInsertAtStack[stackIndex] = null;
218+
inheritedLabelFrameIndexAtStack[stackIndex] = null;
219+
} else if (parentStackIndex === null) {
220+
labelFrameIndexToInsertAtStack[stackIndex] = rootLabelFrameIndex;
221+
inheritedLabelFrameIndexAtStack[stackIndex] = rootLabelFrameIndex;
222+
stacksToInsertCount++;
223+
} else {
224+
labelFrameIndexToInsertAtStack[stackIndex] = null;
225+
inheritedLabelFrameIndexAtStack[stackIndex] = inheritedLabelFrameIndex;
226+
}
227+
}
228+
229+
// Now compute the new stack table.
230+
const newStackCount = oldStackTable.length + stacksToInsertCount;
231+
const newPrefixCol = new Array<IndexIntoStackTable | null>(newStackCount);
232+
const newFrameCol = new Array<IndexIntoFrameTable>(newStackCount);
233+
const oldStackToNewStackPlusOne = new Int32Array(oldStackTable.length);
234+
let nextNewStackIndex = 0;
235+
for (
236+
let oldStackIndex = 0;
237+
oldStackIndex < oldStackTable.length;
238+
oldStackIndex++
239+
) {
240+
const labelFrameIndexToInsert =
241+
labelFrameIndexToInsertAtStack[oldStackIndex];
242+
const oldPrefix = oldStackTable.prefix[oldStackIndex];
243+
let newPrefix =
244+
oldPrefix !== null ? oldStackToNewStackPlusOne[oldPrefix] - 1 : null;
245+
const frameIndex = oldStackTable.frame[oldStackIndex];
246+
if (labelFrameIndexToInsert !== null) {
247+
const insertedStackIndex = nextNewStackIndex++;
248+
newPrefixCol[insertedStackIndex] = newPrefix;
249+
newFrameCol[insertedStackIndex] = labelFrameIndexToInsert;
250+
newPrefix = insertedStackIndex;
251+
}
252+
const newStackIndex = nextNewStackIndex++;
253+
newPrefixCol[newStackIndex] = newPrefix;
254+
newFrameCol[newStackIndex] = frameIndex;
255+
oldStackToNewStackPlusOne[oldStackIndex] = newStackIndex + 1;
256+
}
257+
258+
if (nextNewStackIndex !== newStackCount) {
259+
console.error('Unexpected new stack count!', {
260+
nextNewStackIndex,
261+
newStackCount,
262+
stacksToInsertCount,
263+
});
264+
}
265+
266+
const stackTable: RawStackTable = {
267+
prefix: newPrefixCol,
268+
frame: newFrameCol,
269+
length: newStackCount,
270+
};
271+
272+
const newShared = { ...profile.shared, stackTable, frameTable, funcTable };
273+
const newThreads = updateRawThreadStacks(profile.threads, (oldStack) =>
274+
oldStack !== null ? oldStackToNewStackPlusOne[oldStack] - 1 : null
275+
);
276+
const newMeta = {
277+
...profile.meta,
278+
categories: newCategories,
279+
};
280+
281+
const newProfile: Profile = {
282+
...profile,
283+
meta: newMeta,
284+
shared: newShared,
285+
threads: newThreads,
286+
};
287+
288+
return newProfile;
289+
}

0 commit comments

Comments
 (0)