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 {assertExists, assertTrue} from '../base/logging'; 18import {Actions} from '../common/actions'; 19import { 20 ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 21 OBJECTS_ALLOCATED_KEY, 22 OBJECTS_ALLOCATED_NOT_FREED_KEY, 23 PERF_SAMPLES_KEY, 24 SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 25} from '../common/flamegraph_util'; 26import {CallsiteInfo, FlamegraphStateViewingOption} from '../common/state'; 27import {timeToCode} from '../common/time'; 28 29import {PerfettoMouseEvent} from './events'; 30import {Flamegraph, NodeRendering} from './flamegraph'; 31import {globals} from './globals'; 32import {showPartialModal} from './modal'; 33import {Panel, PanelSize} from './panel'; 34import {debounce} from './rate_limiters'; 35import {Router} from './router'; 36import {getCurrentTrace} from './sidebar'; 37import {convertTraceToPprofAndDownload} from './trace_converter'; 38 39interface FlamegraphDetailsPanelAttrs {} 40 41const HEADER_HEIGHT = 30; 42 43enum ProfileType { 44 NATIVE_HEAP_PROFILE = 'native', 45 JAVA_HEAP_GRAPH = 'graph', 46 PERF_SAMPLE = 'perf' 47} 48 49function isProfileType(s: string): s is ProfileType { 50 return Object.values(ProfileType).includes(s as ProfileType); 51} 52 53function toProfileType(s: string): ProfileType { 54 if (!isProfileType(s)) { 55 throw new Error('Unknown type ${s}'); 56 } 57 return s; 58} 59 60function toSelectedCallsite(c: CallsiteInfo|undefined): string { 61 if (c !== undefined && c.name !== undefined) { 62 return c.name; 63 } 64 return '(none)'; 65} 66 67const RENDER_SELF_AND_TOTAL: NodeRendering = { 68 selfSize: 'Self', 69 totalSize: 'Total', 70}; 71const RENDER_OBJ_COUNT: NodeRendering = { 72 selfSize: 'Self objects', 73 totalSize: 'Subtree objects', 74}; 75 76export class FlamegraphDetailsPanel extends Panel<FlamegraphDetailsPanelAttrs> { 77 private profileType?: ProfileType = undefined; 78 private ts = 0; 79 private pids: number[] = []; 80 private flamegraph: Flamegraph = new Flamegraph([]); 81 private focusRegex = ''; 82 private updateFocusRegexDebounced = debounce(() => { 83 this.updateFocusRegex(); 84 }, 20); 85 86 view() { 87 const flamegraphDetails = globals.flamegraphDetails; 88 if (flamegraphDetails && flamegraphDetails.type !== undefined && 89 flamegraphDetails.startNs !== undefined && 90 flamegraphDetails.durNs !== undefined && 91 flamegraphDetails.pids !== undefined && 92 flamegraphDetails.upids !== undefined) { 93 this.profileType = toProfileType(flamegraphDetails.type); 94 this.ts = flamegraphDetails.durNs; 95 this.pids = flamegraphDetails.pids; 96 if (flamegraphDetails.flamegraph) { 97 this.flamegraph.updateDataIfChanged( 98 this.nodeRendering(), flamegraphDetails.flamegraph); 99 } 100 const height = flamegraphDetails.flamegraph ? 101 this.flamegraph.getHeight() + HEADER_HEIGHT : 102 0; 103 return m( 104 '.details-panel', 105 { 106 onclick: (e: PerfettoMouseEvent) => { 107 if (this.flamegraph !== undefined) { 108 this.onMouseClick({y: e.layerY, x: e.layerX}); 109 } 110 return false; 111 }, 112 onmousemove: (e: PerfettoMouseEvent) => { 113 if (this.flamegraph !== undefined) { 114 this.onMouseMove({y: e.layerY, x: e.layerX}); 115 globals.rafScheduler.scheduleRedraw(); 116 } 117 }, 118 onmouseout: () => { 119 if (this.flamegraph !== undefined) { 120 this.onMouseOut(); 121 } 122 } 123 }, 124 this.maybeShowModal(flamegraphDetails.graphIncomplete), 125 m('.details-panel-heading.flamegraph-profile', 126 {onclick: (e: MouseEvent) => e.stopPropagation()}, 127 [ 128 m('div.options', 129 [ 130 m('div.title', this.getTitle()), 131 this.getViewingOptionButtons(), 132 ]), 133 m('div.details', 134 [ 135 m('div.selected', 136 `Selected function: ${ 137 toSelectedCallsite( 138 flamegraphDetails.expandedCallsite)}`), 139 m('div.time', 140 `Snapshot time: ${timeToCode(flamegraphDetails.durNs)}`), 141 m('input[type=text][placeholder=Focus]', { 142 oninput: (e: Event) => { 143 const target = (e.target as HTMLInputElement); 144 this.focusRegex = target.value; 145 this.updateFocusRegexDebounced(); 146 }, 147 // Required to stop hot-key handling: 148 onkeydown: (e: Event) => e.stopPropagation(), 149 }), 150 this.profileType === ProfileType.NATIVE_HEAP_PROFILE ? 151 m('button.download', 152 { 153 onclick: () => { 154 this.downloadPprof(); 155 } 156 }, 157 m('i.material-icons', 'file_download'), 158 'Download profile') : 159 null 160 ]), 161 ]), 162 m(`div[style=height:${height}px]`), 163 ); 164 } else { 165 return m( 166 '.details-panel', 167 m('.details-panel-heading', m('h2', `Flamegraph Profile`))); 168 } 169 } 170 171 172 private maybeShowModal(graphIncomplete?: boolean): m.Vnode|undefined { 173 if (!graphIncomplete || globals.state.flamegraphModalDismissed) { 174 return undefined; 175 } 176 return showPartialModal({ 177 title: 'The flamegraph is incomplete', 178 content: 179 m('div', 180 m('div', 181 'The current trace does not have a fully formed flamegraph.')), 182 buttons: [ 183 { 184 text: 'Show the errors', 185 primary: true, 186 id: 'incomplete_graph_show', 187 action: () => { 188 Router.navigate('#!/info'); 189 } 190 }, 191 { 192 text: 'Skip', 193 primary: false, 194 id: 'incomplete_graph_skip', 195 action: () => { 196 globals.dispatch(Actions.dismissFlamegraphModal({})); 197 globals.rafScheduler.scheduleFullRedraw(); 198 } 199 } 200 ], 201 }); 202 } 203 204 private getTitle(): string { 205 switch (this.profileType!) { 206 case ProfileType.NATIVE_HEAP_PROFILE: 207 return 'Heap Profile:'; 208 case ProfileType.JAVA_HEAP_GRAPH: 209 return 'Java Heap:'; 210 case ProfileType.PERF_SAMPLE: 211 return 'Perf sample:'; 212 default: 213 throw new Error('unknown type'); 214 } 215 } 216 217 private nodeRendering(): NodeRendering { 218 if (this.profileType === undefined) { 219 return {}; 220 } 221 const viewingOption = globals.state.currentFlamegraphState!.viewingOption; 222 switch (this.profileType) { 223 case ProfileType.JAVA_HEAP_GRAPH: 224 if (viewingOption === OBJECTS_ALLOCATED_NOT_FREED_KEY) { 225 return RENDER_OBJ_COUNT; 226 } else { 227 return RENDER_SELF_AND_TOTAL; 228 } 229 case ProfileType.NATIVE_HEAP_PROFILE: 230 case ProfileType.PERF_SAMPLE: 231 return RENDER_SELF_AND_TOTAL; 232 default: 233 throw new Error('unknown type'); 234 } 235 } 236 237 private updateFocusRegex() { 238 globals.dispatch(Actions.changeFocusFlamegraphState({ 239 focusRegex: this.focusRegex, 240 })); 241 } 242 243 getViewingOptionButtons(): m.Children { 244 return m( 245 'div', 246 ...FlamegraphDetailsPanel.selectViewingOptions( 247 assertExists(this.profileType))); 248 } 249 250 downloadPprof() { 251 const engine = Object.values(globals.state.engines)[0]; 252 if (!engine) return; 253 getCurrentTrace() 254 .then(file => { 255 assertTrue( 256 this.pids.length === 1, 257 'Native profiles can only contain one pid.'); 258 convertTraceToPprofAndDownload(file, this.pids[0], this.ts); 259 }) 260 .catch(error => { 261 throw new Error(`Failed to get current trace ${error}`); 262 }); 263 } 264 265 private changeFlamegraphData() { 266 const data = globals.flamegraphDetails; 267 const flamegraphData = data.flamegraph === undefined ? [] : data.flamegraph; 268 this.flamegraph.updateDataIfChanged( 269 this.nodeRendering(), flamegraphData, data.expandedCallsite); 270 } 271 272 renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { 273 this.changeFlamegraphData(); 274 const current = globals.state.currentFlamegraphState; 275 if (current === null) return; 276 const unit = 277 current.viewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY || 278 current.viewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ? 279 'B' : 280 ''; 281 this.flamegraph.draw(ctx, size.width, size.height, 0, HEADER_HEIGHT, unit); 282 } 283 284 onMouseClick({x, y}: {x: number, y: number}): boolean { 285 const expandedCallsite = this.flamegraph.onMouseClick({x, y}); 286 globals.dispatch(Actions.expandFlamegraphState({expandedCallsite})); 287 return true; 288 } 289 290 onMouseMove({x, y}: {x: number, y: number}): boolean { 291 this.flamegraph.onMouseMove({x, y}); 292 return true; 293 } 294 295 onMouseOut() { 296 this.flamegraph.onMouseOut(); 297 } 298 299 private static selectViewingOptions(profileType: ProfileType) { 300 switch (profileType) { 301 case ProfileType.PERF_SAMPLE: 302 return [this.buildButtonComponent(PERF_SAMPLES_KEY, 'samples')]; 303 case ProfileType.JAVA_HEAP_GRAPH: 304 return [ 305 this.buildButtonComponent( 306 SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'), 307 this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'objects') 308 ]; 309 case ProfileType.NATIVE_HEAP_PROFILE: 310 return [ 311 this.buildButtonComponent( 312 SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'), 313 this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'objects'), 314 this.buildButtonComponent( 315 ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 'alloc space'), 316 this.buildButtonComponent(OBJECTS_ALLOCATED_KEY, 'alloc objects') 317 ]; 318 default: 319 throw new Error(`Unexpected profile type ${profileType}`); 320 } 321 } 322 323 private static buildButtonComponent( 324 viewingOption: FlamegraphStateViewingOption, text: string) { 325 const buttonsClass = 326 (globals.state.currentFlamegraphState && 327 globals.state.currentFlamegraphState.viewingOption === viewingOption) ? 328 '.chosen' : 329 ''; 330 return m( 331 `button${buttonsClass}`, 332 { 333 onclick: () => { 334 globals.dispatch( 335 Actions.changeViewFlamegraphState({viewingOption})); 336 } 337 }, 338 text); 339 } 340} 341