Skip to content

File tree

7 files changed

+141
-122
lines changed

7 files changed

+141
-122
lines changed

src/components/shared/Backtrace.js

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@
66

77
import React from 'react';
88
import classNames from 'classnames';
9-
import { filterCallNodeAndCategoryPathByImplementation } from 'firefox-profiler/profile-logic/transforms';
10-
import {
11-
getFuncNamesAndOriginsForPath,
12-
convertStackToCallNodeAndCategoryPath,
13-
} from 'firefox-profiler/profile-logic/profile-data';
9+
import { getBacktraceItemsForStack } from 'firefox-profiler/profile-logic/transforms';
1410

1511
import type {
1612
CategoryList,
@@ -34,15 +30,11 @@ type Props = {|
3430
export function Backtrace(props: Props) {
3531
const { stackIndex, thread, implementationFilter, maxStacks, categories } =
3632
props;
37-
const callNodePath = filterCallNodeAndCategoryPathByImplementation(
38-
thread,
33+
const funcNamesAndOrigins = getBacktraceItemsForStack(
34+
stackIndex,
3935
implementationFilter,
40-
convertStackToCallNodeAndCategoryPath(thread, stackIndex)
41-
);
42-
const funcNamesAndOrigins = getFuncNamesAndOriginsForPath(
43-
callNodePath,
4436
thread
45-
).reverse();
37+
);
4638

4739
if (funcNamesAndOrigins.length) {
4840
return (

src/components/shared/MarkerContextMenu.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,7 @@ import type {
4242
import type { ConnectedProps } from 'firefox-profiler/utils/connect';
4343
import { getImplementationFilter } from 'firefox-profiler/selectors/url-state';
4444

45-
import { filterCallNodeAndCategoryPathByImplementation } from 'firefox-profiler/profile-logic/transforms';
46-
import {
47-
convertStackToCallNodeAndCategoryPath,
48-
getFuncNamesAndOriginsForPath,
49-
} from 'firefox-profiler/profile-logic/profile-data';
45+
import { getBacktraceItemsForStack } from 'firefox-profiler/profile-logic/transforms';
5046
import { getThreadSelectorsFromThreadsKey } from 'firefox-profiler/selectors/per-thread';
5147

5248
import './MarkerContextMenu.css';
@@ -161,13 +157,12 @@ class MarkerContextMenuImpl extends PureComponent<Props> {
161157
return '';
162158
}
163159

164-
const path = filterCallNodeAndCategoryPathByImplementation(
165-
thread,
160+
const funcNamesAndOrigins = getBacktraceItemsForStack(
161+
stack,
166162
implementationFilter,
167-
convertStackToCallNodeAndCategoryPath(thread, stack)
168-
);
163+
thread
164+
).reverse();
169165

170-
const funcNamesAndOrigins = getFuncNamesAndOriginsForPath(path, thread);
171166
return funcNamesAndOrigins
172167
.map(({ funcName, origin }) => `${funcName} [${origin}]`)
173168
.join('\n');

src/components/shared/SampleTooltipContents.js

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import { Backtrace } from './Backtrace';
99
import { TooltipDetailSeparator } from '../tooltip/TooltipDetails';
1010
import {
1111
getCategoryPairLabel,
12-
getFuncNamesAndOriginsForPath,
13-
convertStackToCallNodeAndCategoryPath,
12+
isSampleWithNonEmptyStack,
1413
} from 'firefox-profiler/profile-logic/profile-data';
1514
import { getFormattedTimelineValue } from 'firefox-profiler/profile-logic/committed-ranges';
1615
import {
@@ -26,7 +25,6 @@ import type {
2625
Milliseconds,
2726
} from 'firefox-profiler/types';
2827
import type { CpuRatioInTimeRange } from './thread/ActivityGraphFills';
29-
import { ensureExists } from '../../utils/flow';
3028

3129
type CPUProps = CpuRatioInTimeRange;
3230

@@ -134,21 +132,10 @@ export class SampleTooltipContents extends React.PureComponent<Props> {
134132
let hasStack = false;
135133
let formattedSampleTime = null;
136134
if (sampleIndex !== null) {
137-
const { samples, stackTable } = rangeFilteredThread;
135+
const { samples } = rangeFilteredThread;
138136
const sampleTime = samples.time[sampleIndex];
139-
const stackIndex = samples.stack[sampleIndex];
140-
const hasSamples = samples.length > 0 && stackTable.length > 1;
141-
142-
if (hasSamples) {
143-
const stack = getFuncNamesAndOriginsForPath(
144-
convertStackToCallNodeAndCategoryPath(
145-
rangeFilteredThread,
146-
ensureExists(stackIndex)
147-
),
148-
rangeFilteredThread
149-
);
150-
hasStack = stack.length > 1 || stack[0].funcName !== '(root)';
151-
}
137+
138+
hasStack = isSampleWithNonEmptyStack(sampleIndex, rangeFilteredThread);
152139

153140
formattedSampleTime = getFormattedTimelineValue(
154141
sampleTime - zeroAt,

src/profile-logic/profile-data.js

Lines changed: 23 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ import type {
7171
PageList,
7272
CallNodeTable,
7373
CallNodePath,
74-
CallNodeAndCategoryPath,
7574
IndexIntoCallNodeTable,
7675
AccumulatedCounterSamples,
7776
SamplesLikeTable,
@@ -2148,30 +2147,6 @@ export function processEventDelays(
21482147
};
21492148
}
21502149

2151-
/**
2152-
* This function converts a stack information into a call node and
2153-
* category path structure.
2154-
*/
2155-
export function convertStackToCallNodeAndCategoryPath(
2156-
thread: Thread,
2157-
stack: IndexIntoStackTable
2158-
): CallNodeAndCategoryPath {
2159-
const { stackTable, frameTable } = thread;
2160-
const path = [];
2161-
for (
2162-
let stackIndex = stack;
2163-
stackIndex !== null;
2164-
stackIndex = stackTable.prefix[stackIndex]
2165-
) {
2166-
const index = stackTable.frame[stackIndex];
2167-
path.push({
2168-
category: stackTable.category[stackIndex],
2169-
func: frameTable.func[index],
2170-
});
2171-
}
2172-
return path.reverse();
2173-
}
2174-
21752150
/**
21762151
* Compute maximum depth of call stack for a given thread, and return maxDepth+1.
21772152
* This value can be used as the length for any per-depth arrays.
@@ -2883,34 +2858,32 @@ export function reserveFunctionsInThread(
28832858
}
28842859
28852860
/**
2886-
* From a valid call node path, this function returns a list of information
2887-
* about each function in this path: their names and their origins.
2861+
* Returns whether the given sample has a stack which is non-null and not just
2862+
* a single function with the name '(root)'.
28882863
*/
2889-
export function getFuncNamesAndOriginsForPath(
2890-
path: CallNodeAndCategoryPath,
2864+
export function isSampleWithNonEmptyStack(
2865+
sampleIndex: IndexIntoSamplesTable,
28912866
thread: Thread
2892-
): Array<{
2893-
funcName: string,
2894-
category: IndexIntoCategoryList,
2895-
isFrameLabel: boolean,
2896-
origin: string,
2897-
}> {
2898-
const { funcTable, stringTable, resourceTable } = thread;
2867+
): boolean {
2868+
const { samples, stackTable, frameTable, funcTable, stringTable } = thread;
28992869
2900-
return path.map((frame) => {
2901-
const { category, func } = frame;
2902-
return {
2903-
funcName: stringTable.getString(funcTable.name[func]),
2904-
category: category,
2905-
isFrameLabel: funcTable.resource[func] === -1,
2906-
origin: getOriginAnnotationForFunc(
2907-
func,
2908-
funcTable,
2909-
resourceTable,
2910-
stringTable
2911-
),
2912-
};
2913-
});
2870+
const stackIndex = samples.stack[sampleIndex];
2871+
if (stackIndex === null) {
2872+
return false;
2873+
}
2874+
2875+
if (stackTable.prefix[stackIndex] !== null) {
2876+
// Stack contains at least two frames.
2877+
return true;
2878+
}
2879+
2880+
// Stack is only a single frame. Is it the '(root)' frame that Firefox puts
2881+
// in its profiles?
2882+
const frameIndex = stackTable.frame[stackIndex];
2883+
const funcIndex = frameTable.func[frameIndex];
2884+
const funcNameStringIndex = funcTable.name[funcIndex];
2885+
const funcName = stringTable.getString(funcNameStringIndex);
2886+
return funcName !== '(root)';
29142887
}
29152888
29162889
/**

src/profile-logic/transforms.js

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
updateThreadStacks,
1313
updateThreadStacksByGeneratingNewStackColumns,
1414
getMapStackUpdater,
15+
getOriginAnnotationForFunc,
1516
} from './profile-data';
1617
import { timeCode } from '../utils/time-code';
1718
import { assertExhaustiveCheck, convertToTransformType } from '../utils/flow';
@@ -32,7 +33,6 @@ import type {
3233
IndexIntoStackTable,
3334
IndexIntoResourceTable,
3435
CallNodePath,
35-
CallNodeAndCategoryPath,
3636
CallNodeTable,
3737
StackType,
3838
ImplementationFilter,
@@ -1563,15 +1563,72 @@ export function filterCallNodePathByImplementation(
15631563
);
15641564
}
15651565

1566-
export function filterCallNodeAndCategoryPathByImplementation(
1567-
thread: Thread,
1566+
// User-facing properties about a stack frame.
1567+
export type BacktraceItem = {|
1568+
// The function name of the stack frame.
1569+
funcName: string,
1570+
// The frame category of the stack frame.
1571+
category: IndexIntoCategoryList,
1572+
// Whether this frame is a label frame.
1573+
isFrameLabel: boolean,
1574+
// A string which is usually displayed after the function name, and which
1575+
// describes, in some way, where this function or frame came from.
1576+
// If known, this contains the file name of the function, and the line and
1577+
// column number of the frame, i.e. the spot within the function that was
1578+
// being executed.
1579+
// If the source file name is not known, this might be the name of a native
1580+
// library instead.
1581+
// May also be empty.
1582+
origin: string,
1583+
|};
1584+
1585+
/**
1586+
* Convert the stack into an array of "backtrace items" for each stack frame.
1587+
* The returned array is ordered from callee-most to caller-most, i.e. the root
1588+
* caller is at the end.
1589+
*/
1590+
export function getBacktraceItemsForStack(
1591+
stack: IndexIntoStackTable,
15681592
implementationFilter: ImplementationFilter,
1569-
path: CallNodeAndCategoryPath
1570-
): CallNodeAndCategoryPath {
1593+
thread: Thread
1594+
): BacktraceItem[] {
1595+
const { funcTable, stringTable, resourceTable } = thread;
1596+
1597+
const { stackTable, frameTable } = thread;
1598+
const unfilteredPath = [];
1599+
for (
1600+
let stackIndex = stack;
1601+
stackIndex !== null;
1602+
stackIndex = stackTable.prefix[stackIndex]
1603+
) {
1604+
const frameIndex = stackTable.frame[stackIndex];
1605+
unfilteredPath.push({
1606+
category: stackTable.category[stackIndex],
1607+
funcIndex: frameTable.func[frameIndex],
1608+
frameLine: frameTable.line[frameIndex],
1609+
frameColumn: frameTable.column[frameIndex],
1610+
});
1611+
}
1612+
15711613
const funcMatchesImplementation = FUNC_MATCHES[implementationFilter];
1572-
return path.filter((funcIndex) =>
1573-
funcMatchesImplementation(thread, funcIndex.func)
1614+
const path = unfilteredPath.filter(({ funcIndex }) =>
1615+
funcMatchesImplementation(thread, funcIndex)
15741616
);
1617+
return path.map(({ category, funcIndex, frameLine, frameColumn }) => {
1618+
return {
1619+
funcName: stringTable.getString(funcTable.name[funcIndex]),
1620+
category: category,
1621+
isFrameLabel: funcTable.resource[funcIndex] === -1,
1622+
origin: getOriginAnnotationForFunc(
1623+
funcIndex,
1624+
funcTable,
1625+
resourceTable,
1626+
stringTable,
1627+
frameLine,
1628+
frameColumn
1629+
),
1630+
};
1631+
});
15751632
}
15761633

15771634
/**

src/test/unit/profile-data.test.js

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
getInvertedCallNodeInfo,
1717
filterThreadByImplementation,
1818
getSampleIndexClosestToStartTime,
19-
convertStackToCallNodeAndCategoryPath,
2019
getSampleIndexToCallNodeIndex,
2120
getTreeOrderComparator,
2221
getSamplesSelectedStates,
@@ -46,6 +45,7 @@ import {
4645
import {
4746
funcHasDirectRecursiveCall,
4847
funcHasRecursiveCall,
48+
getBacktraceItemsForStack,
4949
} from '../../profile-logic/transforms';
5050

5151
import type { Thread, IndexIntoStackTable } from 'firefox-profiler/types';
@@ -949,24 +949,6 @@ describe('funcHasDirectRecursiveCall and funcHasRecursiveCall', function () {
949949
});
950950
});
951951

952-
describe('convertStackToCallNodeAndCategoryPath', function () {
953-
it('correctly returns a call node path for a stack', function () {
954-
const profile = getCallNodeProfile();
955-
const { derivedThreads } = getProfileWithDicts(profile);
956-
const [thread] = derivedThreads;
957-
const stack1 = thread.samples.stack[0];
958-
const stack2 = thread.samples.stack[1];
959-
if (stack1 === null || stack2 === null) {
960-
// Makes flow happy
961-
throw new Error("stack shouldn't be null");
962-
}
963-
let callNodePath = convertStackToCallNodeAndCategoryPath(thread, stack1);
964-
expect(callNodePath.map((f) => f.func)).toEqual([0, 1, 2, 3, 4]);
965-
callNodePath = convertStackToCallNodeAndCategoryPath(thread, stack2);
966-
expect(callNodePath.map((f) => f.func)).toEqual([0, 1, 2, 3, 5]);
967-
});
968-
});
969-
970952
describe('getSamplesSelectedStates', function () {
971953
function setup(textSamples) {
972954
const {
@@ -1566,3 +1548,43 @@ describe('getNativeSymbolInfo', function () {
15661548
});
15671549
});
15681550
});
1551+
1552+
describe('getBacktraceItemsForStack', function () {
1553+
function getBacktraceString(thread, sampleIndex): string {
1554+
return getBacktraceItemsForStack(
1555+
ensureExists(thread.samples.stack[sampleIndex]),
1556+
'combined',
1557+
thread
1558+
)
1559+
.map(({ funcName, origin }) => `${funcName} ${origin}`)
1560+
.join('\n');
1561+
}
1562+
1563+
it('returns backtrace items in the right order and with frame line numbers', function () {
1564+
const { derivedThreads } = getProfileFromTextSamples(`
1565+
A[file:one.js][line:20] A[file:one.js][line:21] A[file:one.js][line:20]
1566+
B[file:one.js][line:30] D[file:one.js][line:50] B[file:one.js][line:31]
1567+
C[file:two.js][line:10] C[file:two.js][line:11] C[file:two.js][line:12]
1568+
D[file:one.js][line:51]
1569+
`);
1570+
1571+
const [thread] = derivedThreads;
1572+
1573+
expect(getBacktraceString(thread, 0)).toMatchInlineSnapshot(`
1574+
"C two.js:10
1575+
B one.js:30
1576+
A one.js:20"
1577+
`);
1578+
expect(getBacktraceString(thread, 1)).toMatchInlineSnapshot(`
1579+
"C two.js:11
1580+
D one.js:50
1581+
A one.js:21"
1582+
`);
1583+
expect(getBacktraceString(thread, 2)).toMatchInlineSnapshot(`
1584+
"D one.js:51
1585+
C two.js:12
1586+
B one.js:31
1587+
A one.js:20"
1588+
`);
1589+
});
1590+
});

src/types/profile-derived.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -431,13 +431,6 @@ export type AddressProof = {|
431431
*/
432432
export type CallNodePath = IndexIntoFuncTable[];
433433

434-
export type CallNodeAndCategory = {|
435-
func: IndexIntoFuncTable,
436-
category: IndexIntoCategoryList,
437-
|};
438-
439-
export type CallNodeAndCategoryPath = CallNodeAndCategory[];
440-
441434
/**
442435
* This type contains the first derived `Marker[]` information, plus an IndexedArray
443436
* to get back to the RawMarkerTable.

0 commit comments

Comments
 (0)