1// Copyright (C) 2024 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use size 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'; 16import {Disposable, DisposableStack} from '../base/disposable'; 17import {AggregationPanel} from './aggregation_panel'; 18import {globals} from './globals'; 19import {isEmptyData} from '../common/aggregation_data'; 20import {DetailsShell} from '../widgets/details_shell'; 21import {Button, ButtonBar} from '../widgets/button'; 22import {raf} from '../core/raf_scheduler'; 23import {EmptyState} from '../widgets/empty_state'; 24import {FlowEventsAreaSelectedPanel} from './flow_events_panel'; 25import {PivotTable} from './pivot_table'; 26import { 27 FlamegraphDetailsPanel, 28 FlamegraphSelectionParams, 29} from './flamegraph_panel'; 30import {ProfileType, TrackState} from '../common/state'; 31import {assertExists} from '../base/logging'; 32import {Monitor} from '../base/monitor'; 33import {PERF_SAMPLES_PROFILE_TRACK_KIND} from '../core/track_kinds'; 34 35interface View { 36 key: string; 37 name: string; 38 content: m.Children; 39} 40 41class AreaDetailsPanel implements m.ClassComponent { 42 private readonly monitor = new Monitor([() => globals.state.selection]); 43 private currentTab: string | undefined = undefined; 44 private flamegraphSelection?: FlamegraphSelectionParams; 45 46 private getCurrentView(): string | undefined { 47 const types = this.getViews().map(({key}) => key); 48 49 if (types.length === 0) { 50 return undefined; 51 } 52 53 if (this.currentTab === undefined) { 54 return types[0]; 55 } 56 57 if (!types.includes(this.currentTab)) { 58 return types[0]; 59 } 60 61 return this.currentTab; 62 } 63 64 private getViews(): View[] { 65 const views = []; 66 67 this.flamegraphSelection = this.computeFlamegraphSelection(); 68 if (this.flamegraphSelection !== undefined) { 69 views.push({ 70 key: 'flamegraph_selection', 71 name: 'Flamegraph Selection', 72 content: m(FlamegraphDetailsPanel, { 73 cache: globals.areaFlamegraphCache, 74 selection: this.flamegraphSelection, 75 }), 76 }); 77 } 78 79 for (const [key, value] of globals.aggregateDataStore.entries()) { 80 if (!isEmptyData(value)) { 81 views.push({ 82 key: value.tabName, 83 name: value.tabName, 84 content: m(AggregationPanel, {kind: key, key, data: value}), 85 }); 86 } 87 } 88 89 const pivotTableState = globals.state.nonSerializableState.pivotTable; 90 if (pivotTableState.selectionArea !== undefined) { 91 views.push({ 92 key: 'pivot_table', 93 name: 'Pivot Table', 94 content: m(PivotTable, { 95 selectionArea: pivotTableState.selectionArea, 96 }), 97 }); 98 } 99 100 // Add this after all aggregation panels, to make it appear after 'Slices' 101 if (globals.selectedFlows.length > 0) { 102 views.push({ 103 key: 'selected_flows', 104 name: 'Flow Events', 105 content: m(FlowEventsAreaSelectedPanel), 106 }); 107 } 108 109 return views; 110 } 111 112 view(_: m.Vnode): m.Children { 113 const views = this.getViews(); 114 const currentViewKey = this.getCurrentView(); 115 116 const aggregationButtons = views.map(({key, name}) => { 117 return m(Button, { 118 onclick: () => { 119 this.currentTab = key; 120 raf.scheduleFullRedraw(); 121 }, 122 key, 123 label: name, 124 active: currentViewKey === key, 125 }); 126 }); 127 128 if (currentViewKey === undefined) { 129 return this.renderEmptyState(); 130 } 131 132 const content = views.find(({key}) => key === currentViewKey)?.content; 133 if (content === undefined) { 134 return this.renderEmptyState(); 135 } 136 137 return m( 138 DetailsShell, 139 { 140 title: 'Area Selection', 141 description: m(ButtonBar, aggregationButtons), 142 }, 143 content, 144 ); 145 } 146 147 private renderEmptyState(): m.Children { 148 return m( 149 EmptyState, 150 { 151 className: 'pf-noselection', 152 title: 'Unsupported area selection', 153 }, 154 'No details available for this area selection', 155 ); 156 } 157 158 private computeFlamegraphSelection() { 159 const currentSelection = globals.state.selection; 160 if (currentSelection.kind !== 'area') { 161 return undefined; 162 } 163 if (!this.monitor.ifStateChanged()) { 164 // If the selection has not changed, just return a copy of the last seen 165 // selection. 166 return this.flamegraphSelection; 167 } 168 const upids = []; 169 for (const trackId of currentSelection.tracks) { 170 const track: TrackState | undefined = globals.state.tracks[trackId]; 171 const trackInfo = globals.trackManager.resolveTrackInfo(track?.uri); 172 if (trackInfo?.kind !== PERF_SAMPLES_PROFILE_TRACK_KIND) { 173 continue; 174 } 175 upids.push(assertExists(trackInfo.upid)); 176 } 177 if (upids.length === 0) { 178 return undefined; 179 } 180 return { 181 profileType: ProfileType.PERF_SAMPLE, 182 start: currentSelection.start, 183 end: currentSelection.end, 184 upids, 185 }; 186 } 187} 188 189export class AggregationsTabs implements Disposable { 190 private trash = new DisposableStack(); 191 192 constructor() { 193 const unregister = globals.tabManager.registerDetailsPanel({ 194 render(selection) { 195 if (selection.kind === 'area') { 196 return m(AreaDetailsPanel); 197 } else { 198 return undefined; 199 } 200 }, 201 }); 202 203 this.trash.use(unregister); 204 } 205 206 dispose(): void { 207 this.trash.dispose(); 208 } 209} 210