Skip to content

Commit

Permalink
Collapse function subtree transform; resolves #545
Browse files Browse the repository at this point in the history
  • Loading branch information
gregtatum committed Jan 24, 2018
1 parent 0df9951 commit baf1757
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 37 deletions.
15 changes: 15 additions & 0 deletions src/components/calltree/ProfileCallTreeContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class ProfileCallTreeContextMenu extends PureComponent<Props> {
const transformType = convertToTransformType(type);
if (transformType) {
this.addTransformToStack(transformType);
return;
}

switch (type) {
Expand Down Expand Up @@ -223,6 +224,13 @@ class ProfileCallTreeContextMenu extends PureComponent<Props> {
});
break;
}
case 'collapse-function-subtree': {
addTransformToStack(threadIndex, {
type: 'collapse-function-subtree',
funcIndex: selectedFunc,
});
break;
}
default:
assertExhaustiveCheck(type);
}
Expand Down Expand Up @@ -339,6 +347,13 @@ class ProfileCallTreeContextMenu extends PureComponent<Props> {
? 'Focus on calls made by this function'
: 'Focus on function'}
</MenuItem>
<MenuItem
onClick={this.handleClick}
data={{ type: 'collapse-function-subtree' }}
>
<span className="profileCallTreeContextMenuIcon profileCallTreeContextMenuIconCollapse" />
{'Collapse function’s subtree across the entire tree'}
</MenuItem>
{nameForResource
? <MenuItem
onClick={this.handleClick}
Expand Down
140 changes: 134 additions & 6 deletions src/profile-logic/transforms.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,20 @@ import type {
} from '../types/profile';
import type { CallNodePath } from '../types/profile-derived';
import type { ImplementationFilter } from '../types/actions';
import type { Transform, TransformStack } from '../types/transforms';
import type {
Transform,
TransformType,
TransformStack,
} from '../types/transforms';

/**
* This file contains the functions and logic for working with and applying transforms
* to profile data.
*/

// Create mappings from a transform name, to a url-friendly short name.
export const TRANSFORM_TO_SHORT_KEY = {};
export const SHORT_KEY_TO_TRANSFORM = {};
const TRANSFORM_TO_SHORT_KEY: { [TransformType]: string } = {};
const SHORT_KEY_TO_TRANSFORM: { [string]: TransformType } = {};
[
'focus-subtree',
'focus-function',
Expand All @@ -40,7 +44,8 @@ export const SHORT_KEY_TO_TRANSFORM = {};
'drop-function',
'collapse-resource',
'collapse-direct-recursion',
].forEach(transform => {
'collapse-function-subtree',
].forEach((transform: TransformType) => {
// This is kind of an awkward switch, but it ensures we've exhaustively checked that
// we have a mapping for every transform.
let shortKey;
Expand All @@ -66,6 +71,9 @@ export const SHORT_KEY_TO_TRANSFORM = {};
case 'collapse-direct-recursion':
shortKey = 'rec';
break;
case 'collapse-function-subtree':
shortKey = 'cfs';
break;
default: {
throw assertExhaustiveCheck(transform);
}
Expand Down Expand Up @@ -94,10 +102,13 @@ export function parseTransforms(stringValue: string = ''): TransformStack {
const tuple = s.split('-');
const shortKey = tuple[0];
const type = convertToTransformType(SHORT_KEY_TO_TRANSFORM[shortKey]);
if (!type) {
if (type === null) {
console.error('Unrecognized transform was passed to the URL.', shortKey);
return;
}
// This switch breaks down each transform into the minimum amount of data needed
// to represent it in the URL. Each transform has slightly different requirements
// as defined in src/types/transforms.js.
switch (type) {
case 'collapse-resource': {
// e.g. "cr-js-325-8"
Expand Down Expand Up @@ -138,8 +149,9 @@ export function parseTransforms(stringValue: string = ''): TransformStack {
break;
}
case 'merge-function':
case 'focus-function':
case 'drop-function':
case 'focus-function': {
case 'collapse-function-subtree': {
// e.g. "mf-325"
const [, funcIndexRaw] = tuple;
const funcIndex = parseInt(funcIndexRaw, 10);
Expand All @@ -164,6 +176,12 @@ export function parseTransforms(stringValue: string = ''): TransformStack {
funcIndex,
});
break;
case 'collapse-function-subtree':
transforms.push({
type: 'collapse-function-subtree',
funcIndex,
});
break;
default:
throw new Error('Unmatched transform.');
}
Expand Down Expand Up @@ -222,9 +240,14 @@ export function stringifyTransforms(transforms: TransformStack = []): string {
'Expected to be able to convert a transform into its short key.'
);
}
// This switch breaks down each transform into shared groups of what data
// they need, as defined in src/types/transforms.js. For instance some transforms
// need only a funcIndex, while some care about the current implemention, or
// other pieces of data.
switch (transform.type) {
case 'merge-function':
case 'drop-function':
case 'collapse-function-subtree':
return `${shortKey}-${transform.funcIndex}`;
case 'focus-function': {
let string = `${shortKey}-${transform.funcIndex}`;
Expand Down Expand Up @@ -290,6 +313,7 @@ export function getTransformLabels(
case 'merge-function':
case 'drop-function':
case 'collapse-direct-recursion':
case 'collapse-function-subtree':
funcIndex = transform.funcIndex;
break;
default:
Expand All @@ -311,6 +335,8 @@ export function getTransformLabels(
return `Drop: ${funcName}`;
case 'collapse-direct-recursion':
return `Collapse recursion: ${funcName}`;
case 'collapse-function-subtree':
return `Collapse subtree: ${funcName}`;
default:
throw assertExhaustiveCheck(transform);
}
Expand Down Expand Up @@ -350,6 +376,11 @@ export function applyTransformToCallNodePath(
transform.funcIndex,
callNodePath
);
case 'collapse-function-subtree':
return _collapseFunctionSubtreeInCallNodePath(
transform.funcIndex,
callNodePath
);
default:
throw assertExhaustiveCheck(transform);
}
Expand Down Expand Up @@ -438,6 +469,14 @@ function _collapseDirectRecursionInCallNodePath(
return newPath;
}

function _collapseFunctionSubtreeInCallNodePath(
funcIndex: IndexIntoFuncTable,
callNodePath: CallNodePath
) {
const index = callNodePath.indexOf(funcIndex);
return index === -1 ? callNodePath : callNodePath.slice(0, index + 1);
}

function _callNodePathHasPrefixPath(
prefixPath: CallNodePath,
callNodePath: CallNodePath
Expand Down Expand Up @@ -470,6 +509,8 @@ export function mergeCallNode(
IndexIntoStackTable | null,
IndexIntoStackTable | null
> = new Map();
// A root stack's prefix will be null. Maintain that relationship from old to new
// stacks by mapping from null to null.
oldStackToNewStack.set(null, null);
const newStackTable = {
length: 0,
Expand Down Expand Up @@ -568,6 +609,8 @@ export function mergeFunction(
IndexIntoStackTable | null,
IndexIntoStackTable | null
> = new Map();
// A root stack's prefix will be null. Maintain that relationship from old to new
// stacks by mapping from null to null.
oldStackToNewStack.set(null, null);
const newStackTable = {
length: 0,
Expand Down Expand Up @@ -688,6 +731,8 @@ export function collapseResource(
const collapsedStacks: Set<IndexIntoStackTable | null> = new Set();
const funcMatchesImplementation = FUNC_MATCHES[implementation];

// A root stack's prefix will be null. Maintain that relationship from old to new
// stacks by mapping from null to null.
oldStackToNewStack.set(null, null);
// A new func and frame will be created on the first stack that is found that includes
// the given resource.
Expand Down Expand Up @@ -815,6 +860,8 @@ export function collapseDirectRecursion(
IndexIntoStackTable | null,
IndexIntoStackTable | null
> = new Map();
// A root stack's prefix will be null. Maintain that relationship from old to new
// stacks by mapping from null to null.
oldStackToNewStack.set(null, null);
const recursiveStacks = new Set();
const newStackTable = {
Expand Down Expand Up @@ -905,6 +952,81 @@ const FUNC_MATCHES = {
},
};

export function collapseFunctionSubtree(
thread: Thread,
funcToCollapse: IndexIntoFuncTable
): Thread {
const { stackTable, frameTable, samples } = thread;
const oldStackToNewStack: Map<
IndexIntoStackTable | null,
IndexIntoStackTable | null
> = new Map();
// A root stack's prefix will be null. Maintain that relationship from old to new
// stacks by mapping from null to null.
oldStackToNewStack.set(null, null);
const collapsedStacks = new Set();
const newStackTable = {
length: 0,
prefix: [],
frame: [],
};

for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) {
const prefix = stackTable.prefix[stackIndex];
if (
// The previous stack was collapsed, this one is collapsed too.
collapsedStacks.has(prefix)
) {
// Only remember that this stack is collapsed.
const newPrefixStackIndex = oldStackToNewStack.get(prefix);
if (newPrefixStackIndex === undefined) {
throw new Error('newPrefixStackIndex cannot be undefined');
}
// Many collapsed stacks will potentially all point to the first stack that used the
// funcToCollapse, so newPrefixStackIndex will potentially be assigned to many
// stacks. This is what actually "collapses" a stack.
oldStackToNewStack.set(stackIndex, newPrefixStackIndex);
collapsedStacks.add(stackIndex);
} else {
// Add this stack.
const newStackIndex = newStackTable.length++;
const newStackPrefix = oldStackToNewStack.get(prefix);
if (newStackPrefix === undefined) {
throw new Error(
'The newStackPrefix must exist because prefix < stackIndex as the StackTable is ordered.'
);
}

const frameIndex = stackTable.frame[stackIndex];
newStackTable.prefix[newStackIndex] = newStackPrefix;
newStackTable.frame[newStackIndex] = frameIndex;
oldStackToNewStack.set(stackIndex, newStackIndex);

// If this is the function to collapse, keep the stack, but note that its children
// should be discarded.
const funcIndex = frameTable.func[frameIndex];
if (funcToCollapse === funcIndex) {
collapsedStacks.add(stackIndex);
}
}
}
const newSamples = Object.assign({}, samples, {
stack: samples.stack.map(oldStack => {
const newStack = oldStackToNewStack.get(oldStack);
if (newStack === undefined) {
throw new Error(
'Converting from the old stack to a new stack cannot be undefined'
);
}
return newStack;
}),
});
return Object.assign({}, thread, {
stackTable: newStackTable,
samples: newSamples,
});
}

/**
* Filter thread to only contain stacks which start with a CallNodePath, and
* only samples with those stacks. The new stacks' roots will be frames whose
Expand All @@ -924,6 +1046,8 @@ export function focusSubtree(
IndexIntoStackTable | null,
IndexIntoStackTable | null
> = new Map();
// A root stack's prefix will be null. Maintain that relationship from old to new
// stacks by mapping from null to null.
oldStackToNewStack.set(null, null);
const newStackTable = {
length: 0,
Expand Down Expand Up @@ -1010,6 +1134,8 @@ export function focusInvertedSubtree(
}

const oldStackToNewStack = new Map();
// A root stack's prefix will be null. Maintain that relationship from old to new
// stacks by mapping from null to null.
oldStackToNewStack.set(null, null);
const newSamples = Object.assign({}, samples, {
stack: samples.stack.map(stackIndex => {
Expand All @@ -1036,6 +1162,8 @@ export function focusFunction(
IndexIntoStackTable | null,
IndexIntoStackTable | null
> = new Map();
// A root stack's prefix will be null. Maintain that relationship from old to new
// stacks by mapping from null to null.
oldStackToNewStack.set(null, null);
const newStackTable = {
length: 0,
Expand Down
14 changes: 9 additions & 5 deletions src/reducers/profile-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import type {
SymbolicationStatus,
ThreadViewOptions,
} from '../types/reducers';
import type { TransformStack } from '../types/transforms';
import type { Transform, TransformStack } from '../types/transforms';

function profile(
state: Profile = ProfileData.getEmptyProfile(),
Expand Down Expand Up @@ -512,9 +512,8 @@ export const selectorsForThread = (
return ProfileData.filterThreadToRange(thread, start, end);
}
);
const applyTransform = (thread, transform) => {
const { type } = transform;
switch (type) {
const applyTransform = (thread: Thread, transform: Transform) => {
switch (transform.type) {
case 'focus-subtree':
return transform.inverted
? Transforms.focusInvertedSubtree(
Expand Down Expand Up @@ -551,8 +550,13 @@ export const selectorsForThread = (
transform.funcIndex,
transform.implementation
);
case 'collapse-function-subtree':
return Transforms.collapseFunctionSubtree(
thread,
transform.funcIndex
);
default:
throw assertExhaustiveCheck(type);
throw assertExhaustiveCheck(transform);
}
};
// It becomes very expensive to apply each transform over and over again as they
Expand Down
9 changes: 9 additions & 0 deletions src/test/fixtures/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,12 @@ export function formatTree(
);
}, previousString);
}

/**
* Formatting a tree like this allows the assertions to not be as snapshots.
* This makes it easier to debug and read tests, while still giving nice test output
* when they fail.
*/
export function formatTreeAsArray(callTree: CallTree): string[] {
return formatTree(callTree).split('\n').filter(a => a);
}
Loading

0 comments on commit baf1757

Please sign in to comment.