• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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