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, {Vnode} from 'mithril'; 16 17import {findRef} from '../base/dom_utils'; 18import {assertExists, assertTrue} from '../base/logging'; 19import {Duration, time} from '../base/time'; 20import {Actions} from '../common/actions'; 21import { 22 CallsiteInfo, 23 FlamegraphViewingOption, 24 defaultViewingOption, 25 expandCallsites, 26 findRootSize, 27 mergeCallsites, 28 viewingOptions, 29} from '../common/flamegraph_util'; 30import {ProfileType} from '../common/state'; 31import {raf} from '../core/raf_scheduler'; 32import {Button} from '../widgets/button'; 33import {Icon} from '../widgets/icon'; 34import {Modal, ModalAttrs} from '../widgets/modal'; 35import {Popup} from '../widgets/popup'; 36import {EmptyState} from '../widgets/empty_state'; 37import {Spinner} from '../widgets/spinner'; 38 39import {Flamegraph, NodeRendering} from './flamegraph'; 40import {globals} from './globals'; 41import {debounce} from './rate_limiters'; 42import {Router} from './router'; 43import {ButtonBar} from '../widgets/button'; 44import {DurationWidget} from './widgets/duration'; 45import {DetailsShell} from '../widgets/details_shell'; 46import {Intent} from '../widgets/common'; 47import {Engine, NUM, STR} from '../public'; 48import {Monitor} from '../base/monitor'; 49import {arrayEquals} from '../base/array_utils'; 50import {getCurrentTrace} from './sidebar'; 51import {convertTraceToPprofAndDownload} from './trace_converter'; 52import {AsyncLimiter} from '../base/async_limiter'; 53import {FlamegraphCache} from '../core/flamegraph_cache'; 54 55const HEADER_HEIGHT = 30; 56 57export function profileType(s: string): ProfileType { 58 if (isProfileType(s)) { 59 return s; 60 } 61 if (s.startsWith('heap_profile')) { 62 return ProfileType.HEAP_PROFILE; 63 } 64 throw new Error('Unknown type ${s}'); 65} 66 67function isProfileType(s: string): s is ProfileType { 68 return Object.values(ProfileType).includes(s as ProfileType); 69} 70 71function getFlamegraphType(type: ProfileType) { 72 switch (type) { 73 case ProfileType.HEAP_PROFILE: 74 case ProfileType.MIXED_HEAP_PROFILE: 75 case ProfileType.NATIVE_HEAP_PROFILE: 76 case ProfileType.JAVA_HEAP_SAMPLES: 77 return 'native'; 78 case ProfileType.JAVA_HEAP_GRAPH: 79 return 'graph'; 80 case ProfileType.PERF_SAMPLE: 81 return 'perf'; 82 default: 83 const exhaustiveCheck: never = type; 84 throw new Error(`Unhandled case: ${exhaustiveCheck}`); 85 } 86} 87 88const HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS = [ 89 FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY, 90 FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY, 91] as const; 92 93export type HeapGraphDominatorTreeViewingOption = 94 (typeof HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS)[number]; 95 96export function isHeapGraphDominatorTreeViewingOption( 97 option: FlamegraphViewingOption, 98): option is HeapGraphDominatorTreeViewingOption { 99 return ( 100 HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS as readonly FlamegraphViewingOption[] 101 ).includes(option); 102} 103 104const MIN_PIXEL_DISPLAYED = 1; 105 106function toSelectedCallsite(c: CallsiteInfo | undefined): string { 107 if (c !== undefined && c.name !== undefined) { 108 return c.name; 109 } 110 return '(none)'; 111} 112 113const RENDER_SELF_AND_TOTAL: NodeRendering = { 114 selfSize: 'Self', 115 totalSize: 'Total', 116}; 117const RENDER_OBJ_COUNT: NodeRendering = { 118 selfSize: 'Self objects', 119 totalSize: 'Subtree objects', 120}; 121 122export interface FlamegraphSelectionParams { 123 readonly profileType: ProfileType; 124 readonly upids: number[]; 125 readonly start: time; 126 readonly end: time; 127} 128 129interface FlamegraphDetailsPanelAttrs { 130 cache: FlamegraphCache; 131 selection: FlamegraphSelectionParams; 132} 133 134interface FlamegraphResult { 135 queryResults: ReadonlyArray<CallsiteInfo>; 136 incomplete: boolean; 137 renderResults?: ReadonlyArray<CallsiteInfo>; 138} 139 140interface FlamegraphState { 141 selection: FlamegraphSelectionParams; 142 viewingOption: FlamegraphViewingOption; 143 focusRegex: string; 144 result?: FlamegraphResult; 145 selectedCallsites: Readonly<{ 146 [key: string]: CallsiteInfo | undefined; 147 }>; 148} 149 150export class FlamegraphDetailsPanel 151 implements m.ClassComponent<FlamegraphDetailsPanelAttrs> 152{ 153 private undebouncedFocusRegex = ''; 154 private updateFocusRegexDebounced = debounce(() => { 155 if (this.state === undefined) { 156 return; 157 } 158 this.state.focusRegex = this.undebouncedFocusRegex; 159 raf.scheduleFullRedraw(); 160 }, 20); 161 162 private flamegraph: Flamegraph = new Flamegraph([]); 163 private queryLimiter = new AsyncLimiter(); 164 165 private state?: FlamegraphState; 166 private queryMonitor = new Monitor([ 167 () => this.state?.selection, 168 () => this.state?.focusRegex, 169 () => this.state?.viewingOption, 170 ]); 171 private selectedCallsitesMonitor = new Monitor([ 172 () => this.state?.selection, 173 () => this.state?.focusRegex, 174 ]); 175 private renderResultMonitor = new Monitor([ 176 () => this.state?.result?.queryResults, 177 () => this.state?.selectedCallsites, 178 ]); 179 180 view({attrs}: Vnode<FlamegraphDetailsPanelAttrs>) { 181 if (attrs.selection === undefined) { 182 this.state = undefined; 183 } else if ( 184 attrs.selection.profileType !== this.state?.selection.profileType || 185 attrs.selection.start !== this.state.selection.start || 186 attrs.selection.end !== this.state.selection.end || 187 !arrayEquals(attrs.selection.upids, this.state.selection.upids) 188 ) { 189 this.state = { 190 selection: attrs.selection, 191 focusRegex: '', 192 viewingOption: defaultViewingOption(attrs.selection.profileType), 193 selectedCallsites: {}, 194 }; 195 } 196 if (this.state === undefined) { 197 return m( 198 '.details-panel', 199 m('.details-panel-heading', m('h2', `Flamegraph Profile`)), 200 ); 201 } 202 203 if (this.queryMonitor.ifStateChanged()) { 204 this.state.result = undefined; 205 const state = this.state; 206 this.queryLimiter.schedule(() => { 207 return FlamegraphDetailsPanel.fetchQueryResults( 208 assertExists(this.getCurrentEngine()), 209 attrs.cache, 210 state, 211 ); 212 }); 213 } 214 215 if (this.selectedCallsitesMonitor.ifStateChanged()) { 216 this.state.selectedCallsites = {}; 217 } 218 219 if ( 220 this.renderResultMonitor.ifStateChanged() && 221 this.state.result !== undefined 222 ) { 223 const selected = this.state.selectedCallsites[this.state.viewingOption]; 224 const expanded = expandCallsites( 225 this.state.result.queryResults, 226 selected?.id ?? -1, 227 ); 228 this.state.result.renderResults = mergeCallsites( 229 expanded, 230 FlamegraphDetailsPanel.getMinSizeDisplayed( 231 expanded, 232 selected?.totalSize, 233 ), 234 ); 235 } 236 237 let height: number | undefined; 238 if (this.state.result?.renderResults !== undefined) { 239 this.flamegraph.updateDataIfChanged( 240 this.nodeRendering(), 241 this.state.result.renderResults, 242 this.state.selectedCallsites[this.state.viewingOption], 243 ); 244 height = this.flamegraph.getHeight() + HEADER_HEIGHT; 245 } else { 246 height = undefined; 247 } 248 249 return m( 250 '.flamegraph-profile', 251 this.maybeShowModal(), 252 m( 253 DetailsShell, 254 { 255 fillParent: true, 256 title: m( 257 'div.title', 258 this.getTitle(), 259 this.state.selection.profileType === 260 ProfileType.MIXED_HEAP_PROFILE && 261 m( 262 Popup, 263 { 264 trigger: m(Icon, {icon: 'warning'}), 265 }, 266 m( 267 '', 268 {style: {width: '300px'}}, 269 'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.', 270 ), 271 ), 272 ':', 273 ), 274 description: this.getViewingOptionButtons(), 275 buttons: [ 276 m( 277 'div.selected', 278 `Selected function: ${toSelectedCallsite( 279 this.state.selectedCallsites[this.state.viewingOption], 280 )}`, 281 ), 282 m( 283 'div.time', 284 `Snapshot time: `, 285 m(DurationWidget, { 286 dur: this.state.selection.end - this.state.selection.start, 287 }), 288 ), 289 m('input[type=text][placeholder=Focus]', { 290 oninput: (e: Event) => { 291 const target = e.target as HTMLInputElement; 292 this.undebouncedFocusRegex = target.value; 293 this.updateFocusRegexDebounced(); 294 }, 295 // Required to stop hot-key handling: 296 onkeydown: (e: Event) => e.stopPropagation(), 297 }), 298 (this.state.selection.profileType === 299 ProfileType.NATIVE_HEAP_PROFILE || 300 this.state.selection.profileType === 301 ProfileType.JAVA_HEAP_SAMPLES) && 302 m(Button, { 303 icon: 'file_download', 304 intent: Intent.Primary, 305 onclick: () => { 306 this.downloadPprof(); 307 raf.scheduleFullRedraw(); 308 }, 309 }), 310 ], 311 }, 312 m( 313 '.flamegraph-content', 314 this.state.result === undefined 315 ? m( 316 '.loading-container', 317 m( 318 EmptyState, 319 { 320 icon: 'bar_chart', 321 title: 'Computing graph ...', 322 className: 'flamegraph-loading', 323 }, 324 m(Spinner, {easing: true}), 325 ), 326 ) 327 : m(`canvas[ref=canvas]`, { 328 style: `height:${height}px; width:100%`, 329 onmousemove: (e: MouseEvent) => { 330 const {offsetX, offsetY} = e; 331 this.flamegraph.onMouseMove({x: offsetX, y: offsetY}); 332 raf.scheduleFullRedraw(); 333 }, 334 onmouseout: () => { 335 this.flamegraph.onMouseOut(); 336 raf.scheduleFullRedraw(); 337 }, 338 onclick: (e: MouseEvent) => { 339 if ( 340 this.state === undefined || 341 this.state.result === undefined 342 ) { 343 return; 344 } 345 const {offsetX, offsetY} = e; 346 const cs = {...this.state.selectedCallsites}; 347 cs[this.state.viewingOption] = this.flamegraph.onMouseClick({ 348 x: offsetX, 349 y: offsetY, 350 }); 351 this.state.selectedCallsites = cs; 352 raf.scheduleFullRedraw(); 353 }, 354 }), 355 ), 356 ), 357 ); 358 } 359 360 private getTitle(): string { 361 const state = assertExists(this.state); 362 switch (state.selection.profileType) { 363 case ProfileType.MIXED_HEAP_PROFILE: 364 return 'Mixed heap profile'; 365 case ProfileType.HEAP_PROFILE: 366 return 'Heap profile'; 367 case ProfileType.NATIVE_HEAP_PROFILE: 368 return 'Native heap profile'; 369 case ProfileType.JAVA_HEAP_SAMPLES: 370 return 'Java heap samples'; 371 case ProfileType.JAVA_HEAP_GRAPH: 372 return 'Java heap graph'; 373 case ProfileType.PERF_SAMPLE: 374 return 'Profile'; 375 default: 376 throw new Error('unknown type'); 377 } 378 } 379 380 private nodeRendering(): NodeRendering { 381 const state = assertExists(this.state); 382 const profileType = state.selection.profileType; 383 switch (profileType) { 384 case ProfileType.JAVA_HEAP_GRAPH: 385 if ( 386 state.viewingOption === 387 FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY || 388 state.viewingOption === 389 FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY 390 ) { 391 return RENDER_OBJ_COUNT; 392 } else { 393 return RENDER_SELF_AND_TOTAL; 394 } 395 case ProfileType.MIXED_HEAP_PROFILE: 396 case ProfileType.HEAP_PROFILE: 397 case ProfileType.NATIVE_HEAP_PROFILE: 398 case ProfileType.JAVA_HEAP_SAMPLES: 399 case ProfileType.PERF_SAMPLE: 400 return RENDER_SELF_AND_TOTAL; 401 default: 402 const exhaustiveCheck: never = profileType; 403 throw new Error(`Unhandled case: ${exhaustiveCheck}`); 404 } 405 } 406 407 private getViewingOptionButtons(): m.Children { 408 const ret = []; 409 const state = assertExists(this.state); 410 for (const {option, name} of viewingOptions(state.selection.profileType)) { 411 ret.push( 412 m(Button, { 413 label: name, 414 active: option === state.viewingOption, 415 onclick: () => { 416 const state = assertExists(this.state); 417 state.viewingOption = option; 418 raf.scheduleFullRedraw(); 419 }, 420 }), 421 ); 422 } 423 return m(ButtonBar, ret); 424 } 425 426 onupdate({dom}: m.VnodeDOM<FlamegraphDetailsPanelAttrs>) { 427 const canvas = findRef(dom, 'canvas'); 428 if (canvas === null || !(canvas instanceof HTMLCanvasElement)) { 429 return; 430 } 431 if (!this.state?.result?.renderResults) { 432 return; 433 } 434 canvas.width = canvas.offsetWidth * devicePixelRatio; 435 canvas.height = canvas.offsetHeight * devicePixelRatio; 436 437 const ctx = canvas.getContext('2d'); 438 if (ctx === null) { 439 return; 440 } 441 442 ctx.clearRect(0, 0, canvas.width, canvas.height); 443 ctx.save(); 444 ctx.scale(devicePixelRatio, devicePixelRatio); 445 const {offsetWidth: width, offsetHeight: height} = canvas; 446 const unit = 447 this.state.viewingOption === 448 FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY || 449 this.state.viewingOption === 450 FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY || 451 this.state.viewingOption === 452 FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY 453 ? 'B' 454 : ''; 455 this.flamegraph.draw(ctx, width, height, 0, 0, unit); 456 ctx.restore(); 457 } 458 459 private static async fetchQueryResults( 460 engine: Engine, 461 cache: FlamegraphCache, 462 state: FlamegraphState, 463 ) { 464 const table = await FlamegraphDetailsPanel.prepareViewsAndTables( 465 engine, 466 cache, 467 state, 468 ); 469 const queryResults = 470 await FlamegraphDetailsPanel.getFlamegraphDataFromTables( 471 engine, 472 table, 473 state.viewingOption, 474 state.focusRegex, 475 ); 476 477 let incomplete = false; 478 if (state.selection.profileType === ProfileType.JAVA_HEAP_GRAPH) { 479 const it = await engine.query(` 480 select value from stats 481 where severity = 'error' and name = 'heap_graph_non_finalized_graph' 482 `); 483 incomplete = it.firstRow({value: NUM}).value > 0; 484 } 485 state.result = { 486 queryResults, 487 incomplete, 488 }; 489 raf.scheduleFullRedraw(); 490 } 491 492 private static async prepareViewsAndTables( 493 engine: Engine, 494 cache: FlamegraphCache, 495 state: FlamegraphState, 496 ): Promise<string> { 497 const flamegraphType = getFlamegraphType(state.selection.profileType); 498 if (state.selection.profileType === ProfileType.PERF_SAMPLE) { 499 let upid: string; 500 let upidGroup: string; 501 if (state.selection.upids.length > 1) { 502 upid = `NULL`; 503 upidGroup = `'${this.serializeUpidGroup(state.selection.upids)}'`; 504 } else { 505 upid = `${state.selection.upids[0]}`; 506 upidGroup = `NULL`; 507 } 508 return cache.getTableName( 509 engine, 510 ` 511 select 512 id, 513 name, 514 map_name, 515 parent_id, 516 depth, 517 cumulative_size, 518 cumulative_alloc_size, 519 cumulative_count, 520 cumulative_alloc_count, 521 size, 522 alloc_size, 523 count, 524 alloc_count, 525 source_file, 526 line_number 527 from experimental_flamegraph( 528 '${flamegraphType}', 529 NULL, 530 '>=${state.selection.start},<=${state.selection.end}', 531 ${upid}, 532 ${upidGroup}, 533 '${state.focusRegex}' 534 ) 535 `, 536 ); 537 } 538 if ( 539 state.selection.profileType === ProfileType.JAVA_HEAP_GRAPH && 540 isHeapGraphDominatorTreeViewingOption(state.viewingOption) 541 ) { 542 assertTrue(state.selection.start == state.selection.end); 543 return cache.getTableName( 544 engine, 545 await this.loadHeapGraphDominatorTreeQuery( 546 engine, 547 cache, 548 state.selection.upids[0], 549 state.selection.start, 550 ), 551 ); 552 } 553 assertTrue(state.selection.start == state.selection.end); 554 return cache.getTableName( 555 engine, 556 ` 557 select 558 id, 559 name, 560 map_name, 561 parent_id, 562 depth, 563 cumulative_size, 564 cumulative_alloc_size, 565 cumulative_count, 566 cumulative_alloc_count, 567 size, 568 alloc_size, 569 count, 570 alloc_count, 571 source_file, 572 line_number 573 from experimental_flamegraph( 574 '${flamegraphType}', 575 ${state.selection.start}, 576 NULL, 577 ${state.selection.upids[0]}, 578 NULL, 579 '${state.focusRegex}' 580 ) 581 `, 582 ); 583 } 584 585 private static async loadHeapGraphDominatorTreeQuery( 586 engine: Engine, 587 cache: FlamegraphCache, 588 upid: number, 589 timestamp: time, 590 ) { 591 const outputTableName = `heap_graph_type_dominated_${upid}_${timestamp}`; 592 const outputQuery = `SELECT * FROM ${outputTableName}`; 593 if (cache.hasQuery(outputQuery)) { 594 return outputQuery; 595 } 596 597 await engine.query(` 598 INCLUDE PERFETTO MODULE memory.heap_graph_dominator_tree; 599 600 -- heap graph dominator tree with objects as nodes and all relavant 601 -- object self stats and dominated stats 602 CREATE PERFETTO TABLE _heap_graph_object_dominated AS 603 SELECT 604 node.id, 605 node.idom_id, 606 node.dominated_obj_count, 607 node.dominated_size_bytes + node.dominated_native_size_bytes AS dominated_size, 608 node.depth, 609 obj.type_id, 610 obj.root_type, 611 obj.self_size + obj.native_size AS self_size 612 FROM memory_heap_graph_dominator_tree node 613 JOIN heap_graph_object obj USING(id) 614 WHERE obj.upid = ${upid} AND obj.graph_sample_ts = ${timestamp} 615 -- required to accelerate the recursive cte below 616 ORDER BY idom_id; 617 618 -- calculate for each object node in the dominator tree the 619 -- HASH(path of type_id's from the super root to the object) 620 CREATE PERFETTO TABLE _dominator_tree_path_hash AS 621 WITH RECURSIVE _tree_visitor(id, path_hash) AS ( 622 SELECT 623 id, 624 HASH( 625 CAST(type_id AS TEXT) || '-' || IFNULL(root_type, '') 626 ) AS path_hash 627 FROM _heap_graph_object_dominated 628 WHERE depth = 1 629 UNION ALL 630 SELECT 631 child.id, 632 HASH(CAST(parent.path_hash AS TEXT) || '/' || CAST(type_id AS TEXT)) AS path_hash 633 FROM _heap_graph_object_dominated child 634 JOIN _tree_visitor parent ON child.idom_id = parent.id 635 ) 636 SELECT * from _tree_visitor 637 ORDER BY id; 638 639 -- merge object nodes with the same path into one "class type node", so the 640 -- end result is a tree where nodes are identified by their types and the 641 -- dominator relationships are preserved. 642 CREATE PERFETTO TABLE ${outputTableName} AS 643 SELECT 644 map.path_hash as id, 645 COALESCE(cls.deobfuscated_name, cls.name, '[NULL]') || IIF( 646 node.root_type IS NOT NULL, 647 ' [' || node.root_type || ']', '' 648 ) AS name, 649 IFNULL(parent_map.path_hash, -1) AS parent_id, 650 node.depth - 1 AS depth, 651 sum(dominated_size) AS cumulative_size, 652 -1 AS cumulative_alloc_size, 653 sum(dominated_obj_count) AS cumulative_count, 654 -1 AS cumulative_alloc_count, 655 '' as map_name, 656 '' as source_file, 657 -1 as line_number, 658 sum(self_size) AS size, 659 count(*) AS count 660 FROM _heap_graph_object_dominated node 661 JOIN _dominator_tree_path_hash map USING(id) 662 LEFT JOIN _dominator_tree_path_hash parent_map ON node.idom_id = parent_map.id 663 JOIN heap_graph_class cls ON node.type_id = cls.id 664 GROUP BY map.path_hash, name, parent_id, depth, map_name, source_file, line_number; 665 666 -- These are intermediates and not needed 667 DROP TABLE _heap_graph_object_dominated; 668 DROP TABLE _dominator_tree_path_hash; 669 `); 670 671 return outputQuery; 672 } 673 674 private static async getFlamegraphDataFromTables( 675 engine: Engine, 676 tableName: string, 677 viewingOption: FlamegraphViewingOption, 678 focusRegex: string, 679 ) { 680 let orderBy = ''; 681 let totalColumnName: 682 | 'cumulativeSize' 683 | 'cumulativeAllocSize' 684 | 'cumulativeCount' 685 | 'cumulativeAllocCount' = 'cumulativeSize'; 686 let selfColumnName: 'size' | 'count' = 'size'; 687 // TODO(fmayer): Improve performance so this is no longer necessary. 688 // Alternatively consider collapsing frames of the same label. 689 const maxDepth = 100; 690 switch (viewingOption) { 691 case FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY: 692 orderBy = `where cumulative_alloc_size > 0 and depth < ${maxDepth} order by depth, parent_id, 693 cumulative_alloc_size desc, name`; 694 totalColumnName = 'cumulativeAllocSize'; 695 selfColumnName = 'size'; 696 break; 697 case FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY: 698 orderBy = `where cumulative_count > 0 and depth < ${maxDepth} order by depth, parent_id, 699 cumulative_count desc, name`; 700 totalColumnName = 'cumulativeCount'; 701 selfColumnName = 'count'; 702 break; 703 case FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY: 704 orderBy = `where cumulative_alloc_count > 0 and depth < ${maxDepth} order by depth, parent_id, 705 cumulative_alloc_count desc, name`; 706 totalColumnName = 'cumulativeAllocCount'; 707 selfColumnName = 'count'; 708 break; 709 case FlamegraphViewingOption.PERF_SAMPLES_KEY: 710 case FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY: 711 orderBy = `where cumulative_size > 0 and depth < ${maxDepth} order by depth, parent_id, 712 cumulative_size desc, name`; 713 totalColumnName = 'cumulativeSize'; 714 selfColumnName = 'size'; 715 break; 716 case FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY: 717 orderBy = `where depth < ${maxDepth} order by depth, 718 cumulativeCount desc, name`; 719 totalColumnName = 'cumulativeCount'; 720 selfColumnName = 'count'; 721 break; 722 case FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY: 723 orderBy = `where depth < ${maxDepth} order by depth, 724 cumulativeSize desc, name`; 725 totalColumnName = 'cumulativeSize'; 726 selfColumnName = 'size'; 727 break; 728 default: 729 const exhaustiveCheck: never = viewingOption; 730 throw new Error(`Unhandled case: ${exhaustiveCheck}`); 731 break; 732 } 733 734 const callsites = await engine.query(` 735 SELECT 736 id as hash, 737 IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name, 738 IFNULL(parent_id, -1) as parentHash, 739 depth, 740 cumulative_size as cumulativeSize, 741 cumulative_alloc_size as cumulativeAllocSize, 742 cumulative_count as cumulativeCount, 743 cumulative_alloc_count as cumulativeAllocCount, 744 map_name as mapping, 745 size, 746 count, 747 IFNULL(source_file, '') as sourceFile, 748 IFNULL(line_number, -1) as lineNumber 749 from ${tableName} 750 ${orderBy} 751 `); 752 753 const flamegraphData: CallsiteInfo[] = []; 754 const hashToindex: Map<number, number> = new Map(); 755 const it = callsites.iter({ 756 hash: NUM, 757 name: STR, 758 parentHash: NUM, 759 depth: NUM, 760 cumulativeSize: NUM, 761 cumulativeAllocSize: NUM, 762 cumulativeCount: NUM, 763 cumulativeAllocCount: NUM, 764 mapping: STR, 765 sourceFile: STR, 766 lineNumber: NUM, 767 size: NUM, 768 count: NUM, 769 }); 770 for (let i = 0; it.valid(); ++i, it.next()) { 771 const hash = it.hash; 772 let name = it.name; 773 const parentHash = it.parentHash; 774 const depth = it.depth; 775 const totalSize = it[totalColumnName]; 776 const selfSize = it[selfColumnName]; 777 const mapping = it.mapping; 778 const highlighted = 779 focusRegex !== '' && 780 name.toLocaleLowerCase().includes(focusRegex.toLocaleLowerCase()); 781 const parentId = hashToindex.has(+parentHash) 782 ? hashToindex.get(+parentHash)! 783 : -1; 784 785 let location: string | undefined; 786 if (/[a-zA-Z]/i.test(it.sourceFile)) { 787 location = it.sourceFile; 788 if (it.lineNumber !== -1) { 789 location += `:${it.lineNumber}`; 790 } 791 } 792 793 if (depth === maxDepth - 1) { 794 name += ' [tree truncated]'; 795 } 796 // Instead of hash, we will store index of callsite in this original array 797 // as an id of callsite. That way, we have quicker access to parent and it 798 // will stay unique: 799 hashToindex.set(hash, i); 800 801 flamegraphData.push({ 802 id: i, 803 totalSize, 804 depth, 805 parentId, 806 name, 807 selfSize, 808 mapping, 809 merged: false, 810 highlighted, 811 location, 812 }); 813 } 814 return flamegraphData; 815 } 816 817 private async downloadPprof() { 818 if (this.state === undefined) { 819 return; 820 } 821 const engine = this.getCurrentEngine(); 822 if (engine === undefined) { 823 return; 824 } 825 try { 826 assertTrue( 827 this.state.selection.upids.length === 1, 828 'Native profiles can only contain one pid.', 829 ); 830 const pid = await engine.query( 831 `select pid from process where upid = ${this.state.selection.upids[0]}`, 832 ); 833 const trace = await getCurrentTrace(); 834 convertTraceToPprofAndDownload( 835 trace, 836 pid.firstRow({pid: NUM}).pid, 837 this.state.selection.start, 838 ); 839 } catch (error) { 840 throw new Error(`Failed to get current trace ${error}`); 841 } 842 } 843 844 private maybeShowModal() { 845 const state = assertExists(this.state); 846 if (state.result?.incomplete === undefined || !state.result.incomplete) { 847 return undefined; 848 } 849 if (globals.state.flamegraphModalDismissed) { 850 return undefined; 851 } 852 return m(Modal, { 853 title: 'The flamegraph is incomplete', 854 vAlign: 'TOP', 855 content: m( 856 'div', 857 'The current trace does not have a fully formed flamegraph', 858 ), 859 buttons: [ 860 { 861 text: 'Show the errors', 862 primary: true, 863 action: () => Router.navigate('#!/info'), 864 }, 865 { 866 text: 'Skip', 867 action: () => { 868 globals.dispatch(Actions.dismissFlamegraphModal({})); 869 raf.scheduleFullRedraw(); 870 }, 871 }, 872 ], 873 } as ModalAttrs); 874 } 875 876 private static getMinSizeDisplayed( 877 flamegraphData: ReadonlyArray<CallsiteInfo>, 878 rootSize?: number, 879 ): number { 880 const timeState = globals.state.frontendLocalState.visibleState; 881 const dur = globals.stateVisibleTime().duration; 882 // TODO(stevegolton): Does this actually do what we want??? 883 let width = Duration.toSeconds(dur / timeState.resolution); 884 // TODO(168048193): Remove screen size hack: 885 width = Math.max(width, 800); 886 if (rootSize === undefined) { 887 rootSize = findRootSize(flamegraphData); 888 } 889 return (MIN_PIXEL_DISPLAYED * rootSize) / width; 890 } 891 892 private static serializeUpidGroup(upids: number[]) { 893 return new Array(upids).join(); 894 } 895 896 private getCurrentEngine() { 897 const engineId = globals.getCurrentEngine()?.id; 898 if (engineId === undefined) return undefined; 899 return globals.engines.get(engineId); 900 } 901} 902