Skip to content

Commit c176744

Browse files
authored
Add fallback to funcTable.lineNumber when computing line timings (#5644)
When computing line timings for source view, we now fall back to using funcTable.lineNumber (the function's start line) when frameTable.line (the frame's execution line) is null. This is particularly useful for JS JIT'ed functions where frame line information might be missing.
1 parent e413415 commit c176744

File tree

3 files changed

+85
-1
lines changed

3 files changed

+85
-1
lines changed

src/profile-logic/line-timings.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ export function getStackLineInfo(
8282

8383
if (sourceIndexOfThisStack === sourceViewSourceIndex) {
8484
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+
}
8589
if (selfLine !== null) {
8690
// Add this stack's line to this stack's totalLines. The rest of this stack's
8791
// totalLines is the same as for the parent stack.
@@ -120,6 +124,7 @@ export function getStackLineInfo(
120124
export function getStackLineInfoForCallNode(
121125
stackTable: StackTable,
122126
frameTable: FrameTable,
127+
funcTable: FuncTable,
123128
callNodeIndex: IndexIntoCallNodeTable,
124129
callNodeInfo: CallNodeInfo
125130
): StackLineInfo {
@@ -128,12 +133,14 @@ export function getStackLineInfoForCallNode(
128133
? getStackLineInfoForCallNodeInverted(
129134
stackTable,
130135
frameTable,
136+
funcTable,
131137
callNodeIndex,
132138
callNodeInfoInverted
133139
)
134140
: getStackLineInfoForCallNodeNonInverted(
135141
stackTable,
136142
frameTable,
143+
funcTable,
137144
callNodeIndex,
138145
callNodeInfo
139146
);
@@ -205,6 +212,7 @@ export function getStackLineInfoForCallNode(
205212
export function getStackLineInfoForCallNodeNonInverted(
206213
stackTable: StackTable,
207214
frameTable: FrameTable,
215+
funcTable: FuncTable,
208216
callNodeIndex: IndexIntoCallNodeTable,
209217
callNodeInfo: CallNodeInfo
210218
): StackLineInfo {
@@ -229,6 +237,11 @@ export function getStackLineInfoForCallNodeNonInverted(
229237
// the same as the given call node's func and file.
230238
const frame = stackTable.frame[stackIndex];
231239
selfLine = frameTable.line[frame];
240+
// Fallback to func line info if frame line info is not available
241+
if (selfLine === null) {
242+
const func = frameTable.func[frame];
243+
selfLine = funcTable.lineNumber[func];
244+
}
232245
if (selfLine !== null) {
233246
totalLines = new Set([selfLine]);
234247
}
@@ -280,6 +293,7 @@ export function getStackLineInfoForCallNodeNonInverted(
280293
export function getStackLineInfoForCallNodeInverted(
281294
stackTable: StackTable,
282295
frameTable: FrameTable,
296+
funcTable: FuncTable,
283297
callNodeIndex: IndexIntoCallNodeTable,
284298
callNodeInfo: CallNodeInfoInverted
285299
): StackLineInfo {
@@ -318,7 +332,12 @@ export function getStackLineInfoForCallNodeInverted(
318332
// This stack contributes to the call node's total time.
319333
// We don't need to check the stack's func or file because it'll be
320334
// the same as the given call node's func and file.
321-
const line = frameTable.line[frameForCallNode];
335+
let line = frameTable.line[frameForCallNode];
336+
// Fallback to func line info if frame line info is not available
337+
if (line === null) {
338+
const func = frameTable.func[frameForCallNode];
339+
line = funcTable.lineNumber[func];
340+
}
322341
if (line !== null) {
323342
totalLines = new Set([line]);
324343
if (callNodeIsRootOfInvertedTree) {

src/selectors/per-thread/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ export const selectedNodeSelectors: NodeSelectors = (() => {
294294
return getStackLineInfoForCallNode(
295295
stackTable,
296296
frameTable,
297+
funcTable,
297298
selectedCallNodeIndex,
298299
callNodeInfo
299300
);

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,40 @@ describe('getLineTimings for getStackLineInfo', function () {
112112
expect(lineTimingsTwo.selfLineHits.get(40)).toBe(1);
113113
expect(lineTimingsTwo.selfLineHits.size).toBe(2); // no other hits
114114
});
115+
116+
it('falls back to funcTable.lineNumber when frameTable.line is null', function () {
117+
// Create a profile with frames that have null line numbers
118+
const { derivedThreads } = getProfileFromTextSamples(`
119+
A[file:file.js][line:20]
120+
B[file:file.js][line:30]
121+
`);
122+
const [thread] = derivedThreads;
123+
const { stackTable, frameTable, funcTable, samples, stringTable } = thread;
124+
125+
// Manually set frameTable.line to null for the leaf frame
126+
// to simulate a case where frame line info is missing
127+
const leafFrame = stackTable.frame[stackTable.length - 1];
128+
frameTable.line[leafFrame] = null;
129+
130+
// Set funcTable.lineNumber to a value for the func of that frame
131+
const func = frameTable.func[leafFrame];
132+
funcTable.lineNumber[func] = 35;
133+
134+
const fileStringIndex = stringTable.indexForString('file.js');
135+
const fileSourceIndex = thread.sources.filename.indexOf(fileStringIndex);
136+
const stackLineInfo = getStackLineInfo(
137+
stackTable,
138+
frameTable,
139+
funcTable,
140+
fileSourceIndex
141+
);
142+
const lineTimings = getLineTimings(stackLineInfo, samples);
143+
144+
// The fallback should use funcTable.lineNumber[func] = 35
145+
expect(lineTimings.selfLineHits.get(35)).toBe(1);
146+
expect(lineTimings.totalLineHits.get(20)).toBe(1);
147+
expect(lineTimings.totalLineHits.get(35)).toBe(1);
148+
});
115149
});
116150

117151
describe('getLineTimings for getStackLineInfoForCallNode', function () {
@@ -141,6 +175,7 @@ describe('getLineTimings for getStackLineInfoForCallNode', function () {
141175
const stackLineInfo = getStackLineInfoForCallNode(
142176
stackTable,
143177
frameTable,
178+
funcTable,
144179
callNodeIndex,
145180
callNodeInfo
146181
);
@@ -268,4 +303,33 @@ describe('getLineTimings for getStackLineInfoForCallNode', function () {
268303
expect(lineTimingsDBA.totalLineHits.size).toBe(1); // no other hits
269304
expect(lineTimingsDC.selfLineHits.size).toBe(0); // no self line hits
270305
});
306+
307+
it('falls back to funcTable.lineNumber when frameTable.line is null', function () {
308+
const { derivedThreads, funcNamesDictPerThread, defaultCategory } =
309+
getProfileFromTextSamples(`
310+
A[file:file.js][line:20]
311+
B[file:file.js][line:30]
312+
`);
313+
314+
const [{ A, B }] = funcNamesDictPerThread;
315+
const [thread] = derivedThreads;
316+
const { stackTable, frameTable, funcTable } = thread;
317+
318+
// Manually set frameTable.line to null for the leaf frame
319+
// to simulate a case where frame line info is missing
320+
const leafFrame = stackTable.frame[stackTable.length - 1];
321+
frameTable.line[leafFrame] = null;
322+
323+
// Set funcTable.lineNumber to a value for the func of that frame
324+
const func = frameTable.func[leafFrame];
325+
funcTable.lineNumber[func] = 35;
326+
327+
// Compute the line timings for the child call node.
328+
// The fallback should use funcTable.lineNumber[func] = 35
329+
const lineTimingsChild = getTimings(thread, [A, B], defaultCategory, false);
330+
expect(lineTimingsChild.totalLineHits.get(35)).toBe(1);
331+
expect(lineTimingsChild.totalLineHits.size).toBe(1); // no other hits
332+
expect(lineTimingsChild.selfLineHits.get(35)).toBe(1);
333+
expect(lineTimingsChild.selfLineHits.size).toBe(1); // no other hits
334+
});
271335
});

0 commit comments

Comments
 (0)