Merge "perfetto-ui: Show CPU slice aggregation"
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index 77c2c33..f116981 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -24,11 +24,12 @@
     height: 28px;
     min-height: 28px;
     display: grid;
-    grid-auto-columns: 1fr 30px;
-    grid-template-areas: "tabs button";
+    grid-auto-columns: 1fr 60px;
+    grid-template-areas: "tabs buttons";
 
     .tabs {
       display: flex;
+      grid-area: tabs;
 
       .tab {
         font-family: 'Google Sans';
@@ -59,17 +60,25 @@
 
     i.material-icons {
       font-size: 24px;
-      float: right;
       vertical-align: middle;
       margin-right: 5px;
       margin-top: 1px;
       &:hover{
         cursor: pointer;
       }
+      &[disabled] {
+        color: rgb(219, 219, 219);
+        &:hover{
+          cursor: default;
+        }
+      }
+    }
+
+    .buttons {
+      grid-area: buttons;
     }
 
     .handle-title {
-      float: left;
       font-family: 'Google Sans';
       color: #3c4b5d;
       margin-left: 5px;
@@ -89,6 +98,12 @@
     top: 0px;
     display: flex;
     background: white;
+    table {
+      padding: 0 10px;
+      th {
+        font-weight: 500;
+      }
+    }
     h2 {
       font-size: 16px;
       font-family: 'Google Sans';
@@ -99,9 +114,9 @@
     }
     &.heap-profile {
       display: flex;
-    justify-content: space-between;
-    align-content: center;
-    height: 30px;
+      justify-content: space-between;
+      align-content: center;
+      height: 30px;
       padding: 0px;
       font-size: 12px;
       * {
@@ -154,9 +169,7 @@
     @include transition(0.1s);
     font-size: 14px;
     line-height: 18px;
-    width: 50%;
-    min-width: 200px;
-    max-width: 50%;
+    width: 100%;
     table-layout: fixed;
     word-wrap: break-word;
     padding: 10px;
@@ -196,6 +209,10 @@
   }
 }
 
+table.half-width {
+  max-width: 50%;
+}
+
 .notes-editor-panel {
   padding: 10px;
   display: flex;
diff --git a/ui/src/common/aggregation_data.ts b/ui/src/common/aggregation_data.ts
new file mode 100644
index 0000000..3823150
--- /dev/null
+++ b/ui/src/common/aggregation_data.ts
@@ -0,0 +1,23 @@
+// Copyright (C) 2019 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.
+
+export interface AggregateCpuData {
+  strings: string[];
+  procNameId: Uint16Array;
+  pid: Uint32Array;
+  threadNameId: Uint16Array;
+  tid: Uint32Array;
+  totalDur: Float64Array;
+  occurrences: Uint16Array;
+}
diff --git a/ui/src/controller/aggregation_controller.ts b/ui/src/controller/aggregation_controller.ts
new file mode 100644
index 0000000..cdbb44f
--- /dev/null
+++ b/ui/src/controller/aggregation_controller.ts
@@ -0,0 +1,118 @@
+// Copyright (C) 2019 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 {AggregateCpuData} from '../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';
+
+export interface AggregationControllerArgs {
+  engine: Engine;
+}
+
+export class AggregationController extends Controller<'main'> {
+  private previousArea: TimestampedAreaSelection = {lastUpdate: 0};
+  private requestingData = false;
+  private queuedRequest = false;
+  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) {
+      return;
+    }
+    if (this.requestingData) {
+      this.queuedRequest = true;
+    } 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,
+        count(1)
+        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 data: AggregateCpuData = {
+                strings: [],
+                procNameId: new Uint16Array(numRows),
+                pid: new Uint32Array(numRows),
+                threadNameId: new Uint16Array(numRows),
+                tid: new Uint32Array(numRows),
+                totalDur: new Float64Array(numRows),
+                occurrences: new Uint16Array(numRows)
+              };
+
+              const stringIndexes = new Map<string, number>();
+              function internString(str: string) {
+                let idx = stringIndexes.get(str);
+                if (idx !== undefined) return idx;
+                idx = data.strings.length;
+                data.strings.push(str);
+                stringIndexes.set(str, idx);
+                return idx;
+              }
+
+              for (let row = 0; row < numRows; row++) {
+                const cols = result.columns;
+                data.procNameId[row] = internString(cols[0].stringValues![row]);
+                data.pid[row] = cols[1].longValues![row] as number;
+                data.threadNameId[row] =
+                    internString(cols[2].stringValues![row]);
+                data.tid[row] = cols[3].longValues![row] as number;
+                data.totalDur[row] = cols[4].longValues![row] as number;
+                data.occurrences[row] = cols[5].longValues![row] as number;
+              }
+              globals.publish('AggregateCpuData', 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/globals.ts b/ui/src/controller/globals.ts
index f8b51b3..e9d3652 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';
+    'RecordingLog'|'SearchResult'|'AggregateCpuData';
 
 export interface App {
   state: State;
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 9bb47d5..88e2190 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -49,6 +49,7 @@
 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 {globals} from './globals';
 import {
@@ -153,7 +154,8 @@
         const heapProfileArgs: HeapProfileControllerArgs = {engine};
         childControllers.push(
             Child('heapProfile', HeapProfileController, heapProfileArgs));
-
+        childControllers.push(
+            Child('aggregation', AggregationController, {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 17ec194..67f7f8f 100644
--- a/ui/src/frontend/aggregation_panel.ts
+++ b/ui/src/frontend/aggregation_panel.ts
@@ -13,12 +13,53 @@
 // limitations under the License.
 
 import * as m from 'mithril';
+
+import {AggregateCpuData} from '../common/aggregation_data';
+
+import {globals} from './globals';
 import {Panel} from './panel';
 
 export class AggregationPanel extends Panel {
   view() {
-    return m('.details-panel', m('.details-panel-heading', 'Work in Progress'));
+    const data = globals.aggregateCpuData;
+    return m(
+        '.details-panel',
+        m('.details-panel-heading.aggregation',
+          m('table',
+            m('tr',
+              m('th', 'Process'),
+              m('th', 'Thread'),
+              m('th', 'Wall duration (ms)'),
+              m('th', 'Avg. Wall duration (ms)'),
+              m('th', 'Occurrences')))),
+        m(
+            '.details-table.aggregation',
+            m('table', this.getRows(data)),
+            ));
+  }
+
+  getRows(data: AggregateCpuData) {
+    if (!data.strings || !data.procNameId || !data.threadNameId || !data.pid ||
+        !data.tid || !data.totalDur || !data.occurrences) {
+      return;
+    }
+    const rows = [];
+    for (let i = 0; i < data.pid.length; i++) {
+      const row =
+          [m('tr',
+             m('td', `${data.strings[data.procNameId[i]]} [${data.pid[i]}]`),
+             m('td', `${data.strings[data.threadNameId[i]]} [${data.tid[i]}]`),
+             m('td', `${data.totalDur[i] / 1000000}`),
+             m('td',
+               `${
+                   +
+                   (data.totalDur[i] / data.occurrences[i] / 1000000)
+                       .toFixed(6)}`),
+             m('td', `${data.occurrences[i]}`))];
+      rows.push(row);
+    }
+    return rows;
   }
 
   renderCanvas() {}
-}
\ No newline at end of file
+}
diff --git a/ui/src/frontend/chrome_slice_panel.ts b/ui/src/frontend/chrome_slice_panel.ts
index b43286e..68c7fb0 100644
--- a/ui/src/frontend/chrome_slice_panel.ts
+++ b/ui/src/frontend/chrome_slice_panel.ts
@@ -28,7 +28,7 @@
           m('.details-panel-heading', m('h2', `Slice Details`)),
           m(
               '.details-table',
-              [m('table',
+              [m('table.half-width',
                  [
                    m('tr', m('th', `Name`), m('td', `${sliceInfo.name}`)),
                    (sliceInfo.category === '[NULL]') ?
diff --git a/ui/src/frontend/counter_panel.ts b/ui/src/frontend/counter_panel.ts
index df207ff..4a3ccd7 100644
--- a/ui/src/frontend/counter_panel.ts
+++ b/ui/src/frontend/counter_panel.ts
@@ -32,7 +32,7 @@
           m('.details-panel-heading', m('h2', `Counter Details`)),
           m(
               '.details-table',
-              [m('table',
+              [m('table.half-width',
                  [
                    m('tr',
                      m('th', `Start time`),
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
index 74eff69..0e47ffe 100644
--- a/ui/src/frontend/details_panel.ts
+++ b/ui/src/frontend/details_panel.ts
@@ -32,24 +32,17 @@
 const DOWN_ICON = 'keyboard_arrow_down';
 const DRAG_HANDLE_HEIGHT_PX = 28;
 const DEFAULT_DETAILS_HEIGHT_PX = 230 + DRAG_HANDLE_HEIGHT_PX;
-let HEAP_PROFILE_DETAILS_HEIGHT_PX = DEFAULT_DETAILS_HEIGHT_PX;
 
-function generateHeapProfileHeight() {
+function getFullScreenHeight() {
   const panelContainer =
-      document.querySelector('.scrolling-panel-container') as HTMLElement;
-  if (HEAP_PROFILE_DETAILS_HEIGHT_PX === DEFAULT_DETAILS_HEIGHT_PX &&
-      panelContainer !== null) {
-    HEAP_PROFILE_DETAILS_HEIGHT_PX = panelContainer.clientHeight;
+      document.querySelector('.pan-and-zoom-content') as HTMLElement;
+  if (panelContainer !== null) {
+    return panelContainer.clientHeight;
+  } else {
+    return DEFAULT_DETAILS_HEIGHT_PX;
   }
 }
 
-function getHeightForDetailsPanel(): number {
-  return globals.state.currentSelection &&
-          globals.state.currentSelection.kind === 'HEAP_PROFILE' ?
-      HEAP_PROFILE_DETAILS_HEIGHT_PX :
-      DEFAULT_DETAILS_HEIGHT_PX;
-}
-
 function hasLogs(): boolean {
   const data = globals.trackDataStore.get(LogExistsKey) as LogExists;
   return data && data.exists;
@@ -61,7 +54,7 @@
   tabs: Tab[];
 }
 
-export type Tab = 'current_selection'|'time_range'|'android_logs';
+export type Tab = 'current_selection'|'cpu_slices'|'android_logs';
 
 class DragHandle implements m.ClassComponent<DragHandleAttrs> {
   private dragStartHeight = 0;
@@ -69,9 +62,12 @@
   private previousHeight = this.height;
   private resize: (height: number) => void = () => {};
   private isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
+  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>([
     ['current_selection', 'Current Selection'],
-    ['time_range', 'Time Range'],
+    ['cpu_slices', 'CPU Slices'],
     ['android_logs', 'Android Logs']
   ]);
 
@@ -80,6 +76,7 @@
     this.resize = attrs.resize;
     this.height = attrs.height;
     this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
+    this.fullscreenHeight = getFullScreenHeight();
     const elem = dom as HTMLElement;
     new DragGestureHandler(
         elem,
@@ -95,9 +92,11 @@
   }
 
   onDrag(_x: number, y: number) {
-    const newHeight = this.dragStartHeight + (DRAG_HANDLE_HEIGHT_PX / 2) - y;
-    this.isClosed = Math.floor(newHeight) <= DRAG_HANDLE_HEIGHT_PX;
-    this.resize(Math.floor(newHeight));
+    const newHeight =
+        Math.floor(this.dragStartHeight + (DRAG_HANDLE_HEIGHT_PX / 2) - y);
+    this.isClosed = newHeight <= DRAG_HANDLE_HEIGHT_PX;
+    this.isFullscreen = newHeight >= this.fullscreenHeight;
+    this.resize(newHeight);
     globals.rafScheduler.scheduleFullRedraw();
   }
 
@@ -129,22 +128,36 @@
     return m(
         '.handle',
         m('.tabs', attrs.tabs.map(renderTab)),
-        m('i.material-icons',
-          {
-            onclick: () => {
-              if (this.height === DRAG_HANDLE_HEIGHT_PX) {
+        m('.buttons',
+          m('i.material-icons',
+            {
+              onclick: () => {
                 this.isClosed = false;
-                this.resize(this.previousHeight);
-              } else {
-                this.isClosed = true;
-                this.previousHeight = this.height;
-                this.resize(DRAG_HANDLE_HEIGHT_PX);
-              }
-              globals.rafScheduler.scheduleFullRedraw();
+                this.isFullscreen = true;
+                this.resize(this.fullscreenHeight);
+                globals.rafScheduler.scheduleFullRedraw();
+              },
+              title: 'Open fullscreen',
+              disabled: this.isFullscreen
             },
-            title
-          },
-          icon));
+            'vertical_align_top'),
+          m('i.material-icons',
+            {
+              onclick: () => {
+                if (this.height === DRAG_HANDLE_HEIGHT_PX) {
+                  this.isClosed = false;
+                  this.resize(this.previousHeight);
+                } else {
+                  this.isFullscreen = false;
+                  this.isClosed = true;
+                  this.previousHeight = this.height;
+                  this.resize(DRAG_HANDLE_HEIGHT_PX);
+                }
+                globals.rafScheduler.scheduleFullRedraw();
+              },
+              title
+            },
+            icon)));
   }
 }
 
@@ -152,7 +165,6 @@
   private detailsHeight = DRAG_HANDLE_HEIGHT_PX;
   // Used to set details panel to default height on selection.
   private showDetailsPanel = true;
-  private lastSelectedKind?: string;
 
   view() {
     const detailsPanels: Map<Tab, AnyAttrsVnode> = new Map();
@@ -179,7 +191,6 @@
           detailsPanels.set(
               'current_selection',
               m(HeapProfileDetailsPanel, {key: 'heap_profile'}));
-          generateHeapProfileHeight();
           break;
         case 'CHROME_SLICE':
           detailsPanels.set('current_selection', m(ChromeSliceDetailsPanel));
@@ -203,17 +214,14 @@
     }
 
     if (globals.frontendLocalState.selectedArea.area !== undefined) {
-      detailsPanels.set('time_range', m(AggregationPanel));
+      detailsPanels.set('cpu_slices', m(AggregationPanel));
     }
 
     const wasShowing = this.showDetailsPanel;
-    const changedSelection =
-        curSelection && this.lastSelectedKind !== curSelection.kind;
     this.showDetailsPanel = detailsPanels.size > 0;
-    this.lastSelectedKind = curSelection ? curSelection.kind : undefined;
-    // Pop up details panel on first selection.
-    if (!wasShowing && changedSelection && this.showDetailsPanel) {
-      this.detailsHeight = getHeightForDetailsPanel();
+    // The first time the details panel appears, it should be default height.
+    if (!wasShowing && this.showDetailsPanel) {
+      this.detailsHeight = DEFAULT_DETAILS_HEIGHT_PX;
     }
 
     const panel = globals.frontendLocalState.currentTab ?
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index aa3d489..b38fe94 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -229,7 +229,7 @@
       lastUpdate: Date.now() / 1000
     };
     this.selectAreaDebounced();
-    globals.frontendLocalState.currentTab = 'time_range';
+    globals.frontendLocalState.currentTab = 'cpu_slices';
     globals.rafScheduler.scheduleFullRedraw();
   }
 
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index c31ca65..0fb518d 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -14,6 +14,7 @@
 
 import {assertExists} from '../base/logging';
 import {DeferredAction} from '../common/actions';
+import {AggregateCpuData} from '../common/aggregation_data';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
 import {CallsiteInfo, createEmptyState, State} from '../common/state';
 
@@ -96,6 +97,15 @@
   private _numQueriesQueued = 0;
   private _bufferUsage?: number = undefined;
   private _recordingLog?: string = undefined;
+  private _aggregateCpuData: AggregateCpuData = {
+    strings: [],
+    procNameId: new Uint16Array(0),
+    pid: new Uint32Array(0),
+    threadNameId: new Uint16Array(0),
+    tid: new Uint32Array(0),
+    totalDur: new Float64Array(0),
+    occurrences: new Uint16Array(0)
+  };
   private _currentSearchResults: CurrentSearchResults = {
     sliceIds: new Float64Array(0),
     tsStarts: new Float64Array(0),
@@ -180,6 +190,14 @@
     this._counterDetails = assertExists(click);
   }
 
+  get aggregateCpuData(): AggregateCpuData {
+    return assertExists(this._aggregateCpuData);
+  }
+
+  set aggregateCpuData(value: AggregateCpuData) {
+    this._aggregateCpuData = value;
+  }
+
   get heapProfileDetails() {
     return assertExists(this._heapProfileDetails);
   }
@@ -260,6 +278,15 @@
       sources: [],
       totalResults: 0,
     };
+    this._aggregateCpuData = {
+      strings: [],
+      procNameId: new Uint16Array(0),
+      pid: new Uint32Array(0),
+      threadNameId: new Uint16Array(0),
+      tid: new Uint32Array(0),
+      totalDur: new Float64Array(0),
+      occurrences: new Uint16Array(0)
+    };
   }
 
   // Used when switching to the legacy TraceViewer UI.
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 08951d1..c9f75bc 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -21,6 +21,7 @@
 import {assertExists, reportError, setErrorHandler} from '../base/logging';
 import {forwardRemoteCalls} from '../base/remote';
 import {Actions} from '../common/actions';
+import {AggregateCpuData} from '../common/aggregation_data';
 import {
   LogBoundsKey,
   LogEntriesKey,
@@ -174,6 +175,11 @@
     this.redraw();
   }
 
+  publishAggregateCpuData(args: AggregateCpuData) {
+    globals.aggregateCpuData = args;
+    this.redraw();
+  }
+
   private redraw(): void {
     if (globals.state.route &&
         globals.state.route !== this.router.getRouteFromHash()) {
diff --git a/ui/src/frontend/slice_panel.ts b/ui/src/frontend/slice_panel.ts
index 7a37de7..345b714 100644
--- a/ui/src/frontend/slice_panel.ts
+++ b/ui/src/frontend/slice_panel.ts
@@ -46,7 +46,7 @@
     } else {
       return m(
           '.details-table',
-          m('table',
+          m('table.half-width',
             [
               m('tr',
                 m('th', `Process`),
diff --git a/ui/src/frontend/thread_state_panel.ts b/ui/src/frontend/thread_state_panel.ts
index 7ad6127..78fb4db 100644
--- a/ui/src/frontend/thread_state_panel.ts
+++ b/ui/src/frontend/thread_state_panel.ts
@@ -37,7 +37,7 @@
       return m(
           '.details-panel',
           m('.details-panel-heading', m('h2', 'Thread State')),
-          m('.details-table', [m('table', [
+          m('.details-table', [m('table.half-width', [
               m('tr',
                 m('th', `Start time`),
                 m('td',