1// Copyright (C) 2024 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this 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, assertFalse} from '../../base/logging'; 18import {createPerfettoTable} from '../../trace_processor/sql_utils'; 19import {extensions} from '../../components/extensions'; 20import {time} from '../../base/time'; 21import {uuidv4Sql} from '../../base/uuid'; 22import { 23 QueryFlamegraph, 24 QueryFlamegraphMetric, 25 metricsFromTableOrSubquery, 26} from '../../components/query_flamegraph'; 27import {convertTraceToPprofAndDownload} from '../../frontend/trace_converter'; 28import {Timestamp} from '../../components/widgets/timestamp'; 29import { 30 TrackEventDetailsPanel, 31 TrackEventDetailsPanelSerializeArgs, 32} from '../../public/details_panel'; 33import {Trace} from '../../public/trace'; 34import {NUM} from '../../trace_processor/query_result'; 35import {Button} from '../../widgets/button'; 36import {Intent} from '../../widgets/common'; 37import {DetailsShell} from '../../widgets/details_shell'; 38import {Icon} from '../../widgets/icon'; 39import {Modal, showModal} from '../../widgets/modal'; 40import {Popup} from '../../widgets/popup'; 41import { 42 Flamegraph, 43 FLAMEGRAPH_STATE_SCHEMA, 44 FlamegraphState, 45 FlamegraphOptionalAction, 46} from '../../widgets/flamegraph'; 47import {SqlTableDescription} from '../../components/widgets/sql/table/table_description'; 48import {StandardColumn} from '../../components/widgets/sql/table/columns'; 49 50export enum ProfileType { 51 HEAP_PROFILE = 'heap_profile', 52 MIXED_HEAP_PROFILE = 'heap_profile:com.android.art,libc.malloc', 53 NATIVE_HEAP_PROFILE = 'heap_profile:libc.malloc', 54 JAVA_HEAP_SAMPLES = 'heap_profile:com.android.art', 55 JAVA_HEAP_GRAPH = 'graph', 56 PERF_SAMPLE = 'perf', 57 INSTRUMENTS_SAMPLE = 'instruments', 58} 59 60export function profileType(s: string): ProfileType { 61 if (s === 'heap_profile:libc.malloc,com.android.art') { 62 s = 'heap_profile:com.android.art,libc.malloc'; 63 } 64 if (Object.values(ProfileType).includes(s as ProfileType)) { 65 return s as ProfileType; 66 } 67 if (s.startsWith('heap_profile')) { 68 return ProfileType.HEAP_PROFILE; 69 } 70 throw new Error('Unknown type ${s}'); 71} 72 73interface Props { 74 ts: time; 75 type: ProfileType; 76} 77 78export class HeapProfileFlamegraphDetailsPanel 79 implements TrackEventDetailsPanel 80{ 81 private readonly flamegraph: QueryFlamegraph; 82 private readonly props: Props; 83 private flamegraphModalDismissed = false; 84 85 readonly serialization: TrackEventDetailsPanelSerializeArgs<FlamegraphState>; 86 87 constructor( 88 private trace: Trace, 89 private heapGraphIncomplete: boolean, 90 private upid: number, 91 profileType: ProfileType, 92 ts: time, 93 ) { 94 const metrics = flamegraphMetrics(trace, profileType, ts, upid); 95 this.serialization = { 96 schema: FLAMEGRAPH_STATE_SCHEMA, 97 state: Flamegraph.createDefaultState(metrics), 98 }; 99 this.flamegraph = new QueryFlamegraph(trace, metrics, this.serialization); 100 this.props = {ts, type: profileType}; 101 } 102 103 render() { 104 const {type, ts} = this.props; 105 return m( 106 '.flamegraph-profile', 107 this.maybeShowModal(this.trace, type, this.heapGraphIncomplete), 108 m( 109 DetailsShell, 110 { 111 fillParent: true, 112 title: m( 113 '.title', 114 getFlamegraphTitle(type), 115 type === ProfileType.MIXED_HEAP_PROFILE && 116 m( 117 Popup, 118 { 119 trigger: m(Icon, {icon: 'warning'}), 120 }, 121 m( 122 '', 123 {style: {width: '300px'}}, 124 'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.', 125 ), 126 ), 127 ), 128 description: [], 129 buttons: [ 130 m('.time', `Snapshot time: `, m(Timestamp, {ts})), 131 (type === ProfileType.NATIVE_HEAP_PROFILE || 132 type === ProfileType.JAVA_HEAP_SAMPLES) && 133 m(Button, { 134 icon: 'file_download', 135 intent: Intent.Primary, 136 onclick: () => { 137 downloadPprof(this.trace, this.upid, ts); 138 }, 139 }), 140 ], 141 }, 142 assertExists(this.flamegraph).render(), 143 ), 144 ); 145 } 146 147 private maybeShowModal( 148 trace: Trace, 149 type: ProfileType, 150 heapGraphIncomplete: boolean, 151 ) { 152 if (type !== ProfileType.JAVA_HEAP_GRAPH || !heapGraphIncomplete) { 153 return undefined; 154 } 155 if (this.flamegraphModalDismissed) { 156 return undefined; 157 } 158 return m(Modal, { 159 title: 'The flamegraph is incomplete', 160 vAlign: 'TOP', 161 content: m( 162 'div', 163 'The current trace does not have a fully formed flamegraph', 164 ), 165 buttons: [ 166 { 167 text: 'Show the errors', 168 primary: true, 169 action: () => trace.navigate('#!/info'), 170 }, 171 { 172 text: 'Skip', 173 action: () => { 174 this.flamegraphModalDismissed = true; 175 }, 176 }, 177 ], 178 }); 179 } 180} 181 182function flamegraphMetrics( 183 trace: Trace, 184 type: ProfileType, 185 ts: time, 186 upid: number, 187): ReadonlyArray<QueryFlamegraphMetric> { 188 switch (type) { 189 case ProfileType.NATIVE_HEAP_PROFILE: 190 return flamegraphMetricsForHeapProfile(ts, upid, [ 191 { 192 name: 'Unreleased Malloc Size', 193 unit: 'B', 194 columnName: 'self_size', 195 }, 196 { 197 name: 'Unreleased Malloc Count', 198 unit: '', 199 columnName: 'self_count', 200 }, 201 { 202 name: 'Total Malloc Size', 203 unit: 'B', 204 columnName: 'self_alloc_size', 205 }, 206 { 207 name: 'Total Malloc Count', 208 unit: '', 209 columnName: 'self_alloc_count', 210 }, 211 ]); 212 case ProfileType.HEAP_PROFILE: 213 return flamegraphMetricsForHeapProfile(ts, upid, [ 214 { 215 name: 'Unreleased Size', 216 unit: 'B', 217 columnName: 'self_size', 218 }, 219 { 220 name: 'Unreleased Count', 221 unit: '', 222 columnName: 'self_count', 223 }, 224 { 225 name: 'Total Size', 226 unit: 'B', 227 columnName: 'self_alloc_size', 228 }, 229 { 230 name: 'Total Count', 231 unit: '', 232 columnName: 'self_alloc_count', 233 }, 234 ]); 235 case ProfileType.JAVA_HEAP_SAMPLES: 236 return flamegraphMetricsForHeapProfile(ts, upid, [ 237 { 238 name: 'Total Allocation Size', 239 unit: 'B', 240 columnName: 'self_size', 241 }, 242 { 243 name: 'Total Allocation Count', 244 unit: '', 245 columnName: 'self_count', 246 }, 247 ]); 248 case ProfileType.MIXED_HEAP_PROFILE: 249 return flamegraphMetricsForHeapProfile(ts, upid, [ 250 { 251 name: 'Allocation Size (malloc + java)', 252 unit: 'B', 253 columnName: 'self_size', 254 }, 255 { 256 name: 'Allocation Count (malloc + java)', 257 unit: '', 258 columnName: 'self_count', 259 }, 260 ]); 261 case ProfileType.JAVA_HEAP_GRAPH: 262 return [ 263 { 264 name: 'Object Size', 265 unit: 'B', 266 dependencySql: 267 'include perfetto module android.memory.heap_graph.class_tree;', 268 statement: ` 269 select 270 id, 271 parent_id as parentId, 272 ifnull(name, '[Unknown]') as name, 273 root_type, 274 heap_type, 275 self_size as value, 276 self_count, 277 path_hash_stable 278 from _heap_graph_class_tree 279 where graph_sample_ts = ${ts} and upid = ${upid} 280 `, 281 unaggregatableProperties: [ 282 {name: 'root_type', displayName: 'Root Type'}, 283 {name: 'heap_type', displayName: 'Heap Type'}, 284 ], 285 aggregatableProperties: [ 286 { 287 name: 'self_count', 288 displayName: 'Self Count', 289 mergeAggregation: 'SUM', 290 }, 291 { 292 name: 'path_hash_stable', 293 displayName: 'Path Hash', 294 mergeAggregation: 'CONCAT_WITH_COMMA', 295 isVisible: false, 296 }, 297 ], 298 optionalNodeActions: getHeapGraphNodeOptionalActions(trace, false), 299 optionalRootActions: getHeapGraphRootOptionalActions(trace, false), 300 }, 301 { 302 name: 'Object Count', 303 unit: '', 304 dependencySql: 305 'include perfetto module android.memory.heap_graph.class_tree;', 306 statement: ` 307 select 308 id, 309 parent_id as parentId, 310 ifnull(name, '[Unknown]') as name, 311 root_type, 312 heap_type, 313 self_size, 314 self_count as value, 315 path_hash_stable 316 from _heap_graph_class_tree 317 where graph_sample_ts = ${ts} and upid = ${upid} 318 `, 319 unaggregatableProperties: [ 320 {name: 'root_type', displayName: 'Root Type'}, 321 {name: 'heap_type', displayName: 'Heap Type'}, 322 ], 323 aggregatableProperties: [ 324 { 325 name: 'path_hash_stable', 326 displayName: 'Path Hash', 327 mergeAggregation: 'CONCAT_WITH_COMMA', 328 isVisible: false, 329 }, 330 ], 331 optionalNodeActions: getHeapGraphNodeOptionalActions(trace, false), 332 optionalRootActions: getHeapGraphRootOptionalActions(trace, false), 333 }, 334 { 335 name: 'Dominated Object Size', 336 unit: 'B', 337 dependencySql: 338 'include perfetto module android.memory.heap_graph.dominator_class_tree;', 339 statement: ` 340 select 341 id, 342 parent_id as parentId, 343 ifnull(name, '[Unknown]') as name, 344 root_type, 345 heap_type, 346 self_size as value, 347 self_count, 348 path_hash_stable 349 from _heap_graph_dominator_class_tree 350 where graph_sample_ts = ${ts} and upid = ${upid} 351 `, 352 unaggregatableProperties: [ 353 {name: 'root_type', displayName: 'Root Type'}, 354 {name: 'heap_type', displayName: 'Heap Type'}, 355 ], 356 aggregatableProperties: [ 357 { 358 name: 'self_count', 359 displayName: 'Self Count', 360 mergeAggregation: 'SUM', 361 }, 362 { 363 name: 'path_hash_stable', 364 displayName: 'Path Hash', 365 mergeAggregation: 'CONCAT_WITH_COMMA', 366 isVisible: false, 367 }, 368 ], 369 optionalNodeActions: getHeapGraphNodeOptionalActions(trace, true), 370 optionalRootActions: getHeapGraphRootOptionalActions(trace, true), 371 }, 372 { 373 name: 'Dominated Object Count', 374 unit: '', 375 dependencySql: 376 'include perfetto module android.memory.heap_graph.dominator_class_tree;', 377 statement: ` 378 select 379 id, 380 parent_id as parentId, 381 ifnull(name, '[Unknown]') as name, 382 root_type, 383 heap_type, 384 self_size, 385 self_count as value, 386 path_hash_stable 387 from _heap_graph_dominator_class_tree 388 where graph_sample_ts = ${ts} and upid = ${upid} 389 `, 390 unaggregatableProperties: [ 391 {name: 'root_type', displayName: 'Root Type'}, 392 {name: 'heap_type', displayName: 'Heap Type'}, 393 ], 394 aggregatableProperties: [ 395 { 396 name: 'path_hash_stable', 397 displayName: 'Path Hash', 398 mergeAggregation: 'CONCAT_WITH_COMMA', 399 isVisible: false, 400 }, 401 ], 402 optionalNodeActions: getHeapGraphNodeOptionalActions(trace, true), 403 optionalRootActions: getHeapGraphRootOptionalActions(trace, true), 404 }, 405 ]; 406 case ProfileType.PERF_SAMPLE: 407 throw new Error('Perf sample not supported'); 408 case ProfileType.INSTRUMENTS_SAMPLE: 409 throw new Error('Instruments sample not supported'); 410 } 411} 412 413function flamegraphMetricsForHeapProfile( 414 ts: time, 415 upid: number, 416 metrics: {name: string; unit: string; columnName: string}[], 417) { 418 return metricsFromTableOrSubquery( 419 ` 420 ( 421 select 422 id, 423 parent_id as parentId, 424 name, 425 mapping_name, 426 source_file, 427 cast(line_number AS text) as line_number, 428 self_size, 429 self_count, 430 self_alloc_size, 431 self_alloc_count 432 from _android_heap_profile_callstacks_for_allocations!(( 433 select 434 callsite_id, 435 size, 436 count, 437 max(size, 0) as alloc_size, 438 max(count, 0) as alloc_count 439 from heap_profile_allocation a 440 where a.ts <= ${ts} and a.upid = ${upid} 441 )) 442 ) 443 `, 444 metrics, 445 'include perfetto module android.memory.heap_profile.callstacks', 446 [{name: 'mapping_name', displayName: 'Mapping'}], 447 [ 448 { 449 name: 'source_file', 450 displayName: 'Source File', 451 mergeAggregation: 'ONE_OR_NULL', 452 }, 453 { 454 name: 'line_number', 455 displayName: 'Line Number', 456 mergeAggregation: 'ONE_OR_NULL', 457 }, 458 ], 459 ); 460} 461 462function getFlamegraphTitle(type: ProfileType) { 463 switch (type) { 464 case ProfileType.HEAP_PROFILE: 465 return 'Heap profile'; 466 case ProfileType.JAVA_HEAP_GRAPH: 467 return 'Java heap graph'; 468 case ProfileType.JAVA_HEAP_SAMPLES: 469 return 'Java heap samples'; 470 case ProfileType.MIXED_HEAP_PROFILE: 471 return 'Mixed heap profile'; 472 case ProfileType.NATIVE_HEAP_PROFILE: 473 return 'Native heap profile'; 474 case ProfileType.PERF_SAMPLE: 475 assertFalse(false, 'Perf sample not supported'); 476 return 'Impossible'; 477 case ProfileType.INSTRUMENTS_SAMPLE: 478 assertFalse(false, 'Instruments sample not supported'); 479 return 'Impossible'; 480 } 481} 482 483async function downloadPprof(trace: Trace, upid: number, ts: time) { 484 const pid = await trace.engine.query( 485 `select pid from process where upid = ${upid}`, 486 ); 487 if (!trace.traceInfo.downloadable) { 488 showModal({ 489 title: 'Download not supported', 490 content: m('div', 'This trace file does not support downloads'), 491 }); 492 } 493 const blob = await trace.getTraceFile(); 494 convertTraceToPprofAndDownload(blob, pid.firstRow({pid: NUM}).pid, ts); 495} 496 497function getHeapGraphObjectReferencesView( 498 isDominator: boolean, 499): SqlTableDescription { 500 return { 501 name: `_heap_graph${tableModifier(isDominator)}object_references`, 502 columns: [ 503 new StandardColumn('path_hash'), 504 new StandardColumn('outgoing_reference_count'), 505 new StandardColumn('class_name'), 506 new StandardColumn('self_size'), 507 new StandardColumn('native_size'), 508 new StandardColumn('heap_type'), 509 new StandardColumn('root_type'), 510 new StandardColumn('reachable'), 511 ], 512 }; 513} 514 515function getHeapGraphIncomingReferencesView( 516 isDominator: boolean, 517): SqlTableDescription { 518 return { 519 name: `_heap_graph${tableModifier(isDominator)}incoming_references`, 520 columns: [ 521 new StandardColumn('path_hash'), 522 new StandardColumn('class_name'), 523 new StandardColumn('field_name'), 524 new StandardColumn('field_type_name'), 525 new StandardColumn('self_size'), 526 new StandardColumn('native_size'), 527 new StandardColumn('heap_type'), 528 new StandardColumn('root_type'), 529 new StandardColumn('reachable'), 530 ], 531 }; 532} 533 534function getHeapGraphOutgoingReferencesView( 535 isDominator: boolean, 536): SqlTableDescription { 537 return { 538 name: `_heap_graph${tableModifier(isDominator)}outgoing_references`, 539 columns: [ 540 new StandardColumn('path_hash'), 541 new StandardColumn('class_name'), 542 new StandardColumn('field_name'), 543 new StandardColumn('field_type_name'), 544 new StandardColumn('self_size'), 545 new StandardColumn('native_size'), 546 new StandardColumn('heap_type'), 547 new StandardColumn('root_type'), 548 new StandardColumn('reachable'), 549 ], 550 }; 551} 552 553function getHeapGraphRetainingObjectCountsView( 554 isDominator: boolean, 555): SqlTableDescription { 556 return { 557 name: `_heap_graph${tableModifier(isDominator)}retaining_object_counts`, 558 columns: [ 559 new StandardColumn('class_name'), 560 new StandardColumn('count'), 561 new StandardColumn('total_size'), 562 new StandardColumn('total_native_size'), 563 new StandardColumn('heap_type'), 564 new StandardColumn('root_type'), 565 new StandardColumn('reachable'), 566 ], 567 }; 568} 569 570function getHeapGraphRetainedObjectCountsView( 571 isDominator: boolean, 572): SqlTableDescription { 573 return { 574 name: `_heap_graph${tableModifier(isDominator)}retained_object_counts`, 575 columns: [ 576 new StandardColumn('class_name'), 577 new StandardColumn('count'), 578 new StandardColumn('total_size'), 579 new StandardColumn('total_native_size'), 580 new StandardColumn('heap_type'), 581 new StandardColumn('root_type'), 582 new StandardColumn('reachable'), 583 ], 584 }; 585} 586 587function getHeapGraphDuplicateObjectsView( 588 isDominator: boolean, 589): SqlTableDescription { 590 return { 591 name: `_heap_graph${tableModifier(isDominator)}duplicate_objects`, 592 columns: [ 593 new StandardColumn('class_name'), 594 new StandardColumn('path_count'), 595 new StandardColumn('object_count'), 596 new StandardColumn('total_size'), 597 new StandardColumn('total_native_size'), 598 ], 599 }; 600} 601 602function getHeapGraphNodeOptionalActions( 603 trace: Trace, 604 isDominator: boolean, 605): ReadonlyArray<FlamegraphOptionalAction> { 606 return [ 607 { 608 name: 'Objects', 609 execute: async (kv: ReadonlyMap<string, string>) => { 610 const value = kv.get('path_hash_stable'); 611 if (value !== undefined) { 612 const uuid = uuidv4Sql(); 613 const pathHashTableName = `_heap_graph_filtered_path_hashes_${uuid}`; 614 await createPerfettoTable( 615 trace.engine, 616 pathHashTableName, 617 pathHashesToTableStatement(value), 618 ); 619 620 const tableName = `_heap_graph${tableModifier(isDominator)}object_references`; 621 const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes, ${pathHashTableName}`; 622 const macroExpr = `_heap_graph_object_references_agg!(${macroArgs})`; 623 const statement = `CREATE OR REPLACE PERFETTO TABLE ${tableName} AS SELECT * FROM ${macroExpr};`; 624 625 // Create view to be returned 626 await trace.engine.query(statement); 627 extensions.addLegacySqlTableTab(trace, { 628 table: getHeapGraphObjectReferencesView(isDominator), 629 }); 630 } 631 }, 632 }, 633 634 // Group for Direct References 635 { 636 name: 'Direct References', 637 // No execute function for parent menu items 638 subActions: [ 639 { 640 name: 'Incoming references', 641 execute: async (kv: ReadonlyMap<string, string>) => { 642 const value = kv.get('path_hash_stable'); 643 if (value !== undefined) { 644 const uuid = uuidv4Sql(); 645 const pathHashTableName = `_heap_graph_filtered_path_hashes_${uuid}`; 646 await createPerfettoTable( 647 trace.engine, 648 pathHashTableName, 649 pathHashesToTableStatement(value), 650 ); 651 652 const tableName = `_heap_graph${tableModifier(isDominator)}incoming_references`; 653 const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes, ${pathHashTableName}`; 654 const macroExpr = `_heap_graph_incoming_references_agg!(${macroArgs})`; 655 const statement = `CREATE OR REPLACE PERFETTO TABLE ${tableName} AS SELECT * FROM ${macroExpr};`; 656 657 // Create view to be returned 658 await trace.engine.query(statement); 659 extensions.addLegacySqlTableTab(trace, { 660 table: getHeapGraphIncomingReferencesView(isDominator), 661 }); 662 } 663 }, 664 }, 665 { 666 name: 'Outgoing references', 667 execute: async (kv: ReadonlyMap<string, string>) => { 668 const value = kv.get('path_hash_stable'); 669 if (value !== undefined) { 670 const uuid = uuidv4Sql(); 671 const pathHashTableName = `_heap_graph_filtered_path_hashes_${uuid}`; 672 await createPerfettoTable( 673 trace.engine, 674 pathHashTableName, 675 pathHashesToTableStatement(value), 676 ); 677 678 const tableName = `_heap_graph${tableModifier(isDominator)}outgoing_references`; 679 const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes, ${pathHashTableName}`; 680 const macroExpr = `_heap_graph_outgoing_references_agg!(${macroArgs})`; 681 const statement = `CREATE OR REPLACE PERFETTO TABLE ${tableName} AS SELECT * FROM ${macroExpr};`; 682 683 // Create view to be returned 684 await trace.engine.query(statement); 685 extensions.addLegacySqlTableTab(trace, { 686 table: getHeapGraphOutgoingReferencesView(isDominator), 687 }); 688 } 689 }, 690 }, 691 ], 692 }, 693 694 // Group for Indirect References 695 { 696 name: 'Indirect References', 697 // No execute function for parent menu items 698 subActions: [ 699 { 700 name: 'Retained objects', 701 execute: async (kv: ReadonlyMap<string, string>) => { 702 const value = kv.get('path_hash_stable'); 703 if (value !== undefined) { 704 const uuid = uuidv4Sql(); 705 const pathHashTableName = `_heap_graph_filtered_path_hashes_${uuid}`; 706 await createPerfettoTable( 707 trace.engine, 708 pathHashTableName, 709 pathHashesToTableStatement(value), 710 ); 711 712 const tableName = `_heap_graph${tableModifier(isDominator)}retained_object_counts`; 713 const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes, ${pathHashTableName}`; 714 const macroExpr = `_heap_graph_retained_object_count_agg!(${macroArgs})`; 715 const statement = `CREATE OR REPLACE PERFETTO TABLE ${tableName} AS SELECT * FROM ${macroExpr};`; 716 717 // Create view to be returned 718 await trace.engine.query(statement); 719 extensions.addLegacySqlTableTab(trace, { 720 table: getHeapGraphRetainedObjectCountsView(isDominator), 721 }); 722 } 723 }, 724 }, 725 { 726 name: 'Retaining objects', 727 execute: async (kv: ReadonlyMap<string, string>) => { 728 const value = kv.get('path_hash_stable'); 729 if (value !== undefined) { 730 const uuid = uuidv4Sql(); 731 const pathHashTableName = `_heap_graph_filtered_path_hashes_${uuid}`; 732 await createPerfettoTable( 733 trace.engine, 734 pathHashTableName, 735 pathHashesToTableStatement(value), 736 ); 737 738 const tableName = `_heap_graph${tableModifier(isDominator)}retaining_object_counts`; 739 const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes, ${pathHashTableName}`; 740 const macroExpr = `_heap_graph_retaining_object_count_agg!(${macroArgs})`; 741 const statement = `CREATE OR REPLACE PERFETTO TABLE ${tableName} AS SELECT * FROM ${macroExpr};`; 742 743 // Create view to be returned 744 await trace.engine.query(statement); 745 extensions.addLegacySqlTableTab(trace, { 746 table: getHeapGraphRetainingObjectCountsView(isDominator), 747 }); 748 } 749 }, 750 }, 751 ], 752 }, 753 ]; 754} 755 756function getHeapGraphRootOptionalActions( 757 trace: Trace, 758 isDominator: boolean, 759): ReadonlyArray<FlamegraphOptionalAction> { 760 return [ 761 { 762 name: 'Reference paths by class', 763 execute: async (_kv: ReadonlyMap<string, string>) => { 764 const viewName = `_heap_graph${tableModifier(isDominator)}duplicate_objects`; 765 const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes`; 766 const macroExpr = `_heap_graph_duplicate_objects_agg!(${macroArgs})`; 767 const statement = `CREATE OR REPLACE PERFETTO VIEW ${viewName} AS SELECT * FROM ${macroExpr};`; 768 769 // Create view to be returned 770 await trace.engine.query(statement); 771 extensions.addLegacySqlTableTab(trace, { 772 table: getHeapGraphDuplicateObjectsView(isDominator), 773 }); 774 }, 775 }, 776 ]; 777} 778 779function tableModifier(isDominator: boolean): string { 780 return isDominator ? '_dominator_' : '_'; 781} 782 783function pathHashesToTableStatement(commaSeparatedValues: string): string { 784 // Split the string by commas and trim whitespace 785 const individualValues = commaSeparatedValues.split(',').map((v) => v.trim()); 786 787 // Wrap each value with parentheses 788 const wrappedValues = individualValues.map((value) => `(${value})`); 789 790 // Join with commas and create the complete WITH clause 791 const valuesClause = `values${wrappedValues.join(', ')}`; 792 return `WITH temp_table(path_hash) AS (${valuesClause}) SELECT * FROM temp_table`; 793} 794