| // 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 m from 'mithril'; |
| |
| import {Actions} from '../common/actions'; |
| import {isEmptyData} from '../common/aggregation_data'; |
| import {LogExists, LogExistsKey} from '../common/logs'; |
| import {addSelectionChangeObserver} from '../common/selection_observer'; |
| import {Selection} from '../common/state'; |
| import {DebugSliceDetailsTab} from '../tracks/debug/details_tab'; |
| |
| import {AggregationPanel} from './aggregation_panel'; |
| import {ChromeSliceDetailsPanel} from './chrome_slice_panel'; |
| import {CounterDetailsPanel} from './counter_panel'; |
| import {CpuProfileDetailsPanel} from './cpu_profile_panel'; |
| import {DEFAULT_DETAILS_CONTENT_HEIGHT} from './css_constants'; |
| import {DragGestureHandler} from './drag_gesture_handler'; |
| import {FlamegraphDetailsPanel} from './flamegraph_panel'; |
| import { |
| FlowEventsAreaSelectedPanel, |
| FlowEventsPanel, |
| } from './flow_events_panel'; |
| import {FtracePanel} from './ftrace_panel'; |
| import {globals} from './globals'; |
| import {LogPanel} from './logs_panel'; |
| import {NotesEditorTab} from './notes_panel'; |
| import {AnyAttrsVnode, PanelContainer} from './panel_container'; |
| import {PivotTable} from './pivot_table'; |
| import {SliceDetailsPanel} from './slice_details_panel'; |
| import {ThreadStateTab} from './thread_state_tab'; |
| |
| const UP_ICON = 'keyboard_arrow_up'; |
| const DOWN_ICON = 'keyboard_arrow_down'; |
| const DRAG_HANDLE_HEIGHT_PX = 28; |
| |
| function getDetailsHeight() { |
| // This needs to be a function instead of a const to ensure the CSS constants |
| // have been initialized by the time we perform this calculation; |
| return DEFAULT_DETAILS_CONTENT_HEIGHT + DRAG_HANDLE_HEIGHT_PX; |
| } |
| |
| function getFullScreenHeight() { |
| const panelContainer = |
| document.querySelector('.pan-and-zoom-content') as HTMLElement; |
| if (panelContainer !== null) { |
| return panelContainer.clientHeight; |
| } else { |
| return getDetailsHeight(); |
| } |
| } |
| |
| function hasLogs(): boolean { |
| const data = globals.trackDataStore.get(LogExistsKey) as LogExists; |
| return data && data.exists; |
| } |
| |
| interface Tab { |
| key: string; |
| name: string; |
| } |
| |
| interface DragHandleAttrs { |
| height: number; |
| resize: (height: number) => void; |
| tabs: Tab[]; |
| currentTabKey?: string; |
| } |
| |
| class DragHandle implements m.ClassComponent<DragHandleAttrs> { |
| private dragStartHeight = 0; |
| private height = 0; |
| 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 = getDetailsHeight(); |
| |
| oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) { |
| 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, |
| this.onDrag.bind(this), |
| this.onDragStart.bind(this), |
| this.onDragEnd.bind(this)); |
| } |
| |
| onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) { |
| this.resize = attrs.resize; |
| this.height = attrs.height; |
| this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX; |
| } |
| |
| onDrag(_x: number, y: number) { |
| 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(); |
| } |
| |
| onDragStart(_x: number, _y: number) { |
| this.dragStartHeight = this.height; |
| } |
| |
| onDragEnd() {} |
| |
| view({attrs}: m.CVnode<DragHandleAttrs>) { |
| const icon = this.isClosed ? UP_ICON : DOWN_ICON; |
| const title = this.isClosed ? 'Show panel' : 'Hide panel'; |
| const renderTab = (tab: Tab) => { |
| if (attrs.currentTabKey === tab.key) { |
| return m('.tab[active]', tab.name); |
| } |
| return m( |
| '.tab', |
| { |
| onclick: () => { |
| globals.dispatch(Actions.setCurrentTab({tab: tab.key})); |
| }, |
| }, |
| tab.name); |
| }; |
| return m( |
| '.handle', |
| m('.tabs', attrs.tabs.map(renderTab)), |
| m('.buttons', |
| m('i.material-icons', |
| { |
| onclick: () => { |
| this.isClosed = false; |
| this.isFullscreen = true; |
| this.resize(this.fullscreenHeight); |
| globals.rafScheduler.scheduleFullRedraw(); |
| }, |
| title: 'Open fullscreen', |
| disabled: this.isFullscreen, |
| }, |
| 'vertical_align_top'), |
| m('i.material-icons', |
| { |
| onclick: () => { |
| if (this.height === DRAG_HANDLE_HEIGHT_PX) { |
| this.isClosed = false; |
| if (this.previousHeight === 0) { |
| this.previousHeight = getDetailsHeight(); |
| } |
| 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))); |
| } |
| } |
| |
| function handleSelectionChange(newSelection?: Selection, _?: Selection): void { |
| const currentSelectionTag = 'current_selection'; |
| const bottomTabList = globals.bottomTabList; |
| if (!bottomTabList) return; |
| if (newSelection === undefined) { |
| bottomTabList.closeTabByTag(currentSelectionTag); |
| return; |
| } |
| switch (newSelection.kind) { |
| case 'NOTE': |
| bottomTabList.addTab({ |
| kind: NotesEditorTab.kind, |
| tag: currentSelectionTag, |
| config: { |
| id: newSelection.id, |
| }, |
| }); |
| break; |
| case 'AREA': |
| if (newSelection.noteId !== undefined) { |
| bottomTabList.addTab({ |
| kind: NotesEditorTab.kind, |
| tag: currentSelectionTag, |
| config: { |
| id: newSelection.noteId, |
| }, |
| }); |
| } |
| break; |
| case 'THREAD_STATE': |
| bottomTabList.addTab({ |
| kind: ThreadStateTab.kind, |
| tag: currentSelectionTag, |
| config: { |
| id: newSelection.id, |
| }, |
| }); |
| break; |
| case 'DEBUG_SLICE': |
| bottomTabList.addTab({ |
| kind: DebugSliceDetailsTab.kind, |
| tag: currentSelectionTag, |
| config: { |
| sqlTableName: newSelection.sqlTableName, |
| id: newSelection.id, |
| }, |
| }); |
| break; |
| default: |
| bottomTabList.closeTabByTag(currentSelectionTag); |
| } |
| } |
| addSelectionChangeObserver(handleSelectionChange); |
| |
| export class DetailsPanel implements m.ClassComponent { |
| private detailsHeight = getDetailsHeight(); |
| |
| view() { |
| interface DetailsPanel { |
| key: string; |
| name: string; |
| vnode: AnyAttrsVnode; |
| } |
| |
| const detailsPanels: DetailsPanel[] = []; |
| |
| if (globals.bottomTabList) { |
| for (const tab of globals.bottomTabList.getTabs()) { |
| detailsPanels.push({ |
| key: tab.tag ?? tab.uuid, |
| name: tab.getTitle(), |
| vnode: tab.createPanelVnode(), |
| }); |
| } |
| } |
| |
| const curSelection = globals.state.currentSelection; |
| if (curSelection) { |
| switch (curSelection.kind) { |
| case 'NOTE': |
| // Handled in handleSelectionChange. |
| break; |
| case 'AREA': |
| if (globals.flamegraphDetails.isInAreaSelection) { |
| detailsPanels.push({ |
| key: 'flamegraph_selection', |
| name: 'Flamegraph Selection', |
| vnode: m(FlamegraphDetailsPanel, {key: 'flamegraph'}), |
| }); |
| } |
| break; |
| case 'SLICE': |
| detailsPanels.push({ |
| key: 'current_selection', |
| name: 'Current Selection', |
| vnode: m(SliceDetailsPanel, { |
| key: 'slice', |
| }), |
| }); |
| break; |
| case 'COUNTER': |
| detailsPanels.push({ |
| key: 'current_selection', |
| name: 'Current Selection', |
| vnode: m(CounterDetailsPanel, { |
| key: 'counter', |
| }), |
| }); |
| break; |
| case 'PERF_SAMPLES': |
| case 'HEAP_PROFILE': |
| detailsPanels.push({ |
| key: 'current_selection', |
| name: 'Current Selection', |
| vnode: m(FlamegraphDetailsPanel, {key: 'flamegraph'}), |
| }); |
| break; |
| case 'CPU_PROFILE_SAMPLE': |
| detailsPanels.push({ |
| key: 'current_selection', |
| name: 'Current Selection', |
| vnode: m(CpuProfileDetailsPanel, { |
| key: 'cpu_profile_sample', |
| }), |
| }); |
| break; |
| case 'CHROME_SLICE': |
| detailsPanels.push({ |
| key: 'current_selection', |
| name: 'Current Selection', |
| vnode: m(ChromeSliceDetailsPanel, {key: 'chrome_slice'}), |
| }); |
| break; |
| default: |
| break; |
| } |
| } |
| if (hasLogs()) { |
| detailsPanels.push({ |
| key: 'android_logs', |
| name: 'Android Logs', |
| vnode: m(LogPanel, {key: 'logs_panel'}), |
| }); |
| } |
| |
| const trackGroup = globals.state.trackGroups['ftrace-track-group']; |
| if (trackGroup) { |
| const {collapsed} = trackGroup; |
| if (!collapsed) { |
| detailsPanels.push({ |
| key: 'ftrace_events', |
| name: 'Ftrace Events', |
| vnode: m(FtracePanel, {key: 'ftrace_panel'}), |
| }); |
| } |
| } |
| |
| if (globals.state.nonSerializableState.pivotTable.selectionArea !== |
| undefined) { |
| detailsPanels.push({ |
| key: 'pivot_table', |
| name: 'Pivot Table', |
| vnode: m(PivotTable, { |
| key: 'pivot_table', |
| selectionArea: |
| globals.state.nonSerializableState.pivotTable.selectionArea, |
| }), |
| }); |
| } |
| |
| if (globals.connectedFlows.length > 0) { |
| detailsPanels.push({ |
| key: 'bound_flows', |
| name: 'Flow Events', |
| vnode: m(FlowEventsPanel, {key: 'flow_events'}), |
| }); |
| } |
| |
| for (const [key, value] of globals.aggregateDataStore.entries()) { |
| if (!isEmptyData(value)) { |
| detailsPanels.push({ |
| key: value.tabName, |
| name: value.tabName, |
| vnode: m(AggregationPanel, {kind: key, key, data: value}), |
| }); |
| } |
| } |
| |
| // Add this after all aggregation panels, to make it appear after 'Slices' |
| if (globals.selectedFlows.length > 0) { |
| detailsPanels.push({ |
| key: 'selected_flows', |
| name: 'Flow Events', |
| vnode: m(FlowEventsAreaSelectedPanel, {key: 'flow_events_area'}), |
| }); |
| } |
| |
| let currentTabDetails = |
| detailsPanels.find((tab) => tab.key === globals.state.currentTab); |
| if (currentTabDetails === undefined && detailsPanels.length > 0) { |
| currentTabDetails = detailsPanels[0]; |
| } |
| |
| const panel = currentTabDetails?.vnode; |
| const panels = panel ? [panel] : []; |
| |
| return m( |
| '.details-content', |
| { |
| style: { |
| height: `${this.detailsHeight}px`, |
| display: detailsPanels.length > 0 ? null : 'none', |
| }, |
| }, |
| m(DragHandle, { |
| resize: (height: number) => { |
| this.detailsHeight = Math.max(height, DRAG_HANDLE_HEIGHT_PX); |
| }, |
| height: this.detailsHeight, |
| tabs: detailsPanels.map((tab) => { |
| return {key: tab.key, name: tab.name}; |
| }), |
| currentTabKey: currentTabDetails?.key, |
| }), |
| m('.details-panel-container.x-scrollable', |
| m(PanelContainer, {doesScroll: true, panels, kind: 'DETAILS'}))); |
| } |
| } |