xref: /aosp_15_r20/external/perfetto/ui/src/frontend/aggregation_tab.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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 {AggregationPanel} from './aggregation_panel';
17import {isEmptyData} from '../public/aggregation';
18import {DetailsShell} from '../widgets/details_shell';
19import {Button, ButtonBar} from '../widgets/button';
20import {raf} from '../core/raf_scheduler';
21import {EmptyState} from '../widgets/empty_state';
22import {FlowEventsAreaSelectedPanel} from './flow_events_panel';
23import {PivotTable} from './pivot_table';
24import {AreaSelection} from '../public/selection';
25import {Monitor} from '../base/monitor';
26import {
27  CPU_PROFILE_TRACK_KIND,
28  PERF_SAMPLES_PROFILE_TRACK_KIND,
29  SLICE_TRACK_KIND,
30} from '../public/track_kinds';
31import {
32  QueryFlamegraph,
33  metricsFromTableOrSubquery,
34} from '../components/query_flamegraph';
35import {DisposableStack} from '../base/disposable_stack';
36import {assertExists} from '../base/logging';
37import {TraceImpl} from '../core/trace_impl';
38import {Trace} from '../public/trace';
39import {Flamegraph} from '../widgets/flamegraph';
40
41interface View {
42  key: string;
43  name: string;
44  content: m.Children;
45}
46
47export type AreaDetailsPanelAttrs = {trace: TraceImpl};
48
49class AreaDetailsPanel implements m.ClassComponent<AreaDetailsPanelAttrs> {
50  private trace: TraceImpl;
51  private monitor: Monitor;
52  private currentTab: string | undefined = undefined;
53  private cpuProfileFlamegraph?: QueryFlamegraph;
54  private perfSampleFlamegraph?: QueryFlamegraph;
55  private sliceFlamegraph?: QueryFlamegraph;
56
57  constructor({attrs}: m.CVnode<AreaDetailsPanelAttrs>) {
58    this.trace = attrs.trace;
59    this.monitor = new Monitor([() => this.trace.selection.selection]);
60  }
61
62  private getCurrentView(): string | undefined {
63    const types = this.getViews().map(({key}) => key);
64
65    if (types.length === 0) {
66      return undefined;
67    }
68
69    if (this.currentTab === undefined) {
70      return types[0];
71    }
72
73    if (!types.includes(this.currentTab)) {
74      return types[0];
75    }
76
77    return this.currentTab;
78  }
79
80  private getViews(): View[] {
81    const views: View[] = [];
82
83    for (const aggregator of this.trace.selection.aggregation.aggregators) {
84      const aggregatorId = aggregator.id;
85      const value =
86        this.trace.selection.aggregation.getAggregatedData(aggregatorId);
87      if (value !== undefined && !isEmptyData(value)) {
88        views.push({
89          key: value.tabName,
90          name: value.tabName,
91          content: m(AggregationPanel, {
92            aggregatorId,
93            data: value,
94            trace: this.trace,
95          }),
96        });
97      }
98    }
99
100    const pivotTableState = this.trace.pivotTable.state;
101    const tree = pivotTableState.queryResult?.tree;
102    if (
103      pivotTableState.selectionArea != undefined &&
104      (tree === undefined || tree.children.size > 0 || tree?.rows.length > 0)
105    ) {
106      views.push({
107        key: 'pivot_table',
108        name: 'Pivot Table',
109        content: m(PivotTable, {
110          trace: this.trace,
111          selectionArea: pivotTableState.selectionArea,
112        }),
113      });
114    }
115
116    this.addFlamegraphView(this.trace, this.monitor.ifStateChanged(), views);
117
118    // Add this after all aggregation panels, to make it appear after 'Slices'
119    if (this.trace.flows.selectedFlows.length > 0) {
120      views.push({
121        key: 'selected_flows',
122        name: 'Flow Events',
123        content: m(FlowEventsAreaSelectedPanel, {trace: this.trace}),
124      });
125    }
126
127    return views;
128  }
129
130  view(): m.Children {
131    const views = this.getViews();
132    const currentViewKey = this.getCurrentView();
133
134    const aggregationButtons = views.map(({key, name}) => {
135      return m(Button, {
136        onclick: () => {
137          this.currentTab = key;
138          raf.scheduleFullRedraw();
139        },
140        key,
141        label: name,
142        active: currentViewKey === key,
143      });
144    });
145
146    if (currentViewKey === undefined) {
147      return this.renderEmptyState();
148    }
149
150    const content = views.find(({key}) => key === currentViewKey)?.content;
151    if (content === undefined) {
152      return this.renderEmptyState();
153    }
154
155    return m(
156      DetailsShell,
157      {
158        title: 'Area Selection',
159        description: m(ButtonBar, aggregationButtons),
160      },
161      content,
162    );
163  }
164
165  private renderEmptyState(): m.Children {
166    return m(
167      EmptyState,
168      {
169        className: 'pf-noselection',
170        title: 'Unsupported area selection',
171      },
172      'No details available for this area selection',
173    );
174  }
175
176  private addFlamegraphView(trace: Trace, isChanged: boolean, views: View[]) {
177    this.cpuProfileFlamegraph = this.computeCpuProfileFlamegraph(
178      trace,
179      isChanged,
180    );
181    if (this.cpuProfileFlamegraph !== undefined) {
182      views.push({
183        key: 'cpu_profile_flamegraph_selection',
184        name: 'CPU Profile Sample Flamegraph',
185        content: this.cpuProfileFlamegraph.render(),
186      });
187    }
188    this.perfSampleFlamegraph = this.computePerfSampleFlamegraph(
189      trace,
190      isChanged,
191    );
192    if (this.perfSampleFlamegraph !== undefined) {
193      views.push({
194        key: 'perf_sample_flamegraph_selection',
195        name: 'Perf Sample Flamegraph',
196        content: this.perfSampleFlamegraph.render(),
197      });
198    }
199    this.sliceFlamegraph = this.computeSliceFlamegraph(trace, isChanged);
200    if (this.sliceFlamegraph !== undefined) {
201      views.push({
202        key: 'slice_flamegraph_selection',
203        name: 'Slice Flamegraph',
204        content: this.sliceFlamegraph.render(),
205      });
206    }
207  }
208
209  private computeCpuProfileFlamegraph(trace: Trace, isChanged: boolean) {
210    const currentSelection = trace.selection.selection;
211    if (currentSelection.kind !== 'area') {
212      return undefined;
213    }
214    if (!isChanged) {
215      // If the selection has not changed, just return a copy of the last seen
216      // attrs.
217      return this.cpuProfileFlamegraph;
218    }
219    const utids = [];
220    for (const trackInfo of currentSelection.tracks) {
221      if (trackInfo?.tags?.kind === CPU_PROFILE_TRACK_KIND) {
222        utids.push(trackInfo.tags?.utid);
223      }
224    }
225    if (utids.length === 0) {
226      return undefined;
227    }
228    const metrics = metricsFromTableOrSubquery(
229      `
230        (
231          select
232            id,
233            parent_id as parentId,
234            name,
235            mapping_name,
236            source_file,
237            cast(line_number AS text) as line_number,
238            self_count
239          from _callstacks_for_callsites!((
240            select p.callsite_id
241            from cpu_profile_stack_sample p
242            where p.ts >= ${currentSelection.start}
243              and p.ts <= ${currentSelection.end}
244              and p.utid in (${utids.join(',')})
245          ))
246        )
247      `,
248      [
249        {
250          name: 'CPU Profile Samples',
251          unit: '',
252          columnName: 'self_count',
253        },
254      ],
255      'include perfetto module callstacks.stack_profile',
256      [{name: 'mapping_name', displayName: 'Mapping'}],
257      [
258        {
259          name: 'source_file',
260          displayName: 'Source File',
261          mergeAggregation: 'ONE_OR_NULL',
262        },
263        {
264          name: 'line_number',
265          displayName: 'Line Number',
266          mergeAggregation: 'ONE_OR_NULL',
267        },
268      ],
269    );
270    return new QueryFlamegraph(trace, metrics, {
271      state: Flamegraph.createDefaultState(metrics),
272    });
273  }
274
275  private computePerfSampleFlamegraph(trace: Trace, isChanged: boolean) {
276    const currentSelection = trace.selection.selection;
277    if (currentSelection.kind !== 'area') {
278      return undefined;
279    }
280    if (!isChanged) {
281      // If the selection has not changed, just return a copy of the last seen
282      // attrs.
283      return this.perfSampleFlamegraph;
284    }
285    const upids = getUpidsFromPerfSampleAreaSelection(currentSelection);
286    const utids = getUtidsFromPerfSampleAreaSelection(currentSelection);
287    if (utids.length === 0 && upids.length === 0) {
288      return undefined;
289    }
290    const metrics = metricsFromTableOrSubquery(
291      `
292        (
293          select id, parent_id as parentId, name, self_count
294          from _callstacks_for_callsites!((
295            select p.callsite_id
296            from perf_sample p
297            join thread t using (utid)
298            where p.ts >= ${currentSelection.start}
299              and p.ts <= ${currentSelection.end}
300              and (
301                p.utid in (${utids.join(',')})
302                or t.upid in (${upids.join(',')})
303              )
304          ))
305        )
306      `,
307      [
308        {
309          name: 'Perf Samples',
310          unit: '',
311          columnName: 'self_count',
312        },
313      ],
314      'include perfetto module linux.perf.samples',
315    );
316    return new QueryFlamegraph(trace, metrics, {
317      state: Flamegraph.createDefaultState(metrics),
318    });
319  }
320
321  private computeSliceFlamegraph(trace: Trace, isChanged: boolean) {
322    const currentSelection = trace.selection.selection;
323    if (currentSelection.kind !== 'area') {
324      return undefined;
325    }
326    if (!isChanged) {
327      // If the selection has not changed, just return a copy of the last seen
328      // attrs.
329      return this.sliceFlamegraph;
330    }
331    const trackIds = [];
332    for (const trackInfo of currentSelection.tracks) {
333      if (trackInfo?.tags?.kind !== SLICE_TRACK_KIND) {
334        continue;
335      }
336      if (trackInfo.tags?.trackIds === undefined) {
337        continue;
338      }
339      trackIds.push(...trackInfo.tags.trackIds);
340    }
341    if (trackIds.length === 0) {
342      return undefined;
343    }
344    const metrics = metricsFromTableOrSubquery(
345      `
346        (
347          select *
348          from _viz_slice_ancestor_agg!((
349            select s.id, s.dur
350            from slice s
351            left join slice t on t.parent_id = s.id
352            where s.ts >= ${currentSelection.start}
353              and s.ts <= ${currentSelection.end}
354              and s.track_id in (${trackIds.join(',')})
355              and t.id is null
356          ))
357        )
358      `,
359      [
360        {
361          name: 'Duration',
362          unit: 'ns',
363          columnName: 'self_dur',
364        },
365        {
366          name: 'Samples',
367          unit: '',
368          columnName: 'self_count',
369        },
370      ],
371      'include perfetto module viz.slices;',
372    );
373    return new QueryFlamegraph(trace, metrics, {
374      state: Flamegraph.createDefaultState(metrics),
375    });
376  }
377}
378
379export class AggregationsTabs implements Disposable {
380  private trash = new DisposableStack();
381
382  constructor(trace: TraceImpl) {
383    const unregister = trace.tabs.registerDetailsPanel({
384      render(selection) {
385        if (selection.kind === 'area') {
386          return m(AreaDetailsPanel, {trace});
387        } else {
388          return undefined;
389        }
390      },
391    });
392
393    this.trash.use(unregister);
394  }
395
396  [Symbol.dispose]() {
397    this.trash.dispose();
398  }
399}
400
401function getUpidsFromPerfSampleAreaSelection(currentSelection: AreaSelection) {
402  const upids = [];
403  for (const trackInfo of currentSelection.tracks) {
404    if (
405      trackInfo?.tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND &&
406      trackInfo.tags?.utid === undefined
407    ) {
408      upids.push(assertExists(trackInfo.tags?.upid));
409    }
410  }
411  return upids;
412}
413
414function getUtidsFromPerfSampleAreaSelection(currentSelection: AreaSelection) {
415  const utids = [];
416  for (const trackInfo of currentSelection.tracks) {
417    if (
418      trackInfo?.tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND &&
419      trackInfo.tags?.utid !== undefined
420    ) {
421      utids.push(trackInfo.tags?.utid);
422    }
423  }
424  return utids;
425}
426