| // 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 {Actions} from '../common/actions'; |
| import { |
| ALLOC_SPACE_MEMORY_ALLOCATED_KEY, |
| OBJECTS_ALLOCATED_KEY, |
| OBJECTS_ALLOCATED_NOT_FREED_KEY, |
| SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, |
| } from '../common/flamegraph_util'; |
| import { |
| CallsiteInfo, |
| HeapProfileFlamegraphViewingOption |
| } from '../common/state'; |
| import {timeToCode} from '../common/time'; |
| |
| import {PerfettoMouseEvent} from './events'; |
| import {Flamegraph, NodeRendering} from './flamegraph'; |
| import {globals} from './globals'; |
| import {Panel, PanelSize} from './panel'; |
| import {debounce} from './rate_limiters'; |
| |
| interface HeapProfileDetailsPanelAttrs {} |
| |
| const HEADER_HEIGHT = 30; |
| |
| enum ProfileType { |
| NATIVE_HEAP_PROFILE = 'native', |
| JAVA_HEAP_GRAPH = 'graph', |
| } |
| |
| 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 HeapProfileDetailsPanel extends |
| Panel<HeapProfileDetailsPanelAttrs> { |
| private profileType?: ProfileType = undefined; |
| private ts = 0; |
| private pid = 0; |
| private flamegraph: Flamegraph = new Flamegraph([]); |
| private focusRegex = ''; |
| private updateFocusRegexDebounced = debounce(() => { |
| this.updateFocusRegex(); |
| }, 20); |
| |
| view() { |
| const heapDumpInfo = globals.heapProfileDetails; |
| if (heapDumpInfo && heapDumpInfo.type !== undefined && |
| heapDumpInfo.ts !== undefined && heapDumpInfo.tsNs !== undefined && |
| heapDumpInfo.pid !== undefined && heapDumpInfo.upid !== undefined) { |
| this.profileType = toProfileType(heapDumpInfo.type); |
| this.ts = heapDumpInfo.tsNs; |
| this.pid = heapDumpInfo.pid; |
| if (heapDumpInfo.flamegraph) { |
| this.flamegraph.updateDataIfChanged( |
| this.nodeRendering(), heapDumpInfo.flamegraph); |
| } |
| const height = heapDumpInfo.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(); |
| } |
| } |
| }, |
| m('.details-panel-heading.heap-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(heapDumpInfo.expandedCallsite)}`), |
| m('div.time', |
| `Snapshot time: ${timeToCode(heapDumpInfo.ts)}`), |
| 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', `Heap Profile`))); |
| } |
| } |
| |
| private getTitle(): string { |
| switch (this.profileType!) { |
| case ProfileType.NATIVE_HEAP_PROFILE: |
| return 'Heap Profile:'; |
| case ProfileType.JAVA_HEAP_GRAPH: |
| return 'Java Heap:'; |
| default: |
| throw new Error('unknown type'); |
| } |
| } |
| |
| private nodeRendering(): NodeRendering { |
| if (this.profileType === undefined) { |
| return {}; |
| } |
| const viewingOption = |
| globals.state.currentHeapProfileFlamegraph!.viewingOption; |
| switch (this.profileType) { |
| case ProfileType.NATIVE_HEAP_PROFILE: |
| return RENDER_SELF_AND_TOTAL; |
| case ProfileType.JAVA_HEAP_GRAPH: |
| if (viewingOption === OBJECTS_ALLOCATED_NOT_FREED_KEY) { |
| return RENDER_OBJ_COUNT; |
| } else { |
| return RENDER_SELF_AND_TOTAL; |
| } |
| default: |
| throw new Error('unknown type'); |
| } |
| } |
| |
| private updateFocusRegex() { |
| globals.dispatch(Actions.changeFocusHeapProfileFlamegraph({ |
| focusRegex: this.focusRegex, |
| })); |
| } |
| |
| getButtonsClass(button: HeapProfileFlamegraphViewingOption): string { |
| if (globals.state.currentHeapProfileFlamegraph === null) return ''; |
| return globals.state.currentHeapProfileFlamegraph.viewingOption === button ? |
| '.chosen' : |
| ''; |
| } |
| |
| getViewingOptionButtons(): m.Children { |
| const viewingOptions = [ |
| m(`button${this.getButtonsClass(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY)}`, |
| { |
| onclick: () => { |
| this.changeViewingOption(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY); |
| } |
| }, |
| 'space'), |
| m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_NOT_FREED_KEY)}`, |
| { |
| onclick: () => { |
| this.changeViewingOption(OBJECTS_ALLOCATED_NOT_FREED_KEY); |
| } |
| }, |
| 'objects'), |
| ]; |
| |
| if (this.profileType === ProfileType.NATIVE_HEAP_PROFILE) { |
| viewingOptions.push( |
| m(`button${this.getButtonsClass(ALLOC_SPACE_MEMORY_ALLOCATED_KEY)}`, |
| { |
| onclick: () => { |
| this.changeViewingOption(ALLOC_SPACE_MEMORY_ALLOCATED_KEY); |
| } |
| }, |
| 'alloc space'), |
| m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_KEY)}`, |
| { |
| onclick: () => { |
| this.changeViewingOption(OBJECTS_ALLOCATED_KEY); |
| } |
| }, |
| 'alloc objects')); |
| } |
| return m('div', ...viewingOptions); |
| } |
| |
| changeViewingOption(viewingOption: HeapProfileFlamegraphViewingOption) { |
| globals.dispatch(Actions.changeViewHeapProfileFlamegraph({viewingOption})); |
| } |
| |
| downloadPprof() { |
| const engine = Object.values(globals.state.engines)[0]; |
| if (!engine) return; |
| const src = engine.source; |
| globals.dispatch( |
| Actions.convertTraceToPprof({pid: this.pid, ts1: this.ts, src})); |
| } |
| |
| private changeFlamegraphData() { |
| const data = globals.heapProfileDetails; |
| 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.currentHeapProfileFlamegraph; |
| 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.expandHeapProfileFlamegraph({expandedCallsite})); |
| return true; |
| } |
| |
| onMouseMove({x, y}: {x: number, y: number}): boolean { |
| this.flamegraph.onMouseMove({x, y}); |
| return true; |
| } |
| |
| onMouseOut() { |
| this.flamegraph.onMouseOut(); |
| } |
| } |