perfetto-ui: Make aggregation controller a base class

Aggregation controllers now work similarly to the track controllers
so that each type of aggregation can be kept separate and work in
a similar way.

Change-Id: I1350517df372e5a86b95769a2ba4162144171c9e
diff --git a/ui/src/controller/aggregation_controller.ts b/ui/src/controller/aggregation_controller.ts
index 526efda..7bb77be 100644
--- a/ui/src/controller/aggregation_controller.ts
+++ b/ui/src/controller/aggregation_controller.ts
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {AggregateData} from '../common/aggregation_data';
+import {AggregateData} from 'src/common/aggregation_data';
+
 import {Engine} from '../common/engine';
 import {TimestampedAreaSelection} from '../common/state';
-import {toNs} from '../common/time';
 
 import {Controller} from './controller';
 import {globals} from './globals';
@@ -24,20 +24,24 @@
   engine: Engine;
 }
 
-export class AggregationController extends Controller<'main'> {
+export abstract class AggregationController extends Controller<'main'> {
   private previousArea: TimestampedAreaSelection = {lastUpdate: 0};
   private requestingData = false;
   private queuedRequest = false;
+
+  // Must be overridden by the aggregation implementation. It is invoked
+  // whenever the selected area is changed and returns data to be published.
+  abstract async onAreaSelectionChange(
+      engine: Engine, area: TimestampedAreaSelection): Promise<AggregateData>;
+
   constructor(private args: AggregationControllerArgs) {
     super('main');
   }
 
   run() {
     const selectedArea = globals.state.frontendLocalState.selectedArea;
-    const area = selectedArea.area;
-    if (!area ||
-        this.previousArea &&
-            this.previousArea.lastUpdate >= selectedArea.lastUpdate) {
+    if (this.previousArea &&
+        this.previousArea.lastUpdate >= selectedArea.lastUpdate) {
       return;
     }
     if (this.requestingData) {
@@ -45,113 +49,18 @@
     } else {
       this.requestingData = true;
       Object.assign(this.previousArea, selectedArea);
-
-      this.args.engine.getCpus().then(cpusInTrace => {
-        const selectedCpuTracks =
-            cpusInTrace.filter(x => area.tracks.includes((x + 1).toString()));
-
-        const query =
-            `SELECT process.name, pid, thread.name, tid, sum(dur) AS total_dur,
-        sum(dur)/count(1) as avg_dur,
-        count(1) as occurences
-        FROM process
-        JOIN thread USING(upid)
-        JOIN thread_state USING(utid)
-        WHERE cpu IN (${selectedCpuTracks}) AND
-        state = "Running" AND
-        thread_state.ts + thread_state.dur > ${toNs(area.startSec)} AND
-        thread_state.ts < ${toNs(area.endSec)}
-        GROUP BY utid ORDER BY total_dur DESC`;
-
-        this.args.engine.query(query)
-            .then(result => {
-              if (globals.state.frontendLocalState.selectedArea.lastUpdate >
-                  selectedArea.lastUpdate) {
-                return;
-              }
-
-              const numRows = +result.numRecords;
-              const aggregateData: AggregateData = {
-                columns: [
-                  {
-                    title: 'Process',
-                    kind: 'STRING',
-                    data: new Uint16Array(numRows)
-                  },
-                  {
-                    title: 'PID',
-                    kind: 'NUMBER',
-                    data: new Uint16Array(numRows)
-                  },
-                  {
-                    title: 'Thread',
-                    kind: 'STRING',
-                    data: new Uint16Array(numRows)
-                  },
-                  {
-                    title: 'TID',
-                    kind: 'NUMBER',
-                    data: new Uint16Array(numRows)
-                  },
-                  {
-                    title: 'Wall duration (ms)',
-                    kind: 'TIMESTAMP_NS',
-                    data: new Float64Array(numRows)
-                  },
-                  {
-                    title: 'Avg Wall duration (ms)',
-                    kind: 'TIMESTAMP_NS',
-                    data: new Float64Array(numRows)
-                  },
-                  {
-                    title: 'Occurrences',
-                    kind: 'NUMBER',
-                    data: new Uint16Array(numRows)
-                  }
-                ],
-                strings: [],
-              };
-
-              const stringIndexes = new Map<string, number>();
-              function internString(str: string) {
-                let idx = stringIndexes.get(str);
-                if (idx !== undefined) return idx;
-                idx = aggregateData.strings.length;
-                aggregateData.strings.push(str);
-                stringIndexes.set(str, idx);
-                return idx;
-              }
-
-              for (let row = 0; row < numRows; row++) {
-                const cols = result.columns;
-                aggregateData.columns[0].data[row] =
-                    internString(cols[0].stringValues![row]);
-                aggregateData.columns[1].data[row] =
-                    cols[1].longValues![row] as number;
-                aggregateData.columns[2].data[row] =
-                    internString(cols[2].stringValues![row]);
-                aggregateData.columns[3].data[row] =
-                    cols[3].longValues![row] as number;
-                aggregateData.columns[4].data[row] =
-                    cols[4].longValues![row] as number;
-                aggregateData.columns[5].data[row] =
-                    cols[5].longValues![row] as number;
-                aggregateData.columns[6].data[row] =
-                    cols[6].longValues![row] as number;
-              }
-              globals.publish('AggregateCpuData', aggregateData);
-            })
-            .catch(reason => {
-              console.error(reason);
-            })
-            .finally(() => {
-              this.requestingData = false;
-              if (this.queuedRequest) {
-                this.queuedRequest = false;
-                this.run();
-              }
-            });
-      });
+      this.onAreaSelectionChange(this.args.engine, selectedArea)
+          .then(data => globals.publish('AggregateData', data))
+          .catch(reason => {
+            console.error(reason);
+          })
+          .finally(() => {
+            this.requestingData = false;
+            if (this.queuedRequest) {
+              this.queuedRequest = false;
+              this.run();
+            }
+          });
     }
   }
 }
\ No newline at end of file
diff --git a/ui/src/controller/cpu_aggregation_controller.ts b/ui/src/controller/cpu_aggregation_controller.ts
new file mode 100644
index 0000000..167e536
--- /dev/null
+++ b/ui/src/controller/cpu_aggregation_controller.ts
@@ -0,0 +1,95 @@
+// 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.
+
+import {AggregateData} from '../common/aggregation_data';
+import {Engine} from '../common/engine';
+import {TimestampedAreaSelection} from '../common/state';
+import {toNs} from '../common/time';
+
+import {AggregationController} from './aggregation_controller';
+
+export class CpuAggregationController extends AggregationController {
+  async onAreaSelectionChange(
+      engine: Engine, selectedArea: TimestampedAreaSelection) {
+    const area = selectedArea.area;
+    if (area === undefined) {
+      return {columns: [], strings: []};
+    }
+
+    const cpusInTrace = await engine.getCpus();
+    const selectedCpuTracks =
+        cpusInTrace.filter(x => area.tracks.includes((x + 1).toString()));
+
+    const query =
+        `SELECT process.name, pid, thread.name, tid, sum(dur) AS total_dur,
+      sum(dur)/count(1) as avg_dur,
+      count(1) as occurences
+      FROM process
+      JOIN thread USING(upid)
+      JOIN thread_state USING(utid)
+      WHERE cpu IN (${selectedCpuTracks}) AND
+      state = "Running" AND
+      thread_state.ts + thread_state.dur > ${toNs(area.startSec)} AND
+      thread_state.ts < ${toNs(area.endSec)}
+      GROUP BY utid ORDER BY total_dur DESC`;
+
+    const result = await engine.query(query);
+
+    const numRows = +result.numRecords;
+    const aggregateData: AggregateData = {
+      columns: [
+        {title: 'Process', kind: 'STRING', data: new Uint16Array(numRows)},
+        {title: 'PID', kind: 'NUMBER', data: new Uint16Array(numRows)},
+        {title: 'Thread', kind: 'STRING', data: new Uint16Array(numRows)},
+        {title: 'TID', kind: 'NUMBER', data: new Uint16Array(numRows)},
+        {
+          title: 'Wall duration (ms)',
+          kind: 'TIMESTAMP_NS',
+          data: new Float64Array(numRows)
+        },
+        {
+          title: 'Avg Wall duration (ms)',
+          kind: 'TIMESTAMP_NS',
+          data: new Float64Array(numRows)
+        },
+        {title: 'Occurrences', kind: 'NUMBER', data: new Uint16Array(numRows)}
+      ],
+      strings: [],
+    };
+
+    const stringIndexes = new Map<string, number>();
+    function internString(str: string) {
+      let idx = stringIndexes.get(str);
+      if (idx !== undefined) return idx;
+      idx = aggregateData.strings.length;
+      aggregateData.strings.push(str);
+      stringIndexes.set(str, idx);
+      return idx;
+    }
+
+    for (let row = 0; row < numRows; row++) {
+      const cols = result.columns;
+      aggregateData.columns[0].data[row] =
+          internString(cols[0].stringValues![row]);
+      aggregateData.columns[1].data[row] = cols[1].longValues![row] as number;
+      aggregateData.columns[2].data[row] =
+          internString(cols[2].stringValues![row]);
+      aggregateData.columns[3].data[row] = cols[3].longValues![row] as number;
+      aggregateData.columns[4].data[row] = cols[4].longValues![row] as number;
+      aggregateData.columns[5].data[row] = cols[5].longValues![row] as number;
+      aggregateData.columns[6].data[row] = cols[6].longValues![row] as number;
+    }
+    return aggregateData;
+  }
+}
diff --git a/ui/src/controller/globals.ts b/ui/src/controller/globals.ts
index e9d3652..7e99ab6 100644
--- a/ui/src/controller/globals.ts
+++ b/ui/src/controller/globals.ts
@@ -23,7 +23,7 @@
 type PublishKinds = 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|
     'LegacyTrace'|'SliceDetails'|'CounterDetails'|'HeapProfileDetails'|
     'HeapProfileFlamegraph'|'FileDownload'|'Loading'|'Search'|'BufferUsage'|
-    'RecordingLog'|'SearchResult'|'AggregateCpuData';
+    'RecordingLog'|'SearchResult'|'AggregateData';
 
 export interface App {
   state: State;
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 327a411..9865405 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -49,8 +49,8 @@
 import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state/common';
 
-import {AggregationController} from './aggregation_controller';
 import {Child, Children, Controller} from './controller';
+import {CpuAggregationController} from './cpu_aggregation_controller';
 import {globals} from './globals';
 import {
   HeapProfileController,
@@ -155,7 +155,7 @@
         childControllers.push(
             Child('heapProfile', HeapProfileController, heapProfileArgs));
         childControllers.push(
-            Child('aggregation', AggregationController, {engine}));
+            Child('aggregation', CpuAggregationController, {engine}));
         childControllers.push(Child('search', SearchController, {
           engine,
           app: globals,
diff --git a/ui/src/frontend/aggregation_panel.ts b/ui/src/frontend/aggregation_panel.ts
index 692f3c0..f0ea1f2 100644
--- a/ui/src/frontend/aggregation_panel.ts
+++ b/ui/src/frontend/aggregation_panel.ts
@@ -24,9 +24,8 @@
 }
 
 export class AggregationPanel extends Panel<AggregationPanelAttrs> {
-  view({attrs}: m.CVnode<AggregationPanelAttrs>) {
+  view() {
     // In the future we will get different data based on the kind.
-    if (attrs.kind !== 'CPU') return;
     const data = globals.aggregateCpuData;
     return m(
         '.details-panel',
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
index 4a283c0..a9590fe 100644
--- a/ui/src/frontend/details_panel.ts
+++ b/ui/src/frontend/details_panel.ts
@@ -213,7 +213,8 @@
       detailsPanels.set('android_logs', m(LogPanel, {}));
     }
 
-    if (globals.frontendLocalState.selectedArea.area !== undefined) {
+    if (globals.aggregateCpuData.columns.length > 0 &&
+        globals.aggregateCpuData.columns[0].data.length > 0) {
       detailsPanels.set('cpu_slices', m(AggregationPanel, {kind: 'CPU'}));
     }
 
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 0aac9a3..b8bbe83 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -175,7 +175,7 @@
     this.redraw();
   }
 
-  publishAggregateCpuData(args: AggregateData) {
+  publishAggregateData(args: AggregateData) {
     globals.aggregateCpuData = args;
     this.redraw();
   }
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 9ba8637..3a252fb 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -110,10 +110,10 @@
          this.prevAreaSelection.lastUpdate >= selection.lastUpdate) ||
         area === undefined ||
         globals.frontendLocalState.areaY.start === undefined ||
-        globals.frontendLocalState.areaY.end === undefined) {
+        globals.frontendLocalState.areaY.end === undefined ||
+        this.panelPositions.length === 0) {
       return;
     }
-
     // Only get panels from the current panel container if the selection began
     // in this container.
     const panelContainerTop = this.panelPositions[0].y;