// 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 { 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); } }