perfetto-ui: Add thread state aggregation

Bug:118899922
Change-Id: Ie224e1d2e935e92fe8590de563587729dc8f2ea2
diff --git a/ui/src/common/aggregation_data.ts b/ui/src/common/aggregation_data.ts
index e7438e7..f4f3d74 100644
--- a/ui/src/common/aggregation_data.ts
+++ b/ui/src/common/aggregation_data.ts
@@ -34,4 +34,4 @@
   columns: Column[];
   // For string interning.
   strings: string[];
-}
+}
\ No newline at end of file
diff --git a/ui/src/controller/aggregation_controller.ts b/ui/src/controller/aggregation/aggregation_controller.ts
similarity index 83%
rename from ui/src/controller/aggregation_controller.ts
rename to ui/src/controller/aggregation/aggregation_controller.ts
index 7bb77be..5c41231 100644
--- a/ui/src/controller/aggregation_controller.ts
+++ b/ui/src/controller/aggregation/aggregation_controller.ts
@@ -12,16 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {AggregateData} from 'src/common/aggregation_data';
 
-import {Engine} from '../common/engine';
-import {TimestampedAreaSelection} from '../common/state';
+import {AggregateData} from '../../common/aggregation_data';
 
-import {Controller} from './controller';
-import {globals} from './globals';
+import {Engine} from '../../common/engine';
+import {TimestampedAreaSelection} from '../../common/state';
+
+import {Controller} from '../controller';
+import {globals} from '../globals';
 
 export interface AggregationControllerArgs {
   engine: Engine;
+  kind: string;
 }
 
 export abstract class AggregationController extends Controller<'main'> {
@@ -50,7 +52,9 @@
       this.requestingData = true;
       Object.assign(this.previousArea, selectedArea);
       this.onAreaSelectionChange(this.args.engine, selectedArea)
-          .then(data => globals.publish('AggregateData', data))
+          .then(
+              data => globals.publish(
+                  'AggregateData', {data, kind: this.args.kind}))
           .catch(reason => {
             console.error(reason);
           })
diff --git a/ui/src/controller/cpu_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
similarity index 94%
rename from ui/src/controller/cpu_aggregation_controller.ts
rename to ui/src/controller/aggregation/cpu_aggregation_controller.ts
index 167e536..6ca135a 100644
--- a/ui/src/controller/cpu_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
@@ -12,11 +12,10 @@
 // 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 {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 {
diff --git a/ui/src/controller/cpu_aggregation_controller.ts b/ui/src/controller/aggregation/thread_aggregation_controller.ts
similarity index 70%
copy from ui/src/controller/cpu_aggregation_controller.ts
copy to ui/src/controller/aggregation/thread_aggregation_controller.ts
index 167e536..f522b8c 100644
--- a/ui/src/controller/cpu_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/thread_aggregation_controller.ts
@@ -12,37 +12,49 @@
 // 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 {AggregateData} from '../../common/aggregation_data';
+import {Engine} from '../../common/engine';
+import {TimestampedAreaSelection} from '../../common/state';
+import {translateState} from '../../common/thread_state';
+import {toNs} from '../../common/time';
+import {
+  Config,
+  THREAD_STATE_TRACK_KIND
+} from '../../tracks/thread_state/common';
+import {globals} from '../globals';
 
 import {AggregationController} from './aggregation_controller';
 
-export class CpuAggregationController extends AggregationController {
+
+export class ThreadAggregationController extends AggregationController {
   async onAreaSelectionChange(
       engine: Engine, selectedArea: TimestampedAreaSelection) {
     const area = selectedArea.area;
     if (area === undefined) {
       return {columns: [], strings: []};
     }
+    // TODO(taylori): Thread state tracks should have a real track id in the
+    // trace processor.
+    const utids = [];
+    for (const trackId of area.tracks) {
+      const track = globals.state.tracks[trackId];
+      if (track.kind === THREAD_STATE_TRACK_KIND) {
+        utids.push((track.config as Config).utid);
+      }
+    }
 
-    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,
+    const query = `SELECT process.name, pid, thread.name, tid,
+      state,
+      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
+      WHERE utid IN (${utids}) 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`;
+      GROUP BY utid, state ORDER BY total_dur DESC`;
 
     const result = await engine.query(query);
 
@@ -53,6 +65,7 @@
         {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: 'State', kind: 'STRING', data: new Uint16Array(numRows)},
         {
           title: 'Wall duration (ms)',
           kind: 'TIMESTAMP_NS',
@@ -86,9 +99,11 @@
       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[4].data[row] =
+          internString(translateState(cols[4].stringValues![row]));
       aggregateData.columns[5].data[row] = cols[5].longValues![row] as number;
       aggregateData.columns[6].data[row] = cols[6].longValues![row] as number;
+      aggregateData.columns[7].data[row] = cols[7].longValues![row] as number;
     }
     return aggregateData;
   }
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 9865405..5f112d7 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -49,8 +49,13 @@
 import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state/common';
 
+import {
+  CpuAggregationController
+} from './aggregation/cpu_aggregation_controller';
+import {
+  ThreadAggregationController
+} from './aggregation/thread_aggregation_controller';
 import {Child, Children, Controller} from './controller';
-import {CpuAggregationController} from './cpu_aggregation_controller';
 import {globals} from './globals';
 import {
   HeapProfileController,
@@ -154,8 +159,14 @@
         const heapProfileArgs: HeapProfileControllerArgs = {engine};
         childControllers.push(
             Child('heapProfile', HeapProfileController, heapProfileArgs));
-        childControllers.push(
-            Child('aggregation', CpuAggregationController, {engine}));
+        childControllers.push(Child(
+            'cpu_aggregation',
+            CpuAggregationController,
+            {engine, kind: 'cpu'}));
+        childControllers.push(Child(
+            'thread_aggregation',
+            ThreadAggregationController,
+            {engine, kind: 'thread_state'}));
         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 f0ea1f2..ba2347d 100644
--- a/ui/src/frontend/aggregation_panel.ts
+++ b/ui/src/frontend/aggregation_panel.ts
@@ -15,25 +15,22 @@
 import * as m from 'mithril';
 
 import {AggregateData} from '../common/aggregation_data';
-
-import {globals} from './globals';
 import {Panel} from './panel';
 
 export interface AggregationPanelAttrs {
-  kind: 'CPU';
+  data: AggregateData;
 }
 
 export class AggregationPanel extends Panel<AggregationPanelAttrs> {
-  view() {
-    // In the future we will get different data based on the kind.
-    const data = globals.aggregateCpuData;
+  view({attrs}: m.CVnode<AggregationPanelAttrs>) {
     return m(
         '.details-panel',
         m('.details-panel-heading.aggregation',
-          m('table', m('tr', data.columns.map(col => (m('th', col.title)))))),
+          m('table',
+            m('tr', attrs.data.columns.map(col => (m('th', col.title)))))),
         m(
             '.details-table.aggregation',
-            m('table', this.getRows(data)),
+            m('table', this.getRows(attrs.data)),
             ));
   }
 
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
index a9590fe..8501d73 100644
--- a/ui/src/frontend/details_panel.ts
+++ b/ui/src/frontend/details_panel.ts
@@ -51,11 +51,9 @@
 interface DragHandleAttrs {
   height: number;
   resize: (height: number) => void;
-  tabs: Tab[];
+  tabs: string[];
 }
 
-export type Tab = 'current_selection'|'cpu_slices'|'android_logs';
-
 class DragHandle implements m.ClassComponent<DragHandleAttrs> {
   private dragStartHeight = 0;
   private height = 0;
@@ -65,10 +63,11 @@
   private isFullscreen = false;
   // We can't get real fullscreen height until the pan_and_zoom_handler exists.
   private fullscreenHeight = DEFAULT_DETAILS_HEIGHT_PX;
-  private tabNames = new Map<Tab, string>([
+  private tabNames = new Map<string, string>([
     ['current_selection', 'Current Selection'],
-    ['cpu_slices', 'CPU Slices'],
-    ['android_logs', 'Android Logs']
+    ['cpu', 'CPU Slices'],
+    ['android_logs', 'Android Logs'],
+    ['thread_state', 'Thread States']
   ]);
 
 
@@ -109,11 +108,14 @@
   view({attrs}: m.CVnode<DragHandleAttrs>) {
     const icon = this.isClosed ? UP_ICON : DOWN_ICON;
     const title = this.isClosed ? 'Show panel' : 'Hide panel';
-    const renderTab = (key: Tab) => {
+    const renderTab = (key: string) => {
       if (globals.frontendLocalState.currentTab === key ||
           globals.frontendLocalState.currentTab === undefined &&
               attrs.tabs[0] === key) {
-        return m('.tab[active]', this.tabNames.get(key));
+        return m(
+            '.tab[active]',
+            this.tabNames.get(key) === undefined ? key :
+                                                   this.tabNames.get(key));
       }
       return m(
           '.tab',
@@ -167,7 +169,7 @@
   private showDetailsPanel = true;
 
   view() {
-    const detailsPanels: Map<Tab, AnyAttrsVnode> = new Map();
+    const detailsPanels: Map<string, AnyAttrsVnode> = new Map();
     const curSelection = globals.state.currentSelection;
     if (curSelection) {
       switch (curSelection.kind) {
@@ -213,9 +215,10 @@
       detailsPanels.set('android_logs', m(LogPanel, {}));
     }
 
-    if (globals.aggregateCpuData.columns.length > 0 &&
-        globals.aggregateCpuData.columns[0].data.length > 0) {
-      detailsPanels.set('cpu_slices', m(AggregationPanel, {kind: 'CPU'}));
+    for (const [key, value] of globals.aggregateDataStore.entries()) {
+      if (value.columns.length > 0 && value.columns[0].data.length > 0) {
+        detailsPanels.set(key, m(AggregationPanel, {data: value}));
+      }
     }
 
     const wasShowing = this.showDetailsPanel;
@@ -225,7 +228,8 @@
       this.detailsHeight = DEFAULT_DETAILS_HEIGHT_PX;
     }
 
-    const panel = globals.frontendLocalState.currentTab ?
+    const panel = globals.frontendLocalState.currentTab &&
+            detailsPanels.has(globals.frontendLocalState.currentTab) ?
         detailsPanels.get(globals.frontendLocalState.currentTab) :
         detailsPanels.values().next().value;
     const panels = panel ? [panel] : [];
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index a966a45..f66b500 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -24,7 +24,6 @@
 import {TimeSpan} from '../common/time';
 
 import {randomColor} from './colorizer';
-import {Tab} from './details_panel';
 import {globals} from './globals';
 import {TimeScale} from './time_scale';
 
@@ -103,7 +102,7 @@
   visibleTracks = new Set<string>();
   prevVisibleTracks = new Set<string>();
   searchIndex = -1;
-  currentTab?: Tab;
+  currentTab?: string;
   scrollToTrackId?: string|number;
   httpRpcState: HttpRpcState = {connected: false};
   newVersionAvailable = false;
@@ -234,7 +233,6 @@
       lastUpdate: Date.now() / 1000
     };
     this.selectAreaDebounced();
-    globals.frontendLocalState.currentTab = 'cpu_slices';
     globals.rafScheduler.scheduleFullRedraw();
   }
 
@@ -284,7 +282,6 @@
       }));
     }
 
-    globals.frontendLocalState.currentTab = 'cpu_slices';
     globals.rafScheduler.scheduleFullRedraw();
   }
 
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 10db5be..4ac3260 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -25,6 +25,7 @@
 type Dispatch = (action: DeferredAction) => void;
 type TrackDataStore = Map<string, {}>;
 type QueryResultsStore = Map<string, {}>;
+type AggregateDataStore = Map<string, AggregateData>;
 type Args = Map<string, string>;
 export interface SliceDetails {
   ts?: number;
@@ -94,6 +95,7 @@
   private _trackDataStore?: TrackDataStore = undefined;
   private _queryResults?: QueryResultsStore = undefined;
   private _overviewStore?: OverviewStore = undefined;
+  private _aggregateDataStore?: AggregateDataStore = undefined;
   private _threadMap?: ThreadMap = undefined;
   private _sliceDetails?: SliceDetails = undefined;
   private _counterDetails?: CounterDetails = undefined;
@@ -101,10 +103,7 @@
   private _numQueriesQueued = 0;
   private _bufferUsage?: number = undefined;
   private _recordingLog?: string = undefined;
-  private _aggregateCpuData: AggregateData = {
-    strings: [],
-    columns: [],
-  };
+
   private _currentSearchResults: CurrentSearchResults = {
     sliceIds: new Float64Array(0),
     tsStarts: new Float64Array(0),
@@ -131,6 +130,7 @@
     this._trackDataStore = new Map<string, {}>();
     this._queryResults = new Map<string, {}>();
     this._overviewStore = new Map<string, QuantizedLoad[]>();
+    this._aggregateDataStore = new Map<string, AggregateData>();
     this._threadMap = new Map<number, ThreadDesc>();
     this._sliceDetails = {};
     this._counterDetails = {};
@@ -194,12 +194,8 @@
     this._counterDetails = assertExists(click);
   }
 
-  get aggregateCpuData(): AggregateData {
-    return assertExists(this._aggregateCpuData);
-  }
-
-  set aggregateCpuData(value: AggregateData) {
-    this._aggregateCpuData = value;
+  get aggregateDataStore(): AggregateDataStore {
+    return assertExists(this._aggregateDataStore);
   }
 
   get heapProfileDetails() {
@@ -246,6 +242,10 @@
     this._recordingLog = recordingLog;
   }
 
+  setAggregateData(kind: string, data: AggregateData) {
+    this.aggregateDataStore.set(kind, data);
+  }
+
   getCurResolution() {
     // Truncate the resolution to the closest power of 2.
     // This effectively means the resolution changes every 6 zoom levels.
@@ -274,6 +274,7 @@
     this._overviewStore = undefined;
     this._threadMap = undefined;
     this._sliceDetails = undefined;
+    this._aggregateDataStore = undefined;
     this._numQueriesQueued = 0;
     this._currentSearchResults = {
       sliceIds: new Float64Array(0),
@@ -283,10 +284,6 @@
       sources: [],
       totalResults: 0,
     };
-    this._aggregateCpuData = {
-      strings: [],
-      columns: [],
-    };
   }
 
   // Used when switching to the legacy TraceViewer UI.
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index b8bbe83..7823f28 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -175,8 +175,8 @@
     this.redraw();
   }
 
-  publishAggregateData(args: AggregateData) {
-    globals.aggregateCpuData = args;
+  publishAggregateData(args: {data: AggregateData, kind: string}) {
+    globals.setAggregateData(args.kind, args.data);
     this.redraw();
   }