blob: 368ab3221c083bfe33f1f206e635245a96b86515 [file] [log] [blame]
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use size file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as m from 'mithril';
import {assertExists, assertTrue} from '../base/logging';
import {Actions} from '../common/actions';
import {
ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
OBJECTS_ALLOCATED_KEY,
OBJECTS_ALLOCATED_NOT_FREED_KEY,
PERF_SAMPLES_KEY,
SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
} from '../common/flamegraph_util';
import {CallsiteInfo, FlamegraphStateViewingOption} from '../common/state';
import {timeToCode} from '../common/time';
import {PerfettoMouseEvent} from './events';
import {Flamegraph, NodeRendering} from './flamegraph';
import {globals} from './globals';
import {showPartialModal} from './modal';
import {Panel, PanelSize} from './panel';
import {debounce} from './rate_limiters';
import {Router} from './router';
import {getCurrentTrace} from './sidebar';
import {convertTraceToPprofAndDownload} from './trace_converter';
interface FlamegraphDetailsPanelAttrs {}
const HEADER_HEIGHT = 30;
enum ProfileType {
NATIVE_HEAP_PROFILE = 'native',
JAVA_HEAP_GRAPH = 'graph',
PERF_SAMPLE = 'perf'
}
function isProfileType(s: string): s is ProfileType {
return Object.values(ProfileType).includes(s as ProfileType);
}
function toProfileType(s: string): ProfileType {
if (!isProfileType(s)) {
throw new Error('Unknown type ${s}');
}
return s;
}
function toSelectedCallsite(c: CallsiteInfo|undefined): string {
if (c !== undefined && c.name !== undefined) {
return c.name;
}
return '(none)';
}
const RENDER_SELF_AND_TOTAL: NodeRendering = {
selfSize: 'Self',
totalSize: 'Total',
};
const RENDER_OBJ_COUNT: NodeRendering = {
selfSize: 'Self objects',
totalSize: 'Subtree objects',
};
export class FlamegraphDetailsPanel extends Panel<FlamegraphDetailsPanelAttrs> {
private profileType?: ProfileType = undefined;
private ts = 0;
private pids: number[] = [];
private flamegraph: Flamegraph = new Flamegraph([]);
private focusRegex = '';
private updateFocusRegexDebounced = debounce(() => {
this.updateFocusRegex();
}, 20);
view() {
const flamegraphDetails = globals.flamegraphDetails;
if (flamegraphDetails && flamegraphDetails.type !== undefined &&
flamegraphDetails.startNs !== undefined &&
flamegraphDetails.durNs !== undefined &&
flamegraphDetails.pids !== undefined &&
flamegraphDetails.upids !== undefined) {
this.profileType = toProfileType(flamegraphDetails.type);
this.ts = flamegraphDetails.durNs;
this.pids = flamegraphDetails.pids;
if (flamegraphDetails.flamegraph) {
this.flamegraph.updateDataIfChanged(
this.nodeRendering(), flamegraphDetails.flamegraph);
}
const height = flamegraphDetails.flamegraph ?
this.flamegraph.getHeight() + HEADER_HEIGHT :
0;
return m(
'.details-panel',
{
onclick: (e: PerfettoMouseEvent) => {
if (this.flamegraph !== undefined) {
this.onMouseClick({y: e.layerY, x: e.layerX});
}
return false;
},
onmousemove: (e: PerfettoMouseEvent) => {
if (this.flamegraph !== undefined) {
this.onMouseMove({y: e.layerY, x: e.layerX});
globals.rafScheduler.scheduleRedraw();
}
},
onmouseout: () => {
if (this.flamegraph !== undefined) {
this.onMouseOut();
}
}
},
this.maybeShowModal(flamegraphDetails.graphIncomplete),
m('.details-panel-heading.flamegraph-profile',
{onclick: (e: MouseEvent) => e.stopPropagation()},
[
m('div.options',
[
m('div.title', this.getTitle()),
this.getViewingOptionButtons(),
]),
m('div.details',
[
m('div.selected',
`Selected function: ${
toSelectedCallsite(
flamegraphDetails.expandedCallsite)}`),
m('div.time',
`Snapshot time: ${timeToCode(flamegraphDetails.durNs)}`),
m('input[type=text][placeholder=Focus]', {
oninput: (e: Event) => {
const target = (e.target as HTMLInputElement);
this.focusRegex = target.value;
this.updateFocusRegexDebounced();
},
// Required to stop hot-key handling:
onkeydown: (e: Event) => e.stopPropagation(),
}),
this.profileType === ProfileType.NATIVE_HEAP_PROFILE ?
m('button.download',
{
onclick: () => {
this.downloadPprof();
}
},
m('i.material-icons', 'file_download'),
'Download profile') :
null
]),
]),
m(`div[style=height:${height}px]`),
);
} else {
return m(
'.details-panel',
m('.details-panel-heading', m('h2', `Flamegraph Profile`)));
}
}
private maybeShowModal(graphIncomplete?: boolean): m.Vnode|undefined {
if (!graphIncomplete || globals.state.flamegraphModalDismissed) {
return undefined;
}
return showPartialModal({
title: 'The flamegraph is incomplete',
content:
m('div',
m('div',
'The current trace does not have a fully formed flamegraph.')),
buttons: [
{
text: 'Show the errors',
primary: true,
id: 'incomplete_graph_show',
action: () => {
Router.navigate('#!/info');
}
},
{
text: 'Skip',
primary: false,
id: 'incomplete_graph_skip',
action: () => {
globals.dispatch(Actions.dismissFlamegraphModal({}));
globals.rafScheduler.scheduleFullRedraw();
}
}
],
});
}
private getTitle(): string {
switch (this.profileType!) {
case ProfileType.NATIVE_HEAP_PROFILE:
return 'Heap Profile:';
case ProfileType.JAVA_HEAP_GRAPH:
return 'Java Heap:';
case ProfileType.PERF_SAMPLE:
return 'Perf sample:';
default:
throw new Error('unknown type');
}
}
private nodeRendering(): NodeRendering {
if (this.profileType === undefined) {
return {};
}
const viewingOption = globals.state.currentFlamegraphState!.viewingOption;
switch (this.profileType) {
case ProfileType.JAVA_HEAP_GRAPH:
if (viewingOption === OBJECTS_ALLOCATED_NOT_FREED_KEY) {
return RENDER_OBJ_COUNT;
} else {
return RENDER_SELF_AND_TOTAL;
}
case ProfileType.NATIVE_HEAP_PROFILE:
case ProfileType.PERF_SAMPLE:
return RENDER_SELF_AND_TOTAL;
default:
throw new Error('unknown type');
}
}
private updateFocusRegex() {
globals.dispatch(Actions.changeFocusFlamegraphState({
focusRegex: this.focusRegex,
}));
}
getViewingOptionButtons(): m.Children {
return m(
'div',
...FlamegraphDetailsPanel.selectViewingOptions(
assertExists(this.profileType)));
}
downloadPprof() {
const engine = Object.values(globals.state.engines)[0];
if (!engine) return;
getCurrentTrace()
.then(file => {
assertTrue(
this.pids.length === 1,
'Native profiles can only contain one pid.');
convertTraceToPprofAndDownload(file, this.pids[0], this.ts);
})
.catch(error => {
throw new Error(`Failed to get current trace ${error}`);
});
}
private changeFlamegraphData() {
const data = globals.flamegraphDetails;
const flamegraphData = data.flamegraph === undefined ? [] : data.flamegraph;
this.flamegraph.updateDataIfChanged(
this.nodeRendering(), flamegraphData, data.expandedCallsite);
}
renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
this.changeFlamegraphData();
const current = globals.state.currentFlamegraphState;
if (current === null) return;
const unit =
current.viewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
current.viewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ?
'B' :
'';
this.flamegraph.draw(ctx, size.width, size.height, 0, HEADER_HEIGHT, unit);
}
onMouseClick({x, y}: {x: number, y: number}): boolean {
const expandedCallsite = this.flamegraph.onMouseClick({x, y});
globals.dispatch(Actions.expandFlamegraphState({expandedCallsite}));
return true;
}
onMouseMove({x, y}: {x: number, y: number}): boolean {
this.flamegraph.onMouseMove({x, y});
return true;
}
onMouseOut() {
this.flamegraph.onMouseOut();
}
private static selectViewingOptions(profileType: ProfileType) {
switch (profileType) {
case ProfileType.PERF_SAMPLE:
return [this.buildButtonComponent(PERF_SAMPLES_KEY, 'samples')];
case ProfileType.JAVA_HEAP_GRAPH:
return [
this.buildButtonComponent(
SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'),
this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'objects')
];
case ProfileType.NATIVE_HEAP_PROFILE:
return [
this.buildButtonComponent(
SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'),
this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'objects'),
this.buildButtonComponent(
ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 'alloc space'),
this.buildButtonComponent(OBJECTS_ALLOCATED_KEY, 'alloc objects')
];
default:
throw new Error(`Unexpected profile type ${profileType}`);
}
}
private static buildButtonComponent(
viewingOption: FlamegraphStateViewingOption, text: string) {
const buttonsClass =
(globals.state.currentFlamegraphState &&
globals.state.currentFlamegraphState.viewingOption === viewingOption) ?
'.chosen' :
'';
return m(
`button${buttonsClass}`,
{
onclick: () => {
globals.dispatch(
Actions.changeViewFlamegraphState({viewingOption}));
}
},
text);
}
}