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