blob: 3edce5dd7dadd2e39d347f7dd429b5e9b3b331cb [file] [log] [blame]
// 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 {Gate} from '../base/mithril_utils';
import {exists} from '../base/utils';
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 {AggregationPanel} from './aggregation_panel';
import {ChromeSliceDetailsTab} from './chrome_slice_details_tab';
import {CounterDetailsPanel} from './counter_panel';
import {CpuProfileDetailsPanel} from './cpu_profile_panel';
import {DEFAULT_DETAILS_CONTENT_HEIGHT} from './css_constants';
import {DragHandle} from './drag_handle';
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 {PivotTable} from './pivot_table';
import {SliceDetailsPanel} from './slice_details_panel';
import {ThreadStateTab} from './thread_state_tab';
const DRAG_HANDLE_HEIGHT_PX = 28;
export const CURRENT_SELECTION_TAG = 'current_selection';
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 hasLogs(): boolean {
const data =
globals.trackDataStore.get(LogExistsKey) as LogExists | undefined;
return Boolean(data?.exists);
}
function handleSelectionChange(
newSelection: Selection|undefined, openCurrentSelectionTab: boolean): void {
const currentSelectionTag = CURRENT_SELECTION_TAG;
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,
},
select: openCurrentSelectionTab,
});
break;
case 'AREA':
if (newSelection.noteId !== undefined) {
bottomTabList.addTab({
kind: NotesEditorTab.kind,
tag: currentSelectionTag,
config: {
id: newSelection.noteId,
},
select: openCurrentSelectionTab,
});
}
break;
case 'THREAD_STATE':
bottomTabList.addTab({
kind: ThreadStateTab.kind,
tag: currentSelectionTag,
config: {
id: newSelection.id,
},
select: openCurrentSelectionTab,
});
break;
case 'GENERIC_SLICE':
bottomTabList.addTab({
kind: newSelection.detailsPanelConfig.kind,
tag: currentSelectionTag,
config: newSelection.detailsPanelConfig.config,
select: openCurrentSelectionTab,
});
break;
case 'CHROME_SLICE':
bottomTabList.addTab({
kind: ChromeSliceDetailsTab.kind,
tag: currentSelectionTag,
config: {
id: newSelection.id,
table: newSelection.table,
},
select: openCurrentSelectionTab,
});
break;
default:
bottomTabList.closeTabByTag(currentSelectionTag);
}
}
addSelectionChangeObserver(handleSelectionChange);
export class DetailsPanel implements m.ClassComponent {
private detailsHeight = getDetailsHeight();
view() {
interface DetailsPanel {
key: string;
name: string;
vnode: m.Children;
}
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.renderPanel(),
});
}
}
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;
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'];
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
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;
if (!exists(panel)) {
return null;
}
return [
m(DragHandle, {
resize: (height: number) => {
this.detailsHeight = Math.max(height, 0);
},
height: this.detailsHeight,
tabs: detailsPanels.map((tab) => {
return {key: tab.key, title: tab.name};
}),
currentTabKey: currentTabDetails?.key,
onTabClick: (key) => {
globals.dispatch(Actions.setCurrentTab({tab: key}));
},
}),
m(
'.details-panel-container',
{
style: {height: `${this.detailsHeight}px`},
},
detailsPanels.map((tab) => {
const active = tab === currentTabDetails;
return m(Gate, {open: active}, tab.vnode);
}),
),
];
}
}