1// Copyright (C) 2025 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {DatasetSliceTrack} from '../../components/tracks/dataset_slice_track'; 16import {PerfettoPlugin} from '../../public/plugin'; 17import {Trace} from '../../public/trace'; 18import {TrackNode} from '../../public/workspace'; 19import {SourceDataset} from '../../trace_processor/dataset'; 20import {LONG, NUM, STR} from '../../trace_processor/query_result'; 21import {Engine} from '../../trace_processor/engine'; 22 23export default class implements PerfettoPlugin { 24 static readonly id = 'com.android.AvfVmCpuTimeline'; 25 26 private readonly validTargets = new Map<number, string>(); 27 28 async onTraceLoad(ctx: Trace): Promise<void> { 29 this.validTargets.clear(); 30 await this.findValidTargets(ctx.engine); 31 32 if (this.validTargets.size === 0) { 33 alert('The loaded trace does not contain any valid Avf VM targets!'); 34 } else { 35 const defaultTargetId = this.validTargets.keys().next().value; 36 await this.createTargetVmTrack(ctx, defaultTargetId); 37 38 ctx.commands.registerCommand({ 39 id: `${ctx.pluginId}#SelectAvfVmUtid`, 40 name: 'Select Avf VM utid to add track', 41 callback: async () => { 42 if (this.validTargets.size === 0) { 43 alert('Available ValidTargets set exhausted! Do Refresh...'); 44 } else { 45 const utid = await this.selectValidTarget(); 46 await this.createTargetVmTrack(ctx, utid); 47 } 48 }, 49 defaultHotkey: 'Shift+V', 50 }); 51 } 52 } 53 54 async createTargetVmTrack(ctx: Trace, targetUtid: number) { 55 const title = `Avf VM CPU Timeline utid:${targetUtid}`; 56 const uri = `com.android.AvfVmCpuTimeline#AvfVmCpuTimeline${targetUtid}`; 57 58 this.validTargets.delete(targetUtid); 59 60 const query = ` 61 SELECT 62 sched.id AS id, 63 ts, 64 dur, 65 cpu, 66 priority, 67 utid, 68 name, 69 cpu AS depth 70 FROM sched 71 JOIN thread 72 USING (utid) 73 WHERE 74 utid == ${targetUtid} 75 `; 76 77 ctx.tracks.registerTrack({ 78 uri, 79 title, 80 track: new DatasetSliceTrack({ 81 trace: ctx, 82 uri, 83 dataset: new SourceDataset({ 84 src: query, 85 schema: { 86 id: NUM, 87 ts: LONG, 88 dur: LONG, 89 cpu: NUM, 90 priority: NUM, 91 utid: NUM, 92 name: STR, 93 depth: NUM, 94 }, 95 }), 96 // Blank details panel - overrides details panel that assumes slices are 97 // from the slice table. 98 detailsPanel: () => { 99 return { 100 render: () => undefined, 101 }; 102 }, 103 }), 104 }); 105 106 const trackNode = new TrackNode({uri, title, sortOrder: -90}); 107 ctx.workspace.addChildInOrder(trackNode); 108 } 109 110 async findValidTargets(engine: Engine) { 111 const queryResult = await engine.query(` 112 SELECT 113 sched.id as id, 114 utid, 115 thread.name as threadName 116 FROM sched 117 JOIN thread 118 USING (utid) 119 WHERE threadName LIKE '%vhost%' OR threadName LIKE '%vcpu%' 120 `); 121 122 const qRow = queryResult.iter({ 123 id: NUM, 124 utid: NUM, 125 threadName: STR, 126 }); 127 while (qRow.valid()) { 128 if (!this.validTargets.has(qRow.utid)) { 129 // collect unique thread.utid in the available targets map 130 this.validTargets.set(qRow.utid, qRow.threadName); 131 } 132 qRow.next(); 133 } 134 } 135 136 async selectValidTarget(): Promise<number> { 137 const input = prompt(this.prepareSelectMessage()); 138 if (input !== null) { 139 const checkId = Number(input); 140 if (!isNaN(checkId) && this.validTargets.has(checkId)) { 141 return checkId; 142 } 143 } 144 145 const defaultTarget = this.validTargets.keys().next().value; 146 alert(`Invalid Target selected! Using default value: ${defaultTarget}`); 147 return defaultTarget; 148 } 149 150 private prepareSelectMessage(): string { 151 let message = 'Available target IDs are:\n'; 152 this.validTargets.forEach((id, name) => { 153 message += `${id} : ${name}\n`; 154 }); 155 message += `\nEnter targetID to add track:`; 156 return message; 157 } 158} 159