| // 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 {Gpu} from '../../components/gpu'; |
| import type {PerfettoPlugin} from '../../public/plugin'; |
| import type {Trace} from '../../public/trace'; |
| import {COUNTER_TRACK_KIND, SLICE_TRACK_KIND} from '../../public/track_kinds'; |
| import {getMachineCount, getTrackName} from '../../public/utils'; |
| import {TrackNode} from '../../public/workspace'; |
| import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result'; |
| import {createPerfettoTable} from '../../trace_processor/sql_utils'; |
| import StandardGroupsPlugin from '../dev.perfetto.StandardGroups'; |
| import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack'; |
| import {TraceProcessorCounterTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_counter_track'; |
| import {createTraceProcessorSliceTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_slice_track'; |
| |
| // GPU frequency track that converts kHz values to Hz so that the generic |
| // counter renderer produces correct SI-prefixed labels (e.g. "2 GHz"). |
| class GpuFreqTrack extends TraceProcessorCounterTrack { |
| constructor( |
| trace: Trace, |
| uri: string, |
| freqTrackId: number, |
| trackName: string, |
| ) { |
| super({ |
| trace, |
| uri, |
| unit: 'Hz', |
| trackId: freqTrackId, |
| trackName, |
| rootTable: 'counter', |
| sqlSource: ` |
| select id, ts, value * 1000 as value, arg_set_id |
| from counter |
| where track_id = ${freqTrackId} |
| `, |
| }); |
| } |
| } |
| |
| interface GpuCounterSchema { |
| readonly type: string; |
| readonly group: string | undefined; |
| // Sort order for the top-level group under GPU. Groups with undefined |
| // sortOrder default to 0 (top); summary-only groups use higher values. |
| readonly groupSortOrder: number | undefined; |
| // When set, the track is named "${gpu.displayName} ${gpuTrackName}" instead |
| // of using the DB track name. This avoids redundant prefixes like |
| // "GPU 0 GPU Memory" by allowing explicit control (e.g., "GPU 0 Memory"). |
| readonly gpuTrackName: string | undefined; |
| } |
| |
| interface GpuSliceSchema { |
| readonly type: string; |
| readonly group: string | undefined; |
| readonly groupSortOrder: number | undefined; |
| } |
| |
| // Sort order base for summary-only groups (Counters, Hardware Queues, etc.) |
| // that should appear below leaf tracks (Frequency, Memory). |
| export const SUMMARY_GROUP_SORT_BASE = 1000000; |
| |
| const GPU_COUNTER_SCHEMAS: ReadonlyArray<GpuCounterSchema> = [ |
| { |
| type: 'gpu_counter', |
| group: 'Counters', |
| groupSortOrder: SUMMARY_GROUP_SORT_BASE, |
| gpuTrackName: undefined, |
| }, |
| { |
| type: 'gpu_memory', |
| group: undefined, |
| groupSortOrder: undefined, |
| gpuTrackName: 'Memory', |
| }, |
| { |
| type: 'virtgpu_latency', |
| group: 'Virtgpu Latency', |
| groupSortOrder: SUMMARY_GROUP_SORT_BASE, |
| gpuTrackName: undefined, |
| }, |
| { |
| type: 'virtgpu_num_free', |
| group: 'Virtgpu num_free', |
| groupSortOrder: SUMMARY_GROUP_SORT_BASE, |
| gpuTrackName: undefined, |
| }, |
| { |
| type: 'vulkan_device_mem_allocation', |
| group: 'Vulkan Allocations', |
| groupSortOrder: SUMMARY_GROUP_SORT_BASE, |
| gpuTrackName: undefined, |
| }, |
| { |
| type: 'vulkan_device_mem_bind', |
| group: 'Vulkan Binds', |
| groupSortOrder: SUMMARY_GROUP_SORT_BASE, |
| gpuTrackName: undefined, |
| }, |
| { |
| type: 'vulkan_driver_mem', |
| group: 'Vulkan Driver Memory', |
| groupSortOrder: SUMMARY_GROUP_SORT_BASE, |
| gpuTrackName: undefined, |
| }, |
| ]; |
| |
| const GPU_SLICE_SCHEMAS: ReadonlyArray<GpuSliceSchema> = [ |
| {type: 'mali_mcu_state', group: undefined, groupSortOrder: undefined}, |
| { |
| type: 'virtgpu_queue_event', |
| group: 'Virtio GPU Events', |
| groupSortOrder: SUMMARY_GROUP_SORT_BASE, |
| }, |
| { |
| type: 'gpu_render_stage', |
| group: 'Hardware Queues', |
| groupSortOrder: SUMMARY_GROUP_SORT_BASE, |
| }, |
| {type: 'vulkan_events', group: undefined, groupSortOrder: undefined}, |
| {type: 'gpu_log', group: undefined, groupSortOrder: undefined}, |
| {type: 'graphics_frame_event', group: undefined, groupSortOrder: undefined}, |
| ]; |
| |
| // Track ordering |
| // --------------- |
| // GPU tracks are sorted using TrackNode.sortOrder combined with insertion |
| // order (addChildInOrder inserts before the first child with a strictly |
| // greater sortOrder; equal values preserve insertion order). |
| // |
| // 1. GPU identity: tracks for different GPUs are separated by |
| // Gpu.sortOrder (= machine * MAX_GPUS_PER_MACHINE + gpu), so GPU 0 |
| // appears before GPU 1, and machines sort first. |
| // 2. Leaf before summary: leaf tracks (Frequency, Memory) use low |
| // sortOrder values (>= 0). Summary-only groups (Counters, Hardware |
| // Queues, etc.) use SUMMARY_GROUP_SORT_BASE so they appear below |
| // leaf tracks. |
| // 3. Alphabetical: SQL queries use ORDER BY lower(name) so leaf tracks |
| // are iterated — and thus inserted — in alphabetical order. Custom |
| // counter group nodes are assigned incrementing positive sortOrder |
| // values (starting above leaf tracks) based on alphabetical group |
| // name, ensuring they also appear in alphabetical order among |
| // themselves. |
| export default class GpuPlugin implements PerfettoPlugin { |
| static readonly id = 'dev.perfetto.Gpu'; |
| static readonly dependencies = [ |
| StandardGroupsPlugin, |
| TraceProcessorTrackPlugin, |
| ]; |
| |
| private groups = new Map<string, TrackNode>(); |
| private gpuCount = 0; |
| private numMachines = 0; |
| |
| async onTraceLoad(ctx: Trace): Promise<void> { |
| const gpuCountResult = await ctx.engine.query(` |
| select count(*) as cnt from gpu |
| `); |
| this.gpuCount = gpuCountResult.firstRow({cnt: NUM}).cnt; |
| this.numMachines = await getMachineCount(ctx.engine); |
| |
| await this.addGpuFreq(ctx); |
| await this.addCounters(ctx); |
| await this.addSlices(ctx); |
| } |
| |
| private async addGpuFreq(ctx: Trace) { |
| const result = await ctx.engine.query(` |
| select |
| gct.id, |
| gct.gpu_id as gpuId, |
| gct.machine_id as machineId, |
| m.name as machineName, |
| m.label_index as machineLabelIndex, |
| gct.ugpu, |
| g.name as gpuName |
| from gpu_counter_track gct |
| join _counter_track_summary using (id) |
| left join gpu g on gct.ugpu = g.id |
| left join machine m on m.id = gct.machine_id |
| where gct.name = 'gpufreq' |
| order by machineId, gct.ugpu |
| `); |
| |
| const tracks: Array<{ |
| id: number; |
| gpu: Gpu; |
| }> = []; |
| const it = result.iter({ |
| id: NUM, |
| gpuId: NUM, |
| machineId: NUM, |
| machineName: STR_NULL, |
| machineLabelIndex: NUM_NULL, |
| ugpu: NUM_NULL, |
| gpuName: STR_NULL, |
| }); |
| for (; it.valid(); it.next()) { |
| tracks.push({ |
| id: it.id, |
| gpu: new Gpu( |
| it.ugpu ?? it.gpuId, |
| it.gpuId, |
| it.machineId, |
| it.gpuName ?? undefined, |
| it.machineName ?? undefined, |
| it.machineLabelIndex ?? undefined, |
| this.numMachines, |
| ), |
| }); |
| } |
| |
| if (tracks.length === 0) return; |
| |
| const gpuGroup = this.getGpuGroup(ctx); |
| |
| // Only create a sub-group if there's more than one track. |
| let parent: TrackNode; |
| if (tracks.length > 1) { |
| parent = this.getGroupByName(gpuGroup, 'Frequency', null); |
| } else { |
| parent = gpuGroup; |
| } |
| |
| for (const {id, gpu} of tracks) { |
| const uri = `/gpu_frequency_${gpu.ugpu}`; |
| const name = `${gpu.displayName} Frequency${gpu.maybeMachineLabel()}`; |
| ctx.tracks.registerTrack({ |
| uri, |
| tags: { |
| kinds: [COUNTER_TRACK_KIND], |
| trackIds: [id], |
| }, |
| renderer: new GpuFreqTrack(ctx, uri, id, name), |
| }); |
| parent.addChildInOrder( |
| new TrackNode({ |
| uri, |
| name, |
| sortOrder: gpu.sortOrder, |
| }), |
| ); |
| } |
| } |
| |
| private getGpuGroup(ctx: Trace): TrackNode { |
| return ctx.plugins |
| .getPlugin(StandardGroupsPlugin) |
| .getOrCreateStandardGroup(ctx.defaultWorkspace, 'GPU'); |
| } |
| |
| private addToGpuGroup( |
| ctx: Trace, |
| group: string | undefined, |
| groupSortOrder: number | undefined, |
| gpu: Gpu | null, |
| track: TrackNode, |
| ) { |
| const gpuGroup = this.getGpuGroup(ctx); |
| |
| if (gpu !== null && group !== undefined && this.gpuCount > 1) { |
| const parentGroup = this.getGroupByName( |
| gpuGroup, |
| group, |
| null, |
| groupSortOrder, |
| ); |
| const gpuSubGroupName = `${gpu.displayName} ${group}${gpu.maybeMachineLabel()}`; |
| const scopeId = |
| gpu.machine > 0 ? `${gpu.gpu}_m${gpu.machine}` : `${gpu.gpu}`; |
| const gpuSubGroup = this.getGroupByName( |
| parentGroup, |
| gpuSubGroupName, |
| scopeId, |
| gpu.sortOrder, |
| ); |
| gpuSubGroup.addChildInOrder(track); |
| } else { |
| this.getGroupByName( |
| gpuGroup, |
| group, |
| null, |
| groupSortOrder, |
| ).addChildInOrder(track); |
| } |
| } |
| |
| private getGroupByName( |
| node: TrackNode, |
| group: string | undefined, |
| scopeId: string | null, |
| sortOrder?: number, |
| ): TrackNode { |
| if (group === undefined) { |
| return node; |
| } |
| const parentId = node.uri ?? 'root'; |
| const groupId = `gpu_group_${scopeId}_${parentId}_${group.toLowerCase().replace(' ', '_')}`; |
| const groupNode = this.groups.get(groupId); |
| if (groupNode) { |
| return groupNode; |
| } |
| const newGroup = new TrackNode({ |
| uri: `/${group}`, |
| isSummary: true, |
| name: group, |
| collapsed: true, |
| sortOrder, |
| }); |
| node.addChildInOrder(newGroup); |
| this.groups.set(groupId, newGroup); |
| return newGroup; |
| } |
| |
| private async addCounters(ctx: Trace) { |
| const counterTypes = GPU_COUNTER_SCHEMAS.map((s) => `'${s.type}'`).join( |
| ',', |
| ); |
| const result = await ctx.engine.query(` |
| with tracks_summary as ( |
| select |
| ct.type, |
| ct.name, |
| ct.id, |
| ct.unit, |
| ct.machine_id, |
| extract_arg(ct.dimension_arg_set_id, 'ugpu') as ugpu, |
| extract_arg(ct.dimension_arg_set_id, 'gpu') as gpu_id, |
| extract_arg(ct.source_arg_set_id, 'description') as description, |
| g.name as gpu_name, |
| m.name as machine_name, |
| m.label_index as machine_label_index |
| from counter_track ct |
| join _counter_track_summary using (id) |
| left join gpu g on extract_arg(ct.dimension_arg_set_id, 'ugpu') = g.id |
| left join machine m on m.id = ct.machine_id |
| where ct.type in (${counterTypes}) |
| order by ct.name |
| ) |
| select * from tracks_summary |
| order by lower(name) |
| `); |
| |
| const schemas = new Map(GPU_COUNTER_SCHEMAS.map((x) => [x.type, x])); |
| const counterTracks: Array<{ |
| schema: GpuCounterSchema; |
| gpu: Gpu | null; |
| trackName: string; |
| baseName: string; |
| uri: string; |
| trackId: number; |
| }> = []; |
| const it = result.iter({ |
| id: NUM, |
| type: STR, |
| name: STR_NULL, |
| unit: STR_NULL, |
| gpu_id: NUM_NULL, |
| machine_id: NUM, |
| description: STR_NULL, |
| ugpu: NUM_NULL, |
| gpu_name: STR_NULL, |
| machine_name: STR_NULL, |
| machine_label_index: NUM_NULL, |
| }); |
| for (; it.valid(); it.next()) { |
| const { |
| type, |
| id: trackId, |
| name, |
| unit, |
| gpu_id: gpuId, |
| machine_id: machineId, |
| description, |
| ugpu, |
| gpu_name: gpuName, |
| machine_name: machineName, |
| machine_label_index: machineLabelIndex, |
| } = it; |
| const schema = schemas.get(type); |
| if (schema === undefined) { |
| continue; |
| } |
| const gpu = |
| gpuId !== null |
| ? new Gpu( |
| ugpu ?? trackId, |
| gpuId, |
| machineId, |
| gpuName ?? undefined, |
| machineName ?? undefined, |
| machineLabelIndex ?? undefined, |
| this.numMachines, |
| ) |
| : null; |
| let trackName = getTrackName({name, kind: COUNTER_TRACK_KIND}); |
| if (gpu !== null && schema.gpuTrackName !== undefined) { |
| trackName = `${gpu.displayName} ${schema.gpuTrackName}${gpu.maybeMachineLabel()}`; |
| } |
| const uri = `/counter_${ugpu ?? trackId}_${trackId}`; |
| |
| ctx.tracks.registerTrack({ |
| uri, |
| description: description ?? undefined, |
| tags: { |
| kinds: [COUNTER_TRACK_KIND], |
| trackIds: [trackId], |
| type, |
| name: name ?? undefined, |
| unit: unit ?? undefined, |
| description: description ?? undefined, |
| }, |
| renderer: new TraceProcessorCounterTrack({ |
| trace: ctx, |
| uri, |
| unit: unit ?? undefined, |
| trackId, |
| trackName, |
| }), |
| }); |
| |
| counterTracks.push({ |
| schema, |
| gpu, |
| trackName, |
| baseName: getTrackName({name, kind: COUNTER_TRACK_KIND}), |
| uri, |
| trackId, |
| }); |
| } |
| |
| // Query custom counter groups. |
| const groupResult = await ctx.engine.query(` |
| select |
| group_id as groupId, |
| name as groupName, |
| track_id as trackId |
| from gpu_counter_group |
| where name is not null |
| order by lower(name), group_id |
| `); |
| |
| // Map trackId -> {groupId, groupName} for custom group assignment. |
| // Also collect unique groups in alphabetical order for sort ordering. |
| const trackGroupMap = new Map< |
| number, |
| {groupId: number; groupName: string} |
| >(); |
| const uniqueGroups = new Map<number, string>(); |
| const groupIt = groupResult.iter({ |
| groupId: NUM, |
| groupName: STR_NULL, |
| trackId: NUM, |
| }); |
| for (; groupIt.valid(); groupIt.next()) { |
| if (groupIt.groupName !== null) { |
| // Use the first named group for each track. |
| if (!trackGroupMap.has(groupIt.trackId)) { |
| trackGroupMap.set(groupIt.trackId, { |
| groupId: groupIt.groupId, |
| groupName: groupIt.groupName, |
| }); |
| } |
| if (!uniqueGroups.has(groupIt.groupId)) { |
| uniqueGroups.set(groupIt.groupId, groupIt.groupName); |
| } |
| } |
| } |
| |
| // Assign sort orders to custom groups: alphabetical, all above leaf |
| // tracks so groups appear after ungrouped counter tracks. |
| const CUSTOM_GROUP_SORT_BASE = 10000; |
| const sortedGroupIds = [...uniqueGroups.entries()] |
| .sort((a, b) => a[1].localeCompare(b[1])) |
| .map(([id]) => id); |
| const groupSortOrder = new Map<number, number>(); |
| for (let i = 0; i < sortedGroupIds.length; i++) { |
| groupSortOrder.set(sortedGroupIds[i], CUSTOM_GROUP_SORT_BASE + i); |
| } |
| |
| // Count ungrouped tracks per type to decide if a sub-group is needed, |
| // matching the Frequency pattern: only create a sub-group when |
| // there are multiple tracks of the same type. |
| const ungroupedCounts = new Map<string, number>(); |
| for (const {schema, trackId} of counterTracks) { |
| const customGroup = trackGroupMap.get(trackId); |
| if (schema.group === undefined && customGroup === undefined) { |
| ungroupedCounts.set( |
| schema.type, |
| (ungroupedCounts.get(schema.type) ?? 0) + 1, |
| ); |
| } |
| } |
| |
| for (const { |
| schema, |
| gpu, |
| trackName, |
| baseName, |
| uri, |
| trackId, |
| } of counterTracks) { |
| const trackNode = new TrackNode({ |
| uri, |
| name: trackName, |
| sortOrder: gpu?.sortOrder ?? 0, |
| }); |
| |
| // Check if this track has a custom group assignment. |
| const customGroup = trackGroupMap.get(trackId); |
| if (customGroup !== undefined && schema.type === 'gpu_counter') { |
| const gpuGroup = this.getGpuGroup(ctx); |
| const countersGroup = this.getGroupByName( |
| gpuGroup, |
| schema.group, |
| null, |
| schema.groupSortOrder, |
| ); |
| |
| // Place track under its custom group within Counters. |
| let parent: TrackNode; |
| const scopeId = |
| gpu !== null |
| ? gpu.machine > 0 |
| ? `${gpu.gpu}_m${gpu.machine}` |
| : `${gpu.gpu}` |
| : null; |
| if (gpu !== null && this.gpuCount > 1) { |
| const gpuSubGroupName = `${gpu.displayName} ${schema.group}${gpu.maybeMachineLabel()}`; |
| parent = this.getGroupByName( |
| countersGroup, |
| gpuSubGroupName, |
| scopeId, |
| gpu.sortOrder, |
| ); |
| } else { |
| parent = countersGroup; |
| } |
| |
| // Create custom group node using numeric ID as cache key. |
| const key = `gpu_custom_group_${scopeId}_${customGroup.groupId}`; |
| let groupNode = this.groups.get(key); |
| if (!groupNode) { |
| groupNode = new TrackNode({ |
| uri: `/gpu_group_${customGroup.groupId}`, |
| isSummary: true, |
| name: customGroup.groupName, |
| collapsed: true, |
| sortOrder: |
| groupSortOrder.get(customGroup.groupId) ?? CUSTOM_GROUP_SORT_BASE, |
| }); |
| parent.addChildInOrder(groupNode); |
| this.groups.set(key, groupNode); |
| } |
| |
| groupNode.addChildInOrder(trackNode); |
| continue; |
| } |
| |
| let group = schema.group; |
| let groupGpu = gpu; |
| if (group === undefined && (ungroupedCounts.get(schema.type) ?? 0) > 1) { |
| // Multiple tracks of the same ungrouped type: create a sub-group |
| // and add tracks directly under it without per-GPU sub-groups, |
| // matching how addGpuFreq handles Frequency tracks. |
| group = schema.gpuTrackName ?? baseName; |
| groupGpu = null; |
| } |
| this.addToGpuGroup( |
| ctx, |
| group, |
| schema.groupSortOrder, |
| groupGpu, |
| trackNode, |
| ); |
| } |
| } |
| |
| private async addSlices(ctx: Trace) { |
| const sliceTypes = GPU_SLICE_SCHEMAS.map((s) => `'${s.type}'`).join(','); |
| |
| await using _ = await createPerfettoTable({ |
| name: '__gpu_tracks_to_create', |
| engine: ctx.engine, |
| as: ` |
| with grouped as materialized ( |
| select |
| t.type, |
| min(t.name) as name, |
| lower(min(t.name)) as lower_name, |
| extract_arg(t.dimension_arg_set_id, 'ugpu') as ugpu, |
| t.machine_id, |
| extract_arg(t.source_arg_set_id, 'description') as description, |
| min(t.id) minTrackId, |
| group_concat(t.id) as trackIds, |
| count() as trackCount, |
| __max_layout_depth(count(), group_concat(t.id)) as maxDepth, |
| extract_arg(t.dimension_arg_set_id, 'gpu') as gpu_id, |
| g.name as gpu_name, |
| m.name as machine_name, |
| m.label_index as machine_label_index |
| from _slice_track_summary s |
| join track t using (id) |
| left join gpu g on extract_arg(t.dimension_arg_set_id, 'ugpu') = g.id |
| left join machine m on m.id = t.machine_id |
| where t.type in (${sliceTypes}) |
| group by type, t.track_group_id, ifnull(t.track_group_id, t.id), |
| extract_arg(t.dimension_arg_set_id, 'ugpu') |
| ) |
| select * from grouped |
| order by lower_name |
| `, |
| }); |
| |
| const result = await ctx.engine.query( |
| 'select * from __gpu_tracks_to_create', |
| ); |
| |
| const schemas = new Map(GPU_SLICE_SCHEMAS.map((x) => [x.type, x])); |
| const it = result.iter({ |
| type: STR, |
| name: STR_NULL, |
| gpu_id: NUM_NULL, |
| machine_id: NUM, |
| trackIds: STR, |
| maxDepth: NUM, |
| description: STR_NULL, |
| ugpu: NUM_NULL, |
| gpu_name: STR_NULL, |
| machine_name: STR_NULL, |
| machine_label_index: NUM_NULL, |
| }); |
| for (; it.valid(); it.next()) { |
| const { |
| trackIds: rawTrackIds, |
| type, |
| name, |
| maxDepth, |
| gpu_id: gpuId, |
| machine_id: machineId, |
| ugpu, |
| gpu_name: gpuName, |
| machine_name: machineName, |
| machine_label_index: machineLabelIndex, |
| } = it; |
| const schema = schemas.get(type); |
| if (schema === undefined) { |
| continue; |
| } |
| const trackIds = rawTrackIds.split(',').map((v) => Number(v)); |
| const gpu = |
| gpuId !== null |
| ? new Gpu( |
| ugpu ?? trackIds[0], |
| gpuId, |
| machineId, |
| gpuName ?? undefined, |
| machineName ?? undefined, |
| machineLabelIndex ?? undefined, |
| this.numMachines, |
| ) |
| : null; |
| const trackName = getTrackName({name, kind: SLICE_TRACK_KIND}); |
| const uri = `/slice_${ugpu ?? trackIds[0]}_${trackIds[0]}`; |
| |
| ctx.tracks.registerTrack({ |
| uri, |
| tags: { |
| kinds: [SLICE_TRACK_KIND], |
| trackIds, |
| type, |
| }, |
| renderer: await createTraceProcessorSliceTrack({ |
| trace: ctx, |
| uri, |
| maxDepth, |
| trackIds, |
| }), |
| }); |
| this.addToGpuGroup( |
| ctx, |
| schema.group, |
| schema.groupSortOrder, |
| gpu, |
| new TrackNode({ |
| uri, |
| name: trackName, |
| sortOrder: gpu?.sortOrder ?? 0, |
| }), |
| ); |
| } |
| } |
| } |