• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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