blob: 98ab6ffb3c4965f86e6a660f2e5b9b4257f5313f [file]
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this 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 {BigintMath} from '../../base/bigint_math';
import {exists} from '../../base/utils';
import type {SliceDetails} from '../sql_utils/slice';
import {Anchor} from '../../widgets/anchor';
import {MenuItem, PopupMenu} from '../../widgets/menu';
import {Section} from '../../widgets/section';
import {SqlRef} from '../../widgets/sql_ref';
import {Tree, TreeNode} from '../../widgets/tree';
import {
type BreakdownByThreadState,
BreakdownByThreadStateTreeNode,
} from './thread_state';
import {DurationWidget} from '../widgets/duration';
import {renderProcessRef} from '../widgets/process';
import {renderThreadRef} from '../widgets/thread';
import {Timestamp} from '../widgets/timestamp';
import type {Trace} from '../../public/trace';
import {
type DistributionPanelAttrs,
openDistributionTab,
} from '../distribution_panel';
import {
LONG,
NUM,
type Row,
STR_NULL,
} from '../../trace_processor/query_result';
import {Time} from '../../base/time';
import {type Dataset, UnionDataset} from '../../trace_processor/dataset';
export type DistributionScope = 'track' | 'all';
export function sliceDistributionCellRenderers(
trace: Trace,
): Record<string, (value: Row[string]) => m.Children> {
return {
ts: (value) =>
typeof value === 'bigint'
? m(Timestamp, {trace, ts: Time.fromRaw(value)})
: String(value ?? ''),
dur: (value) =>
typeof value === 'bigint'
? m(DurationWidget, {trace, dur: value})
: String(value ?? ''),
};
}
const SLICE_DISTRIBUTION_SCHEMA = {
id: NUM,
name: STR_NULL,
dur: LONG,
ts: LONG,
};
export function findSliceTrackDataset(
trace: Trace,
trackId: number,
): Dataset | undefined {
const track = trace.tracks.findTrack((t) =>
t.tags?.trackIds?.includes(trackId),
);
const dataset = track?.renderer.getDataset?.();
if (dataset === undefined || !dataset.implements(SLICE_DISTRIBUTION_SCHEMA)) {
return undefined;
}
return dataset;
}
// Mirrors the dataset-union approach used by trace search (core/dataset_search.ts):
// the "whole trace" scope is the union of what each track exposes rather than a
// raw query against the slice table.
export function findWholeTraceSliceDataset(trace: Trace): Dataset | undefined {
const datasets: Dataset[] = [];
for (const track of trace.tracks.getAllTracks()) {
const dataset = track.renderer.getDataset?.();
if (dataset?.implements(SLICE_DISTRIBUTION_SCHEMA)) {
datasets.push(dataset);
}
}
if (datasets.length === 0) return undefined;
if (datasets.length === 1) return datasets[0];
return UnionDataset.create(datasets);
}
export function sliceDistributionConfig(
trace: Trace,
sliceName: string,
dataset: Dataset,
scope: DistributionScope,
): Omit<DistributionPanelAttrs, 'trace'> {
const scopeLabel = scope === 'track' ? 'this track' : 'across trace';
return {
title: `${sliceName} (${scopeLabel})`,
dataset,
filter: {col: 'name', eq: sliceName},
valueColumn: 'dur',
idColumn: 'id',
sqlTable: 'slice',
displayColumns: ['ts', 'dur'],
cellRenderers: sliceDistributionCellRenderers(trace),
};
}
function resolveSliceDataset(
trace: Trace,
trackId: number,
scope: DistributionScope,
): Dataset | undefined {
return scope === 'track'
? findSliceTrackDataset(trace, trackId)
: findWholeTraceSliceDataset(trace);
}
function openSliceDistribution(
trace: Trace,
sliceName: string,
trackId: number,
scope: DistributionScope,
): void {
const dataset = resolveSliceDataset(trace, trackId, scope);
if (dataset === undefined) return;
openDistributionTab(
trace,
sliceDistributionConfig(trace, sliceName, dataset, scope),
);
}
function renderMatchingSlicesMenu(
trace: Trace,
slice: SliceDetails,
): m.Children {
const sliceName = slice.name;
if (sliceName === undefined) {
return m(MenuItem, {
label: 'Slices with the same name',
disabled: true,
});
}
const item = (label: string, scope: DistributionScope) =>
m(MenuItem, {
label,
onclick: () =>
openSliceDistribution(trace, sliceName, slice.trackId, scope),
});
return [
item('Slices with the same name (this track)', 'track'),
item('Slices with the same name (across trace)', 'all'),
];
}
export function renderDetails(
trace: Trace,
slice: SliceDetails,
durationBreakdown?: BreakdownByThreadState,
) {
return m(
Section,
{title: 'Details'},
m(
Tree,
m(TreeNode, {
left: 'Name',
right: m(
PopupMenu,
{
trigger: m(Anchor, slice.name),
},
renderMatchingSlicesMenu(trace, slice),
),
}),
m(TreeNode, {
left: 'Category',
right:
!slice.category || slice.category === '[NULL]'
? 'N/A'
: slice.category,
}),
m(TreeNode, {
left: 'Start time',
right: m(Timestamp, {trace, ts: slice.ts}),
}),
exists(slice.absTime) &&
m(TreeNode, {left: 'Absolute Time', right: slice.absTime}),
m(
TreeNode,
{
left: 'Duration',
right: m(DurationWidget, {trace, dur: slice.dur}),
},
exists(durationBreakdown) &&
slice.dur > 0 &&
m(BreakdownByThreadStateTreeNode, {
trace,
data: durationBreakdown,
dur: slice.dur,
}),
),
renderThreadDuration(trace, slice),
slice.thread &&
m(TreeNode, {
left: 'Thread',
right: renderThreadRef(trace, slice.thread),
}),
slice.process &&
m(TreeNode, {
left: 'Process',
right: renderProcessRef(trace, slice.process),
}),
slice.process &&
exists(slice.process.uid) &&
m(TreeNode, {
left: 'User ID',
right: slice.process.uid,
}),
slice.process &&
slice.process.packageName &&
m(TreeNode, {
left: 'Package name',
right: slice.process.packageName,
}),
slice.process &&
exists(slice.process.versionCode) &&
m(TreeNode, {
left: 'Version code',
right: slice.process.versionCode,
}),
m(TreeNode, {
left: 'SQL ID',
right: m(SqlRef, {table: 'slice', id: slice.id}),
}),
),
);
}
function renderThreadDuration(trace: Trace, sliceInfo: SliceDetails) {
if (exists(sliceInfo.threadTs) && exists(sliceInfo.threadDur)) {
// If we have valid thread duration, also display a percentage of
// |threadDur| compared to |dur|.
const ratio = BigintMath.ratio(sliceInfo.threadDur, sliceInfo.dur);
const threadDurFractionSuffix =
sliceInfo.threadDur === -1n ? '' : ` (${(ratio * 100).toFixed(2)}%)`;
return m(TreeNode, {
left: 'Thread duration',
right: [
m(DurationWidget, {trace, dur: sliceInfo.threadDur}),
threadDurFractionSuffix,
],
});
} else {
return undefined;
}
}