Skip to content
Merged
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
68 changes: 1 addition & 67 deletions src/actions/profile-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,7 @@ import {
getInvertCallstack,
getHash,
} from 'firefox-profiler/selectors/url-state';
import {
getCallNodePathFromIndex,
getSampleIndexToCallNodeIndex,
getSampleCategories,
findBestAncestorCallNode,
} from 'firefox-profiler/profile-logic/profile-data';
import { getCallNodePathFromIndex } from 'firefox-profiler/profile-logic/profile-data';
import {
assertExhaustiveCheck,
getFirstItemFromSet,
Expand Down Expand Up @@ -206,67 +201,6 @@ export function selectRootCallNode(
};
}

/**
* This function provides a different strategy for selecting call nodes. It selects
* a "best" ancestor call node, but also expands out its children nodes to the
* actual call node that was clicked. See findBestAncestorCallNode for more
* on the "best" call node.
*/
export function selectBestAncestorCallNodeAndExpandCallTree(
threadsKey: ThreadsKey,
sampleIndex: IndexIntoSamplesTable
): ThunkAction<boolean> {
return (dispatch, getState) => {
const threadSelectors = getThreadSelectorsFromThreadsKey(threadsKey);
const fullThread = threadSelectors.getRangeFilteredThread(getState());
const filteredThread = threadSelectors.getFilteredThread(getState());
const unfilteredStack = fullThread.samples.stack[sampleIndex];
const callNodeInfo = threadSelectors.getCallNodeInfo(getState());

if (unfilteredStack === null) {
return false;
}

const { callNodeTable, stackIndexToCallNodeIndex } = callNodeInfo;
const sampleIndexToCallNodeIndex = getSampleIndexToCallNodeIndex(
filteredThread.samples.stack,
stackIndexToCallNodeIndex
);
const clickedCallNode = sampleIndexToCallNodeIndex[sampleIndex];
const clickedCategory = fullThread.stackTable.category[unfilteredStack];

if (clickedCallNode === null) {
return false;
}

const sampleCategories = getSampleCategories(
fullThread.samples,
fullThread.stackTable
);
const bestAncestorCallNode = findBestAncestorCallNode(
callNodeInfo,
sampleIndexToCallNodeIndex,
sampleCategories,
clickedCallNode,
clickedCategory
);

// In one dispatch, change the selected call node to the best ancestor call node, but
// also expand out to the clicked call node.
dispatch(
changeSelectedCallNode(
threadsKey,
// Select the best ancestor call node.
getCallNodePathFromIndex(bestAncestorCallNode, callNodeTable),
// Also expand the children nodes out further below it to what was actually
// clicked.
getCallNodePathFromIndex(clickedCallNode, callNodeTable)
)
);
return true;
};
}

/**
* This selects a set of thread from thread indexes.
* Please use it in tests only.
Expand Down
118 changes: 0 additions & 118 deletions src/profile-logic/profile-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -2447,124 +2447,6 @@ export function getTreeOrderComparator(
};
}

/**
* This is the root-most call node for which, if selected, only the clicked category
* is highlighted in the thread activity graph. In other words, it's the root-most call
* node which only 'contains' samples whose sample category is the clicked category.
*/
export function findBestAncestorCallNode(
callNodeInfo: CallNodeInfo,
sampleCallNodes: Array<IndexIntoCallNodeTable | null>,
sampleCategories: Array<IndexIntoCategoryList | null>,
clickedCallNode: IndexIntoCallNodeTable,
clickedCategory: IndexIntoCategoryList
): IndexIntoCallNodeTable {
const { callNodeTable } = callNodeInfo;
if (callNodeTable.category[clickedCallNode] !== clickedCategory) {
return clickedCallNode;
}

// Compute the callNodesOnSameCategoryPath.
// Given a call node path with some arbitrary categories, e.g. A, B, C
//
// Categories: A -> A -> B -> B -> C -> C -> C
// Node Indexes: 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6

// This loop will select the leaf-most call nodes that match the leaf call-node's
// category. Running the above path through this loop would produce the list:
//
// Categories: [C, C, C]
// Node Indexes: [6, 5, 4] (note the reverse order)
const callNodesOnSameCategoryPath = [clickedCallNode];
let callNode = clickedCallNode;
while (true) {
const parentCallNode = callNodeTable.prefix[callNode];
if (parentCallNode === -1) {
// The entire call path is just clickedCategory.
return clickedCallNode; // TODO: is this a useful behavior?
}
if (callNodeTable.category[parentCallNode] !== clickedCategory) {
break;
}
callNodesOnSameCategoryPath.push(parentCallNode);
callNode = parentCallNode;
}

// Now find the callNode in callNodesOnSameCategoryPath with the lowest depth
// such that selecting it will not highlight any samples whose unfiltered
// category is different from clickedCategory. If no such callNode exists,
// return clickedCallNode.

const clickedDepth = callNodeTable.depth[clickedCallNode];
// The handledCallNodes is used as a Map<CallNodeIndex, bool>.
const handledCallNodes = new Uint8Array(callNodeTable.length);

function limitSameCategoryPathToCommonAncestor(callNode) {
// The callNode argument is the leaf call node of a sample whose sample category is a
// different category than clickedCategory. If callNode's ancestor path crosses
// callNodesOnSameCategoryPath, that implies that callNode would be highlighted
// if we were to select the root-most node in callNodesOnSameCategoryPath.
// If that is the case, we need to truncate callNodesOnSameCategoryPath in such
// a way that the root-most node in that list is no longer an ancestor of callNode.
const walkUpToDepth =
clickedDepth - (callNodesOnSameCategoryPath.length - 1);
let depth = callNodeTable.depth[callNode];

// Go from leaf to root in the call nodes.
while (depth >= walkUpToDepth) {
if (handledCallNodes[callNode]) {
// This call node was already handled. Stop checking.
return;
}
handledCallNodes[callNode] = 1;
if (depth <= clickedDepth) {
// This call node's depth is less than the clicked depth, it needs to be
// checked to see if the call node is in the callNodesOnSameCategoryPath.
if (callNode === callNodesOnSameCategoryPath[clickedDepth - depth]) {
// Remove some of the call nodes, as they are not on the same path.
// This is done by shortening the array length. Keep in mind that this
// array is in the opposite order of a CallNodePath, with the leaf-most
// nodes first, and the root-most last.
callNodesOnSameCategoryPath.length = clickedDepth - depth;
return;
}
}
callNode = callNodeTable.prefix[callNode];
depth--;
}
}

// Go through every sample and look at each sample's call node.
for (let sample = 0; sample < sampleCallNodes.length; sample++) {
if (
sampleCategories[sample] !== clickedCategory &&
sampleCallNodes[sample] !== null
) {
// This sample's category is a different one than the one clicked. Make
// sure to limit the callNodesOnSameCategoryPath to just the call nodes
// that share the same common ancestor.
limitSameCategoryPathToCommonAncestor(sampleCallNodes[sample]);
}
}

if (callNodesOnSameCategoryPath.length > 0) {
// The last call node in this list will be the root-most call node that has
// the same category on the path as the clicked call node.
return callNodesOnSameCategoryPath[callNodesOnSameCategoryPath.length - 1];
}
return clickedCallNode;
}

/**
* Look at the leaf-most stack for every sample, and take its category.
*/
export function getSampleCategories(
samples: SamplesTable,
stackTable: StackTable
): Array<IndexIntoSamplesTable | null> {
return samples.stack.map((s) => (s !== null ? stackTable.category[s] : null));
}

export function getFriendlyStackTypeName(
implementation: StackImplementation
): string {
Expand Down