Add focus regex for heapprofiles

Plumb a regex for filtering which slices appear in the flamechart.

Also fix a bug where clicking on the UI at top of the flamechart
reset slice selection.

Bug: 149833691
Change-Id: I777e7764616afbf80cfceb5a6980b6dc9679b88e
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index ffd33eb..f4272e2 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -453,7 +453,7 @@
       id: args.id,
       upid: args.upid,
       ts: args.ts,
-      type: args.type
+      type: args.type,
     };
   },
 
@@ -467,6 +467,7 @@
       ts: args.ts,
       type: args.type,
       viewingOption: DEFAULT_VIEWING_OPTION,
+      focusRegex: '',
     };
   },
 
@@ -483,6 +484,12 @@
     state.currentHeapProfileFlamegraph.viewingOption = args.viewingOption;
   },
 
+  changeFocusHeapProfileFlamegraph(
+      state: StateDraft, args: {focusRegex: string}): void {
+    if (state.currentHeapProfileFlamegraph === null) return;
+    state.currentHeapProfileFlamegraph.focusRegex = args.focusRegex;
+  },
+
   selectChromeSlice(state: StateDraft, args: {id: number, trackId: string}):
       void {
         state.currentSelection = {
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 5bd331a..16ac056 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -186,6 +186,7 @@
   ts: number;
   type: string;
   viewingOption: HeapProfileFlamegraphViewingOption;
+  focusRegex: string;
   expandedCallsite?: CallsiteInfo;
 }
 
diff --git a/ui/src/controller/heap_profile_controller.ts b/ui/src/controller/heap_profile_controller.ts
index 50afa42..d6a22c1 100644
--- a/ui/src/controller/heap_profile_controller.ts
+++ b/ui/src/controller/heap_profile_controller.ts
@@ -35,15 +35,51 @@
 }
 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 HeapProfileController extends Controller<'main'> {
   private flamegraphDatasets: Map<string, CallsiteInfo[]> = new Map();
   private lastSelectedHeapProfile?: HeapProfileFlamegraph;
   private requestingData = false;
   private queuedRequest = false;
   private heapProfileDetails: HeapProfileDetails = {};
+  private cache: TablesCache;
 
   constructor(private args: HeapProfileControllerArgs) {
     super('main');
+    this.cache = new TablesCache(args.engine, 'grouped_callsites');
   }
 
   run() {
@@ -66,6 +102,13 @@
                 Object.assign(this.heapProfileDetails, result);
               }
 
+              // TODO(hjd): Clean this up.
+              if (this.lastSelectedHeapProfile &&
+                  this.lastSelectedHeapProfile.focusRegex !==
+                      selection.focusRegex) {
+                this.flamegraphDatasets.clear();
+              }
+
               this.lastSelectedHeapProfile = this.copyHeapProfile(selection);
 
               const expandedId = selectedHeapProfile.expandedCallsite ?
@@ -86,7 +129,8 @@
                           DEFAULT_VIEWING_OPTION,
                       selection.ts,
                       selectedHeapProfile.upid,
-                      selectedHeapProfile.type)
+                      selectedHeapProfile.type,
+                      selectedHeapProfile.focusRegex)
                   .then(flamegraphData => {
                     if (flamegraphData !== undefined && selection &&
                         selection.kind === selectedHeapProfile.kind &&
@@ -122,7 +166,8 @@
       ts: heapProfile.ts,
       type: heapProfile.type,
       expandedCallsite: heapProfile.expandedCallsite,
-      viewingOption: heapProfile.viewingOption
+      viewingOption: heapProfile.viewingOption,
+      focusRegex: heapProfile.focusRegex,
     };
   }
 
@@ -136,6 +181,7 @@
            this.lastSelectedHeapProfile.upid !== selection.upid ||
            this.lastSelectedHeapProfile.viewingOption !==
                selection.viewingOption ||
+           this.lastSelectedHeapProfile.focusRegex !== selection.focusRegex ||
            this.lastSelectedHeapProfile.expandedCallsite !==
                selection.expandedCallsite)));
   }
@@ -155,7 +201,7 @@
 
   async getFlamegraphData(
       baseKey: string, viewingOption: string, ts: number, upid: number,
-      type: string): Promise<CallsiteInfo[]> {
+      type: string, focusRegex: string): Promise<CallsiteInfo[]> {
     let currentData: CallsiteInfo[];
     const key = `${baseKey}-${viewingOption}`;
     if (this.flamegraphDatasets.has(key)) {
@@ -166,7 +212,8 @@
       // 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, type);
+      const tableName =
+          await this.prepareViewsAndTables(ts, upid, type, focusRegex);
       currentData =
           await this.getFlamegraphDataFromTables(tableName, viewingOption);
       this.flamegraphDatasets.set(key, currentData);
@@ -254,26 +301,22 @@
     return flamegraphData;
   }
 
-  private async prepareViewsAndTables(ts: number, upid: number, type: string):
-      Promise<string> {
+  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.
-    const tableNameGroupedCallsitesForFlamegraph =
-        this.tableName(`grouped_callsites_for_flamegraph`);
+    let whereClause = '';
+    if (focusRegex !== '') {
+      whereClause = `where focus_str = '${focusRegex}'`;
+    }
 
-    await this.args.engine.query(`create temp table if not exists ${
-        tableNameGroupedCallsitesForFlamegraph} as
-        select id, name, map_name, parent_id, depth, cumulative_size,
+    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
-        from experimental_flamegraph(${ts}, ${upid}, '${type}')`);
-    return tableNameGroupedCallsitesForFlamegraph;
-  }
-
-  tableName(name: string): string {
-    const selection = globals.state.currentHeapProfileFlamegraph;
-    if (!selection) return name;
-    return `${name}_${selection.upid}_${selection.ts}`;
+          from experimental_flamegraph(${ts}, ${upid}, '${type}') ${
+            whereClause}`);
   }
 
   getMinSizeDisplayed(flamegraphData: CallsiteInfo[], rootSize?: number):
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index a966a45..8bc6f42 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -26,6 +26,7 @@
 import {randomColor} from './colorizer';
 import {Tab} from './details_panel';
 import {globals} from './globals';
+import {debounce, ratelimit} from './rate_limiters';
 import {TimeScale} from './time_scale';
 
 interface Range {
@@ -40,36 +41,6 @@
   return current;
 }
 
-// Returns a wrapper around |f| which calls f at most once every |ms|ms.
-function ratelimit(f: Function, ms: number): Function {
-  let inProgess = false;
-  return () => {
-    if (inProgess) {
-      return;
-    }
-    inProgess = true;
-    window.setTimeout(() => {
-      f();
-      inProgess = false;
-    }, ms);
-  };
-}
-
-// Returns a wrapper around |f| which waits for a |ms|ms pause in calls
-// before calling |f|.
-function debounce(f: Function, ms: number): Function {
-  let timerId: undefined|number;
-  return () => {
-    if (timerId) {
-      window.clearTimeout(timerId);
-    }
-    timerId = window.setTimeout(() => {
-      f();
-      timerId = undefined;
-    }, ms);
-  };
-}
-
 // Calculate the space a scrollbar takes up so that we can subtract it from
 // the canvas width.
 function calculateScrollbarWidth() {
diff --git a/ui/src/frontend/heap_profile_panel.ts b/ui/src/frontend/heap_profile_panel.ts
index 85b8b7a..8c92778 100644
--- a/ui/src/frontend/heap_profile_panel.ts
+++ b/ui/src/frontend/heap_profile_panel.ts
@@ -27,6 +27,7 @@
 import {Flamegraph} from './flamegraph';
 import {globals} from './globals';
 import {Panel, PanelSize} from './panel';
+import {debounce} from './rate_limiters';
 
 interface HeapProfileDetailsPanelAttrs {}
 
@@ -37,6 +38,11 @@
   private ts = 0;
   private pid = 0;
   private flamegraph: Flamegraph = new Flamegraph([]);
+  private focusRegex = '';
+  private updateFocusRegexDebounced = debounce(() => {
+    this.updateFocusRegex();
+  }, 20);
+
 
   view() {
     const heapDumpInfo = globals.heapProfileDetails;
@@ -76,6 +82,7 @@
             }
           },
           m('.details-panel-heading.heap-profile',
+            {onclick: (e: MouseEvent) => e.stopPropagation()},
             [
               m('div.options',
                 [
@@ -86,6 +93,15 @@
                 [
                   m('div.time',
                     `Snapshot time: ${timeToCode(heapDumpInfo.ts)}`),
+                  m('input[type=text][placeholder=Focus]', {
+                    oninput: (e: Event) => {
+                      const target = (e.target as HTMLInputElement);
+                      this.focusRegex = target.value;
+                      this.updateFocusRegexDebounced();
+                    },
+                    // Required to stop hot-key handling:
+                    onkeydown: (e: Event) => e.stopPropagation(),
+                  }),
                   m('button.download',
                     {
                       onclick: () => {
@@ -105,6 +121,12 @@
     }
   }
 
+  private updateFocusRegex() {
+    globals.dispatch(Actions.changeFocusHeapProfileFlamegraph({
+      focusRegex: this.focusRegex,
+    }));
+  }
+
   getButtonsClass(button: HeapProfileFlamegraphViewingOption): string {
     if (globals.state.currentHeapProfileFlamegraph === null) return '';
     return globals.state.currentHeapProfileFlamegraph.viewingOption === button ?
diff --git a/ui/src/frontend/rate_limiters.ts b/ui/src/frontend/rate_limiters.ts
new file mode 100644
index 0000000..ae212d4
--- /dev/null
+++ b/ui/src/frontend/rate_limiters.ts
@@ -0,0 +1,43 @@
+// Copyright (C) 2020 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.
+
+// Returns a wrapper around |f| which calls f at most once every |ms|ms.
+export function ratelimit(f: Function, ms: number): Function {
+  let inProgess = false;
+  return () => {
+    if (inProgess) {
+      return;
+    }
+    inProgess = true;
+    window.setTimeout(() => {
+      f();
+      inProgess = false;
+    }, ms);
+  };
+}
+
+// Returns a wrapper around |f| which waits for a |ms|ms pause in calls
+// before calling |f|.
+export function debounce(f: Function, ms: number): Function {
+  let timerId: undefined|number;
+  return () => {
+    if (timerId) {
+      window.clearTimeout(timerId);
+    }
+    timerId = window.setTimeout(() => {
+      f();
+      timerId = undefined;
+    }, ms);
+  };
+}