perfetto-ui: Prevent lots of flamegraph queries being queued

If a user clicks on a markers quickly we should prevent queueing up
queries and re-run queries for the last one.

Change-Id: I4e2a822dcd882d40080df9edc45da589cdbeba10
diff --git a/ui/src/controller/heap_profile_controller.ts b/ui/src/controller/heap_profile_controller.ts
index f3f23b5..a556b92 100644
--- a/ui/src/controller/heap_profile_controller.ts
+++ b/ui/src/controller/heap_profile_controller.ts
@@ -24,6 +24,8 @@
   SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY
 } from '../common/flamegraph_util';
 import {CallsiteInfo, HeapProfileFlamegraph} from '../common/state';
+import {fromNs} from '../common/time';
+import {HeapProfileDetails} from '../frontend/globals';
 
 import {Controller} from './controller';
 import {globals} from './globals';
@@ -36,6 +38,9 @@
 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 = {};
 
   constructor(private args: HeapProfileControllerArgs) {
     super('main');
@@ -46,70 +51,102 @@
 
     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;
+    if (this.shouldRequestData(selection)) {
+      if (this.requestingData) {
+        this.queuedRequest = true;
+      } else {
+        this.requestingData = true;
+        const selectedHeapProfile: HeapProfileFlamegraph =
+            this.copyHeapProfile(selection);
 
-        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}`;
-
-        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);
+        this.getHeapProfileMetadata(
+                selectedHeapProfile.ts, selectedHeapProfile.upid)
+            .then(result => {
+              if (result !== undefined) {
+                Object.assign(this.heapProfileDetails, result);
               }
+
+              this.lastSelectedHeapProfile = this.copyHeapProfile(selection);
+
+              const expandedId = selectedHeapProfile.expandedCallsite ?
+                  selectedHeapProfile.expandedCallsite.id :
+                  -1;
+              const rootSize =
+                  selectedHeapProfile.expandedCallsite === undefined ?
+                  undefined :
+                  selectedHeapProfile.expandedCallsite.totalSize;
+
+              const key =
+                  `${selectedHeapProfile.upid};${selectedHeapProfile.ts}`;
+
+              this.getFlamegraphData(
+                      key,
+                      selectedHeapProfile.viewingOption ?
+                          selectedHeapProfile.viewingOption :
+                          DEFAULT_VIEWING_OPTION,
+                      selection.ts,
+                      selectedHeapProfile.upid)
+                  .then(flamegraphData => {
+                    if (flamegraphData !== undefined && selection &&
+                        selection.kind === selectedHeapProfile.kind &&
+                        selection.id === selectedHeapProfile.id &&
+                        selection.ts === selectedHeapProfile.ts) {
+                      const expandedFlamegraphData =
+                          expandCallsites(flamegraphData, expandedId);
+                      this.prepareAndMergeCallsites(
+                          expandedFlamegraphData,
+                          this.lastSelectedHeapProfile!.viewingOption,
+                          rootSize,
+                          this.lastSelectedHeapProfile!.expandedCallsite);
+                    }
+                  })
+                  .finally(() => {
+                    this.requestingData = false;
+                    if (this.queuedRequest) {
+                      this.queuedRequest = false;
+                      this.run();
+                    }
+                  });
             });
       }
     }
   }
 
+  private copyHeapProfile(heapProfile: HeapProfileFlamegraph):
+      HeapProfileFlamegraph {
+    return {
+      kind: heapProfile.kind,
+      id: heapProfile.id,
+      upid: heapProfile.upid,
+      ts: heapProfile.ts,
+      expandedCallsite: heapProfile.expandedCallsite,
+      viewingOption: heapProfile.viewingOption
+    };
+  }
+
+  private shouldRequestData(selection: HeapProfileFlamegraph) {
+    return selection.kind === 'HEAP_PROFILE_FLAMEGRAPH' &&
+        (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)));
+  }
+
   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});
+    this.heapProfileDetails.flamegraph = mergedFlamegraphData;
+    this.heapProfileDetails.expandedCallsite = expandedCallsite;
+    this.heapProfileDetails.viewingOption = viewingOption;
+    globals.publish('HeapProfileDetails', this.heapProfileDetails);
   }
 
 
@@ -308,4 +345,29 @@
     }
     return MIN_PIXEL_DISPLAYED * rootSize / width;
   }
+
+  async getHeapProfileMetadata(ts: number, upid: number) {
+    // Don't do anything if selection of the marker stayed the same.
+    if ((this.lastSelectedHeapProfile !== undefined &&
+         ((this.lastSelectedHeapProfile.ts === ts &&
+           this.lastSelectedHeapProfile.upid === upid)))) {
+      return undefined;
+    }
+
+    // Collecting data for more information about heap profile, such as:
+    // total memory allocated, memory that is allocated and not freed.
+    const pidValue = await this.args.engine.query(
+        `select pid from process where upid = ${upid}`);
+    const pid = pidValue.columns[0].longValues![0];
+    const allocatedMemory = await this.args.engine.query(
+        `select sum(size) from heap_profile_allocation where ts <= ${
+            ts} and size > 0 and upid = ${upid}`);
+    const allocated = allocatedMemory.columns[0].longValues![0];
+    const allocatedNotFreedMemory = await this.args.engine.query(
+        `select sum(size) from heap_profile_allocation where ts <= ${
+            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, upid};
+  }
 }
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 3b257d3..540ce00 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -14,12 +14,7 @@
 
 import {Engine} from '../common/engine';
 import {fromNs, toNs} from '../common/time';
-import {
-  CounterDetails,
-  HeapProfileDetails,
-  SliceDetails
-} from '../frontend/globals';
-
+import {CounterDetails, SliceDetails} from '../frontend/globals';
 import {Controller} from './controller';
 import {globals} from './globals';
 
@@ -65,18 +60,7 @@
 
     if (selectedId === undefined) return;
 
-    if (selection.kind === 'HEAP_PROFILE') {
-      const selected: HeapProfileDetails = {};
-      const ts = selection.ts;
-      const upid = selection.upid;
-      this.heapDumpDetails(ts, upid).then(results => {
-        if (results !== undefined && selection &&
-            selection.kind === selectedKind && selection.id === selectedId) {
-          Object.assign(selected, results);
-          globals.publish('HeapProfileDetails', selected);
-        }
-      });
-    } else if (selection.kind === 'COUNTER') {
+    if (selection.kind === 'COUNTER') {
       const selected: CounterDetails = {};
       this.counterDetails(selection.leftTs, selection.rightTs, selection.id)
           .then(results => {
@@ -139,24 +123,6 @@
     });
   }
 
-  async heapDumpDetails(ts: number, upid: number) {
-    // Collecting data for more information about heap profile, such as:
-    // total memory allocated, memory that is allocated and not freed.
-    const pidValue = await this.args.engine.query(
-        `select pid from process where upid = ${upid}`);
-    const pid = pidValue.columns[0].longValues![0];
-    const allocatedMemory = await this.args.engine.query(
-        `select sum(size) from heap_profile_allocation where ts <= ${
-            ts} and size > 0 and upid = ${upid}`);
-    const allocated = allocatedMemory.columns[0].longValues![0];
-    const allocatedNotFreedMemory = await this.args.engine.query(
-        `select sum(size) from heap_profile_allocation where ts <= ${
-            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, upid};
-  }
-
   async counterDetails(ts: number, rightTs: number, id: number) {
     const counter = await this.args.engine.query(
         `SELECT value FROM counter_values WHERE ts = ${ts} AND counter_id = ${
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 72d21b2..5f32d4f 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -28,7 +28,6 @@
   LogExistsKey
 } from '../common/logs';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
-import {CallsiteInfo} from '../common/state';
 
 import {maybeShowErrorDialog} from './error_dialog';
 import {
@@ -130,17 +129,6 @@
     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();
-  }
-
   publishFileDownload(args: {file: File, name?: string}) {
     const url = URL.createObjectURL(args.file);
     const a = document.createElement('a');