• 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 m from 'mithril';
16
17import {Actions} from '../common/actions';
18import {isEmptyData} from '../common/aggregation_data';
19import {LogExists, LogExistsKey} from '../common/logs';
20import {pluginManager} from '../common/plugins';
21import {addSelectionChangeObserver} from '../common/selection_observer';
22import {Selection} from '../common/state';
23import {DebugSliceDetailsTab} from '../tracks/debug/details_tab';
24import {SCROLL_JANK_PLUGIN_ID} from '../tracks/scroll_jank';
25import {TOP_LEVEL_SCROLL_KIND} from '../tracks/scroll_jank/scroll_track';
26
27import {AggregationPanel} from './aggregation_panel';
28import {ChromeSliceDetailsPanel} from './chrome_slice_panel';
29import {CounterDetailsPanel} from './counter_panel';
30import {CpuProfileDetailsPanel} from './cpu_profile_panel';
31import {DEFAULT_DETAILS_CONTENT_HEIGHT} from './css_constants';
32import {DragGestureHandler} from './drag_gesture_handler';
33import {FlamegraphDetailsPanel} from './flamegraph_panel';
34import {
35  FlowEventsAreaSelectedPanel,
36  FlowEventsPanel,
37} from './flow_events_panel';
38import {FtracePanel} from './ftrace_panel';
39import {globals} from './globals';
40import {LogPanel} from './logs_panel';
41import {NotesEditorTab} from './notes_panel';
42import {AnyAttrsVnode, PanelContainer} from './panel_container';
43import {PivotTable} from './pivot_table';
44import {SliceDetailsPanel} from './slice_details_panel';
45import {ThreadStateTab} from './thread_state_tab';
46
47const UP_ICON = 'keyboard_arrow_up';
48const DOWN_ICON = 'keyboard_arrow_down';
49const DRAG_HANDLE_HEIGHT_PX = 28;
50
51export const CURRENT_SELECTION_TAG = 'current_selection';
52
53function getDetailsHeight() {
54  // This needs to be a function instead of a const to ensure the CSS constants
55  // have been initialized by the time we perform this calculation;
56  return DEFAULT_DETAILS_CONTENT_HEIGHT + DRAG_HANDLE_HEIGHT_PX;
57}
58
59function getFullScreenHeight() {
60  const panelContainer =
61      document.querySelector('.pan-and-zoom-content') as HTMLElement;
62  if (panelContainer !== null) {
63    return panelContainer.clientHeight;
64  } else {
65    return getDetailsHeight();
66  }
67}
68
69function hasLogs(): boolean {
70  const data = globals.trackDataStore.get(LogExistsKey) as LogExists;
71  return data && data.exists;
72}
73
74interface Tab {
75  key: string;
76  name: string;
77}
78
79interface DragHandleAttrs {
80  height: number;
81  resize: (height: number) => void;
82  tabs: Tab[];
83  currentTabKey?: string;
84}
85
86class DragHandle implements m.ClassComponent<DragHandleAttrs> {
87  private dragStartHeight = 0;
88  private height = 0;
89  private previousHeight = this.height;
90  private resize: (height: number) => void = () => {};
91  private isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
92  private isFullscreen = false;
93  // We can't get real fullscreen height until the pan_and_zoom_handler exists.
94  private fullscreenHeight = getDetailsHeight();
95
96  oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) {
97    this.resize = attrs.resize;
98    this.height = attrs.height;
99    this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
100    this.fullscreenHeight = getFullScreenHeight();
101    const elem = dom as HTMLElement;
102    new DragGestureHandler(
103        elem,
104        this.onDrag.bind(this),
105        this.onDragStart.bind(this),
106        this.onDragEnd.bind(this));
107  }
108
109  onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) {
110    this.resize = attrs.resize;
111    this.height = attrs.height;
112    this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
113  }
114
115  onDrag(_x: number, y: number) {
116    const newHeight =
117        Math.floor(this.dragStartHeight + (DRAG_HANDLE_HEIGHT_PX / 2) - y);
118    this.isClosed = newHeight <= DRAG_HANDLE_HEIGHT_PX;
119    this.isFullscreen = newHeight >= this.fullscreenHeight;
120    this.resize(newHeight);
121    globals.rafScheduler.scheduleFullRedraw();
122  }
123
124  onDragStart(_x: number, _y: number) {
125    this.dragStartHeight = this.height;
126  }
127
128  onDragEnd() {}
129
130  view({attrs}: m.CVnode<DragHandleAttrs>) {
131    const icon = this.isClosed ? UP_ICON : DOWN_ICON;
132    const title = this.isClosed ? 'Show panel' : 'Hide panel';
133    const renderTab = (tab: Tab) => {
134      if (attrs.currentTabKey === tab.key) {
135        return m('.tab[active]', tab.name);
136      }
137      return m(
138          '.tab',
139          {
140            onclick: () => {
141              globals.dispatch(Actions.setCurrentTab({tab: tab.key}));
142            },
143          },
144          tab.name);
145    };
146    return m(
147        '.handle',
148        m('.tabs', attrs.tabs.map(renderTab)),
149        m('.buttons',
150          m('i.material-icons',
151            {
152              onclick: () => {
153                this.isClosed = false;
154                this.isFullscreen = true;
155                this.resize(this.fullscreenHeight);
156                globals.rafScheduler.scheduleFullRedraw();
157              },
158              title: 'Open fullscreen',
159              disabled: this.isFullscreen,
160            },
161            'vertical_align_top'),
162          m('i.material-icons',
163            {
164              onclick: () => {
165                if (this.height === DRAG_HANDLE_HEIGHT_PX) {
166                  this.isClosed = false;
167                  if (this.previousHeight === 0) {
168                    this.previousHeight = getDetailsHeight();
169                  }
170                  this.resize(this.previousHeight);
171                } else {
172                  this.isFullscreen = false;
173                  this.isClosed = true;
174                  this.previousHeight = this.height;
175                  this.resize(DRAG_HANDLE_HEIGHT_PX);
176                }
177                globals.rafScheduler.scheduleFullRedraw();
178              },
179              title,
180            },
181            icon)));
182  }
183}
184
185function handleSelectionChange(newSelection?: Selection, _?: Selection): void {
186  const currentSelectionTag = CURRENT_SELECTION_TAG;
187  const bottomTabList = globals.bottomTabList;
188  if (!bottomTabList) return;
189  if (newSelection === undefined) {
190    bottomTabList.closeTabByTag(currentSelectionTag);
191    return;
192  }
193  switch (newSelection.kind) {
194    case 'NOTE':
195      bottomTabList.addTab({
196        kind: NotesEditorTab.kind,
197        tag: currentSelectionTag,
198        config: {
199          id: newSelection.id,
200        },
201      });
202      break;
203    case 'AREA':
204      if (newSelection.noteId !== undefined) {
205        bottomTabList.addTab({
206          kind: NotesEditorTab.kind,
207          tag: currentSelectionTag,
208          config: {
209            id: newSelection.noteId,
210          },
211        });
212      }
213      break;
214    case 'THREAD_STATE':
215      bottomTabList.addTab({
216        kind: ThreadStateTab.kind,
217        tag: currentSelectionTag,
218        config: {
219          id: newSelection.id,
220        },
221      });
222      break;
223    case 'DEBUG_SLICE':
224      bottomTabList.addTab({
225        kind: DebugSliceDetailsTab.kind,
226        tag: currentSelectionTag,
227        config: {
228          sqlTableName: newSelection.sqlTableName,
229          id: newSelection.id,
230        },
231      });
232      break;
233    case TOP_LEVEL_SCROLL_KIND:
234      pluginManager.onDetailsPanelSelectionChange(
235          SCROLL_JANK_PLUGIN_ID, newSelection);
236      break;
237    default:
238      bottomTabList.closeTabByTag(currentSelectionTag);
239  }
240}
241addSelectionChangeObserver(handleSelectionChange);
242
243export class DetailsPanel implements m.ClassComponent {
244  private detailsHeight = getDetailsHeight();
245
246  view() {
247    interface DetailsPanel {
248      key: string;
249      name: string;
250      vnode: AnyAttrsVnode;
251    }
252
253    const detailsPanels: DetailsPanel[] = [];
254
255    if (globals.bottomTabList) {
256      for (const tab of globals.bottomTabList.getTabs()) {
257        detailsPanels.push({
258          key: tab.tag ?? tab.uuid,
259          name: tab.getTitle(),
260          vnode: tab.createPanelVnode(),
261        });
262      }
263    }
264
265    const curSelection = globals.state.currentSelection;
266    if (curSelection) {
267      switch (curSelection.kind) {
268        case 'NOTE':
269          // Handled in handleSelectionChange.
270          break;
271        case 'AREA':
272          if (globals.flamegraphDetails.isInAreaSelection) {
273            detailsPanels.push({
274              key: 'flamegraph_selection',
275              name: 'Flamegraph Selection',
276              vnode: m(FlamegraphDetailsPanel, {key: 'flamegraph'}),
277            });
278          }
279          break;
280        case 'SLICE':
281          detailsPanels.push({
282            key: 'current_selection',
283            name: 'Current Selection',
284            vnode: m(SliceDetailsPanel, {
285              key: 'slice',
286            }),
287          });
288          break;
289        case 'COUNTER':
290          detailsPanels.push({
291            key: 'current_selection',
292            name: 'Current Selection',
293            vnode: m(CounterDetailsPanel, {
294              key: 'counter',
295            }),
296          });
297          break;
298        case 'PERF_SAMPLES':
299        case 'HEAP_PROFILE':
300          detailsPanels.push({
301            key: 'current_selection',
302            name: 'Current Selection',
303            vnode: m(FlamegraphDetailsPanel, {key: 'flamegraph'}),
304          });
305          break;
306        case 'CPU_PROFILE_SAMPLE':
307          detailsPanels.push({
308            key: 'current_selection',
309            name: 'Current Selection',
310            vnode: m(CpuProfileDetailsPanel, {
311              key: 'cpu_profile_sample',
312            }),
313          });
314          break;
315        case 'CHROME_SLICE':
316          detailsPanels.push({
317            key: 'current_selection',
318            name: 'Current Selection',
319            vnode: m(ChromeSliceDetailsPanel, {key: 'chrome_slice'}),
320          });
321          break;
322        default:
323          break;
324      }
325    }
326    if (hasLogs()) {
327      detailsPanels.push({
328        key: 'android_logs',
329        name: 'Android Logs',
330        vnode: m(LogPanel, {key: 'logs_panel'}),
331      });
332    }
333
334    const trackGroup = globals.state.trackGroups['ftrace-track-group'];
335    if (trackGroup) {
336      const {collapsed} = trackGroup;
337      if (!collapsed) {
338        detailsPanels.push({
339          key: 'ftrace_events',
340          name: 'Ftrace Events',
341          vnode: m(FtracePanel, {key: 'ftrace_panel'}),
342        });
343      }
344    }
345
346    if (globals.state.nonSerializableState.pivotTable.selectionArea !==
347        undefined) {
348      detailsPanels.push({
349        key: 'pivot_table',
350        name: 'Pivot Table',
351        vnode: m(PivotTable, {
352          key: 'pivot_table',
353          selectionArea:
354              globals.state.nonSerializableState.pivotTable.selectionArea,
355        }),
356      });
357    }
358
359    if (globals.connectedFlows.length > 0) {
360      detailsPanels.push({
361        key: 'bound_flows',
362        name: 'Flow Events',
363        vnode: m(FlowEventsPanel, {key: 'flow_events'}),
364      });
365    }
366
367    for (const [key, value] of globals.aggregateDataStore.entries()) {
368      if (!isEmptyData(value)) {
369        detailsPanels.push({
370          key: value.tabName,
371          name: value.tabName,
372          vnode: m(AggregationPanel, {kind: key, key, data: value}),
373        });
374      }
375    }
376
377    // Add this after all aggregation panels, to make it appear after 'Slices'
378    if (globals.selectedFlows.length > 0) {
379      detailsPanels.push({
380        key: 'selected_flows',
381        name: 'Flow Events',
382        vnode: m(FlowEventsAreaSelectedPanel, {key: 'flow_events_area'}),
383      });
384    }
385
386    let currentTabDetails =
387        detailsPanels.find((tab) => tab.key === globals.state.currentTab);
388    if (currentTabDetails === undefined && detailsPanels.length > 0) {
389      currentTabDetails = detailsPanels[0];
390    }
391
392    const panel = currentTabDetails?.vnode;
393    const panels = panel ? [panel] : [];
394
395    return m(
396        '.details-content',
397        {
398          style: {
399            height: `${this.detailsHeight}px`,
400            display: detailsPanels.length > 0 ? null : 'none',
401          },
402        },
403        m(DragHandle, {
404          resize: (height: number) => {
405            this.detailsHeight = Math.max(height, DRAG_HANDLE_HEIGHT_PX);
406          },
407          height: this.detailsHeight,
408          tabs: detailsPanels.map((tab) => {
409            return {key: tab.key, name: tab.name};
410          }),
411          currentTabKey: currentTabDetails?.key,
412        }),
413        m('.details-panel-container.x-scrollable',
414          m(PanelContainer, {doesScroll: true, panels, kind: 'DETAILS'})));
415  }
416}
417