blob: 6e6f550387c1a271b7bbadeed948d2be21e521ea [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 m from 'mithril';
import {findRef} from '../base/dom_utils';
import {assertExists, assertTrue} from '../base/logging';
import {Time} from '../base/time';
import {Actions} from '../common/actions';
import {viewingOptions} from '../common/flamegraph_util';
import {
CallsiteInfo,
FlamegraphStateViewingOption,
ProfileType,
} from '../common/state';
import {profileType} from '../controller/flamegraph_controller';
import {raf} from '../core/raf_scheduler';
import {Button} from '../widgets/button';
import {Icon} from '../widgets/icon';
import {Modal, ModalAttrs} from '../widgets/modal';
import {Popup} from '../widgets/popup';
import {EmptyState} from '../widgets/empty_state';
import {Spinner} from '../widgets/spinner';
import {Flamegraph, NodeRendering} from './flamegraph';
import {globals} from './globals';
import {debounce} from './rate_limiters';
import {Router} from './router';
import {getCurrentTrace} from './sidebar';
import {convertTraceToPprofAndDownload} from './trace_converter';
import {ButtonBar} from '../widgets/button';
import {DurationWidget} from './widgets/duration';
import {DetailsShell} from '../widgets/details_shell';
import {Intent} from '../widgets/common';
const HEADER_HEIGHT = 30;
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 implements m.ClassComponent {
private profileType?: ProfileType = undefined;
private ts = Time.ZERO;
private pids: number[] = [];
private flamegraph: Flamegraph = new Flamegraph([]);
private focusRegex = '';
private updateFocusRegexDebounced = debounce(() => {
this.updateFocusRegex();
}, 20);
private canvas?: HTMLCanvasElement;
view() {
const flamegraphDetails = globals.flamegraphDetails;
if (
flamegraphDetails.type !== undefined &&
flamegraphDetails.start !== undefined &&
flamegraphDetails.dur !== undefined &&
flamegraphDetails.pids !== undefined &&
flamegraphDetails.upids !== undefined
) {
this.profileType = profileType(flamegraphDetails.type);
this.ts = Time.add(flamegraphDetails.start, flamegraphDetails.dur);
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(
'.flamegraph-profile',
this.maybeShowModal(flamegraphDetails.graphIncomplete),
m(
DetailsShell,
{
fillParent: true,
title: m(
'div.title',
this.getTitle(),
this.profileType === ProfileType.MIXED_HEAP_PROFILE &&
m(
Popup,
{
trigger: m(Icon, {icon: 'warning'}),
},
m(
'',
{style: {width: '300px'}},
'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.',
),
),
':',
),
description: this.getViewingOptionButtons(),
buttons: [
m(
'div.selected',
`Selected function: ${toSelectedCallsite(
flamegraphDetails.expandedCallsite,
)}`,
),
m(
'div.time',
`Snapshot time: `,
m(DurationWidget, {dur: flamegraphDetails.dur}),
),
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 ||
this.profileType === ProfileType.JAVA_HEAP_SAMPLES) &&
m(Button, {
icon: 'file_download',
intent: Intent.Primary,
onclick: () => {
this.downloadPprof();
},
}),
],
},
m(
'.flamegraph-content',
flamegraphDetails.graphLoading
? m(
'.loading-container',
m(
EmptyState,
{
icon: 'bar_chart',
title: 'Computing graph ...',
className: 'flamegraph-loading',
},
m(Spinner, {easing: true}),
),
)
: m(`canvas[ref=canvas]`, {
style: `height:${height}px; width:100%`,
onmousemove: (e: MouseEvent) => {
const {offsetX, offsetY} = e;
this.onMouseMove({x: offsetX, y: offsetY});
},
onmouseout: () => {
this.onMouseOut();
},
onclick: (e: MouseEvent) => {
const {offsetX, offsetY} = e;
this.onMouseClick({x: offsetX, y: offsetY});
},
}),
),
),
);
} else {
return m(
'.details-panel',
m('.details-panel-heading', m('h2', `Flamegraph Profile`)),
);
}
}
private maybeShowModal(graphIncomplete?: boolean) {
if (!graphIncomplete || globals.state.flamegraphModalDismissed) {
return undefined;
}
return m(Modal, {
title: 'The flamegraph is incomplete',
vAlign: 'TOP',
content: m(
'div',
'The current trace does not have a fully formed flamegraph',
),
buttons: [
{
text: 'Show the errors',
primary: true,
action: () => Router.navigate('#!/info'),
},
{
text: 'Skip',
action: () => {
globals.dispatch(Actions.dismissFlamegraphModal({}));
raf.scheduleFullRedraw();
},
},
],
} as ModalAttrs);
}
private getTitle(): string {
const profileType = this.profileType!;
switch (profileType) {
case ProfileType.MIXED_HEAP_PROFILE:
return 'Mixed heap profile';
case ProfileType.HEAP_PROFILE:
return 'Heap profile';
case ProfileType.NATIVE_HEAP_PROFILE:
return 'Native heap profile';
case ProfileType.JAVA_HEAP_SAMPLES:
return 'Java heap samples';
case ProfileType.JAVA_HEAP_GRAPH:
return 'Java heap graph';
case ProfileType.PERF_SAMPLE:
return 'Profile';
default:
throw new Error('unknown type');
}
}
private nodeRendering(): NodeRendering {
if (this.profileType === undefined) {
return {};
}
const profileType = this.profileType;
const viewingOption: FlamegraphStateViewingOption =
globals.state.currentFlamegraphState!.viewingOption;
switch (profileType) {
case ProfileType.JAVA_HEAP_GRAPH:
if (
viewingOption ===
FlamegraphStateViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY ||
viewingOption ===
FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY
) {
return RENDER_OBJ_COUNT;
} else {
return RENDER_SELF_AND_TOTAL;
}
case ProfileType.MIXED_HEAP_PROFILE:
case ProfileType.HEAP_PROFILE:
case ProfileType.NATIVE_HEAP_PROFILE:
case ProfileType.JAVA_HEAP_SAMPLES:
case ProfileType.PERF_SAMPLE:
return RENDER_SELF_AND_TOTAL;
default:
const exhaustiveCheck: never = profileType;
throw new Error(`Unhandled case: ${exhaustiveCheck}`);
}
}
private updateFocusRegex() {
globals.dispatch(
Actions.changeFocusFlamegraphState({
focusRegex: this.focusRegex,
}),
);
}
getViewingOptionButtons(): m.Children {
return m(
ButtonBar,
...FlamegraphDetailsPanel.selectViewingOptions(
assertExists(this.profileType),
),
);
}
downloadPprof() {
const engine = globals.getCurrentEngine();
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,
);
}
oncreate({dom}: m.CVnodeDOM) {
this.canvas = FlamegraphDetailsPanel.findCanvasElement(dom);
// TODO(stevegolton): If we truely want to be standalone, then we shouldn't
// rely on someone else calling the rafScheduler when the window is resized,
// but it's good enough for now as we know the ViewerPage will do it.
raf.addRedrawCallback(this.rafRedrawCallback);
}
onupdate({dom}: m.CVnodeDOM) {
this.canvas = FlamegraphDetailsPanel.findCanvasElement(dom);
}
onremove(_vnode: m.CVnodeDOM) {
raf.removeRedrawCallback(this.rafRedrawCallback);
}
private static findCanvasElement(
dom: Element,
): HTMLCanvasElement | undefined {
const canvas = findRef(dom, 'canvas');
if (canvas && canvas instanceof HTMLCanvasElement) {
return canvas;
} else {
return undefined;
}
}
private rafRedrawCallback = () => {
if (this.canvas) {
const canvas = this.canvas;
canvas.width = canvas.offsetWidth * devicePixelRatio;
canvas.height = canvas.offsetHeight * devicePixelRatio;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.scale(devicePixelRatio, devicePixelRatio);
const {offsetWidth: width, offsetHeight: height} = canvas;
this.renderLocalCanvas(ctx, width, height);
ctx.restore();
}
}
};
private renderLocalCanvas(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
) {
this.changeFlamegraphData();
const current = globals.state.currentFlamegraphState;
if (current === null) return;
const unit =
current.viewingOption ===
FlamegraphStateViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
current.viewingOption ===
FlamegraphStateViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY ||
current.viewingOption ===
FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY
? 'B'
: '';
this.flamegraph.draw(ctx, width, height, 0, 0, unit);
}
private onMouseClick({x, y}: {x: number; y: number}): boolean {
const expandedCallsite = this.flamegraph.onMouseClick({x, y});
globals.state.currentFlamegraphState &&
globals.dispatch(
Actions.expandFlamegraphState({
expandedCallsite,
viewingOption: globals.state.currentFlamegraphState.viewingOption,
}),
);
return true;
}
private onMouseMove({x, y}: {x: number; y: number}): boolean {
this.flamegraph.onMouseMove({x, y});
raf.scheduleFullRedraw();
return true;
}
private onMouseOut() {
this.flamegraph.onMouseOut();
raf.scheduleFullRedraw();
}
private static selectViewingOptions(profileType: ProfileType) {
const ret = [];
for (const {option, name} of viewingOptions(profileType)) {
ret.push(this.buildButtonComponent(option, name));
}
return ret;
}
private static buildButtonComponent(
viewingOption: FlamegraphStateViewingOption,
text: string,
) {
const active =
globals.state.currentFlamegraphState !== null &&
globals.state.currentFlamegraphState.viewingOption === viewingOption;
return m(Button, {
label: text,
active,
onclick: () => {
globals.dispatch(Actions.changeViewFlamegraphState({viewingOption}));
},
});
}
}