1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use size file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import * as m from 'mithril'; 16 17import {Actions} from '../common/actions'; 18import { 19 ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 20 OBJECTS_ALLOCATED_KEY, 21 OBJECTS_ALLOCATED_NOT_FREED_KEY, 22 SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 23} from '../common/flamegraph_util'; 24import {HeapProfileFlamegraphViewingOption} from '../common/state'; 25import {timeToCode} from '../common/time'; 26 27import {PerfettoMouseEvent} from './events'; 28import {Flamegraph, NodeRendering} from './flamegraph'; 29import {globals} from './globals'; 30import {Panel, PanelSize} from './panel'; 31import {debounce} from './rate_limiters'; 32 33interface HeapProfileDetailsPanelAttrs {} 34 35const HEADER_HEIGHT = 30; 36 37enum ProfileType { 38 NATIVE_HEAP_PROFILE = 'native', 39 JAVA_HEAP_GRAPH = 'graph', 40} 41 42function isProfileType(s: string): s is ProfileType { 43 return Object.values(ProfileType).includes(s as ProfileType); 44} 45 46function toProfileType(s: string): ProfileType { 47 if (!isProfileType(s)) { 48 throw new Error('Unknown type ${s}'); 49 } 50 return s; 51} 52 53const RENDER_SELF_AND_TOTAL: NodeRendering = { 54 selfSize: 'Self', 55 totalSize: 'Total', 56}; 57const RENDER_OBJ_COUNT: NodeRendering = { 58 selfSize: 'Self objects', 59 totalSize: 'Subtree objects', 60}; 61 62export class HeapProfileDetailsPanel extends 63 Panel<HeapProfileDetailsPanelAttrs> { 64 private profileType?: ProfileType = undefined; 65 private ts = 0; 66 private pid = 0; 67 private flamegraph: Flamegraph = new Flamegraph([]); 68 private focusRegex = ''; 69 private updateFocusRegexDebounced = debounce(() => { 70 this.updateFocusRegex(); 71 }, 20); 72 73 view() { 74 const heapDumpInfo = globals.heapProfileDetails; 75 if (heapDumpInfo && heapDumpInfo.type !== undefined && 76 heapDumpInfo.ts !== undefined && heapDumpInfo.tsNs !== undefined && 77 heapDumpInfo.pid !== undefined && heapDumpInfo.upid !== undefined) { 78 this.profileType = toProfileType(heapDumpInfo.type); 79 this.ts = heapDumpInfo.tsNs; 80 this.pid = heapDumpInfo.pid; 81 if (heapDumpInfo.flamegraph) { 82 this.flamegraph.updateDataIfChanged( 83 this.nodeRendering(), heapDumpInfo.flamegraph); 84 } 85 const height = heapDumpInfo.flamegraph ? 86 this.flamegraph.getHeight() + HEADER_HEIGHT : 87 0; 88 return m( 89 '.details-panel', 90 { 91 onclick: (e: PerfettoMouseEvent) => { 92 if (this.flamegraph !== undefined) { 93 this.onMouseClick({y: e.layerY, x: e.layerX}); 94 } 95 return false; 96 }, 97 onmousemove: (e: PerfettoMouseEvent) => { 98 if (this.flamegraph !== undefined) { 99 this.onMouseMove({y: e.layerY, x: e.layerX}); 100 globals.rafScheduler.scheduleRedraw(); 101 } 102 return false; 103 }, 104 onmouseout: () => { 105 if (this.flamegraph !== undefined) { 106 this.onMouseOut(); 107 } 108 } 109 }, 110 m('.details-panel-heading.heap-profile', 111 {onclick: (e: MouseEvent) => e.stopPropagation()}, 112 [ 113 m('div.options', 114 [ 115 m('div.title', this.getTitle()), 116 this.getViewingOptionButtons(), 117 ]), 118 m('div.details', 119 [ 120 m('div.time', 121 `Snapshot time: ${timeToCode(heapDumpInfo.ts)}`), 122 m('input[type=text][placeholder=Focus]', { 123 oninput: (e: Event) => { 124 const target = (e.target as HTMLInputElement); 125 this.focusRegex = target.value; 126 this.updateFocusRegexDebounced(); 127 }, 128 // Required to stop hot-key handling: 129 onkeydown: (e: Event) => e.stopPropagation(), 130 }), 131 this.profileType === ProfileType.NATIVE_HEAP_PROFILE ? 132 m('button.download', 133 { 134 onclick: () => { 135 this.downloadPprof(); 136 } 137 }, 138 m('i.material-icons', 'file_download'), 139 'Download profile') : 140 null 141 ]), 142 ]), 143 m(`div[style=height:${height}px]`), 144 ); 145 } else { 146 return m( 147 '.details-panel', 148 m('.details-panel-heading', m('h2', `Heap Profile`))); 149 } 150 } 151 152 private getTitle(): string { 153 switch (this.profileType!) { 154 case ProfileType.NATIVE_HEAP_PROFILE: 155 return 'Heap Profile:'; 156 case ProfileType.JAVA_HEAP_GRAPH: 157 return 'Java Heap:'; 158 default: 159 throw new Error('unknown type'); 160 } 161 } 162 163 private nodeRendering(): NodeRendering { 164 if (this.profileType === undefined) { 165 return {}; 166 } 167 const viewingOption = 168 globals.state.currentHeapProfileFlamegraph!.viewingOption; 169 switch (this.profileType) { 170 case ProfileType.NATIVE_HEAP_PROFILE: 171 return RENDER_SELF_AND_TOTAL; 172 case ProfileType.JAVA_HEAP_GRAPH: 173 if (viewingOption === OBJECTS_ALLOCATED_NOT_FREED_KEY) { 174 return RENDER_OBJ_COUNT; 175 } else { 176 return RENDER_SELF_AND_TOTAL; 177 } 178 default: 179 throw new Error('unknown type'); 180 } 181 } 182 183 private updateFocusRegex() { 184 globals.dispatch(Actions.changeFocusHeapProfileFlamegraph({ 185 focusRegex: this.focusRegex, 186 })); 187 } 188 189 getButtonsClass(button: HeapProfileFlamegraphViewingOption): string { 190 if (globals.state.currentHeapProfileFlamegraph === null) return ''; 191 return globals.state.currentHeapProfileFlamegraph.viewingOption === button ? 192 '.chosen' : 193 ''; 194 } 195 196 getViewingOptionButtons(): m.Children { 197 const viewingOptions = [ 198 m(`button${this.getButtonsClass(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY)}`, 199 { 200 onclick: () => { 201 this.changeViewingOption(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY); 202 } 203 }, 204 'space'), 205 m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_NOT_FREED_KEY)}`, 206 { 207 onclick: () => { 208 this.changeViewingOption(OBJECTS_ALLOCATED_NOT_FREED_KEY); 209 } 210 }, 211 'objects'), 212 ]; 213 214 if (this.profileType === ProfileType.NATIVE_HEAP_PROFILE) { 215 viewingOptions.push( 216 m(`button${this.getButtonsClass(ALLOC_SPACE_MEMORY_ALLOCATED_KEY)}`, 217 { 218 onclick: () => { 219 this.changeViewingOption(ALLOC_SPACE_MEMORY_ALLOCATED_KEY); 220 } 221 }, 222 'alloc space'), 223 m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_KEY)}`, 224 { 225 onclick: () => { 226 this.changeViewingOption(OBJECTS_ALLOCATED_KEY); 227 } 228 }, 229 'alloc objects')); 230 } 231 return m('div', ...viewingOptions); 232 } 233 234 changeViewingOption(viewingOption: HeapProfileFlamegraphViewingOption) { 235 globals.dispatch(Actions.changeViewHeapProfileFlamegraph({viewingOption})); 236 } 237 238 downloadPprof() { 239 const engine = Object.values(globals.state.engines)[0]; 240 if (!engine) return; 241 const src = engine.source; 242 globals.dispatch( 243 Actions.convertTraceToPprof({pid: this.pid, ts1: this.ts, src})); 244 } 245 246 private changeFlamegraphData() { 247 const data = globals.heapProfileDetails; 248 const flamegraphData = data.flamegraph === undefined ? [] : data.flamegraph; 249 this.flamegraph.updateDataIfChanged( 250 this.nodeRendering(), flamegraphData, data.expandedCallsite); 251 } 252 253 renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { 254 this.changeFlamegraphData(); 255 const current = globals.state.currentHeapProfileFlamegraph; 256 if (current === null) return; 257 const unit = 258 current.viewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY || 259 current.viewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ? 260 'B' : 261 ''; 262 this.flamegraph.draw(ctx, size.width, size.height, 0, HEADER_HEIGHT, unit); 263 } 264 265 onMouseClick({x, y}: {x: number, y: number}): boolean { 266 const expandedCallsite = this.flamegraph.onMouseClick({x, y}); 267 globals.dispatch(Actions.expandHeapProfileFlamegraph({expandedCallsite})); 268 return true; 269 } 270 271 onMouseMove({x, y}: {x: number, y: number}): boolean { 272 this.flamegraph.onMouseMove({x, y}); 273 return true; 274 } 275 276 onMouseOut() { 277 this.flamegraph.onMouseOut(); 278 } 279} 280