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 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 { 27 CallsiteInfo, 28 FlamegraphStateViewingOption, 29 ProfileType, 30} from '../common/state'; 31import {tpTimeToCode} from '../common/time'; 32import {profileType} from '../controller/flamegraph_controller'; 33 34import {Flamegraph, NodeRendering} from './flamegraph'; 35import {globals} from './globals'; 36import {Modal, ModalDefinition} from './modal'; 37import {Panel, PanelSize} from './panel'; 38import {debounce} from './rate_limiters'; 39import {Router} from './router'; 40import {getCurrentTrace} from './sidebar'; 41import {convertTraceToPprofAndDownload} from './trace_converter'; 42import {Button} from './widgets/button'; 43import {findRef} from './widgets/utils'; 44 45interface FlamegraphDetailsPanelAttrs {} 46 47const HEADER_HEIGHT = 30; 48 49function toSelectedCallsite(c: CallsiteInfo|undefined): string { 50 if (c !== undefined && c.name !== undefined) { 51 return c.name; 52 } 53 return '(none)'; 54} 55 56const RENDER_SELF_AND_TOTAL: NodeRendering = { 57 selfSize: 'Self', 58 totalSize: 'Total', 59}; 60const RENDER_OBJ_COUNT: NodeRendering = { 61 selfSize: 'Self objects', 62 totalSize: 'Subtree objects', 63}; 64 65export class FlamegraphDetailsPanel extends Panel<FlamegraphDetailsPanelAttrs> { 66 private profileType?: ProfileType = undefined; 67 private ts = 0n; 68 private pids: number[] = []; 69 private flamegraph: Flamegraph = new Flamegraph([]); 70 private focusRegex = ''; 71 private updateFocusRegexDebounced = debounce(() => { 72 this.updateFocusRegex(); 73 }, 20); 74 private canvas?: HTMLCanvasElement; 75 76 view() { 77 const flamegraphDetails = globals.flamegraphDetails; 78 if (flamegraphDetails && flamegraphDetails.type !== undefined && 79 flamegraphDetails.start !== undefined && 80 flamegraphDetails.dur !== undefined && 81 flamegraphDetails.pids !== undefined && 82 flamegraphDetails.upids !== undefined) { 83 this.profileType = profileType(flamegraphDetails.type); 84 this.ts = flamegraphDetails.start + flamegraphDetails.dur; 85 this.pids = flamegraphDetails.pids; 86 if (flamegraphDetails.flamegraph) { 87 this.flamegraph.updateDataIfChanged( 88 this.nodeRendering(), flamegraphDetails.flamegraph); 89 } 90 const height = flamegraphDetails.flamegraph ? 91 this.flamegraph.getHeight() + HEADER_HEIGHT : 92 0; 93 return m( 94 '.details-panel', 95 this.maybeShowModal(flamegraphDetails.graphIncomplete), 96 m('.details-panel-heading.flamegraph-profile', 97 {onclick: (e: MouseEvent) => e.stopPropagation()}, 98 [ 99 m('div.options', 100 [ 101 m('div.title', this.getTitle()), 102 this.getViewingOptionButtons(), 103 ]), 104 m('div.details', 105 [ 106 m('div.selected', 107 `Selected function: ${ 108 toSelectedCallsite( 109 flamegraphDetails.expandedCallsite)}`), 110 m('div.time', 111 `Snapshot time: ${tpTimeToCode(flamegraphDetails.dur)}`), 112 m('input[type=text][placeholder=Focus]', { 113 oninput: (e: Event) => { 114 const target = (e.target as HTMLInputElement); 115 this.focusRegex = target.value; 116 this.updateFocusRegexDebounced(); 117 }, 118 // Required to stop hot-key handling: 119 onkeydown: (e: Event) => e.stopPropagation(), 120 }), 121 (this.profileType === ProfileType.NATIVE_HEAP_PROFILE || 122 this.profileType === ProfileType.JAVA_HEAP_SAMPLES) && 123 m(Button, { 124 icon: 'file_download', 125 onclick: () => { 126 this.downloadPprof(); 127 }, 128 }), 129 ]), 130 ]), 131 m(`canvas[ref=canvas]`, { 132 style: `height:${height}px; width:100%`, 133 onmousemove: (e: MouseEvent) => { 134 const {offsetX, offsetY} = e; 135 this.onMouseMove({x: offsetX, y: offsetY}); 136 }, 137 onmouseout: () => { 138 this.onMouseOut(); 139 }, 140 onclick: (e: MouseEvent) => { 141 const {offsetX, offsetY} = e; 142 this.onMouseClick({x: offsetX, y: offsetY}); 143 }, 144 }), 145 ); 146 } else { 147 return m( 148 '.details-panel', 149 m('.details-panel-heading', m('h2', `Flamegraph Profile`))); 150 } 151 } 152 153 154 private maybeShowModal(graphIncomplete?: boolean) { 155 if (!graphIncomplete || globals.state.flamegraphModalDismissed) { 156 return undefined; 157 } 158 return m(Modal, { 159 title: 'The flamegraph is incomplete', 160 vAlign: 'TOP', 161 content: m('div', 162 'The current trace does not have a fully formed flamegraph'), 163 buttons: [ 164 { 165 text: 'Show the errors', 166 primary: true, 167 action: () => Router.navigate('#!/info'), 168 }, 169 { 170 text: 'Skip', 171 action: () => { 172 globals.dispatch(Actions.dismissFlamegraphModal({})); 173 globals.rafScheduler.scheduleFullRedraw(); 174 }, 175 }, 176 ], 177 } as ModalDefinition); 178 } 179 180 private getTitle(): string { 181 switch (this.profileType!) { 182 case ProfileType.HEAP_PROFILE: 183 return 'Heap profile:'; 184 case ProfileType.NATIVE_HEAP_PROFILE: 185 return 'Native heap profile:'; 186 case ProfileType.JAVA_HEAP_SAMPLES: 187 return 'Java heap samples:'; 188 case ProfileType.JAVA_HEAP_GRAPH: 189 return 'Java heap graph:'; 190 case ProfileType.PERF_SAMPLE: 191 return 'Profile:'; 192 default: 193 throw new Error('unknown type'); 194 } 195 } 196 197 private nodeRendering(): NodeRendering { 198 if (this.profileType === undefined) { 199 return {}; 200 } 201 const viewingOption = globals.state.currentFlamegraphState!.viewingOption; 202 switch (this.profileType) { 203 case ProfileType.JAVA_HEAP_GRAPH: 204 if (viewingOption === OBJECTS_ALLOCATED_NOT_FREED_KEY) { 205 return RENDER_OBJ_COUNT; 206 } else { 207 return RENDER_SELF_AND_TOTAL; 208 } 209 case ProfileType.HEAP_PROFILE: 210 case ProfileType.NATIVE_HEAP_PROFILE: 211 case ProfileType.JAVA_HEAP_SAMPLES: 212 case ProfileType.PERF_SAMPLE: 213 return RENDER_SELF_AND_TOTAL; 214 default: 215 throw new Error('unknown type'); 216 } 217 } 218 219 private updateFocusRegex() { 220 globals.dispatch(Actions.changeFocusFlamegraphState({ 221 focusRegex: this.focusRegex, 222 })); 223 } 224 225 getViewingOptionButtons(): m.Children { 226 return m( 227 'div', 228 ...FlamegraphDetailsPanel.selectViewingOptions( 229 assertExists(this.profileType))); 230 } 231 232 downloadPprof() { 233 const engine = globals.getCurrentEngine(); 234 if (!engine) return; 235 getCurrentTrace() 236 .then((file) => { 237 assertTrue( 238 this.pids.length === 1, 239 'Native profiles can only contain one pid.'); 240 convertTraceToPprofAndDownload(file, this.pids[0], this.ts); 241 }) 242 .catch((error) => { 243 throw new Error(`Failed to get current trace ${error}`); 244 }); 245 } 246 247 private changeFlamegraphData() { 248 const data = globals.flamegraphDetails; 249 const flamegraphData = data.flamegraph === undefined ? [] : data.flamegraph; 250 this.flamegraph.updateDataIfChanged( 251 this.nodeRendering(), flamegraphData, data.expandedCallsite); 252 } 253 254 oncreate({dom}: m.CVnodeDOM<FlamegraphDetailsPanelAttrs>) { 255 this.canvas = FlamegraphDetailsPanel.findCanvasElement(dom); 256 // TODO(stevegolton): If we truely want to be standalone, then we shouldn't 257 // rely on someone else calling the rafScheduler when the window is resized, 258 // but it's good enough for now as we know the ViewerPage will do it. 259 globals.rafScheduler.addRedrawCallback(this.rafRedrawCallback); 260 } 261 262 onupdate({dom}: m.CVnodeDOM<FlamegraphDetailsPanelAttrs>) { 263 this.canvas = FlamegraphDetailsPanel.findCanvasElement(dom); 264 } 265 266 onremove(_vnode: m.CVnodeDOM<FlamegraphDetailsPanelAttrs>) { 267 globals.rafScheduler.removeRedrawCallback(this.rafRedrawCallback); 268 } 269 270 private static findCanvasElement(dom: Element): HTMLCanvasElement|undefined { 271 const canvas = findRef(dom, 'canvas'); 272 if (canvas && canvas instanceof HTMLCanvasElement) { 273 return canvas; 274 } else { 275 return undefined; 276 } 277 } 278 279 private rafRedrawCallback = () => { 280 if (this.canvas) { 281 const canvas = this.canvas; 282 canvas.width = canvas.offsetWidth * devicePixelRatio; 283 canvas.height = canvas.offsetHeight * devicePixelRatio; 284 const ctx = canvas.getContext('2d'); 285 if (ctx) { 286 ctx.clearRect(0, 0, canvas.width, canvas.height); 287 ctx.save(); 288 ctx.scale(devicePixelRatio, devicePixelRatio); 289 const {offsetWidth: width, offsetHeight: height} = canvas; 290 this.renderLocalCanvas(ctx, {width, height}); 291 ctx.restore(); 292 } 293 } 294 }; 295 296 renderCanvas() { 297 // No-op 298 } 299 300 private renderLocalCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { 301 this.changeFlamegraphData(); 302 const current = globals.state.currentFlamegraphState; 303 if (current === null) return; 304 const unit = 305 current.viewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY || 306 current.viewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ? 307 'B' : 308 ''; 309 this.flamegraph.draw(ctx, size.width, size.height, 0, 0, unit); 310 } 311 312 private onMouseClick({x, y}: {x: number, y: number}): boolean { 313 const expandedCallsite = this.flamegraph.onMouseClick({x, y}); 314 globals.dispatch(Actions.expandFlamegraphState({expandedCallsite})); 315 return true; 316 } 317 318 private onMouseMove({x, y}: {x: number, y: number}): boolean { 319 this.flamegraph.onMouseMove({x, y}); 320 globals.rafScheduler.scheduleFullRedraw(); 321 return true; 322 } 323 324 private onMouseOut() { 325 this.flamegraph.onMouseOut(); 326 globals.rafScheduler.scheduleFullRedraw(); 327 } 328 329 private static selectViewingOptions(profileType: ProfileType) { 330 switch (profileType) { 331 case ProfileType.PERF_SAMPLE: 332 return [this.buildButtonComponent(PERF_SAMPLES_KEY, 'Samples')]; 333 case ProfileType.JAVA_HEAP_GRAPH: 334 return [ 335 this.buildButtonComponent( 336 SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'Size'), 337 this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'Objects'), 338 ]; 339 case ProfileType.HEAP_PROFILE: 340 return [ 341 this.buildButtonComponent( 342 SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'Unreleased size'), 343 this.buildButtonComponent( 344 OBJECTS_ALLOCATED_NOT_FREED_KEY, 'Unreleased count'), 345 this.buildButtonComponent( 346 ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 'Total size'), 347 this.buildButtonComponent(OBJECTS_ALLOCATED_KEY, 'Total count'), 348 ]; 349 case ProfileType.NATIVE_HEAP_PROFILE: 350 return [ 351 this.buildButtonComponent( 352 SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'Unreleased malloc size'), 353 this.buildButtonComponent( 354 OBJECTS_ALLOCATED_NOT_FREED_KEY, 'Unreleased malloc count'), 355 this.buildButtonComponent( 356 ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 'Total malloc size'), 357 this.buildButtonComponent( 358 OBJECTS_ALLOCATED_KEY, 'Total malloc count'), 359 ]; 360 case ProfileType.JAVA_HEAP_SAMPLES: 361 return [ 362 this.buildButtonComponent( 363 ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 'Total allocation size'), 364 this.buildButtonComponent( 365 OBJECTS_ALLOCATED_KEY, 'Total allocation count'), 366 ]; 367 default: 368 throw new Error(`Unexpected profile type ${profileType}`); 369 } 370 } 371 372 private static buildButtonComponent( 373 viewingOption: FlamegraphStateViewingOption, text: string) { 374 const active = 375 (globals.state.currentFlamegraphState !== null && 376 globals.state.currentFlamegraphState.viewingOption === viewingOption); 377 return m(Button, { 378 label: text, 379 active, 380 minimal: true, 381 onclick: () => { 382 globals.dispatch(Actions.changeViewFlamegraphState({viewingOption})); 383 }, 384 }); 385 } 386} 387