Renaming flamegraph variables and classes to support perf.

The main renamings:

Heap profile:
------------------------------------
heap_profile_controller.ts -> flamegraph_controller.ts
heap_profile_panel.ts -> flamegraph_panel.ts
HeapProfileFlamegraph -> FlamegraphState
'HEAP_PROFILE_FLAMEGRAPH' -> 'FLAMEGRAPH_STATE'
state.currentHeapProfileFlamegraph -> state.currentFlamegraphState
HeapProfileFlamegraphViewingOption -> FlamegraphStateViewingOption
HeapProfileControllerArgs -> FlamegraphControllerArgs
HeapProfileDetails -> FlamegraphDetails
HeapProfileController -> FlamegraphController
Actions.expandHeapProfileFlamegraph -> Actions.expandFlamegraphState


Perf:
------------------------------------
perf_samples -> perf_samples_profile

PerfSamplesTrackController -> PerfSamplesProfileTrackController
PerfSamplesTrack -> PerfSamplesProfileTrack
PERF_SAMPLES_TRACK_KIND -> PERF_SAMPLES_PROFILE_TRACK_KIND


CSS:
------------------------------------
heap-profile -> flamegraph-profile



Bug: 195934783
Change-Id: I9fdad27f06eb8f9f4aeea6c6afe02c581d4130c4
diff --git a/ui/src/controller/flamegraph_controller.ts b/ui/src/controller/flamegraph_controller.ts
new file mode 100644
index 0000000..7b853e9
--- /dev/null
+++ b/ui/src/controller/flamegraph_controller.ts
@@ -0,0 +1,400 @@
+// 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,
+  PERF_SAMPLES_KEY,
+  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY
+} from '../common/flamegraph_util';
+import {NUM, STR} from '../common/query_result';
+import {CallsiteInfo, FlamegraphState} from '../common/state';
+import {fromNs} from '../common/time';
+import {FlamegraphDetails} from '../frontend/globals';
+import {publishFlamegraphDetails} from '../frontend/publish';
+
+import {Controller} from './controller';
+import {globals} from './globals';
+
+export interface FlamegraphControllerArgs {
+  engine: Engine;
+}
+const MIN_PIXEL_DISPLAYED = 1;
+
+class TablesCache {
+  private engine: Engine;
+  private cache: Map<string, string>;
+  private prefix: string;
+  private tableId: number;
+  private cacheSizeLimit: number;
+
+  constructor(engine: Engine, prefix: string) {
+    this.engine = engine;
+    this.cache = new Map<string, string>();
+    this.prefix = prefix;
+    this.tableId = 0;
+    this.cacheSizeLimit = 10;
+  }
+
+  async getTableName(query: string): Promise<string> {
+    let tableName = this.cache.get(query);
+    if (tableName === undefined) {
+      // TODO(hjd): This should be LRU.
+      if (this.cache.size > this.cacheSizeLimit) {
+        for (const name of this.cache.values()) {
+          await this.engine.query(`drop table ${name}`);
+        }
+        this.cache.clear();
+      }
+      tableName = `${this.prefix}_${this.tableId++}`;
+      await this.engine.query(
+          `create temp table if not exists ${tableName} as ${query}`);
+      this.cache.set(query, tableName);
+    }
+    return tableName;
+  }
+}
+
+export class FlamegraphController extends Controller<'main'> {
+  private flamegraphDatasets: Map<string, CallsiteInfo[]> = new Map();
+  private lastSelectedFlamegraphState?: FlamegraphState;
+  private requestingData = false;
+  private queuedRequest = false;
+  private flamegraphDetails: FlamegraphDetails = {};
+  private cache: TablesCache;
+
+  constructor(private args: FlamegraphControllerArgs) {
+    super('main');
+    this.cache = new TablesCache(args.engine, 'grouped_callsites');
+  }
+
+  run() {
+    const selection = globals.state.currentFlamegraphState;
+
+    if (!selection) return;
+
+    if (this.shouldRequestData(selection)) {
+      if (this.requestingData) {
+        this.queuedRequest = true;
+      } else {
+        this.requestingData = true;
+        const selectedFlamegraphState: FlamegraphState =
+            this.copyFlamegraphState(selection);
+
+        this.getFlamegraphMetadata(
+                selection.type,
+                selectedFlamegraphState.ts,
+                selectedFlamegraphState.upid)
+            .then(result => {
+              if (result !== undefined) {
+                Object.assign(this.flamegraphDetails, result);
+              }
+
+              // TODO(hjd): Clean this up.
+              if (this.lastSelectedFlamegraphState &&
+                  this.lastSelectedFlamegraphState.focusRegex !==
+                      selection.focusRegex) {
+                this.flamegraphDatasets.clear();
+              }
+
+              this.lastSelectedFlamegraphState =
+                  this.copyFlamegraphState(selection);
+
+              const expandedId = selectedFlamegraphState.expandedCallsite ?
+                  selectedFlamegraphState.expandedCallsite.id :
+                  -1;
+              const rootSize =
+                  selectedFlamegraphState.expandedCallsite === undefined ?
+                  undefined :
+                  selectedFlamegraphState.expandedCallsite.totalSize;
+
+              const key = `${selectedFlamegraphState.upid};${
+                  selectedFlamegraphState.ts}`;
+
+              this.getFlamegraphData(
+                      key,
+                      selectedFlamegraphState.viewingOption ?
+                          selectedFlamegraphState.viewingOption :
+                          DEFAULT_VIEWING_OPTION,
+                      selection.ts,
+                      selectedFlamegraphState.upid,
+                      selectedFlamegraphState.type,
+                      selectedFlamegraphState.focusRegex)
+                  .then(flamegraphData => {
+                    if (flamegraphData !== undefined && selection &&
+                        selection.kind === selectedFlamegraphState.kind &&
+                        selection.id === selectedFlamegraphState.id &&
+                        selection.ts === selectedFlamegraphState.ts) {
+                      const expandedFlamegraphData =
+                          expandCallsites(flamegraphData, expandedId);
+                      this.prepareAndMergeCallsites(
+                          expandedFlamegraphData,
+                          this.lastSelectedFlamegraphState!.viewingOption,
+                          rootSize,
+                          this.lastSelectedFlamegraphState!.expandedCallsite);
+                    }
+                  })
+                  .finally(() => {
+                    this.requestingData = false;
+                    if (this.queuedRequest) {
+                      this.queuedRequest = false;
+                      this.run();
+                    }
+                  });
+            });
+      }
+    }
+  }
+
+  private copyFlamegraphState(flamegraphState: FlamegraphState):
+      FlamegraphState {
+    return {
+      kind: flamegraphState.kind,
+      id: flamegraphState.id,
+      upid: flamegraphState.upid,
+      ts: flamegraphState.ts,
+      type: flamegraphState.type,
+      expandedCallsite: flamegraphState.expandedCallsite,
+      viewingOption: flamegraphState.viewingOption,
+      focusRegex: flamegraphState.focusRegex,
+    };
+  }
+
+  private shouldRequestData(selection: FlamegraphState) {
+    return selection.kind === 'FLAMEGRAPH_STATE' &&
+        (this.lastSelectedFlamegraphState === undefined ||
+         (this.lastSelectedFlamegraphState !== undefined &&
+          (this.lastSelectedFlamegraphState.id !== selection.id ||
+           this.lastSelectedFlamegraphState.ts !== selection.ts ||
+           this.lastSelectedFlamegraphState.type !== selection.type ||
+           this.lastSelectedFlamegraphState.upid !== selection.upid ||
+           this.lastSelectedFlamegraphState.viewingOption !==
+               selection.viewingOption ||
+           this.lastSelectedFlamegraphState.focusRegex !==
+               selection.focusRegex ||
+           this.lastSelectedFlamegraphState.expandedCallsite !==
+               selection.expandedCallsite)));
+  }
+
+  private prepareAndMergeCallsites(
+      flamegraphData: CallsiteInfo[],
+      viewingOption: string|undefined = DEFAULT_VIEWING_OPTION,
+      rootSize?: number, expandedCallsite?: CallsiteInfo) {
+    const mergedFlamegraphData = mergeCallsites(
+        flamegraphData, this.getMinSizeDisplayed(flamegraphData, rootSize));
+    this.flamegraphDetails.flamegraph = mergedFlamegraphData;
+    this.flamegraphDetails.expandedCallsite = expandedCallsite;
+    this.flamegraphDetails.viewingOption = viewingOption;
+    publishFlamegraphDetails(this.flamegraphDetails);
+  }
+
+
+  async getFlamegraphData(
+      baseKey: string, viewingOption: string, ts: number, upid: number,
+      type: string, focusRegex: string): Promise<CallsiteInfo[]> {
+    let currentData: CallsiteInfo[];
+    const key = `${baseKey}-${viewingOption}`;
+    if (this.flamegraphDatasets.has(key)) {
+      currentData = this.flamegraphDatasets.get(key)!;
+    } else {
+      // TODO(hjd): Show loading state.
+
+      // Collecting data for drawing flamegraph for selected profile.
+      // Data needs to be in following format:
+      // id, name, parent_id, depth, total_size
+      const tableName =
+          await this.prepareViewsAndTables(ts, upid, type, focusRegex);
+      currentData = await this.getFlamegraphDataFromTables(
+          tableName, viewingOption, focusRegex);
+      this.flamegraphDatasets.set(key, currentData);
+    }
+    return currentData;
+  }
+
+  async getFlamegraphDataFromTables(
+      tableName: string, viewingOption = DEFAULT_VIEWING_OPTION,
+      focusRegex: string) {
+    let orderBy = '';
+    let totalColumnName: 'cumulativeSize'|'cumulativeAllocSize'|
+        'cumulativeCount'|'cumulativeAllocCount' = 'cumulativeSize';
+    let selfColumnName: 'size'|'count' = 'size';
+    // TODO(fmayer): Improve performance so this is no longer necessary.
+    // Alternatively consider collapsing frames of the same label.
+    const maxDepth = 100;
+    switch (viewingOption) {
+      case ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
+        orderBy = `where cumulative_alloc_size > 0 and depth < ${
+            maxDepth} order by depth, parent_id,
+            cumulative_alloc_size desc, name`;
+        totalColumnName = 'cumulativeAllocSize';
+        selfColumnName = 'size';
+        break;
+      case OBJECTS_ALLOCATED_NOT_FREED_KEY:
+        orderBy = `where cumulative_count > 0 and depth < ${
+            maxDepth} order by depth, parent_id,
+            cumulative_count desc, name`;
+        totalColumnName = 'cumulativeCount';
+        selfColumnName = 'count';
+        break;
+      case OBJECTS_ALLOCATED_KEY:
+        orderBy = `where cumulative_alloc_count > 0 and depth < ${
+            maxDepth} order by depth, parent_id,
+            cumulative_alloc_count desc, name`;
+        totalColumnName = 'cumulativeAllocCount';
+        selfColumnName = 'count';
+        break;
+      case PERF_SAMPLES_KEY:
+      case SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
+        orderBy = `where cumulative_size > 0 and depth < ${
+            maxDepth} order by depth, parent_id,
+            cumulative_size desc, name`;
+        totalColumnName = 'cumulativeSize';
+        selfColumnName = 'size';
+        break;
+      default:
+        break;
+    }
+
+    const callsites = await this.args.engine.query(`
+        SELECT
+        id as hash,
+        IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name,
+        IFNULL(parent_id, -1) as parentHash,
+        depth,
+        cumulative_size as cumulativeSize,
+        cumulative_alloc_size as cumulativeAllocSize,
+        cumulative_count as cumulativeCount,
+        cumulative_alloc_count as cumulativeAllocCount,
+        map_name as mapping,
+        size,
+        count,
+        IFNULL(source_file, '') as sourceFile,
+        IFNULL(line_number, -1) as lineNumber
+        from ${tableName} ${orderBy}`);
+
+    const flamegraphData: CallsiteInfo[] = [];
+    const hashToindex: Map<number, number> = new Map();
+    const it = callsites.iter({
+      hash: NUM,
+      name: STR,
+      parentHash: NUM,
+      depth: NUM,
+      cumulativeSize: NUM,
+      cumulativeAllocSize: NUM,
+      cumulativeCount: NUM,
+      cumulativeAllocCount: NUM,
+      mapping: STR,
+      sourceFile: STR,
+      lineNumber: NUM,
+      size: NUM,
+      count: NUM,
+    });
+    for (let i = 0; it.valid(); ++i, it.next()) {
+      const hash = it.hash;
+      let name = it.name;
+      const parentHash = it.parentHash;
+      const depth = it.depth;
+      const totalSize = it[totalColumnName];
+      const selfSize = it[selfColumnName];
+      const mapping = it.mapping;
+      const highlighted = focusRegex !== '' &&
+          name.toLocaleLowerCase().includes(focusRegex.toLocaleLowerCase());
+      const parentId =
+          hashToindex.has(+parentHash) ? hashToindex.get(+parentHash)! : -1;
+
+      let location: string|undefined;
+      if (/[a-zA-Z]/i.test(it.sourceFile)) {
+        location = it.sourceFile;
+        if (it.lineNumber !== -1) {
+          location += `:${it.lineNumber}`;
+        }
+      }
+
+      if (depth === maxDepth - 1) {
+        name += ' [tree truncated]';
+      }
+      // 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:
+      hashToindex.set(hash, i);
+
+      flamegraphData.push({
+        id: i,
+        totalSize,
+        depth,
+        parentId,
+        name,
+        selfSize,
+        mapping,
+        merged: false,
+        highlighted,
+        location
+      });
+    }
+    return flamegraphData;
+  }
+
+  private async prepareViewsAndTables(
+      ts: number, upid: number, type: string,
+      focusRegex: string): Promise<string> {
+    // Creating unique names for views so we can reuse and not delete them
+    // for each marker.
+    let whereClause = '';
+    if (focusRegex !== '') {
+      whereClause = `where focus_str = '${focusRegex}'`;
+    }
+
+    return this.cache.getTableName(
+        `select id, name, map_name, parent_id, depth, cumulative_size,
+          cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
+          size, alloc_size, count, alloc_count, source_file, line_number
+          from experimental_flamegraph(${ts}, ${upid}, '${type}') ${
+            whereClause}`);
+  }
+
+  getMinSizeDisplayed(flamegraphData: CallsiteInfo[], rootSize?: number):
+      number {
+    const timeState = globals.state.frontendLocalState.visibleState;
+    let width = (timeState.endSec - timeState.startSec) / timeState.resolution;
+    // TODO(168048193): Remove screen size hack:
+    width = Math.max(width, 800);
+    if (rootSize === undefined) {
+      rootSize = findRootSize(flamegraphData);
+    }
+    return MIN_PIXEL_DISPLAYED * rootSize / width;
+  }
+
+  async getFlamegraphMetadata(type: string, ts: number, upid: number) {
+    // Don't do anything if selection of the marker stayed the same.
+    if ((this.lastSelectedFlamegraphState !== undefined &&
+         ((this.lastSelectedFlamegraphState.ts === ts &&
+           this.lastSelectedFlamegraphState.upid === upid)))) {
+      return undefined;
+    }
+
+    // Collecting data for more information about profile, such as:
+    // total memory allocated, memory that is allocated and not freed.
+    const result = await this.args.engine.query(
+        `select pid from process where upid = ${upid}`);
+    const pid = result.firstRow({pid: NUM}).pid;
+    const startTime = fromNs(ts) - globals.state.traceTime.startSec;
+    return {ts: startTime, tsNs: ts, pid, upid, type};
+  }
+}