blob: e410a6083fad0ad93e2f36412c65d426dc3535c3 [file] [log] [blame]
// 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 '../public/lib/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<AreaDetailsPanelAttrs> {
private trace: TraceImpl;
private monitor: Monitor;
private currentTab: string | undefined = undefined;
private cpuProfileFlamegraph?: QueryFlamegraph;
private perfSampleFlamegraph?: QueryFlamegraph;
private sliceFlamegraph?: QueryFlamegraph;
constructor({attrs}: m.CVnode<AreaDetailsPanelAttrs>) {
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;
}