• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2019 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import * as m from 'mithril';
16
17import {LogExists, LogExistsKey} from '../common/logs';
18
19import {AggregationPanel} from './aggregation_panel';
20import {ChromeSliceDetailsPanel} from './chrome_slice_panel';
21import {CounterDetailsPanel} from './counter_panel';
22import {CpuProfileDetailsPanel} from './cpu_profile_panel';
23import {DragGestureHandler} from './drag_gesture_handler';
24import {globals} from './globals';
25import {HeapProfileDetailsPanel} from './heap_profile_panel';
26import {LogPanel} from './logs_panel';
27import {NotesEditorPanel} from './notes_panel';
28import {AnyAttrsVnode, PanelContainer} from './panel_container';
29import {SliceDetailsPanel} from './slice_panel';
30import {ThreadStatePanel} from './thread_state_panel';
31
32const UP_ICON = 'keyboard_arrow_up';
33const DOWN_ICON = 'keyboard_arrow_down';
34const DRAG_HANDLE_HEIGHT_PX = 28;
35const DEFAULT_DETAILS_HEIGHT_PX = 230 + DRAG_HANDLE_HEIGHT_PX;
36
37function getFullScreenHeight() {
38  const panelContainer =
39      document.querySelector('.pan-and-zoom-content') as HTMLElement;
40  if (panelContainer !== null) {
41    return panelContainer.clientHeight;
42  } else {
43    return DEFAULT_DETAILS_HEIGHT_PX;
44  }
45}
46
47function hasLogs(): boolean {
48  const data = globals.trackDataStore.get(LogExistsKey) as LogExists;
49  return data && data.exists;
50}
51
52interface DragHandleAttrs {
53  height: number;
54  resize: (height: number) => void;
55  tabs: string[];
56}
57
58class DragHandle implements m.ClassComponent<DragHandleAttrs> {
59  private dragStartHeight = 0;
60  private height = 0;
61  private previousHeight = this.height;
62  private resize: (height: number) => void = () => {};
63  private isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
64  private isFullscreen = false;
65  // We can't get real fullscreen height until the pan_and_zoom_handler exists.
66  private fullscreenHeight = DEFAULT_DETAILS_HEIGHT_PX;
67  private tabNames = new Map<string, string>([
68    ['current_selection', 'Current Selection'],
69    ['android_logs', 'Android Logs'],
70  ]);
71
72
73  oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) {
74    this.resize = attrs.resize;
75    this.height = attrs.height;
76    this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
77    this.fullscreenHeight = getFullScreenHeight();
78    const elem = dom as HTMLElement;
79    new DragGestureHandler(
80        elem,
81        this.onDrag.bind(this),
82        this.onDragStart.bind(this),
83        this.onDragEnd.bind(this));
84  }
85
86  onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) {
87    this.resize = attrs.resize;
88    this.height = attrs.height;
89    this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
90  }
91
92  onDrag(_x: number, y: number) {
93    const newHeight =
94        Math.floor(this.dragStartHeight + (DRAG_HANDLE_HEIGHT_PX / 2) - y);
95    this.isClosed = newHeight <= DRAG_HANDLE_HEIGHT_PX;
96    this.isFullscreen = newHeight >= this.fullscreenHeight;
97    this.resize(newHeight);
98    globals.rafScheduler.scheduleFullRedraw();
99  }
100
101  onDragStart(_x: number, _y: number) {
102    this.dragStartHeight = this.height;
103  }
104
105  onDragEnd() {}
106
107  view({attrs}: m.CVnode<DragHandleAttrs>) {
108    const icon = this.isClosed ? UP_ICON : DOWN_ICON;
109    const title = this.isClosed ? 'Show panel' : 'Hide panel';
110    const renderTab = (key: string) => {
111      if (globals.frontendLocalState.currentTab === key ||
112          globals.frontendLocalState.currentTab === undefined &&
113              attrs.tabs[0] === key) {
114        return m(
115            '.tab[active]',
116            this.tabNames.get(key) === undefined ? key :
117                                                   this.tabNames.get(key));
118      }
119      return m(
120          '.tab',
121          {
122            onclick: () => {
123              globals.frontendLocalState.currentTab = key;
124              globals.rafScheduler.scheduleFullRedraw();
125            }
126          },
127          this.tabNames.get(key) === undefined ? key : this.tabNames.get(key));
128    };
129    return m(
130        '.handle',
131        m('.tabs', attrs.tabs.map(renderTab)),
132        m('.buttons',
133          m('i.material-icons',
134            {
135              onclick: () => {
136                this.isClosed = false;
137                this.isFullscreen = true;
138                this.resize(this.fullscreenHeight);
139                globals.rafScheduler.scheduleFullRedraw();
140              },
141              title: 'Open fullscreen',
142              disabled: this.isFullscreen
143            },
144            'vertical_align_top'),
145          m('i.material-icons',
146            {
147              onclick: () => {
148                if (this.height === DRAG_HANDLE_HEIGHT_PX) {
149                  this.isClosed = false;
150                  this.resize(this.previousHeight);
151                } else {
152                  this.isFullscreen = false;
153                  this.isClosed = true;
154                  this.previousHeight = this.height;
155                  this.resize(DRAG_HANDLE_HEIGHT_PX);
156                }
157                globals.rafScheduler.scheduleFullRedraw();
158              },
159              title
160            },
161            icon)));
162  }
163}
164
165export class DetailsPanel implements m.ClassComponent {
166  private detailsHeight = DRAG_HANDLE_HEIGHT_PX;
167  // Used to set details panel to default height on selection.
168  private showDetailsPanel = true;
169
170  view() {
171    const detailsPanels: Map<string, AnyAttrsVnode> = new Map();
172    const curSelection = globals.state.currentSelection;
173    if (curSelection) {
174      switch (curSelection.kind) {
175        case 'NOTE':
176          detailsPanels.set('current_selection', m(NotesEditorPanel, {
177                              key: 'notes',
178                              id: curSelection.id,
179                            }));
180          break;
181        case 'SLICE':
182          detailsPanels.set('current_selection', m(SliceDetailsPanel, {
183                              key: 'slice',
184                            }));
185          break;
186        case 'COUNTER':
187          detailsPanels.set('current_selection', m(CounterDetailsPanel, {
188                              key: 'counter',
189                            }));
190          break;
191        case 'HEAP_PROFILE':
192          detailsPanels.set(
193              'current_selection',
194              m(HeapProfileDetailsPanel, {key: 'heap_profile'}));
195          break;
196        case 'CPU_PROFILE_SAMPLE':
197          detailsPanels.set('current_selection', m(CpuProfileDetailsPanel, {
198                              key: 'cpu_profile_sample',
199                            }));
200          break;
201        case 'CHROME_SLICE':
202          detailsPanels.set('current_selection', m(ChromeSliceDetailsPanel));
203          break;
204        case 'THREAD_STATE':
205          detailsPanels.set('current_selection', m(ThreadStatePanel, {
206                              key: 'thread_state',
207                              ts: curSelection.ts,
208                              dur: curSelection.dur,
209                              utid: curSelection.utid,
210                              state: curSelection.state,
211                              cpu: curSelection.cpu
212                            }));
213          break;
214        default:
215          break;
216      }
217    }
218    if (hasLogs()) {
219      detailsPanels.set('android_logs', m(LogPanel, {}));
220    }
221
222    for (const [key, value] of globals.aggregateDataStore.entries()) {
223      if (value.columns.length > 0 && value.columns[0].data.length > 0) {
224        detailsPanels.set(
225            value.tabName, m(AggregationPanel, {kind: key, data: value}));
226      }
227    }
228
229    const wasShowing = this.showDetailsPanel;
230    this.showDetailsPanel = detailsPanels.size > 0;
231    // The first time the details panel appears, it should be default height.
232    if (!wasShowing && this.showDetailsPanel) {
233      this.detailsHeight = DEFAULT_DETAILS_HEIGHT_PX;
234    }
235
236    const panel = globals.frontendLocalState.currentTab &&
237            detailsPanels.has(globals.frontendLocalState.currentTab) ?
238        detailsPanels.get(globals.frontendLocalState.currentTab) :
239        detailsPanels.values().next().value;
240    const panels = panel ? [panel] : [];
241
242    return m(
243        '.details-content',
244        {
245          style: {
246            height: `${this.detailsHeight}px`,
247            display: this.showDetailsPanel ? null : 'none'
248          }
249        },
250        m(DragHandle, {
251          resize: (height: number) => {
252            this.detailsHeight = Math.max(height, DRAG_HANDLE_HEIGHT_PX);
253          },
254          height: this.detailsHeight,
255          tabs: [...detailsPanels.keys()],
256        }),
257        m('.details-panel-container',
258          m(PanelContainer, {doesScroll: true, panels, kind: 'DETAILS'})));
259  }
260}
261