• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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