// Copyright (C) 2024 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use size 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 {AggregationPanel} from './aggregation_panel'; import {isEmptyData} from '../public/aggregation'; import {DetailsShell} from '../widgets/details_shell'; import {Button, ButtonBar} from '../widgets/button'; import {raf} from '../core/raf_scheduler'; import {EmptyState} from '../widgets/empty_state'; import {FlowEventsAreaSelectedPanel} from './flow_events_panel'; import {PivotTable} from './pivot_table'; import {AreaSelection} from '../public/selection'; import {Monitor} from '../base/monitor'; import { CPU_PROFILE_TRACK_KIND, PERF_SAMPLES_PROFILE_TRACK_KIND, SLICE_TRACK_KIND, } from '../public/track_kinds'; import { QueryFlamegraph, metricsFromTableOrSubquery, } from '../components/query_flamegraph'; import {DisposableStack} from '../base/disposable_stack'; import {assertExists} from '../base/logging'; import {TraceImpl} from '../core/trace_impl'; import {Trace} from '../public/trace'; import {Flamegraph} from '../widgets/flamegraph'; interface View { key: string; name: string; content: m.Children; } export type AreaDetailsPanelAttrs = {trace: TraceImpl}; class AreaDetailsPanel implements m.ClassComponent { private trace: TraceImpl; private monitor: Monitor; private currentTab: string | undefined = undefined; private cpuProfileFlamegraph?: QueryFlamegraph; private perfSampleFlamegraph?: QueryFlamegraph; private sliceFlamegraph?: QueryFlamegraph; constructor({attrs}: m.CVnode) { this.trace = attrs.trace; this.monitor = new Monitor([() => this.trace.selection.selection]); } private getCurrentView(): string | undefined { const types = this.getViews().map(({key}) => key); if (types.length === 0) { return undefined; } if (this.currentTab === undefined) { return types[0]; } if (!types.includes(this.currentTab)) { return types[0]; } return this.currentTab; } private getViews(): View[] { const views: View[] = []; for (const aggregator of this.trace.selection.aggregation.aggregators) { const aggregatorId = aggregator.id; const value = this.trace.selection.aggregation.getAggregatedData(aggregatorId); if (value !== undefined && !isEmptyData(value)) { views.push({ key: value.tabName, name: value.tabName, content: m(AggregationPanel, { aggregatorId, data: value, trace: this.trace, }), }); } } const pivotTableState = this.trace.pivotTable.state; const tree = pivotTableState.queryResult?.tree; if ( pivotTableState.selectionArea != undefined && (tree === undefined || tree.children.size > 0 || tree?.rows.length > 0) ) { views.push({ key: 'pivot_table', name: 'Pivot Table', content: m(PivotTable, { trace: this.trace, selectionArea: pivotTableState.selectionArea, }), }); } this.addFlamegraphView(this.trace, this.monitor.ifStateChanged(), views); // Add this after all aggregation panels, to make it appear after 'Slices' if (this.trace.flows.selectedFlows.length > 0) { views.push({ key: 'selected_flows', name: 'Flow Events', content: m(FlowEventsAreaSelectedPanel, {trace: this.trace}), }); } return views; } view(): m.Children { const views = this.getViews(); const currentViewKey = this.getCurrentView(); const aggregationButtons = views.map(({key, name}) => { return m(Button, { onclick: () => { this.currentTab = key; raf.scheduleFullRedraw(); }, key, label: name, active: currentViewKey === key, }); }); if (currentViewKey === undefined) { return this.renderEmptyState(); } const content = views.find(({key}) => key === currentViewKey)?.content; if (content === undefined) { return this.renderEmptyState(); } return m( DetailsShell, { title: 'Area Selection', description: m(ButtonBar, aggregationButtons), }, content, ); } private renderEmptyState(): m.Children { return m( EmptyState, { className: 'pf-noselection', title: 'Unsupported area selection', }, 'No details available for this area selection', ); } private addFlamegraphView(trace: Trace, isChanged: boolean, views: View[]) { this.cpuProfileFlamegraph = this.computeCpuProfileFlamegraph( trace, isChanged, ); if (this.cpuProfileFlamegraph !== undefined) { views.push({ key: 'cpu_profile_flamegraph_selection', name: 'CPU Profile Sample Flamegraph', content: this.cpuProfileFlamegraph.render(), }); } this.perfSampleFlamegraph = this.computePerfSampleFlamegraph( trace, isChanged, ); if (this.perfSampleFlamegraph !== undefined) { views.push({ key: 'perf_sample_flamegraph_selection', name: 'Perf Sample Flamegraph', content: this.perfSampleFlamegraph.render(), }); } this.sliceFlamegraph = this.computeSliceFlamegraph(trace, isChanged); if (this.sliceFlamegraph !== undefined) { views.push({ key: 'slice_flamegraph_selection', name: 'Slice Flamegraph', content: this.sliceFlamegraph.render(), }); } } private computeCpuProfileFlamegraph(trace: Trace, isChanged: boolean) { const currentSelection = trace.selection.selection; if (currentSelection.kind !== 'area') { return undefined; } if (!isChanged) { // If the selection has not changed, just return a copy of the last seen // attrs. return this.cpuProfileFlamegraph; } const utids = []; for (const trackInfo of currentSelection.tracks) { if (trackInfo?.tags?.kind === CPU_PROFILE_TRACK_KIND) { utids.push(trackInfo.tags?.utid); } } if (utids.length === 0) { return undefined; } const metrics = metricsFromTableOrSubquery( ` ( select id, parent_id as parentId, name, mapping_name, source_file, cast(line_number AS text) as line_number, self_count from _callstacks_for_callsites!(( select p.callsite_id from cpu_profile_stack_sample p where p.ts >= ${currentSelection.start} and p.ts <= ${currentSelection.end} and p.utid in (${utids.join(',')}) )) ) `, [ { name: 'CPU Profile Samples', unit: '', columnName: 'self_count', }, ], 'include perfetto module callstacks.stack_profile', [{name: 'mapping_name', displayName: 'Mapping'}], [ { name: 'source_file', displayName: 'Source File', mergeAggregation: 'ONE_OR_NULL', }, { name: 'line_number', displayName: 'Line Number', mergeAggregation: 'ONE_OR_NULL', }, ], ); return new QueryFlamegraph(trace, metrics, { state: Flamegraph.createDefaultState(metrics), }); } private computePerfSampleFlamegraph(trace: Trace, isChanged: boolean) { const currentSelection = trace.selection.selection; if (currentSelection.kind !== 'area') { return undefined; } if (!isChanged) { // If the selection has not changed, just return a copy of the last seen // attrs. return this.perfSampleFlamegraph; } const upids = getUpidsFromPerfSampleAreaSelection(currentSelection); const utids = getUtidsFromPerfSampleAreaSelection(currentSelection); if (utids.length === 0 && upids.length === 0) { return undefined; } const metrics = metricsFromTableOrSubquery( ` ( select id, parent_id as parentId, name, self_count from _callstacks_for_callsites!(( select p.callsite_id from perf_sample p join thread t using (utid) where p.ts >= ${currentSelection.start} and p.ts <= ${currentSelection.end} and ( p.utid in (${utids.join(',')}) or t.upid in (${upids.join(',')}) ) )) ) `, [ { name: 'Perf Samples', unit: '', columnName: 'self_count', }, ], 'include perfetto module linux.perf.samples', ); return new QueryFlamegraph(trace, metrics, { state: Flamegraph.createDefaultState(metrics), }); } private computeSliceFlamegraph(trace: Trace, isChanged: boolean) { const currentSelection = trace.selection.selection; if (currentSelection.kind !== 'area') { return undefined; } if (!isChanged) { // If the selection has not changed, just return a copy of the last seen // attrs. return this.sliceFlamegraph; } const trackIds = []; for (const trackInfo of currentSelection.tracks) { if (trackInfo?.tags?.kind !== SLICE_TRACK_KIND) { continue; } if (trackInfo.tags?.trackIds === undefined) { continue; } trackIds.push(...trackInfo.tags.trackIds); } if (trackIds.length === 0) { return undefined; } const metrics = metricsFromTableOrSubquery( ` ( select * from _viz_slice_ancestor_agg!(( select s.id, s.dur from slice s left join slice t on t.parent_id = s.id where s.ts >= ${currentSelection.start} and s.ts <= ${currentSelection.end} and s.track_id in (${trackIds.join(',')}) and t.id is null )) ) `, [ { name: 'Duration', unit: 'ns', columnName: 'self_dur', }, { name: 'Samples', unit: '', columnName: 'self_count', }, ], 'include perfetto module viz.slices;', ); return new QueryFlamegraph(trace, metrics, { state: Flamegraph.createDefaultState(metrics), }); } } export class AggregationsTabs implements Disposable { private trash = new DisposableStack(); constructor(trace: TraceImpl) { const unregister = trace.tabs.registerDetailsPanel({ render(selection) { if (selection.kind === 'area') { return m(AreaDetailsPanel, {trace}); } else { return undefined; } }, }); this.trash.use(unregister); } [Symbol.dispose]() { this.trash.dispose(); } } function getUpidsFromPerfSampleAreaSelection(currentSelection: AreaSelection) { const upids = []; for (const trackInfo of currentSelection.tracks) { if ( trackInfo?.tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND && trackInfo.tags?.utid === undefined ) { upids.push(assertExists(trackInfo.tags?.upid)); } } return upids; } function getUtidsFromPerfSampleAreaSelection(currentSelection: AreaSelection) { const utids = []; for (const trackInfo of currentSelection.tracks) { if ( trackInfo?.tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND && trackInfo.tags?.utid !== undefined ) { utids.push(trackInfo.tags?.utid); } } return utids; }