| // 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 {sampleColorScheme} from './sample_colors'; | |
| import { | |
| metricsFromTableOrSubquery, | |
| TreeExplorerFetcher, | |
| } from '../../components/tree_explorer_fetcher'; | |
| import {TreeExplorerPanel} from '../../components/tree_explorer_panel'; | |
| import {FlamegraphProfile} from '../../components/flamegraph_profile'; | |
| import {DetailsShell} from '../../widgets/details_shell'; | |
| import {Timestamp} from '../../components/widgets/timestamp'; | |
| import {Time, type time} from '../../base/time'; | |
| import {Memo} from '../../base/memo'; | |
| import { | |
| createDefaultTreeExplorerState, | |
| type TreeExplorerState, | |
| TREE_EXPLORER_STATE_SCHEMA, | |
| } from '../../widgets/tree_explorer'; | |
| import type {Trace} from '../../public/trace'; | |
| import {SliceTrack} from '../../components/tracks/slice_track'; | |
| import type {SourceDataset} from '../../trace_processor/dataset'; | |
| import type {LONG, NUM, STR} from '../../trace_processor/query_result'; | |
| /** | |
| * 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, category: NUM, mappingName: STR} | |
| */ | |
| readonly dataset: SourceDataset<{ | |
| id: typeof NUM; | |
| ts: typeof LONG; | |
| callsiteId: typeof NUM; | |
| category: typeof NUM; | |
| mappingName: typeof STR; | |
| }>; | |
| /** | |
| * 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: TreeExplorerState | undefined, | |
| onDetailsPanelStateChange: (state: TreeExplorerState) => void, | |
| ) { | |
| // The metrics (and so the fetcher which owns the virtual tables built from | |
| // them) depend only on the selected sample timestamp: the memo keeps exactly | |
| // one generation alive and disposes it when the selection moves on. | |
| const fetcherMemo = new Memo<TreeExplorerFetcher>(); | |
| return SliceTrack.create({ | |
| trace, | |
| uri, | |
| dataset: config.dataset, | |
| sliceName: () => config.sliceName, | |
| colorizer: (row) => sampleColorScheme(row.category, row.mappingName), | |
| detailsPanel: (row) => { | |
| const ts = Time.fromRaw(row.ts); | |
| const fetcher = fetcherMemo.use({ | |
| key: {ts}, | |
| compute: () => { | |
| const metrics = 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', | |
| }); | |
| return new TreeExplorerFetcher(trace, metrics); | |
| }, | |
| }); | |
| const metrics = fetcher.metrics; | |
| // Use provided state or create initial state once | |
| let state = detailsPanelState ?? createDefaultTreeExplorerState(metrics); | |
| if (detailsPanelState === undefined) { | |
| onDetailsPanelStateChange(state); | |
| } | |
| return { | |
| load: async () => {}, | |
| render: () => | |
| renderProfilingDetailsPanel( | |
| trace, | |
| ts, | |
| config, | |
| state, | |
| (newState) => { | |
| state = newState; | |
| onDetailsPanelStateChange(newState); | |
| }, | |
| fetcher, | |
| ), | |
| // 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: TREE_EXPLORER_STATE_SCHEMA.optional(), | |
| state: undefined as TreeExplorerState | undefined, | |
| }, | |
| }; | |
| }, | |
| }); | |
| } | |
| /** | |
| * Internal function to render the details panel for a profiling sample. | |
| */ | |
| function renderProfilingDetailsPanel( | |
| trace: Trace, | |
| ts: time, | |
| config: Omit<ProfilingTrackConfig, 'dataset'>, | |
| state: TreeExplorerState, | |
| onStateChange: (state: TreeExplorerState) => void, | |
| fetcher: TreeExplorerFetcher, | |
| ): m.Children { | |
| return m( | |
| FlamegraphProfile, | |
| m( | |
| DetailsShell, | |
| { | |
| fillHeight: true, | |
| title: config.panelTitle, | |
| buttons: m('span', 'Timestamp: ', m(Timestamp, {trace, ts})), | |
| }, | |
| m(TreeExplorerPanel, {fetcher, state, onStateChange}), | |
| ), | |
| ); | |
| } |