blob: 5a3d17150e69fffc5e5efe1c33c5b06a80b9d3e4 [file]
// Copyright (C) 2025 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 {getColorForSample} from '../../components/colorizer';
import {
metricsFromTableOrSubquery,
type QueryFlamegraphMetric,
} from '../../components/query_flamegraph';
import {FlamegraphPanel} from '../../components/flamegraph_panel';
import {DetailsShell} from '../../widgets/details_shell';
import {Timestamp} from '../../components/widgets/timestamp';
import {Time, type time} from '../../base/time';
import {
Flamegraph,
type FlamegraphState,
FLAMEGRAPH_STATE_SCHEMA,
} from '../../widgets/flamegraph';
import type {Trace} from '../../public/trace';
import {SliceTrack} from '../../components/tracks/slice_track';
import type {SourceDataset} from '../../trace_processor/dataset';
/**
* Configuration for creating a profiling track (CPU profile, perf samples, etc)
*/
export interface ProfilingTrackConfig {
/**
* The SourceDataset that provides the profiling samples.
* Must have schema: {id: NUM, ts: LONG, callsiteId: NUM}
*/
readonly dataset: SourceDataset<{
id: number;
ts: bigint;
callsiteId: number;
}>;
/**
* SQL query to get callsite_id values for the details panel.
* This will be passed to _callstacks_for_callsites!()
* Should return rows with a callsite_id column.
*
* Example: `select callsite_id from perf_sample where ts = ${ts}`
*
* Available template variables:
* - ${ts}: The timestamp of the clicked sample
*/
readonly callsiteQuery: (ts: bigint) => string;
/**
* Perfetto SQL module to include for _callstacks_for_callsites function.
*
* Examples:
* - 'callstacks.stack_profile' for CPU profiles
* - 'linux.perf.samples' for perf samples
* - 'appleos.instruments.samples' for Instruments samples
*/
readonly sqlModule: string;
/**
* Name to display for the metric in the flamegraph.
* Examples: 'CPU Profile Samples', 'Perf Samples', 'Instruments Samples'
*/
readonly metricName: string;
/**
* Title to display in the details panel.
* Examples: 'CPU Profile Samples', 'Perf sample', 'Instruments Samples'
*/
readonly panelTitle: string;
/**
* Name to display for each sample slice on the track.
* Examples: 'CPU Sample', 'Perf sample', 'Instruments Sample'
*/
readonly sliceName: string;
}
/**
* Creates a profiling track (CPU profile, perf samples, Instruments samples, etc).
*
* This is a unified factory function that handles all types of profiling tracks.
* The differences between track types are abstracted through the ProfilingTrackConfig.
*
* @param trace - The trace object
* @param uri - Unique URI for this track
* @param config - Configuration specific to the profiling track type
* @param detailsPanelState - Current flamegraph state (for persistence)
* @param onDetailsPanelStateChange - Callback when flamegraph state changes
* @returns A configured SliceTrack instance
*/
export function createProfilingTrack(
trace: Trace,
uri: string,
config: ProfilingTrackConfig,
detailsPanelState: FlamegraphState | undefined,
onDetailsPanelStateChange: (state: FlamegraphState) => void,
) {
return SliceTrack.create({
trace,
uri,
dataset: config.dataset,
sliceName: () => config.sliceName,
colorizer: (row) => getColorForSample(row.callsiteId),
detailsPanel: (row) => {
const ts = Time.fromRaw(row.ts);
const metrics: ReadonlyArray<QueryFlamegraphMetric> =
metricsFromTableOrSubquery({
tableOrSubquery: `
(
select
id,
parent_id as parentId,
name,
mapping_name,
source_file || ':' || line_number as source_location,
self_count
from _callstacks_for_callsites!((
${config.callsiteQuery(ts)}
))
)
`,
tableMetrics: [
{
name: config.metricName,
unit: '',
columnName: 'self_count',
},
],
dependencySql: `include perfetto module ${config.sqlModule}`,
unaggregatableProperties: [
{name: 'mapping_name', displayName: 'Mapping'},
],
aggregatableProperties: [
{
name: 'source_location',
displayName: 'Source Location',
mergeAggregation: 'ONE_OR_SUMMARY',
},
],
nameColumnLabel: 'Symbol',
});
// Use provided state or create initial state once
let state = detailsPanelState ?? Flamegraph.createDefaultState(metrics);
if (detailsPanelState === undefined) {
onDetailsPanelStateChange(state);
}
return {
load: async () => {},
render: () =>
renderProfilingDetailsPanel(
trace,
ts,
config,
state,
(newState) => {
state = newState;
onDetailsPanelStateChange(newState);
},
metrics,
),
// TODO(lalitm): we should be able remove this around the 26Q2 timeframe
// We moved serialization from being attached to selections to instead being
// attached to the plugin that loaded the panel.
serialization: {
schema: FLAMEGRAPH_STATE_SCHEMA.optional(),
state: undefined as FlamegraphState | undefined,
},
};
},
});
}
/**
* Internal function to render the details panel for a profiling sample.
*/
function renderProfilingDetailsPanel(
trace: Trace,
ts: time,
config: ProfilingTrackConfig,
state: FlamegraphState,
onStateChange: (state: FlamegraphState) => void,
metrics: ReadonlyArray<QueryFlamegraphMetric>,
): m.Children {
return m(
'.pf-flamegraph-profile',
m(
DetailsShell,
{
fillHeight: true,
title: config.panelTitle,
buttons: m('span', 'Timestamp: ', m(Timestamp, {trace, ts})),
},
m(FlamegraphPanel, {trace, metrics, state, onStateChange}),
),
);
}