Merge "ui: Two small fixes for run-dev-server"
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',