perfetto-ui: Moving heap profile flamegraph to details panel

Instead of having flamegraph for heap profiles in separate track, moving it
to the details panel which is now scrollable.

Test trace: ?s=25632898bab2c18347f2c2d5cd1c5ddc9e98f1278cb96e896eda5de3adbf3
Change-Id: I0cd5be4599474a98181a54551aa8eddf0e62e01f
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index d5f4d5c..77c2c33 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -97,6 +97,57 @@
         width: 50%;
       }
     }
+    &.heap-profile {
+      display: flex;
+    justify-content: space-between;
+    align-content: center;
+    height: 30px;
+      padding: 0px;
+      font-size: 12px;
+      * {
+        align-self: center;
+      }
+      .options {
+        display: inline-flex;
+        justify-content: space-around;
+      }
+      .details {
+        display: inline-flex;
+        justify-content: flex-end;
+      }
+      button {
+        width: fit-content;
+        height: 20px;
+        padding: 3px;
+        padding-top: 0px;
+        margin: 2px;
+        font-size: 12px;
+        opacity: 0.5;
+        &.download {
+          opacity: 1;
+          padding-top: 3px;
+          height: 21px;
+        }
+        &.chosen {
+          opacity: 1;
+        }
+        .material-icons {
+          font-size: 15px;
+          margin-right: 3px;
+          vertical-align: middle;
+        }
+      }
+      .title {
+        justify-self: start;
+        margin-left: 5px;
+        font-size: 14px;
+        margin-right: 10px;
+      }
+      .time {
+        justify-self: end;
+        margin-right: 10px;
+      }
+    }
   }
 
   table {
@@ -143,18 +194,6 @@
     margin-top: 12px;
     margin-left: 10px;
   }
-
-  .explanation {
-    font-size: 14px;
-    width: 35%;
-    margin-top: 10px;
-    padding-left: 10px;
-  }
-
-  .material-icons {
-    vertical-align: middle;
-    margin-right: 10px;
-  }
 }
 
 .notes-editor-panel {
@@ -201,10 +240,6 @@
   }
 }
 
-.flame-graph-panel {
-  height: 500px;
-}
-
 .log-panel {
   width: 100%;
   height: 100%;
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 552a0b6..9b8c082 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -15,6 +15,7 @@
 import {Draft} from 'immer';
 
 import {assertExists} from '../base/logging';
+import {CallsiteInfo} from '../common/state';
 import {ConvertTrace, ConvertTraceToPprof} from '../controller/trace_converter';
 
 import {
@@ -440,6 +441,18 @@
     };
   },
 
+  expandHeapProfileFlamegraph(
+      state: StateDraft, args: {expandedCallsite?: CallsiteInfo}): void {
+    if (state.currentHeapProfileFlamegraph === null) return;
+    state.currentHeapProfileFlamegraph.expandedCallsite = args.expandedCallsite;
+  },
+
+  changeViewHeapProfileFlamegraph(
+      state: StateDraft, args: {viewingOption: string}): void {
+    if (state.currentHeapProfileFlamegraph === null) return;
+    state.currentHeapProfileFlamegraph.viewingOption = args.viewingOption;
+  },
+
   selectChromeSlice(state: StateDraft, args: {id: number, trackId: string}):
       void {
         state.currentSelection = {
diff --git a/ui/src/tracks/heap_profile_flamegraph/controller_unittest.ts b/ui/src/common/flamegraph_unittest.ts
similarity index 98%
rename from ui/src/tracks/heap_profile_flamegraph/controller_unittest.ts
rename to ui/src/common/flamegraph_unittest.ts
index ef110dd..d9015a8 100644
--- a/ui/src/tracks/heap_profile_flamegraph/controller_unittest.ts
+++ b/ui/src/common/flamegraph_unittest.ts
@@ -1,5 +1,5 @@
-import {CallsiteInfo} from '../../frontend/globals';
-import {mergeCallsites} from './controller';
+import {mergeCallsites} from './flamegraph_util';
+import {CallsiteInfo} from './state';
 
 test('zeroCallsitesMerged', () => {
   const callsites: CallsiteInfo[] = [
diff --git a/ui/src/common/flamegraph_util.ts b/ui/src/common/flamegraph_util.ts
new file mode 100644
index 0000000..f4f4d16
--- /dev/null
+++ b/ui/src/common/flamegraph_util.ts
@@ -0,0 +1,115 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {CallsiteInfo} from '../common/state';
+
+export const SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'space';
+export const ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'alloc_space';
+export const OBJECTS_ALLOCATED_NOT_FREED_KEY = 'objects';
+export const OBJECTS_ALLOCATED_KEY = 'alloc_objects';
+
+export const DEFAULT_VIEWING_OPTION = SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY;
+
+export function expandCallsites(
+    data: CallsiteInfo[], clickedCallsiteIndex: number): CallsiteInfo[] {
+  if (clickedCallsiteIndex === -1) return data;
+  const expandedCallsites: CallsiteInfo[] = [];
+  if (clickedCallsiteIndex >= data.length || clickedCallsiteIndex < -1) {
+    return expandedCallsites;
+  }
+  const clickedCallsite = data[clickedCallsiteIndex];
+  expandedCallsites.unshift(clickedCallsite);
+  // Adding parents
+  let parentId = clickedCallsite.parentId;
+  while (parentId > -1) {
+    expandedCallsites.unshift(data[parentId]);
+    parentId = data[parentId].parentId;
+  }
+  // Adding children
+  const parents: number[] = [];
+  parents.push(clickedCallsiteIndex);
+  for (let i = clickedCallsiteIndex + 1; i < data.length; i++) {
+    const element = data[i];
+    if (parents.includes(element.parentId)) {
+      expandedCallsites.push(element);
+      parents.push(element.id);
+    }
+  }
+  return expandedCallsites;
+}
+
+// Merge callsites that have approximately width less than
+// MIN_PIXEL_DISPLAYED. All small callsites in the same depth and with same
+// parent will be merged to one with total size of all merged callsites.
+export function mergeCallsites(data: CallsiteInfo[], minSizeDisplayed: number) {
+  const mergedData: CallsiteInfo[] = [];
+  const mergedCallsites: Map<number, number> = new Map();
+  for (let i = 0; i < data.length; i++) {
+    // When a small callsite is found, it will be merged with other small
+    // callsites of the same depth. So if the current callsite has already been
+    // merged we can skip it.
+    if (mergedCallsites.has(data[i].id)) {
+      continue;
+    }
+    const copiedCallsite = copyCallsite(data[i]);
+    copiedCallsite.parentId =
+        getCallsitesParentHash(copiedCallsite, mergedCallsites);
+
+    // If current callsite is small, find other small callsites with same depth
+    // and parent and merge them into the current one, marking them as merged.
+    if (copiedCallsite.totalSize <= minSizeDisplayed && i + 1 < data.length) {
+      let j = i + 1;
+      let nextCallsite = data[j];
+      while (j < data.length && copiedCallsite.depth === nextCallsite.depth) {
+        if (copiedCallsite.parentId ===
+                getCallsitesParentHash(nextCallsite, mergedCallsites) &&
+            nextCallsite.totalSize <= minSizeDisplayed) {
+          copiedCallsite.totalSize += nextCallsite.totalSize;
+          mergedCallsites.set(nextCallsite.id, copiedCallsite.id);
+        }
+        j++;
+        nextCallsite = data[j];
+      }
+    }
+    mergedData.push(copiedCallsite);
+  }
+  return mergedData;
+}
+
+function copyCallsite(callsite: CallsiteInfo): CallsiteInfo {
+  return {
+    id: callsite.id,
+    parentId: callsite.parentId,
+    depth: callsite.depth,
+    name: callsite.name,
+    totalSize: callsite.totalSize,
+    mapping: callsite.mapping,
+    selfSize: callsite.selfSize
+  };
+}
+
+function getCallsitesParentHash(
+    callsite: CallsiteInfo, map: Map<number, number>): number {
+  return map.has(callsite.parentId) ? +map.get(callsite.parentId)! :
+                                      callsite.parentId;
+}
+export function findRootSize(data: CallsiteInfo[]) {
+  let totalSize = 0;
+  let i = 0;
+  while (i < data.length && data[i].depth === 0) {
+    totalSize += data[i].totalSize;
+    i++;
+  }
+  return totalSize;
+}
\ No newline at end of file
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index c52cb8a..74ca488 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -41,6 +41,16 @@
 
 export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE'|'FORCE_BUILTIN_WASM';
 
+export interface CallsiteInfo {
+  id: number;
+  parentId: number;
+  depth: number;
+  name?: string;
+  totalSize: number;
+  selfSize: number;
+  mapping: string;
+}
+
 export interface TraceFileSource {
   type: 'FILE';
   file: File;
@@ -153,6 +163,8 @@
   id: number;
   upid: number;
   ts: number;
+  expandedCallsite?: CallsiteInfo;
+  viewingOption?: string;
 }
 
 export interface ChromeSliceSelection {
diff --git a/ui/src/controller/globals.ts b/ui/src/controller/globals.ts
index e268932..1dde45d 100644
--- a/ui/src/controller/globals.ts
+++ b/ui/src/controller/globals.ts
@@ -20,10 +20,10 @@
 import {createEmptyState, State} from '../common/state';
 import {ControllerAny} from './controller';
 
-type PublishKinds =
-    'OverviewData'|'TrackData'|'Threads'|'QueryResult'|'LegacyTrace'|
-    'SliceDetails'|'CounterDetails'|'HeapDumpDetails'|'FileDownload'|'Loading'|
-    'Search'|'BufferUsage'|'RecordingLog'|'SearchResult';
+type PublishKinds = 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|
+    'LegacyTrace'|'SliceDetails'|'CounterDetails'|'HeapProfileDetails'|
+    'HeapProfileFlamegraph'|'FileDownload'|'Loading'|'Search'|'BufferUsage'|
+    'RecordingLog'|'SearchResult';
 
 export interface App {
   state: State;
diff --git a/ui/src/controller/heap_profile_controller.ts b/ui/src/controller/heap_profile_controller.ts
new file mode 100644
index 0000000..4a5d262
--- /dev/null
+++ b/ui/src/controller/heap_profile_controller.ts
@@ -0,0 +1,313 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Engine} from '../common/engine';
+import {
+  ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
+  DEFAULT_VIEWING_OPTION,
+  expandCallsites,
+  findRootSize,
+  mergeCallsites,
+  OBJECTS_ALLOCATED_KEY,
+  OBJECTS_ALLOCATED_NOT_FREED_KEY,
+  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY
+} from '../common/flamegraph_util';
+import {CallsiteInfo, HeapProfileFlamegraph} from '../common/state';
+
+import {Controller} from './controller';
+import {globals} from './globals';
+
+export interface HeapProfileControllerArgs {
+  engine: Engine;
+}
+const MIN_PIXEL_DISPLAYED = 1;
+
+export class HeapProfileController extends Controller<'main'> {
+  private flamegraphDatasets: Map<string, CallsiteInfo[]> = new Map();
+  private lastSelectedHeapProfile?: HeapProfileFlamegraph;
+
+  constructor(private args: HeapProfileControllerArgs) {
+    super('main');
+  }
+
+  run() {
+    const selection = globals.state.currentHeapProfileFlamegraph;
+
+    if (!selection) return;
+
+    if (selection.kind === 'HEAP_PROFILE_FLAMEGRAPH') {
+      if (this.lastSelectedHeapProfile === undefined ||
+          (this.lastSelectedHeapProfile !== undefined &&
+           (this.lastSelectedHeapProfile.id !== selection.id ||
+            this.lastSelectedHeapProfile.ts !== selection.ts ||
+            this.lastSelectedHeapProfile.upid !== selection.upid ||
+            this.lastSelectedHeapProfile.viewingOption !==
+                selection.viewingOption ||
+            this.lastSelectedHeapProfile.expandedCallsite !==
+                selection.expandedCallsite))) {
+        const selectedId = selection.id;
+        const selectedUpid = selection.upid;
+        const selectedKind = selection.kind;
+        const selectedTs = selection.ts;
+        const selectedExpandedCallsite = selection.expandedCallsite;
+        const lastSelectedViewingOption = selection.viewingOption ?
+            selection.viewingOption :
+            DEFAULT_VIEWING_OPTION;
+
+        this.lastSelectedHeapProfile = {
+          kind: selectedKind,
+          id: selectedId,
+          upid: selectedUpid,
+          ts: selectedTs,
+          expandedCallsite: selectedExpandedCallsite,
+          viewingOption: lastSelectedViewingOption
+        };
+
+        const expandedId =
+            selectedExpandedCallsite ? selectedExpandedCallsite.id : -1;
+        const rootSize = selectedExpandedCallsite === undefined ?
+            undefined :
+            selectedExpandedCallsite.totalSize;
+
+        const key = `${selectedUpid};${selectedTs}`;
+
+        // TODO(tneda): Prevent lots of flamegraph queries being queued if a
+        // user clicks lots of the markers quickly.
+        this.getFlamegraphData(
+                key, lastSelectedViewingOption, selection.ts, selectedUpid)
+            .then(flamegraphData => {
+              if (flamegraphData !== undefined && selection &&
+                  selection.kind === selectedKind &&
+                  selection.id === selectedId && selection.ts === selectedTs) {
+                const expandedFlamegraphData =
+                    expandCallsites(flamegraphData, expandedId);
+                this.prepareAndMergeCallsites(
+                    expandedFlamegraphData,
+                    this.lastSelectedHeapProfile!.viewingOption,
+                    rootSize,
+                    this.lastSelectedHeapProfile!.expandedCallsite);
+              }
+            });
+      }
+    }
+  }
+
+  private prepareAndMergeCallsites(
+      flamegraphData: CallsiteInfo[],
+      viewingOption: string|undefined = DEFAULT_VIEWING_OPTION,
+      rootSize?: number, expandedCallsite?: CallsiteInfo) {
+    const mergedFlamegraphData = mergeCallsites(
+        flamegraphData, this.getMinSizeDisplayed(flamegraphData, rootSize));
+    globals.publish(
+        'HeapProfileFlamegraph',
+        {flamegraph: mergedFlamegraphData, expandedCallsite, viewingOption});
+  }
+
+
+  async getFlamegraphData(
+      baseKey: string, viewingOption: string, ts: number,
+      upid: number): Promise<CallsiteInfo[]> {
+    let currentData: CallsiteInfo[];
+    const key = `${baseKey}-${viewingOption}`;
+    if (this.flamegraphDatasets.has(key)) {
+      currentData = this.flamegraphDatasets.get(key)!;
+    } else {
+      // TODO(tneda): Show loading state.
+
+      // Collecting data for drawing flamegraph for selected heap profile.
+      // Data needs to be in following format:
+      // id, name, parent_id, depth, total_size
+      const tableName = await this.prepareViewsAndTables(ts, upid);
+      currentData =
+          await this.getFlamegraphDataFromTables(tableName, viewingOption);
+      this.flamegraphDatasets.set(key, currentData);
+    }
+    return currentData;
+  }
+
+  async getFlamegraphDataFromTables(
+      tableName: string, viewingOption = DEFAULT_VIEWING_OPTION) {
+    let orderBy = '';
+    let sizeIndex = 4;
+    switch (viewingOption) {
+      case SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
+        orderBy = `where size > 0 order by depth, parent_hash, size desc, name`;
+        sizeIndex = 4;
+        break;
+      case ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
+        orderBy =
+            `where alloc_size > 0 order by depth, parent_hash, alloc_size desc,
+            name`;
+        sizeIndex = 5;
+        break;
+      case OBJECTS_ALLOCATED_NOT_FREED_KEY:
+        orderBy =
+            `where count > 0 order by depth, parent_hash, count desc, name`;
+        sizeIndex = 6;
+        break;
+      case OBJECTS_ALLOCATED_KEY:
+        orderBy = `where alloc_count > 0 order by depth, parent_hash,
+            alloc_count desc, name`;
+        sizeIndex = 7;
+        break;
+      default:
+        break;
+    }
+
+    const callsites = await this.args.engine.query(
+        `SELECT hash, name, parent_hash, depth, size, alloc_size, count,
+        alloc_count, map_name, self_size from ${tableName} ${orderBy}`);
+
+    const flamegraphData: CallsiteInfo[] = new Array();
+    const hashToindex: Map<number, number> = new Map();
+    for (let i = 0; i < callsites.numRecords; i++) {
+      const hash = callsites.columns[0].longValues![i];
+      const name = callsites.columns[1].stringValues![i];
+      const parentHash = callsites.columns[2].longValues![i];
+      const depth = +callsites.columns[3].longValues![i];
+      const totalSize = +callsites.columns[sizeIndex].longValues![i];
+      const mapping = callsites.columns[8].stringValues![i];
+      const selfSize = +callsites.columns[9].longValues![i];
+      const parentId =
+          hashToindex.has(+parentHash) ? hashToindex.get(+parentHash)! : -1;
+      hashToindex.set(+hash, i);
+      // Instead of hash, we will store index of callsite in this original array
+      // as an id of callsite. That way, we have quicker access to parent and it
+      // will stay unique.
+      flamegraphData.push(
+          {id: i, totalSize, depth, parentId, name, selfSize, mapping});
+    }
+    return flamegraphData;
+  }
+
+  private async prepareViewsAndTables(ts: number, upid: number):
+      Promise<string> {
+    // Creating unique names for views so we can reuse and not delete them
+    // for each marker.
+    const tableNameCallsiteNameSize =
+        this.tableName(`callsite_with_name_and_size`);
+    const tableNameCallsiteHashNameSize =
+        this.tableName(`callsite_hash_name_size`);
+    const tableNameGroupedCallsitesForFlamegraph =
+        this.tableName(`grouped_callsites_for_flamegraph`);
+    // Joining the callsite table with frame table then with alloc table to get
+    // the size and name for each callsite.
+    // TODO(tneda): Make frame name nullable in the trace processor for
+    // consistency with the other columns.
+    await this.args.engine.query(
+        `create view if not exists ${tableNameCallsiteNameSize} as
+         select id, parent_id, depth, IFNULL(DEMANGLE(name), name) as name,
+            map_name, size, alloc_size, count, alloc_count from (
+         select cs.id as id, parent_id, depth,
+            coalesce(symbols.name,
+                case when fr.name != '' then fr.name else map.name end) as name,
+            map.name as map_name,
+            SUM(IFNULL(size, 0)) as size,
+            SUM(IFNULL(size, 0)) as size,
+            SUM(case when size > 0 then size else 0 end) as alloc_size,
+            SUM(IFNULL(count, 0)) as count,
+            SUM(case when count > 0 then count else 0 end) as alloc_count
+         from stack_profile_callsite cs
+         join stack_profile_frame fr on cs.frame_id = fr.id
+         join stack_profile_mapping map on fr.mapping = map.id
+         inner join (
+              select symbol_set_id, FIRST_VALUE(name) OVER(PARTITION BY
+                symbol_set_id) as name
+              from stack_profile_symbol GROUP BY symbol_set_id
+            ) as symbols using(symbol_set_id)
+         left join heap_profile_allocation alloc on alloc.callsite_id = cs.id
+         and alloc.ts <= ${ts} and alloc.upid = ${upid} group by cs.id)`);
+
+    // Recursive query to compute the hash for each callsite based on names
+    // rather than ids.
+    // We get all the children of the row in question and emit a row with hash
+    // equal hash(name, parent.hash). Roots without the parent will have -1 as
+    // hash.  Slices will be merged into a big slice.
+    await this.args.engine.query(
+        `create view if not exists ${tableNameCallsiteHashNameSize} as
+        with recursive callsite_table_names(
+          id, hash, name, map_name, size, alloc_size, count, alloc_count,
+          parent_hash, depth) AS (
+        select id, hash(name) as hash, name, map_name, size, alloc_size, count,
+          alloc_count, -1, depth
+        from ${tableNameCallsiteNameSize}
+        where depth = 0
+        union all
+        select cs.id, hash(cs.name, ctn.hash) as hash, cs.name, cs.map_name,
+          cs.size, cs.alloc_size, cs.count, cs.alloc_count, ctn.hash, cs.depth
+        from callsite_table_names ctn
+        inner join ${tableNameCallsiteNameSize} cs ON ctn.id = cs.parent_id
+        )
+        select hash, name, map_name, parent_hash, depth, SUM(size) as size,
+          SUM(case when alloc_size > 0 then alloc_size else 0 end)
+            as alloc_size, SUM(count) as count,
+          SUM(case when alloc_count > 0 then alloc_count else 0 end)
+            as alloc_count
+        from callsite_table_names
+        group by hash`);
+
+    // Recursive query to compute the cumulative size of each callsite.
+    // Base case: We get all the callsites where the size is non-zero.
+    // Recursive case: We get the callsite which is the parent of the current
+    //  callsite(in terms of hashes) and emit a row with the size of the current
+    //  callsite plus all the info of the parent.
+    // Grouping: For each callsite, our recursive table has n rows where n is
+    //  the number of descendents with a non-zero self size. We need to group on
+    //  the hash and sum all the sizes to get the cumulative size for each
+    //  callsite hash.
+    await this.args.engine.query(`create temp table if not exists ${
+        tableNameGroupedCallsitesForFlamegraph}
+        as with recursive callsite_children(
+          hash, name, map_name, parent_hash, depth, size, alloc_size, count,
+          alloc_count, self_size, self_alloc_size, self_count, self_alloc_count)
+        as (
+        select hash, name, map_name, parent_hash, depth, size, alloc_size,
+          count, alloc_count, size as self_size, alloc_size as self_alloc_size,
+          count as self_count, alloc_count as self_alloc_count
+        from ${tableNameCallsiteHashNameSize}
+        union all
+        select chns.hash, chns.name, chns.map_name, chns.parent_hash,
+          chns.depth, cc.size, cc.alloc_size, cc.count, cc.alloc_count,
+          chns.size, chns.alloc_size, chns.count, chns.alloc_count
+        from ${tableNameCallsiteHashNameSize} chns
+        inner join callsite_children cc on chns.hash = cc.parent_hash
+        )
+        select hash, name, map_name, parent_hash, depth, SUM(size) as size,
+          SUM(case when alloc_size > 0 then alloc_size else 0 end)
+            as alloc_size, SUM(count) as count,
+          SUM(case when alloc_count > 0 then alloc_count else 0 end) as
+            alloc_count,
+          self_size, self_alloc_size, self_count, self_alloc_count
+        from callsite_children
+        group by hash`);
+    return tableNameGroupedCallsitesForFlamegraph;
+  }
+
+  tableName(name: string): string {
+    const selection = globals.state.currentHeapProfileFlamegraph;
+    if (!selection) return name;
+    return `${name}_${selection.upid}_${selection.ts}`;
+  }
+
+  getMinSizeDisplayed(flamegraphData: CallsiteInfo[], rootSize?: number):
+      number {
+    const timeState = globals.state.frontendLocalState.visibleState;
+    const width =
+        (timeState.endSec - timeState.startSec) / timeState.resolution;
+    if (rootSize === undefined) {
+      rootSize = findRootSize(flamegraphData);
+    }
+    return MIN_PIXEL_DISPLAYED * rootSize / width;
+  }
+}
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 2257307..3b257d3 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -73,7 +73,7 @@
         if (results !== undefined && selection &&
             selection.kind === selectedKind && selection.id === selectedId) {
           Object.assign(selected, results);
-          globals.publish('HeapDumpDetails', selected);
+          globals.publish('HeapProfileDetails', selected);
         }
       });
     } else if (selection.kind === 'COUNTER') {
@@ -154,13 +154,7 @@
             ts} and upid = ${upid}`);
     const allocatedNotFreed = allocatedNotFreedMemory.columns[0].longValues![0];
     const startTime = fromNs(ts) - globals.state.traceTime.startSec;
-    return {
-      ts: startTime,
-      allocated,
-      allocatedNotFreed,
-      tsNs: ts,
-      pid,
-    };
+    return {ts: startTime, allocated, allocatedNotFreed, tsNs: ts, pid, upid};
   }
 
   async counterDetails(ts: number, rightTs: number, id: number) {
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 8c5fb67..e2f692e 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -44,9 +44,6 @@
 import {GPU_FREQ_TRACK_KIND} from '../tracks/gpu_freq/common';
 import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile/common';
 import {
-  HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND
-} from '../tracks/heap_profile_flamegraph/common';
-import {
   PROCESS_SCHEDULING_TRACK_KIND
 } from '../tracks/process_scheduling/common';
 import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
@@ -54,6 +51,10 @@
 
 import {Child, Children, Controller} from './controller';
 import {globals} from './globals';
+import {
+  HeapProfileController,
+  HeapProfileControllerArgs
+} from './heap_profile_controller';
 import {LoadingManager} from './loading_manager';
 import {LogsController} from './logs_controller';
 import {QueryController, QueryControllerArgs} from './query_controller';
@@ -149,6 +150,10 @@
         childControllers.push(
           Child('selection', SelectionController, selectionArgs));
 
+        const heapProfileArgs: HeapProfileControllerArgs = {engine};
+        childControllers.push(
+            Child('heapProfile', HeapProfileController, heapProfileArgs));
+
         childControllers.push(Child('search', SearchController, {
           engine,
           app: globals,
@@ -633,14 +638,6 @@
               trackGroup: pUuid,
               config: {upid}
             });
-
-            tracksToAdd.push({
-              engineId: this.engineId,
-              kind: HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND,
-              name: `Heap Profile Flamegraph`,
-              trackGroup: pUuid,
-              config: {upid}
-            });
           }
 
           if (upidToProcessTracks.has(upid)) {
diff --git a/ui/src/frontend/flamegraph.ts b/ui/src/frontend/flamegraph.ts
index 704d14f..472440a 100644
--- a/ui/src/frontend/flamegraph.ts
+++ b/ui/src/frontend/flamegraph.ts
@@ -15,7 +15,7 @@
 import {searchSegment} from '../base/binary_search';
 import {cropText} from '../common/canvas_utils';
 
-import {CallsiteInfo} from './globals';
+import {CallsiteInfo} from '../common/state';
 
 interface Node {
   width: number;
@@ -59,6 +59,8 @@
   private hoveredCallsite?: CallsiteInfo;
   private clickedCallsite?: CallsiteInfo;
 
+  private startingY = 0;
+
   constructor(flamegraphData: CallsiteInfo[]) {
     this.flamegraphData = flamegraphData;
     this.findMaxDepth();
@@ -97,6 +99,7 @@
    */
   updateDataIfChanged(
       flamegraphData: CallsiteInfo[], clickedCallsite?: CallsiteInfo) {
+    this.clickedCallsite = clickedCallsite;
     if (this.flamegraphData === flamegraphData) {
       return;
     }
@@ -114,6 +117,7 @@
     const name = '____MMMMMMQQwwZZZZZZzzzzzznnnnnnwwwwwwWWWWWqq$$mmmmmm__';
     const charWidth = ctx.measureText(name).width / name.length;
     const nodeHeight = this.getNodeHeight();
+    this.startingY = y;
 
     if (this.flamegraphData === undefined) {
       return;
@@ -167,7 +171,7 @@
           (isFullWidth ? 1 : value.totalSize / parentSize) * parentNode.width;
 
       const currentX = parentNode.nextXForChildren;
-      currentY = nodeHeight * (value.depth + 1);
+      currentY = y + nodeHeight * (value.depth + 1);
 
       // Draw node.
       const name = this.getCallsiteName(value);
@@ -329,17 +333,17 @@
     this.hoveredCallsite = undefined;
   }
 
-  // Returns id of clicked callsite if any.
-  onMouseClick({x, y}: {x: number, y: number}): number {
+  onMouseClick({x, y}: {x: number, y: number}): CallsiteInfo|undefined {
     if (this.isThumbnail) {
-      return -1;
+      return undefined;
     }
     const clickedCallsite = this.findSelectedCallsite(x, y);
-    return clickedCallsite === undefined ? -1 : clickedCallsite.id;
+    return clickedCallsite;
   }
 
   private findSelectedCallsite(x: number, y: number): CallsiteInfo|undefined {
-    const depth = Math.trunc(y / this.getNodeHeight()) - 1;  // at 0 is root
+    const depth = Math.trunc((y - this.startingY) / this.getNodeHeight()) -
+        1;  // at 0 is root
     if (depth >= 0 && this.xStartsPerDepth.has(depth)) {
       const startX = this.searchSmallest(this.xStartsPerDepth.get(depth)!, x);
       const result = this.graphData.get(`${depth};${startX}`);
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 81ae8f6..57389ad 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -15,7 +15,7 @@
 import {assertExists} from '../base/logging';
 import {DeferredAction} from '../common/actions';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
-import {createEmptyState, State} from '../common/state';
+import {CallsiteInfo, createEmptyState, State} from '../common/state';
 
 import {FrontendLocalState} from './frontend_local_state';
 import {RafScheduler} from './raf_scheduler';
@@ -45,22 +45,18 @@
   duration?: number;
 }
 
-export interface CallsiteInfo {
-  id: number;
-  parentId: number;
-  depth: number;
-  name?: string;
-  totalSize: number;
-  selfSize: number;
-  mapping: string;
-}
-
 export interface HeapProfileDetails {
+  id?: number;
   ts?: number;
   tsNs?: number;
   allocated?: number;
   allocatedNotFreed?: number;
   pid?: number;
+  upid?: number;
+  flamegraph?: CallsiteInfo[];
+  expandedCallsite?: CallsiteInfo;
+  viewingOption?: string;
+  expandedId?: number;
 }
 
 export interface QuantizedLoad {
@@ -96,7 +92,7 @@
   private _threadMap?: ThreadMap = undefined;
   private _sliceDetails?: SliceDetails = undefined;
   private _counterDetails?: CounterDetails = undefined;
-  private _heapDumpDetails?: HeapProfileDetails = undefined;
+  private _heapProfileDetails?: HeapProfileDetails = undefined;
   private _numQueriesQueued = 0;
   private _bufferUsage?: number = undefined;
   private _recordingLog?: string = undefined;
@@ -128,7 +124,7 @@
     this._threadMap = new Map<number, ThreadDesc>();
     this._sliceDetails = {};
     this._counterDetails = {};
-    this._heapDumpDetails = {};
+    this._heapProfileDetails = {};
   }
 
   get state(): State {
@@ -184,12 +180,12 @@
     this._counterDetails = assertExists(click);
   }
 
-  get heapDumpDetails() {
-    return assertExists(this._heapDumpDetails);
+  get heapProfileDetails() {
+    return assertExists(this._heapProfileDetails);
   }
 
-  set heapDumpDetails(click: HeapProfileDetails) {
-    this._heapDumpDetails = assertExists(click);
+  set heapProfileDetails(click: HeapProfileDetails) {
+    this._heapProfileDetails = assertExists(click);
   }
 
   set numQueuedQueries(value: number) {
diff --git a/ui/src/frontend/heap_profile_panel.ts b/ui/src/frontend/heap_profile_panel.ts
index 524bb40..c862582 100644
--- a/ui/src/frontend/heap_profile_panel.ts
+++ b/ui/src/frontend/heap_profile_panel.ts
@@ -15,71 +15,139 @@
 import * as m from 'mithril';
 
 import {Actions} from '../common/actions';
+import {
+  ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
+  DEFAULT_VIEWING_OPTION,
+  OBJECTS_ALLOCATED_KEY,
+  OBJECTS_ALLOCATED_NOT_FREED_KEY,
+  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY
+} from '../common/flamegraph_util';
 import {timeToCode} from '../common/time';
 
+import {Flamegraph} from './flamegraph';
 import {globals} from './globals';
-import {Panel} from './panel';
+import {Panel, PanelSize} from './panel';
 
 interface HeapProfileDetailsPanelAttrs {}
 
+const HEADER_HEIGHT = 30;
+
 export class HeapProfileDetailsPanel extends
     Panel<HeapProfileDetailsPanelAttrs> {
   private ts = 0;
   private pid = 0;
+  private flamegraph: Flamegraph = new Flamegraph([]);
+  private currentViewingOption = DEFAULT_VIEWING_OPTION;
 
   view() {
-    const heapDumpInfo = globals.heapDumpDetails;
+    const heapDumpInfo = globals.heapProfileDetails;
     if (heapDumpInfo && heapDumpInfo.ts !== undefined &&
         heapDumpInfo.allocated !== undefined &&
         heapDumpInfo.allocatedNotFreed !== undefined &&
-        heapDumpInfo.tsNs !== undefined && heapDumpInfo.pid !== undefined) {
+        heapDumpInfo.tsNs !== undefined && heapDumpInfo.pid !== undefined &&
+        heapDumpInfo.upid !== undefined) {
       this.ts = heapDumpInfo.tsNs;
       this.pid = heapDumpInfo.pid;
+      if (heapDumpInfo.flamegraph) {
+        this.flamegraph.updateDataIfChanged(heapDumpInfo.flamegraph);
+      }
+      const height = heapDumpInfo.flamegraph ?
+          this.flamegraph.getHeight() + HEADER_HEIGHT :
+          0;
       return m(
           '.details-panel',
-          m('.details-panel-heading', m('h2', `Heap Profile Details`)),
-          m(
-              '.details-table',
-              [m('table',
-                 [
-                   m('tr',
-                     m('th', `Snapshot time`),
-                     m('td', `${timeToCode(heapDumpInfo.ts)}`)),
-                   m('tr',
-                     m('th', `Total allocated:`),
-                     m('td',
-                       `${heapDumpInfo.allocated.toLocaleString()} bytes`)),
-                   m('tr',
-                     m('th', `Allocated not freed:`),
-                     m('td',
-                       `${
-                           heapDumpInfo.allocatedNotFreed
-                               .toLocaleString()} bytes`)),
-                 ])],
-              ),
-          m('.explanation',
-            'Heap profile support is in beta. If you need missing features, ',
-            'download and open it in ',
-            m(`a[href='https://pprof.corp.google.com']`, 'pprof'),
-            ' (Googlers only) or ',
-            m(`a[href='https://www.speedscope.app']`, 'Speedscope'),
-            '.'),
-          m('button',
-            {
-              onclick: () => {
-                this.downloadPprof();
+          {
+            onclick: (e: MouseEvent) => {
+              if (this.flamegraph !== undefined) {
+                this.onMouseClick({y: e.layerY, x: e.layerX});
               }
+              return false;
             },
-            m('i.material-icons', 'file_download'),
-            'Download profile'),
+            onmousemove: (e: MouseEvent) => {
+              if (this.flamegraph !== undefined) {
+                this.onMouseMove({y: e.layerY, x: e.layerX});
+                globals.rafScheduler.scheduleRedraw();
+              }
+              return false;
+            },
+            onmouseout: () => {
+              if (this.flamegraph !== undefined) {
+                this.onMouseOut();
+              }
+            }
+          },
+          m('.details-panel-heading.heap-profile',
+            [
+              m('div.options',
+                [
+                  m('div.title', `Heap Profile:`),
+                  this.getViewingOptionButtons(),
+                ]),
+              m('div.details',
+                [
+                  m('div.time',
+                    `Snapshot time: ${timeToCode(heapDumpInfo.ts)}`),
+                  m('button.download',
+                    {
+                      onclick: () => {
+                        this.downloadPprof();
+                      }
+                    },
+                    m('i.material-icons', 'file_download'),
+                    'Download profile'),
+                ]),
+            ]),
+          m(`div[style=height:${height}px]`),
       );
     } else {
       return m(
           '.details-panel',
-          m('.details-panel-heading', m('h2', `Heap Profile Details`)));
+          m('.details-panel-heading', m('h2', `Heap Profile`)));
     }
   }
 
+  getButtonsClass(viewingOption = DEFAULT_VIEWING_OPTION): string {
+    return this.currentViewingOption === viewingOption ? '.chosen' : '';
+  }
+
+  getViewingOptionButtons(): m.Children {
+    return m(
+        'div',
+        m(`button${this.getButtonsClass(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY)}`,
+          {
+            onclick: () => {
+              this.changeViewingOption(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY);
+            }
+          },
+          'space'),
+        m(`button${this.getButtonsClass(ALLOC_SPACE_MEMORY_ALLOCATED_KEY)}`,
+          {
+            onclick: () => {
+              this.changeViewingOption(ALLOC_SPACE_MEMORY_ALLOCATED_KEY);
+            }
+          },
+          'alloc_space'),
+        m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_NOT_FREED_KEY)}`,
+          {
+            onclick: () => {
+              this.changeViewingOption(OBJECTS_ALLOCATED_NOT_FREED_KEY);
+            }
+          },
+          'objects'),
+        m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_KEY)}`,
+          {
+            onclick: () => {
+              this.changeViewingOption(OBJECTS_ALLOCATED_KEY);
+            }
+          },
+          'alloc_objects'));
+  }
+
+  changeViewingOption(viewingOption: string) {
+    this.currentViewingOption = viewingOption;
+    globals.dispatch(Actions.changeViewHeapProfileFlamegraph({viewingOption}));
+  }
+
   downloadPprof() {
     const engine = Object.values(globals.state.engines)[0];
     if (!engine) return;
@@ -89,5 +157,34 @@
         Actions.convertTraceToPprof({pid: this.pid, ts1: this.ts, src}));
   }
 
-  renderCanvas() {}
+  private changeFlamegraphData() {
+    const data = globals.heapProfileDetails;
+    const flamegraphData = data.flamegraph === undefined ? [] : data.flamegraph;
+    this.flamegraph.updateDataIfChanged(flamegraphData, data.expandedCallsite);
+  }
+
+  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
+    this.changeFlamegraphData();
+    const unit =
+        this.currentViewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
+            this.currentViewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ?
+        'B' :
+        '';
+    this.flamegraph.draw(ctx, size.width, size.height, 0, HEADER_HEIGHT, unit);
+  }
+
+  onMouseClick({x, y}: {x: number, y: number}): boolean {
+    const expandedCallsite = this.flamegraph.onMouseClick({x, y});
+    globals.dispatch(Actions.expandHeapProfileFlamegraph({expandedCallsite}));
+    return true;
+  }
+
+  onMouseMove({x, y}: {x: number, y: number}): boolean {
+    this.flamegraph.onMouseMove({x, y});
+    return true;
+  }
+
+  onMouseOut() {
+    this.flamegraph.onMouseOut();
+  }
 }
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index cce7b1e..72d21b2 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -28,9 +28,7 @@
   LogExistsKey
 } from '../common/logs';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
-import {
-  HeapProfileFlamegraphKey
-} from '../tracks/heap_profile_flamegraph/common';
+import {CallsiteInfo} from '../common/state';
 
 import {maybeShowErrorDialog} from './error_dialog';
 import {
@@ -44,8 +42,7 @@
 import {HomePage} from './home_page';
 import {openBufferWithLegacyTraceViewer} from './legacy_trace_viewer';
 import {postMessageHandler} from './post_message_handler';
-import {RecordPage} from './record_page';
-import {updateAvailableAdbDevices} from './record_page';
+import {RecordPage, updateAvailableAdbDevices} from './record_page';
 import {Router} from './router';
 import {CheckHttpRpcConnection} from './rpc_http_dialog';
 import {ViewerPage} from './viewer_page';
@@ -100,8 +97,6 @@
     if ([LogExistsKey, LogBoundsKey, LogEntriesKey].includes(args.id)) {
       const data = globals.trackDataStore.get(LogExistsKey) as LogExists;
       if (data && data.exists) globals.rafScheduler.scheduleFullRedraw();
-    } else if (HeapProfileFlamegraphKey === args.id) {
-      globals.rafScheduler.scheduleFullRedraw();
     } else {
       globals.rafScheduler.scheduleRedraw();
     }
@@ -130,8 +125,19 @@
     this.redraw();
   }
 
-  publishHeapDumpDetails(click: HeapProfileDetails) {
-    globals.heapDumpDetails = click;
+  publishHeapProfileDetails(click: HeapProfileDetails) {
+    globals.heapProfileDetails = click;
+    this.redraw();
+  }
+
+  publishHeapProfileFlamegraph(args: {
+    flamegraph: CallsiteInfo[],
+    expandedCallsite?: CallsiteInfo,
+    viewingOption?: string
+  }) {
+    globals.heapProfileDetails.flamegraph = args.flamegraph;
+    globals.heapProfileDetails.expandedCallsite = args.expandedCallsite;
+    globals.heapProfileDetails.viewingOption = args.viewingOption;
     this.redraw();
   }
 
diff --git a/ui/src/tracks/all_controller.ts b/ui/src/tracks/all_controller.ts
index 912f34f..ca4f7c8 100644
--- a/ui/src/tracks/all_controller.ts
+++ b/ui/src/tracks/all_controller.ts
@@ -18,7 +18,6 @@
 import './chrome_slices/controller';
 import './counter/controller';
 import './heap_profile/controller';
-import './heap_profile_flamegraph/controller';
 import './cpu_freq/controller';
 import './gpu_freq/controller';
 import './cpu_slices/controller';
diff --git a/ui/src/tracks/all_frontend.ts b/ui/src/tracks/all_frontend.ts
index 2957edf..3ce3de0 100644
--- a/ui/src/tracks/all_frontend.ts
+++ b/ui/src/tracks/all_frontend.ts
@@ -18,7 +18,6 @@
 import './chrome_slices/frontend';
 import './counter/frontend';
 import './heap_profile/frontend';
-import './heap_profile_flamegraph/frontend';
 import './cpu_freq/frontend';
 import './gpu_freq/frontend';
 import './cpu_slices/frontend';
diff --git a/ui/src/tracks/heap_profile_flamegraph/common.ts b/ui/src/tracks/heap_profile_flamegraph/common.ts
deleted file mode 100644
index 4d5c0e7..0000000
--- a/ui/src/tracks/heap_profile_flamegraph/common.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {TrackData} from '../../common/track_data';
-import {CallsiteInfo} from '../../frontend/globals';
-
-export const HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND = 'HeapProfileFlamegraphTrack';
-export const HeapProfileFlamegraphKey = 'heap-profile-flamegraph';
-
-export const SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'space';
-export const ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'alloc_space';
-export const OBJECTS_ALLOCATED_NOT_FREED_KEY = 'objects';
-export const OBJECTS_ALLOCATED_KEY = 'alloc_objects';
-
-export const DEFAULT_VIEWING_OPTION = SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY;
-
-export interface Data extends TrackData {
-  flamegraph: CallsiteInfo[];
-  clickedCallsite?: CallsiteInfo;
-  // undefined means that there is no change since previous value.
-  viewingOption?: string;
-}
-
-export interface Config {
-  upid: number;
-  ts: number;
-  isMinimized: boolean;
-  expandedId: number;
-  viewingOption: string;
-}
diff --git a/ui/src/tracks/heap_profile_flamegraph/controller.ts b/ui/src/tracks/heap_profile_flamegraph/controller.ts
deleted file mode 100644
index d4cf389..0000000
--- a/ui/src/tracks/heap_profile_flamegraph/controller.ts
+++ /dev/null
@@ -1,462 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {Actions} from '../../common/actions';
-import {globals} from '../../controller/globals';
-import {
-  TrackController,
-  trackControllerRegistry
-} from '../../controller/track_controller';
-import {findRootSize} from '../../frontend/flamegraph';
-import {CallsiteInfo} from '../../frontend/globals';
-
-import {
-  ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-  Config,
-  Data,
-  DEFAULT_VIEWING_OPTION,
-  HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND,
-  HeapProfileFlamegraphKey,
-  OBJECTS_ALLOCATED_KEY,
-  OBJECTS_ALLOCATED_NOT_FREED_KEY,
-  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
-} from './common';
-
-const MIN_PIXEL_DISPLAYED = 1;
-
-export function expandCallsites(
-    data: CallsiteInfo[], clickedCallsiteIndex: number): CallsiteInfo[] {
-  if (clickedCallsiteIndex === -1) return data;
-  const expandedCallsites: CallsiteInfo[] = [];
-  if (clickedCallsiteIndex >= data.length || clickedCallsiteIndex < -1) {
-    return expandedCallsites;
-  }
-  const clickedCallsite = data[clickedCallsiteIndex];
-  expandedCallsites.unshift(clickedCallsite);
-  // Adding parents
-  let parentId = clickedCallsite.parentId;
-  while (parentId > -1) {
-    expandedCallsites.unshift(data[parentId]);
-    parentId = data[parentId].parentId;
-  }
-  // Adding children
-  const parents: number[] = [];
-  parents.push(clickedCallsiteIndex);
-  for (let i = clickedCallsiteIndex + 1; i < data.length; i++) {
-    const element = data[i];
-    if (parents.includes(element.parentId)) {
-      expandedCallsites.push(element);
-      parents.push(element.id);
-    }
-  }
-  return expandedCallsites;
-}
-
-// Merge callsites that have approximately width less than
-// MIN_PIXEL_DISPLAYED. All small callsites in the same depth and with same
-// parent will be merged to one callsite with size of the biggest merged
-// callsite.
-export function mergeCallsites(data: CallsiteInfo[], minSizeDisplayed: number) {
-  const mergedData: CallsiteInfo[] = [];
-  const mergedCallsites: Map<number, number> = new Map();
-  for (let i = 0; i < data.length; i++) {
-    // When a small callsite is found, it will be merged with other small
-    // callsites of the same depth. So if the current callsite has already been
-    // merged we can skip it.
-    if (mergedCallsites.has(data[i].id)) {
-      continue;
-    }
-    const copiedCallsite = copyCallsite(data[i]);
-    copiedCallsite.parentId =
-        getCallsitesParentHash(copiedCallsite, mergedCallsites);
-
-    // If current callsite is small, find other small callsites with same depth
-    // and parent and merge them into the current one, marking them as merged.
-    if (copiedCallsite.totalSize <= minSizeDisplayed && i + 1 < data.length) {
-      let j = i + 1;
-      let nextCallsite = data[j];
-      while (j < data.length && copiedCallsite.depth === nextCallsite.depth) {
-        if (copiedCallsite.parentId ===
-                getCallsitesParentHash(nextCallsite, mergedCallsites) &&
-            nextCallsite.totalSize <= minSizeDisplayed) {
-          copiedCallsite.totalSize += nextCallsite.totalSize;
-          mergedCallsites.set(nextCallsite.id, copiedCallsite.id);
-        }
-        j++;
-        nextCallsite = data[j];
-      }
-    }
-    mergedData.push(copiedCallsite);
-  }
-  return mergedData;
-}
-
-function copyCallsite(callsite: CallsiteInfo): CallsiteInfo {
-  return {
-    id: callsite.id,
-    parentId: callsite.parentId,
-    depth: callsite.depth,
-    name: callsite.name,
-    totalSize: callsite.totalSize,
-    mapping: callsite.mapping,
-    selfSize: callsite.selfSize
-  };
-}
-
-function getCallsitesParentHash(
-    callsite: CallsiteInfo, map: Map<number, number>): number {
-  return map.has(callsite.parentId) ? map.get(callsite.parentId)! :
-                                      callsite.parentId;
-}
-
-function getMinSizeDisplayed(
-    flamegraphData: CallsiteInfo[], rootSize?: number): number {
-  const timeState = globals.state.frontendLocalState.visibleState;
-  const width = (timeState.endSec - timeState.startSec) / timeState.resolution;
-  if (rootSize === undefined) {
-    rootSize = findRootSize(flamegraphData);
-  }
-  return MIN_PIXEL_DISPLAYED * rootSize / width;
-}
-
-class HeapProfileFlameraphTrackController extends
-    TrackController<Config, Data> {
-  static readonly kind = HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND;
-  private start = 0;
-  private end = 0;
-  private resolution = 0;
-  private length = 0;
-  private lastSelectedTs?: number;
-  private lastSelectedId?: number;
-  private lastExpandedId?: number;
-  private lastViewingOption?: string;
-
-  private flamegraphDatasets: Map<string, CallsiteInfo[]> = new Map();
-
-  async onBoundsChange(start: number, end: number, resolution: number):
-      Promise<Data> {
-    this.start = start;
-    this.end = end;
-    this.resolution = resolution;
-
-    return this.generateEmptyData();
-  }
-
-  private generateEmptyData(): Data {
-    const data: Data = {
-      start: -1,
-      end: -1,
-      resolution: this.resolution,
-      length: 0,
-      flamegraph: []
-    };
-    return data;
-  }
-
-  run() {
-    const selection = globals.state.currentHeapProfileFlamegraph;
-
-    if (selection && selection.kind === 'HEAP_PROFILE_FLAMEGRAPH') {
-      if (this.lastSelectedId !== selection.id ||
-          this.lastSelectedTs !== selection.ts) {
-        const selectedId = selection.id;
-        const selectedUpid = selection.upid;
-        const selectedKind = selection.kind;
-        const selectedTs = selection.ts;
-        // If we opened new heap profile, we don't want to show it expanded, but
-        // if we are opening trace for the first time with existing state (ie.
-        // via link), we want to show it expanded.
-        this.lastExpandedId = !this.lastSelectedTs && this.config.expandedId ?
-            this.config.expandedId :
-            -1;
-        this.lastSelectedId = selection.id;
-        this.lastSelectedTs = selection.ts;
-        this.lastViewingOption = this.config.viewingOption ?
-            this.config.viewingOption :
-            DEFAULT_VIEWING_OPTION;
-
-        this.config.ts = selectedTs;
-        this.config.upid = selectedUpid;
-        this.config.expandedId = this.lastExpandedId;
-
-        const key = `${selectedUpid};${selectedTs}`;
-
-        // TODO(tneda): Prevent lots of flamegraph queries being queued if a
-        // user clicks lots of the markers quickly.
-        this.getFlamegraphData(
-                key, this.lastViewingOption, selection.ts, selectedUpid)
-            .then(flamegraphData => {
-              if (flamegraphData !== undefined && selection &&
-                  selection.kind === selectedKind &&
-                  selection.id === selectedId && selection.ts === selectedTs) {
-                this.prepareAndMergeCallsites(
-                    flamegraphData, this.lastViewingOption);
-                globals.dispatch(Actions.updateTrackConfig(
-                    {id: this.trackState.id, config: this.config}));
-              }
-            });
-      } else if (
-          this.config.expandedId &&
-          this.config.expandedId !== this.lastExpandedId) {
-        const key = `${this.config.upid};${this.lastSelectedTs}`;
-        this.lastExpandedId = this.config.expandedId;
-        this.getFlamegraphData(
-                key,
-                this.config.viewingOption,
-                this.lastSelectedTs,
-                this.config.upid)
-            .then(flamegraphData => {
-              this.prepareAndMergeCallsites(flamegraphData, key);
-            });
-      } else if (this.config.viewingOption !== this.lastViewingOption) {
-        const key = `${this.config.upid};${this.lastSelectedTs}`;
-        this.lastViewingOption = this.config.viewingOption;
-        this.config.expandedId = -1;
-        this.getFlamegraphData(
-                key,
-                this.config.viewingOption,
-                this.lastSelectedTs,
-                this.config.upid)
-            .then(flamegraphData => {
-              this.prepareAndMergeCallsites(
-                  flamegraphData, this.lastViewingOption);
-            });
-        globals.dispatch(Actions.updateTrackConfig(
-            {id: this.trackState.id, config: this.config}));
-      }
-    } else {
-      globals.publish(
-          'TrackData', {id: HeapProfileFlamegraphKey, data: undefined});
-    }
-  }
-
-  private publishEmptyData() {
-    globals.publish(
-        'TrackData',
-        {id: HeapProfileFlamegraphKey, data: this.generateEmptyData()});
-  }
-
-  private prepareAndMergeCallsites(
-      flamegraphData: CallsiteInfo[],
-      viewingOption: string|undefined = DEFAULT_VIEWING_OPTION) {
-    const expandedFlamegraphData =
-        expandCallsites(flamegraphData, this.config.expandedId);
-    const expandedCallsite = this.config.expandedId === -1 ?
-        undefined :
-        flamegraphData[this.config.expandedId];
-
-    const rootSize =
-        expandedCallsite === undefined ? undefined : expandedCallsite.totalSize;
-
-    const mergedFlamegraphData = mergeCallsites(
-        expandedFlamegraphData,
-        getMinSizeDisplayed(expandedFlamegraphData, rootSize));
-
-    globals.publish('TrackData', {
-      id: HeapProfileFlamegraphKey,
-      data: {
-        start: this.start,
-        end: this.end,
-        resolution: this.resolution,
-        length: this.length,
-        flamegraph: mergedFlamegraphData,
-        clickedCallsite: expandedCallsite,
-        viewingOption
-      }
-    });
-  }
-
-
-  async getFlamegraphData(
-      baseKey: string, viewingOption: string, ts: number,
-      upid: number): Promise<CallsiteInfo[]> {
-    let currentData: CallsiteInfo[];
-    const key = `${baseKey}-${viewingOption}`;
-    if (this.flamegraphDatasets.has(key)) {
-      currentData = this.flamegraphDatasets.get(key)!;
-    } else {
-      // Sending empty data to show Loading state before we get an actual
-      // data.
-      this.publishEmptyData();
-
-      // Collecting data for drawing flamegraph for selected heap profile.
-      // Data needs to be in following format:
-      // id, name, parent_id, depth, total_size
-      const tableName = await this.prepareViewsAndTables(ts, upid);
-      currentData =
-          await this.getFlamegraphDataFromTables(tableName, viewingOption);
-      this.flamegraphDatasets.set(key, currentData);
-    }
-    return currentData;
-  }
-
-  async getFlamegraphDataFromTables(
-      tableName: string, viewingOption = DEFAULT_VIEWING_OPTION) {
-    let orderBy = '';
-    let sizeIndex = 4;
-    switch (viewingOption) {
-      case SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
-        orderBy = `where size > 0 order by depth, parent_hash, size desc, name`;
-        sizeIndex = 4;
-        break;
-      case ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
-        orderBy =
-            `where alloc_size > 0 order by depth, parent_hash, alloc_size desc,
-            name`;
-        sizeIndex = 5;
-        break;
-      case OBJECTS_ALLOCATED_NOT_FREED_KEY:
-        orderBy =
-            `where count > 0 order by depth, parent_hash, count desc, name`;
-        sizeIndex = 6;
-        break;
-      case OBJECTS_ALLOCATED_KEY:
-        orderBy = `where alloc_count > 0 order by depth, parent_hash,
-            alloc_count desc, name`;
-        sizeIndex = 7;
-        break;
-      default:
-        break;
-    }
-
-    const callsites = await this.query(
-        `SELECT hash, name, parent_hash, depth, size, alloc_size, count,
-        alloc_count, map_name, self_size from ${tableName} ${orderBy}`);
-
-    const flamegraphData: CallsiteInfo[] = new Array();
-    const hashToindex: Map<number, number> = new Map();
-    for (let i = 0; i < callsites.numRecords; i++) {
-      const hash = callsites.columns[0].longValues![i];
-      const name = callsites.columns[1].stringValues![i];
-      const parentHash = callsites.columns[2].longValues![i];
-      const depth = +callsites.columns[3].longValues![i];
-      const totalSize = +callsites.columns[sizeIndex].longValues![i];
-      const mapping = callsites.columns[8].stringValues![i];
-      const selfSize = +callsites.columns[9].longValues![i];
-      const parentId =
-          hashToindex.has(+parentHash) ? hashToindex.get(+parentHash)! : -1;
-      hashToindex.set(+hash, i);
-      // Instead of hash, we will store index of callsite in this original array
-      // as an id of callsite. That way, we have quicker access to parent and it
-      // will stay unique.
-      flamegraphData.push(
-          {id: i, totalSize, depth, parentId, name, selfSize, mapping});
-    }
-    return flamegraphData;
-  }
-
-  private async prepareViewsAndTables(ts: number, upid: number):
-      Promise<string> {
-    // Creating unique names for views so we can reuse and not delete them
-    // for each marker.
-    const tableNameCallsiteNameSize =
-        this.tableName(`callsite_with_name_and_size_${ts}`);
-    const tableNameCallsiteHashNameSize =
-        this.tableName(`callsite_hash_name_size_${ts}`);
-    const tableNameGroupedCallsitesForFlamegraph =
-        this.tableName(`grouped_callsites_for_flamegraph${ts}`);
-    // Joining the callsite table with frame table then with alloc table to get
-    // the size and name for each callsite.
-    // TODO(tneda): Make frame name nullable in the trace processor for
-    // consistency with the other columns.
-    await this.query(`create view if not exists ${tableNameCallsiteNameSize} as
-         select id, parent_id, depth, IFNULL(DEMANGLE(name), name) as name,
-            map_name, size, alloc_size, count, alloc_count from (
-         select cs.id as id, parent_id, depth,
-            coalesce(symbols.name,
-                case when fr.name != '' then fr.name else map.name end) as name,
-            map.name as map_name,
-            SUM(IFNULL(size, 0)) as size,
-            SUM(IFNULL(size, 0)) as size,
-            SUM(case when size > 0 then size else 0 end) as alloc_size,
-            SUM(IFNULL(count, 0)) as count,
-            SUM(case when count > 0 then count else 0 end) as alloc_count
-         from stack_profile_callsite cs
-         join stack_profile_frame fr on cs.frame_id = fr.id
-         join stack_profile_mapping map on fr.mapping = map.id
-         inner join (
-              select symbol_set_id, FIRST_VALUE(name) OVER(PARTITION BY
-                symbol_set_id) as name
-              from stack_profile_symbol GROUP BY symbol_set_id
-            ) as symbols using(symbol_set_id)
-         left join heap_profile_allocation alloc on alloc.callsite_id = cs.id
-         and alloc.ts <= ${ts} and alloc.upid = ${upid} group by cs.id)`);
-
-    // Recursive query to compute the hash for each callsite based on names
-    // rather than ids.
-    // We get all the children of the row in question and emit a row with hash
-    // equal hash(name, parent.hash). Roots without the parent will have -1 as
-    // hash.  Slices will be merged into a big slice.
-    await this.query(
-        `create view if not exists ${tableNameCallsiteHashNameSize} as
-        with recursive callsite_table_names(
-          id, hash, name, map_name, size, alloc_size, count, alloc_count,
-          parent_hash, depth) AS (
-        select id, hash(name) as hash, name, map_name, size, alloc_size, count,
-          alloc_count, -1, depth
-        from ${tableNameCallsiteNameSize}
-        where depth = 0
-        union all
-        select cs.id, hash(cs.name, ctn.hash) as hash, cs.name, cs.map_name,
-          cs.size, cs.alloc_size, cs.count, cs.alloc_count, ctn.hash, cs.depth
-        from callsite_table_names ctn
-        inner join ${tableNameCallsiteNameSize} cs ON ctn.id = cs.parent_id
-        )
-        select hash, name, map_name, parent_hash, depth, SUM(size) as size,
-          SUM(case when alloc_size > 0 then alloc_size else 0 end)
-            as alloc_size, SUM(count) as count,
-          SUM(case when alloc_count > 0 then alloc_count else 0 end)
-            as alloc_count
-        from callsite_table_names
-        group by hash`);
-
-    // Recursive query to compute the cumulative size of each callsite.
-    // Base case: We get all the callsites where the size is non-zero.
-    // Recursive case: We get the callsite which is the parent of the current
-    //  callsite(in terms of hashes) and emit a row with the size of the current
-    //  callsite plus all the info of the parent.
-    // Grouping: For each callsite, our recursive table has n rows where n is
-    //  the number of descendents with a non-zero self size. We need to group on
-    //  the hash and sum all the sizes to get the cumulative size for each
-    //  callsite hash.
-    await this.query(`create temp table if not exists ${
-        tableNameGroupedCallsitesForFlamegraph}
-        as with recursive callsite_children(
-          hash, name, map_name, parent_hash, depth, size, alloc_size, count,
-          alloc_count, self_size, self_alloc_size, self_count, self_alloc_count)
-        as (
-        select hash, name, map_name, parent_hash, depth, size, alloc_size,
-          count, alloc_count, size as self_size, alloc_size as self_alloc_size,
-          count as self_count, alloc_count as self_alloc_count
-        from ${tableNameCallsiteHashNameSize}
-        union all
-        select chns.hash, chns.name, chns.map_name, chns.parent_hash,
-          chns.depth, cc.size, cc.alloc_size, cc.count, cc.alloc_count,
-          chns.size, chns.alloc_size, chns.count, chns.alloc_count
-        from ${tableNameCallsiteHashNameSize} chns
-        inner join callsite_children cc on chns.hash = cc.parent_hash
-        )
-        select hash, name, map_name, parent_hash, depth, SUM(size) as size,
-          SUM(case when alloc_size > 0 then alloc_size else 0 end)
-            as alloc_size, SUM(count) as count,
-          SUM(case when alloc_count > 0 then alloc_count else 0 end) as
-            alloc_count,
-          self_size, self_alloc_size, self_count, self_alloc_count
-        from callsite_children
-        group by hash`);
-    return tableNameGroupedCallsitesForFlamegraph;
-  }
-}
-
-trackControllerRegistry.register(HeapProfileFlameraphTrackController);
diff --git a/ui/src/tracks/heap_profile_flamegraph/frontend.ts b/ui/src/tracks/heap_profile_flamegraph/frontend.ts
deleted file mode 100644
index 0a78642..0000000
--- a/ui/src/tracks/heap_profile_flamegraph/frontend.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import * as m from 'mithril';
-
-import {Actions} from '../../common/actions';
-import {TrackState} from '../../common/state';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {Flamegraph} from '../../frontend/flamegraph';
-import {globals} from '../../frontend/globals';
-import {Track} from '../../frontend/track';
-import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
-import {trackRegistry} from '../../frontend/track_registry';
-
-import {
-  ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-  Config,
-  Data,
-  HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND,
-  HeapProfileFlamegraphKey,
-  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY
-} from './common';
-
-const MARGIN = 10;
-
-export class HeapProfileFlamegraphTrack extends Track<Config, Data> {
-  static readonly kind = HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND;
-  private flamegraph: Flamegraph;
-  private currentViewingOption = SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY;
-
-  static create(trackState: TrackState): HeapProfileFlamegraphTrack {
-    return new HeapProfileFlamegraphTrack(trackState);
-  }
-
-  constructor(trackState: TrackState) {
-    super(trackState);
-    this.flamegraph = new Flamegraph([]);
-    this.flamegraph.enableThumbnail(this.config.isMinimized);
-  }
-
-  data() {
-    return globals.trackDataStore.get(HeapProfileFlamegraphKey) as Data;
-  }
-
-  private changeFlamegraphData() {
-    const data = this.data();
-    if (data === undefined) {
-      this.flamegraph.updateDataIfChanged([]);
-    } else {
-      this.flamegraph.updateDataIfChanged(
-          data.flamegraph, data.clickedCallsite);
-      if (data.viewingOption !== undefined) {
-        this.currentViewingOption = data.viewingOption;
-      }
-    }
-  }
-
-  getHeight(): number {
-    const data = this.data();
-    if (data === undefined) {
-      return 0;
-    }
-    if (this.config.isMinimized) {
-      return super.getHeight();
-    }
-    this.changeFlamegraphData();
-    const height = this.flamegraph.getHeight();
-    return Math.max(height + MARGIN, super.getHeight());
-  }
-
-  getWidth(): number {
-    const {visibleWindowTime, timeScale} = globals.frontendLocalState;
-    const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start));
-    const endPx = Math.ceil(timeScale.timeToPx(visibleWindowTime.end));
-    return endPx - startPx;
-  }
-
-  renderCanvas(ctx: CanvasRenderingContext2D) {
-    const data = this.data();
-    if (data !== undefined && data.start === -1) {
-      const {visibleWindowTime, timeScale} = globals.frontendLocalState;
-      checkerboardExcept(
-          ctx,
-          this.getHeight(),
-          timeScale.timeToPx(visibleWindowTime.start),
-          timeScale.timeToPx(visibleWindowTime.end),
-          timeScale.timeToPx(data.start),
-          timeScale.timeToPx(data.end));
-      return;
-    }
-    this.changeFlamegraphData();
-    const unit =
-        this.currentViewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
-            this.currentViewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ?
-        'B' :
-        '';
-    this.flamegraph.draw(ctx, this.getWidth(), this.getHeight(), 0, 0, unit);
-  }
-
-  onMouseClick({x, y}: {x: number, y: number}): boolean {
-    this.config.expandedId = this.flamegraph.onMouseClick({x, y});
-    globals.dispatch(Actions.updateTrackConfig(
-        {id: this.trackState.id, config: this.config}));
-    return true;
-  }
-
-  onMouseMove({x, y}: {x: number, y: number}): boolean {
-    this.flamegraph.onMouseMove({x, y});
-    return true;
-  }
-
-  onMouseOut() {
-    this.flamegraph.onMouseOut();
-  }
-
-  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
-    const buttons: Array<m.Vnode<TrackButtonAttrs>> = [];
-    buttons.push(
-        // Minimize button
-        m(TrackButton, {
-          action: () => {
-            const newIsMinimized = !this.config.isMinimized;
-            this.config.isMinimized = newIsMinimized;
-            Actions.updateTrackConfig(
-                {id: this.trackState.id, config: this.config});
-            this.flamegraph.enableThumbnail(newIsMinimized);
-            globals.rafScheduler.scheduleFullRedraw();
-          },
-          i: this.config.isMinimized ? 'expand_more' : 'expand_less',
-          tooltip: this.config.isMinimized ? 'Maximize' : 'Minimize',
-          selected: this.config.isMinimized,
-        }));
-    return buttons;
-  }
-}
-
-trackRegistry.register(HeapProfileFlamegraphTrack);