ui: Include one or more perf tracks in area selection.

- factor out some of the code in aggregation_controller, so that
it can be reused in flamegraph_controller;
- if area selection changes, call the flamegraph_controller;
- modify FlamegraphState, so it can contain start timestamp, end
timestamp and array of upids;
- in FlamegraphDetails, store a boolean indicating if the area has
changed.

Examples:
a)select part of a perf sample:
https://screenshot.googleplex.com/8TGQYTadUscnTUt
b)select perf samples across multiple processes:
https://screenshot.googleplex.com/3HgpSSQPGRMzjVw

Bug: b/195934783
Change-Id: I2d17b10eb997b37f6d0268f04530314b1aacb11a
diff --git a/ui/src/controller/flamegraph_controller.ts b/ui/src/controller/flamegraph_controller.ts
index 7459c3f..9fec1f5 100644
--- a/ui/src/controller/flamegraph_controller.ts
+++ b/ui/src/controller/flamegraph_controller.ts
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {Actions} from '../common/actions';
 import {Engine} from '../common/engine';
 import {
   ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
@@ -26,10 +27,18 @@
 } 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 {toNs} from '../common/time';
+import {
+  FlamegraphDetails,
+  globals as frontendGlobals
+} from '../frontend/globals';
 import {publishFlamegraphDetails} from '../frontend/publish';
+import {
+  Config as PerfSampleConfig,
+  PERF_SAMPLES_PROFILE_TRACK_KIND
+} from '../tracks/perf_samples_profile/common';
 
+import {AreaSelectionHandler} from './area_selection_handler';
 import {Controller} from './controller';
 import {globals} from './globals';
 
@@ -78,15 +87,46 @@
   private requestingData = false;
   private queuedRequest = false;
   private flamegraphDetails: FlamegraphDetails = {};
+  private areaSelectionHandler: AreaSelectionHandler;
   private cache: TablesCache;
 
   constructor(private args: FlamegraphControllerArgs) {
     super('main');
     this.cache = new TablesCache(args.engine, 'grouped_callsites');
+    this.areaSelectionHandler = new AreaSelectionHandler();
   }
 
   run() {
-    const selection = globals.state.currentFlamegraphState;
+    const [hasAreaChanged, area] = this.areaSelectionHandler.getAreaChange();
+    if (hasAreaChanged) {
+      const upids = [];
+      if (!area) {
+        publishFlamegraphDetails(
+            {...frontendGlobals.flamegraphDetails, isInAreaSelection: false});
+        return;
+      }
+      for (const trackId of area.tracks) {
+        const trackState = frontendGlobals.state.tracks[trackId];
+        if (!trackState ||
+            trackState.kind !== PERF_SAMPLES_PROFILE_TRACK_KIND) {
+          continue;
+        }
+        upids.push((trackState.config as PerfSampleConfig).upid);
+      }
+      if (upids.length === 0) {
+        publishFlamegraphDetails(
+            {...frontendGlobals.flamegraphDetails, isInAreaSelection: false});
+        return;
+      }
+      frontendGlobals.dispatch(Actions.openFlamegraph({
+        upids,
+        startNs: toNs(area.startSec),
+        endNs: toNs(area.endSec),
+        type: 'perf',
+        viewingOption: PERF_SAMPLES_KEY
+      }));
+    }
+    const selection = frontendGlobals.state.currentFlamegraphState;
     if (!selection || !this.shouldRequestData(selection)) {
       return;
     }
@@ -96,15 +136,17 @@
     }
     this.requestingData = true;
 
-    this.assembleFlamegraphDetails(selection);
+    this.assembleFlamegraphDetails(selection, hasAreaChanged);
   }
 
-  private async assembleFlamegraphDetails(selection: FlamegraphState) {
+  private async assembleFlamegraphDetails(
+      selection: FlamegraphState, hasAreaChanged: boolean) {
     const selectedFlamegraphState = {...selection};
     const flamegraphMetadata = await this.getFlamegraphMetadata(
         selection.type,
-        selectedFlamegraphState.ts,
-        selectedFlamegraphState.upid);
+        selectedFlamegraphState.startNs,
+        selectedFlamegraphState.endNs,
+        selectedFlamegraphState.upids);
     if (flamegraphMetadata !== undefined) {
       Object.assign(this.flamegraphDetails, flamegraphMetadata);
     }
@@ -124,7 +166,8 @@
         undefined :
         selectedFlamegraphState.expandedCallsite.totalSize;
 
-    const key = `${selectedFlamegraphState.upid};${selectedFlamegraphState.ts}`;
+    const key = `${selectedFlamegraphState.upids};${
+        selectedFlamegraphState.startNs};${selectedFlamegraphState.endNs}`;
 
     try {
       const flamegraphData = await this.getFlamegraphData(
@@ -132,19 +175,21 @@
           selectedFlamegraphState.viewingOption ?
               selectedFlamegraphState.viewingOption :
               DEFAULT_VIEWING_OPTION,
-          selection.ts,
-          selectedFlamegraphState.upid,
+          selection.startNs,
+          selection.endNs,
+          selectedFlamegraphState.upids,
           selectedFlamegraphState.type,
           selectedFlamegraphState.focusRegex);
       if (flamegraphData !== undefined && selection &&
           selection.kind === selectedFlamegraphState.kind &&
-          selection.id === selectedFlamegraphState.id &&
-          selection.ts === selectedFlamegraphState.ts) {
+          selection.startNs === selectedFlamegraphState.startNs &&
+          selection.endNs === selectedFlamegraphState.endNs) {
         const expandedFlamegraphData =
             expandCallsites(flamegraphData, expandedId);
         this.prepareAndMergeCallsites(
             expandedFlamegraphData,
             this.lastSelectedFlamegraphState.viewingOption,
+            hasAreaChanged,
             rootSize,
             this.lastSelectedFlamegraphState.expandedCallsite);
       }
@@ -160,10 +205,11 @@
   private shouldRequestData(selection: FlamegraphState) {
     return selection.kind === 'FLAMEGRAPH_STATE' &&
         (this.lastSelectedFlamegraphState === undefined ||
-         (this.lastSelectedFlamegraphState.id !== selection.id ||
-          this.lastSelectedFlamegraphState.ts !== selection.ts ||
+         (this.lastSelectedFlamegraphState.startNs !== selection.startNs ||
+          this.lastSelectedFlamegraphState.endNs !== selection.endNs ||
           this.lastSelectedFlamegraphState.type !== selection.type ||
-          this.lastSelectedFlamegraphState.upid !== selection.upid ||
+          !FlamegraphController.areArraysEqual(
+              this.lastSelectedFlamegraphState.upids, selection.upids) ||
           this.lastSelectedFlamegraphState.viewingOption !==
               selection.viewingOption ||
           this.lastSelectedFlamegraphState.focusRegex !==
@@ -175,17 +221,20 @@
   private prepareAndMergeCallsites(
       flamegraphData: CallsiteInfo[],
       viewingOption: string|undefined = DEFAULT_VIEWING_OPTION,
-      rootSize?: number, expandedCallsite?: CallsiteInfo) {
+      hasAreaChanged: boolean, rootSize?: number,
+      expandedCallsite?: CallsiteInfo) {
     this.flamegraphDetails.flamegraph = mergeCallsites(
         flamegraphData, this.getMinSizeDisplayed(flamegraphData, rootSize));
     this.flamegraphDetails.expandedCallsite = expandedCallsite;
     this.flamegraphDetails.viewingOption = viewingOption;
+    this.flamegraphDetails.isInAreaSelection = hasAreaChanged;
     publishFlamegraphDetails(this.flamegraphDetails);
   }
 
   async getFlamegraphData(
-      baseKey: string, viewingOption: string, ts: number, upid: number,
-      type: string, focusRegex: string): Promise<CallsiteInfo[]> {
+      baseKey: string, viewingOption: string, startNs: number, endNs: number,
+      upids: number[], type: string,
+      focusRegex: string): Promise<CallsiteInfo[]> {
     let currentData: CallsiteInfo[];
     const key = `${baseKey}-${viewingOption}`;
     if (this.flamegraphDatasets.has(key)) {
@@ -196,8 +245,8 @@
       // 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);
+      const tableName = await this.prepareViewsAndTables(
+          startNs, endNs, upids, type, focusRegex);
       currentData = await this.getFlamegraphDataFromTables(
           tableName, viewingOption, focusRegex);
       this.flamegraphDatasets.set(key, currentData);
@@ -329,7 +378,7 @@
   }
 
   private async prepareViewsAndTables(
-      ts: number, upid: number, type: string,
+      startNs: number, endNs: number, upids: number[], type: string,
       focusRegex: string): Promise<string> {
     // Creating unique names for views so we can reuse and not delete them
     // for each marker.
@@ -343,19 +392,25 @@
      * TODO(octaviant) this branching should be eliminated for simplicity.
      */
     if (type === 'perf') {
+      let upidConditional = `upid = ${upids[0]}`;
+      if (upids.length > 1) {
+        upidConditional =
+            `upid_group = '${FlamegraphController.serializeUpidGroup(upids)}'`;
+      }
       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
-          where profile_type = "${type}" and ts <= ${ts} and upid = ${upid} 
+          where profile_type = '${type}' and ${startNs} <= ts and ts <= ${
+              endNs} and ${upidConditional}
           ${focusRegexConditional}`);
     }
     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}') ${
+          from experimental_flamegraph(${endNs}, ${upids[0]}, '${type}') ${
             focusRegexConditional}`);
   }
 
@@ -371,20 +426,44 @@
     return MIN_PIXEL_DISPLAYED * rootSize / width;
   }
 
-  async getFlamegraphMetadata(type: string, ts: number, upid: number) {
+  async getFlamegraphMetadata(
+      type: string, startNs: number, endNs: number, upids: 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)))) {
+         ((this.lastSelectedFlamegraphState.startNs === startNs &&
+           this.lastSelectedFlamegraphState.endNs === endNs &&
+           FlamegraphController.areArraysEqual(
+               this.lastSelectedFlamegraphState.upids, upids))))) {
       return undefined;
     }
 
     // Collecting data for more information about profile, such as:
     // total memory allocated, memory that is allocated and not freed.
+    const upidGroup = FlamegraphController.serializeUpidGroup(upids);
+
     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};
+        `select pid from process where upid in (${upidGroup})`);
+    const it = result.iter({pid: NUM});
+    const pids = [];
+    for (let i = 0; it.valid(); ++i, it.next()) {
+      pids.push(it.pid);
+    }
+    return {startNs, durNs: endNs - startNs, pids, upids, type};
+  }
+
+  private static areArraysEqual(a: number[], b: number[]) {
+    if (a.length !== b.length) {
+      return false;
+    }
+    for (let i = 0; i < a.length; i++) {
+      if (a[i] !== b[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static serializeUpidGroup(upids: number[]) {
+    return new Array(upids).join();
   }
 }