Skip to content

Add a "Merge unaccounted native frames" transform #5141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions locales/en-US/app.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ CallNodeContextMenu--transform-merge-call-node = Merge node only
function’s node that called it. It only removes the function from that
specific part of the tree. Any other places the function was called from
will remain in the profile.
CallNodeContextMenu--transform-merge-unaccounted-native-functions = Merge unaccounted native frames
.title =
Removes stack frames such as unsymbolicated JIT frames from the call tree,
and make their callers absorb their cost.
Specifically, this transform merges any native stack frames which were
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit lost on the second part (mind you, not a developer here).

Is this transform merging unaccounted for frames to a shared library (the action is "merge to a shared library"), or is this merging frames not belonging to a shared library ("merge specific frames, i.e. those that don't belong to a library")? Also, what does unaccounted for mean here: not examined, not included, something else?

not accounted to a shared library.

# This is used as the context menu item title for "Focus on function" and "Focus
# on function (inverted)" transforms.
Expand Down Expand Up @@ -1083,6 +1089,12 @@ TransformNavigator--merge-call-node = Merge Node: { $item }
# $item (String) - Name of the function that transform applied to.
TransformNavigator--merge-function = Merge: { $item }

# "Merge unaccounted native frames" transform.
# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=merge
# Corresponds to the context menu item with the l10n ID
# CallNodeContextMenu--transform-merge-unaccounted-native-functions
TransformNavigator--merge-unaccounted-native-functions = Merge unaccounted native

# "Drop function" transform.
# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=drop
# Variables:
Expand Down
7 changes: 7 additions & 0 deletions src/actions/profile-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -2078,6 +2078,13 @@
})
);
break;
case 'U':
dispatch(

Check warning on line 2082 in src/actions/profile-view.js

View check run for this annotation

Codecov / codecov/patch

src/actions/profile-view.js#L2081-L2082

Added lines #L2081 - L2082 were not covered by tests
addTransformToStack(threadsKey, {
type: 'merge-unaccounted-native-functions',
})
);
break;

Check warning on line 2087 in src/actions/profile-view.js

View check run for this annotation

Codecov / codecov/patch

src/actions/profile-view.js#L2087

Added line #L2087 was not covered by tests
case 'd':
dispatch(
addTransformToStack(threadsKey, {
Expand Down
26 changes: 25 additions & 1 deletion src/components/shared/CallNodeContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import {
funcHasDirectRecursiveCall,
funcHasRecursiveCall,
isUnaccountedNativeFunction,
} from 'firefox-profiler/profile-logic/transforms';
import { getFunctionName } from 'firefox-profiler/profile-logic/function-info';
import {
Expand Down Expand Up @@ -333,6 +334,11 @@
funcIndex: selectedFunc,
});
break;
case 'merge-unaccounted-native-functions':
addTransformToStack(threadsKey, {

Check warning on line 338 in src/components/shared/CallNodeContextMenu.js

View check run for this annotation

Codecov / codecov/patch

src/components/shared/CallNodeContextMenu.js#L337-L338

Added lines #L337 - L338 were not covered by tests
type: 'merge-unaccounted-native-functions',
});
break;

Check warning on line 341 in src/components/shared/CallNodeContextMenu.js

View check run for this annotation

Codecov / codecov/patch

src/components/shared/CallNodeContextMenu.js#L341

Added line #L341 was not covered by tests
case 'drop-function':
addTransformToStack(threadsKey, {
type: 'drop-function',
Expand Down Expand Up @@ -484,7 +490,7 @@

const {
callNodeIndex,
thread: { funcTable },
thread: { funcTable, stringTable },
callNodeInfo,
} = rightClickedCallNodeInfo;

Expand All @@ -504,6 +510,11 @@
const fileName =
filePath &&
parseFileNameFromSymbolication(filePath).path.match(/[^\\/]+$/)?.[0];
const isProbablyJIT = isUnaccountedNativeFunction(
funcIndex,
funcTable,
stringTable
);
return (
<>
{fileName ? (
Expand Down Expand Up @@ -545,6 +556,19 @@
content: 'Merge node only',
})}

{isProbablyJIT
? this.renderTransformMenuItem({

Check warning on line 560 in src/components/shared/CallNodeContextMenu.js

View check run for this annotation

Codecov / codecov/patch

src/components/shared/CallNodeContextMenu.js#L560

Added line #L560 was not covered by tests
l10nId:
'CallNodeContextMenu--transform-merge-unaccounted-native-functions',
shortcut: 'U',
icon: 'Merge',
onClick: this._handleClick,
transform: 'merge-unaccounted-native-functions',
title: '',
content: 'Merge unaccounted native frames',
})
: null}

{this.renderTransformMenuItem({
l10nId: inverted
? 'CallNodeContextMenu--transform-focus-function-inverted'
Expand Down
140 changes: 139 additions & 1 deletion src/profile-logic/transforms.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
'focus-category',
'merge-call-node',
'merge-function',
'merge-unaccounted-native-functions',
'drop-function',
'collapse-resource',
'collapse-direct-recursion',
Expand All @@ -91,6 +92,9 @@
case 'merge-function':
shortKey = 'mf';
break;
case 'merge-unaccounted-native-functions':
shortKey = 'mu';
break;
case 'drop-function':
shortKey = 'df';
break;
Expand Down Expand Up @@ -235,6 +239,12 @@
}
break;
}
case 'merge-unaccounted-native-functions': {
transforms.push({
type: 'merge-unaccounted-native-functions',
});
break;
}
case 'focus-category': {
// e.g. "fg-3"
const [, categoryRaw] = tuple;
Expand Down Expand Up @@ -348,6 +358,8 @@
case 'collapse-function-subtree':
case 'focus-function':
return `${shortKey}-${transform.funcIndex}`;
case 'merge-unaccounted-native-functions':
return shortKey;
case 'focus-category':
return `${shortKey}-${transform.category}`;
case 'collapse-resource':
Expand Down Expand Up @@ -432,6 +444,13 @@
}
}

if (transform.type === 'merge-unaccounted-native-functions') {
return {

Check warning on line 448 in src/profile-logic/transforms.js

View check run for this annotation

Codecov / codecov/patch

src/profile-logic/transforms.js#L448

Added line #L448 was not covered by tests
l10nId: 'TransformNavigator--merge-unaccounted-native-functions',
item: '',
};
}

// Lookup function name.
let funcIndex;
switch (transform.type) {
Expand Down Expand Up @@ -517,6 +536,12 @@
return _mergeNodeInCallNodePath(transform.callNodePath, callNodePath);
case 'merge-function':
return _mergeFunctionInCallNodePath(transform.funcIndex, callNodePath);
case 'merge-unaccounted-native-functions':
return _mergeUnaccountedNativeFunctionsInCallNodePath(
callNodePath,
transformedThread.funcTable,
transformedThread.stringTable
);
case 'drop-function':
return _dropFunctionInCallNodePath(transform.funcIndex, callNodePath);
case 'collapse-resource':
Expand Down Expand Up @@ -588,6 +613,38 @@
return callNodePath.filter((nodeFunc) => nodeFunc !== funcIndex);
}

function _mergeUnaccountedNativeFunctionsInCallNodePath(
callNodePath: CallNodePath,
funcTable: FuncTable,
stringTable: UniqueStringArray
): CallNodePath {
return callNodePath.filter((nodeFunc) =>
isUnaccountedNativeFunction(nodeFunc, funcTable, stringTable)

Check warning on line 622 in src/profile-logic/transforms.js

View check run for this annotation

Codecov / codecov/patch

src/profile-logic/transforms.js#L622

Added line #L622 was not covered by tests
);
}

// Returns true if funcIndex is probably a JIT frame outside of any known JIT
// address mappings.
export function isUnaccountedNativeFunction(
funcIndex: IndexIntoFuncTable,
funcTable: FuncTable,
stringTable: UniqueStringArray
): boolean {
if (funcTable.resource[funcIndex] !== -1) {
// This function has a resource. That means it's not "unaccounted".
return false;
}
if (funcTable.isJS[funcIndex]) {
// This function is a JS function. That means it's not a "native" function.
return false;
}
// Ok, so now we either have a native function without a library (otherwise it
// would have a "lib" resource), or we have a label frame. Assume that label
// frames don't start with "0x".
const locationString = stringTable.getString(funcTable.name[funcIndex]);
return locationString.startsWith('0x');
}

function _dropFunctionInCallNodePath(
funcIndex: IndexIntoFuncTable,
callNodePath: CallNodePath
Expand Down Expand Up @@ -859,13 +916,92 @@
});
}

/**
* Returns a Uint8Array filled with zeros and ones.
* result[funcIndex] === 1 if and only if isUnaccountedNativeFunction(funcIndex)
*/
function getUnaccountedNativeFunctions(thread: Thread): Uint8Array {
const { funcTable, stringTable } = thread;
const funcCount = funcTable.length;
const isUnaccountedNativeFunctionArr = new Uint8Array(funcCount);
for (let i = 0; i < funcCount; i++) {
if (isUnaccountedNativeFunction(i, funcTable, stringTable)) {
isUnaccountedNativeFunctionArr[i] = 1;
}
}
return isUnaccountedNativeFunctionArr;
}

export function mergeUnaccountedNativeFunctions(thread: Thread): Thread {
const isUnaccountedNativeFunctionArr = getUnaccountedNativeFunctions(thread);
return mergeFunctions(thread, isUnaccountedNativeFunctionArr);
}

export function mergeFunctions(
thread: Thread,
shouldMergeFunction: Uint8Array
): Thread {
const { stackTable, frameTable } = thread;

// A map oldStack -> newStack+1, implemented as a Uint32Array for performance.
// If newStack+1 is zero it means "null", i.e. this stack was filtered out.
// Typed arrays are initialized to zero, which we interpret as null.
//
// For each old stack, the new stack is computed as follows (with `oldStackFun`
// being the old stack's frame's function):
// - If shouldMergeFunction[oldStackFun] === 0, then the new stack is the same
// as the old stack.
// - If shouldMergeFunction[oldStackFun] === 1, then the new stack is the closest
// ancestor for which shouldMergeFunction[ancestorFunc] is 0, or null if no
// such ancestor exists.
//
// We only compute a new prefix column; the other columns are copied from the
// old stack table. The skipped stacks are "orphaned"; they'll still be present
// in the new stack table but not referenced by samples or other stacks.
const oldStackToNewStackPlusOne = new Uint32Array(stackTable.length);

const stackTableFrameCol = stackTable.frame;
const frameTableFuncCol = frameTable.func;
const oldPrefixCol = stackTable.prefix;
const newPrefixCol = new Array(stackTable.length);

for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) {
const oldPrefix = oldPrefixCol[stackIndex];
const newPrefixPlusOne =
oldPrefix === null ? 0 : oldStackToNewStackPlusOne[oldPrefix];

const frameIndex = stackTableFrameCol[stackIndex];
const funcIndex = frameTableFuncCol[frameIndex];
if (shouldMergeFunction[funcIndex] === 1) {
oldStackToNewStackPlusOne[stackIndex] = newPrefixPlusOne;
} else {
oldStackToNewStackPlusOne[stackIndex] = stackIndex + 1;
}
const newPrefix = newPrefixPlusOne === 0 ? null : newPrefixPlusOne - 1;
newPrefixCol[stackIndex] = newPrefix;
}

const newStackTable = {
...stackTable,
prefix: newPrefixCol,
};

return updateThreadStacks(thread, newStackTable, (oldStack) => {
if (oldStack === null) {
return null;

Check warning on line 991 in src/profile-logic/transforms.js

View check run for this annotation

Codecov / codecov/patch

src/profile-logic/transforms.js#L991

Added line #L991 was not covered by tests
}
const newStackPlusOne = oldStackToNewStackPlusOne[oldStack];
return newStackPlusOne === 0 ? null : newStackPlusOne - 1;
});
}

/**
* Drop any samples that contain the given function.
*/
export function dropFunction(
thread: Thread,
funcIndexToDrop: IndexIntoFuncTable
) {
): Thread {
const { stackTable, frameTable } = thread;

// Go through each stack, and label it as containing the function or not.
Expand Down Expand Up @@ -1774,6 +1910,8 @@
);
case 'merge-function':
return mergeFunction(thread, transform.funcIndex);
case 'merge-unaccounted-native-functions':
return mergeUnaccountedNativeFunctions(thread);
case 'drop-function':
return dropFunction(thread, transform.funcIndex);
case 'focus-function':
Expand Down
64 changes: 64 additions & 0 deletions src/test/store/transforms.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,70 @@ describe('"merge-function" transform', function () {
});
});

describe('"merge-unaccounted-native-functions" transform', function () {
describe('on a call tree', function () {
/**
* Assert this transformation:
*
* A:3,0 A:3,0
* | |
* v merge 0xC,0xE v
* B:3,0 --> B:3,0
* / \ / | \
* v v v v v
* C:2,0 H:1,0 D:1,0 F:1,0 H:1,1
* / \ \ |
* v v v v
* D:1,0 F:1,0 C:1,1 G:1,1
* | |
* v v
* 0xE:1,1 G:1,1
*/
const { profile } = getProfileFromTextSamples(`
A[lib:one][address:20][sym:Asym:20:] A[lib:one][address:21][sym:Asym:20:] A[lib:one][address:20][sym:Asym:20:]
B B B
0xC 0xC H.js
D.js F[lib:two][address:11][sym:Fsym:10:] 0xC
0xE G[lib:one][address:51][sym:Dsym:40:]
`);
const threadIndex = 0;

const { dispatch, getState } = storeWithProfile(profile);
const originalCallTree = selectedThreadSelectors.getCallTree(getState());

it('starts as an unfiltered call tree', function () {
expect(formatTree(originalCallTree)).toEqual([
'- A (total: 3, self: —)',
' - B (total: 3, self: —)',
' - 0xC (total: 2, self: —)',
' - D.js (total: 1, self: —)',
' - 0xE (total: 1, self: 1)',
' - F (total: 1, self: —)',
' - G (total: 1, self: 1)',
' - H.js (total: 1, self: —)',
' - 0xC (total: 1, self: 1)',
]);
});

it('functions 0xC and 0xE are merged into callers', function () {
dispatch(
addTransformToStack(threadIndex, {
type: 'merge-unaccounted-native-functions',
})
);
const callTree = selectedThreadSelectors.getCallTree(getState());
expect(formatTree(callTree)).toEqual([
'- A (total: 3, self: —)',
' - B (total: 3, self: —)',
' - D.js (total: 1, self: 1)',
' - F (total: 1, self: —)',
' - G (total: 1, self: 1)',
' - H.js (total: 1, self: 1)',
]);
});
});
});

describe('"drop-function" transform', function () {
describe('on a call tree', function () {
const {
Expand Down
Loading