Skip to content
Draft
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
158 changes: 126 additions & 32 deletions src/components/calltree/CallTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getScrollToSelectionGeneration,
getFocusCallTreeGeneration,
getPreviewSelection,
getCategories,
} from 'firefox-profiler/selectors/profile';
import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread';
import {
Expand All @@ -37,6 +38,7 @@ import type {
ImplementationFilter,
ThreadsKey,
CallNodeInfo,
CategoryList,
IndexIntoCallNodeTable,
CallNodeDisplayData,
WeightType,
Expand All @@ -54,6 +56,7 @@ type StateProps = {|
+focusCallTreeGeneration: number,
+tree: CallTreeType,
+callNodeInfo: CallNodeInfo,
+categories: CategoryList,
+selectedCallNodeIndex: IndexIntoCallNodeTable | null,
+rightClickedCallNodeIndex: IndexIntoCallNodeTable | null,
+expandedCallNodeIndexes: Array<IndexIntoCallNodeTable | null>,
Expand Down Expand Up @@ -144,27 +147,29 @@ class CallTreeImpl extends PureComponent<Props> {

componentDidMount() {
this.focus();
if (this.props.selectedCallNodeIndex === null) {
this.procureInterestingInitialSelection();
} else if (this._treeView) {
this.maybeProcureInterestingInitialSelection();

if (this.props.selectedCallNodeIndex === null && this._treeView) {
this._treeView.scrollSelectionIntoView();
}
}

componentDidUpdate(prevProps) {
if (
this.props.scrollToSelectionGeneration >
prevProps.scrollToSelectionGeneration
this.props.focusCallTreeGeneration > prevProps.focusCallTreeGeneration
) {
if (this._treeView) {
this._treeView.scrollSelectionIntoView();
}
this.focus();
}

this.maybeProcureInterestingInitialSelection();

if (
this.props.focusCallTreeGeneration > prevProps.focusCallTreeGeneration
this.props.selectedCallNodeIndex !== null &&
this.props.scrollToSelectionGeneration >
prevProps.scrollToSelectionGeneration &&
this._treeView
) {
this.focus();
this._treeView.scrollSelectionIntoView();
}
}

Expand Down Expand Up @@ -228,35 +233,123 @@ class CallTreeImpl extends PureComponent<Props> {
openSourceView(file, 'calltree');
};

procureInterestingInitialSelection() {
maybeProcureInterestingInitialSelection() {
// Expand the heaviest callstack up to a certain depth and select the frame
// at that depth.
const { tree, expandedCallNodeIndexes } = this.props;
const newExpandedCallNodeIndexes = expandedCallNodeIndexes.slice();
const maxInterestingDepth = 17; // scientifically determined
let currentCallNodeIndex = tree.getRoots()[0];
if (currentCallNodeIndex === undefined) {
// This tree is empty.
const {
tree,
expandedCallNodeIndexes,
selectedCallNodeIndex,
callNodeInfo: { callNodeTable },
categories,
} = this.props;

if (selectedCallNodeIndex !== null || expandedCallNodeIndexes.length > 0) {
// Let's not change some existing state.
return;
}
newExpandedCallNodeIndexes.push(currentCallNodeIndex);
for (let i = 0; i < maxInterestingDepth; i++) {
const children = tree.getChildren(currentCallNodeIndex);
if (children.length === 0) {

const newExpandedCallNodeIndexes = [];
// This value is completely arbitrary and looked good on Julien's machine
// when this was implemented. In the future we may want to look at the
// available space instead.
const maxVisibleLines = 70;
const minimalDepth = 10;

const idleCategoryIndex = categories.findIndex(
(category) => category.name === 'Idle'
);

let nodesToVisit = tree.getRoots();
let visibleLinesCount = nodesToVisit.length;

while (nodesToVisit.length) {
const newNodesToVisit = [];
const nonIdleNodes = nodesToVisit.filter(
(nodeIndex) => callNodeTable.category[nodeIndex] !== idleCategoryIndex
);

// Look at the sum of the running times for all non-idle nodes
const sumOfNonIdleRunningTime = nonIdleNodes.reduce(
(sum, nodeIndex) => sum + tree.getNodeData(nodeIndex).total,
0
);
const runningTimeThreshold = sumOfNonIdleRunningTime / 20;

for (let i = 0; i < nonIdleNodes.length; i++) {
const nodeIndex = nonIdleNodes[i];
const nodeData = tree.getNodeData(nodeIndex);

if (nodeData.self > nodeData.total * 0.95) {
// This node doesn't have interesting children, let's move on.
continue;
}

if (i > 0) {
// The first node is always expanded, but let's check whether we
// should open more nodes at this level.
const thisRunningTime = nodeData.total;
if (thisRunningTime < runningTimeThreshold) {
// This node doesn't have a lot of running time compared to all the
// others we consider, let's move to the next one.
continue;
}
}

const children = tree.getChildren(nodeIndex);
const depth = tree.getDepth(nodeIndex);

if (
i !== 0 &&
depth > minimalDepth &&
visibleLinesCount + children.length > maxVisibleLines
) {
// Expanding this node would exceed our budget.
// Let's look at the other nodes in case they have a smaller amount of children.
continue;
}

newExpandedCallNodeIndexes.push(nodeIndex);
newNodesToVisit.push(...children);
visibleLinesCount += children.length;
}

nodesToVisit = newNodesToVisit;
}

if (newExpandedCallNodeIndexes.length > 0) {
// Take care to not trigger a state change if there's nothing to change,
// to avoid infinite render loop.
this._onExpandedCallNodesChange(newExpandedCallNodeIndexes);
}

// Now we want to select the deepest node when following the first
// non-idle children only.
let nodeToSelect = null;
let children = tree.getRoots();
while (true) {
const firstNonIdleChild = children.find(
(nodeIndex) => callNodeTable.category[nodeIndex] !== idleCategoryIndex
);
if (firstNonIdleChild === undefined) {
// No suitable child has been found, let's break out of the loop.
break;
}
currentCallNodeIndex = children[0];
newExpandedCallNodeIndexes.push(currentCallNodeIndex);

// This is a good candidate, until the next iteration at least.
nodeToSelect = firstNonIdleChild;
if (!newExpandedCallNodeIndexes.includes(nodeToSelect)) {
// This is the first node that wasn't expanded in the previous loop,
// then let's stop right here.
break;
}

// Otherwise, let's go deeper.
children = tree.getChildren(firstNonIdleChild);
}
this._onExpandedCallNodesChange(newExpandedCallNodeIndexes);

const category = tree.getDisplayData(currentCallNodeIndex).categoryName;
if (category !== 'Idle') {
// If we selected the call node with a "idle" category, we'd have a
// completely dimmed activity graph because idle stacks are not drawn in
// this graph. Because this isn't probably what the average user wants we
// do it only when the category is something different.
this._onSelectedCallNodeChange(currentCallNodeIndex);

if (nodeToSelect !== null) {
this._onSelectedCallNodeChange(nodeToSelect);
}
}

Expand Down Expand Up @@ -308,6 +401,7 @@ export const CallTree = explicitConnect<{||}, StateProps, DispatchProps>({
focusCallTreeGeneration: getFocusCallTreeGeneration(state),
tree: selectedThreadSelectors.getCallTree(state),
callNodeInfo: selectedThreadSelectors.getCallNodeInfo(state),
categories: getCategories(state),
selectedCallNodeIndex:
selectedThreadSelectors.getSelectedCallNodeIndex(state),
rightClickedCallNodeIndex:
Expand Down
12 changes: 6 additions & 6 deletions src/test/components/ProfileCallTreeView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,14 +283,14 @@ describe('calltree/ProfileCallTreeView', function () {
expect(getRowElement('C')).toHaveClass('isSelected');
});

it('does not select the heaviest stack if it is idle', () => {
it('does select a non-idle stack if the heaviest stack is idle', () => {
const { profile } = getProfileFromTextSamples(`
A A A A A
B C[cat:Idle] C[cat:Idle] C[cat:Idle] D
E E
A A A A A A
B C[cat:Idle] C[cat:Idle] C[cat:Idle] B D
E E F
`);
const { container } = setup(profile);
expect(container.querySelector('.treeViewRow.isSelected')).toBeFalsy();
const { getRowElement } = setup(profile);
expect(getRowElement('E')).toHaveClass('isSelected');
});
});

Expand Down
Loading