Skip to content

Commit

Permalink
Add a category breakdown to the sidebar (Merge PR #1627)
Browse files Browse the repository at this point in the history
  • Loading branch information
julienw committed Jan 16, 2019
2 parents 50b15ea + a03219f commit 5cb122c
Show file tree
Hide file tree
Showing 8 changed files with 549 additions and 69 deletions.
62 changes: 56 additions & 6 deletions src/components/sidebar/CallTreeSidebar.js
Expand Up @@ -12,6 +12,7 @@ import {
selectedNodeSelectors,
} from '../../selectors/per-thread';
import { getSelectedThreadIndex } from '../../selectors/url-state';
import { getCategories } from '../../selectors/profile';
import { getFunctionName } from '../../profile-logic/function-info';
import { assertExhaustiveCheck } from '../../utils/flow';
import CanSelectContent from './CanSelectContent';
Expand All @@ -20,28 +21,38 @@ import type {
ConnectedProps,
ExplicitConnectOptions,
} from '../../utils/connect';
import type { ThreadIndex } from '../../types/profile';
import type { ThreadIndex, CategoryList } from '../../types/profile';
import type {
CallNodeTable,
IndexIntoCallNodeTable,
} from '../../types/profile-derived';
import type { Milliseconds } from '../../types/units';
import type {
BreakdownByImplementation,
BreakdownByCategory,
StackImplementation,
TimingsForPath,
} from '../../profile-logic/profile-data';

type SidebarDetailProps = {|
+label: string,
+color?: string,
+children: React.Node,
|};

function SidebarDetail({ label, children }: SidebarDetailProps) {
function SidebarDetail({ label, color, children }: SidebarDetailProps) {
return (
<React.Fragment>
<div className="sidebar-label">{label}:</div>
<div className="sidebar-value">{children}</div>
{color ? (
<div
className={`sidebar-color category-swatch category-color-${color}`}
title={label}
/>
) : (
<div />
)}
</React.Fragment>
);
}
Expand Down Expand Up @@ -99,20 +110,48 @@ class ImplementationBreakdown extends React.PureComponent<
}
}

type CategoryBreakdownProps = {|
+breakdown: BreakdownByCategory,
+categoryList: CategoryList,
|};

class CategoryBreakdown extends React.PureComponent<CategoryBreakdownProps> {
render() {
const { breakdown, categoryList } = this.props;
const data = breakdown
.map((value, categoryIndex) => {
const category = categoryList[categoryIndex];
return {
group: category.name,
value: value || 0,
color: category.color,
};
})
// sort in descending order
.sort(({ value: valueA }, { value: valueB }) => valueB - valueA);

return <Breakdown data={data} />;
}
}

type BreakdownProps = {|
+data: $ReadOnlyArray<{| group: string, value: Milliseconds |}>,
+data: $ReadOnlyArray<{|
group: string,
color?: string,
value: Milliseconds,
|}>,
|};

// This stateless component is responsible for displaying the implementation
// breakdown. It also computes the percentage from the total time.
function Breakdown({ data }: BreakdownProps) {
const totalTime = data.reduce((result, item) => result + item.value, 0);

return data.filter(({ value }) => value).map(({ group, value }) => {
return data.filter(({ value }) => value).map(({ group, color, value }) => {
const percentage = Math.round(value / totalTime * 100);

return (
<SidebarDetail label={group} key={group}>
<SidebarDetail label={group} color={color} key={group}>
{value}ms ({percentage}%)
</SidebarDetail>
);
Expand All @@ -126,13 +165,14 @@ type StateProps = {|
+name: string,
+lib: string,
+timings: TimingsForPath,
+categoryList: CategoryList,
|};

type Props = ConnectedProps<{||}, StateProps, {||}>;

class CallTreeSidebar extends React.PureComponent<Props> {
render() {
const { selectedNodeIndex, name, lib, timings } = this.props;
const { selectedNodeIndex, name, lib, timings, categoryList } = this.props;
const {
forPath: { selfTime, totalTime },
forFunc: { selfTime: selfTimeForFunc, totalTime: totalTimeForFunc },
Expand Down Expand Up @@ -180,6 +220,15 @@ class CallTreeSidebar extends React.PureComponent<Props> {
<SidebarDetail label="Self Time">
{selfTime.value ? `${selfTime.value}ms (${selfTimePercent}%)` : '—'}
</SidebarDetail>
{totalTime.breakdownByCategory ? (
<>
<h4 className="sidebar-title3">Categories</h4>
<CategoryBreakdown
breakdown={totalTime.breakdownByCategory}
categoryList={categoryList}
/>
</>
) : null}
{totalTime.breakdownByImplementation ? (
<React.Fragment>
<h4 className="sidebar-title3">Implementation – running time</h4>
Expand Down Expand Up @@ -237,6 +286,7 @@ const options: ExplicitConnectOptions<{||}, StateProps, {||}> = {
name: getFunctionName(selectedNodeSelectors.getName(state)),
lib: selectedNodeSelectors.getLib(state),
timings: selectedNodeSelectors.getTimingsForSidebar(state),
categoryList: getCategories(state),
}),
component: CallTreeSidebar,
};
Expand Down
5 changes: 3 additions & 2 deletions src/components/sidebar/sidebar.css
Expand Up @@ -13,13 +13,14 @@

.sidebar-calltree .sidebar-contents-wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr 1fr min-content;
grid-gap: 2px 5px;
align-content: start; /* the grid isn't vertically stretched */
align-items: center;
}

.sidebar-titlegroup, .sidebar-title2, .sidebar-title3 {
grid-column-start: span 2;
grid-column-start: span 3;
min-width: 0; /* With this, the cell's minimum size is 0 instead of min-content */
}

Expand Down
65 changes: 56 additions & 9 deletions src/profile-logic/profile-data.js
Expand Up @@ -11,6 +11,7 @@ import type {
FrameTable,
FuncTable,
ResourceTable,
CategoryList,
IndexIntoCategoryList,
IndexIntoFuncTable,
IndexIntoSamplesTable,
Expand Down Expand Up @@ -278,16 +279,19 @@ export function getLeafFuncIndex(path: CallNodePath): IndexIntoFuncTable {
export type JsImplementation = 'interpreter' | 'ion' | 'baseline' | 'unknown';
export type StackImplementation = 'native' | JsImplementation;
export type BreakdownByImplementation = { [StackImplementation]: Milliseconds };
export type BreakdownByCategory = Milliseconds[]; // { [IndexIntoCategoryList]: Milliseconds }
type ItemTimings = {|
selfTime: {|
// time spent excluding children
value: Milliseconds,
breakdownByImplementation: BreakdownByImplementation | null,
breakdownByCategory: BreakdownByCategory | null,
|},
totalTime: {|
// time spent including children
value: Milliseconds,
breakdownByImplementation: BreakdownByImplementation | null,
breakdownByCategory: BreakdownByCategory | null,
|},
|};

Expand Down Expand Up @@ -333,20 +337,37 @@ export function getTimingsForPath(
{ callNodeTable, stackIndexToCallNodeIndex }: CallNodeInfo,
interval: number,
isInvertedTree: boolean,
thread: Thread
thread: Thread,
categories: CategoryList
): TimingsForPath {
if (!needlePath.length) {
// If the path is empty, which shouldn't usually happen, we return an empty
// structure right away.
// The rest of this function's code assumes a non-empty path.
return {
forPath: {
selfTime: { value: 0, breakdownByImplementation: null },
totalTime: { value: 0, breakdownByImplementation: null },
selfTime: {
value: 0,
breakdownByImplementation: null,
breakdownByCategory: null,
},
totalTime: {
value: 0,
breakdownByImplementation: null,
breakdownByCategory: null,
},
},
forFunc: {
selfTime: { value: 0, breakdownByImplementation: null },
totalTime: { value: 0, breakdownByImplementation: null },
selfTime: {
value: 0,
breakdownByImplementation: null,
breakdownByCategory: null,
},
totalTime: {
value: 0,
breakdownByImplementation: null,
breakdownByCategory: null,
},
},
rootTime: 0,
};
Expand All @@ -357,12 +378,28 @@ export function getTimingsForPath(
const needleFuncIndex = getLeafFuncIndex(needlePath);

const pathTimings: ItemTimings = {
selfTime: { value: 0, breakdownByImplementation: null },
totalTime: { value: 0, breakdownByImplementation: null },
selfTime: {
value: 0,
breakdownByImplementation: null,
breakdownByCategory: null,
},
totalTime: {
value: 0,
breakdownByImplementation: null,
breakdownByCategory: null,
},
};
const funcTimings: ItemTimings = {
selfTime: { value: 0, breakdownByImplementation: null },
totalTime: { value: 0, breakdownByImplementation: null },
selfTime: {
value: 0,
breakdownByImplementation: null,
breakdownByCategory: null,
},
totalTime: {
value: 0,
breakdownByImplementation: null,
breakdownByCategory: null,
},
};
let rootTime = 0;

Expand All @@ -374,6 +411,7 @@ export function getTimingsForPath(
function accumulateDataToTimings(
timings: {
breakdownByImplementation: BreakdownByImplementation | null,
breakdownByCategory: BreakdownByCategory | null,
value: number,
},
stackIndex: IndexIntoStackTable,
Expand All @@ -395,6 +433,15 @@ export function getTimingsForPath(
timings.breakdownByImplementation[implementation] = 0;
}
timings.breakdownByImplementation[implementation] += interval;

// step 4: find the category value for this stack
const categoryIndex = stackTable.category[stackIndex];

// step 5: increment the right value in the category breakdown
if (timings.breakdownByCategory === null) {
timings.breakdownByCategory = Array(categories.length).fill(0);
}
timings.breakdownByCategory[categoryIndex] += interval;
}

// Loop over each sample and accumulate the self time, running time, and
Expand Down
17 changes: 2 additions & 15 deletions src/selectors/per-thread/index.js
Expand Up @@ -132,21 +132,8 @@ export const selectedNodeSelectors: NodeSelectors = (() => {
ProfileSelectors.getProfileInterval,
UrlState.getInvertCallstack,
selectedThreadSelectors.getPreviewFilteredThread,
(
selectedPath,
callNodeInfo,
interval,
isInvertedTree,
thread
): TimingsForPath => {
return ProfileData.getTimingsForPath(
selectedPath,
callNodeInfo,
interval,
isInvertedTree,
thread
);
}
ProfileSelectors.getCategories,
ProfileData.getTimingsForPath
);

return {
Expand Down
8 changes: 4 additions & 4 deletions src/test/components/CallTreeSidebar.test.js
Expand Up @@ -21,10 +21,10 @@ import type { CallNodePath } from '../../types/profile-derived';
describe('CallTreeSidebar', function() {
function setup() {
const { profile, funcNamesDictPerThread } = getProfileFromTextSamples(`
A A A A
B B B B
Cjs Cjs H H
D F I
A A A A
B B B B
Cjs Cjs H[cat:Layout] H[cat:Layout]
D F I[cat:Idle]
Ejs Ejs
`);

Expand Down

0 comments on commit 5cb122c

Please sign in to comment.