1// Copyright (C) 2021 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 { 16 NUM_NULL, 17 STR_NULL, 18 LONG_NULL, 19 NUM, 20 Plugin, 21 PluginContextTrace, 22 PluginDescriptor, 23 PrimaryTrackSortKey, 24 STR, 25 LONG, 26 Engine, 27} from '../../public'; 28import {getTrackName} from '../../public/utils'; 29import {CounterOptions} from '../../frontend/base_counter_track'; 30import {TraceProcessorCounterTrack} from './trace_processor_counter_track'; 31import {CounterDetailsPanel} from './counter_details_panel'; 32import {Time, duration, time} from '../../base/time'; 33import {Optional} from '../../base/utils'; 34 35export const COUNTER_TRACK_KIND = 'CounterTrack'; 36 37const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$'); 38const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:'); 39 40type Modes = CounterOptions['yMode']; 41 42// Sets the default 'mode' for counter tracks. If the regex matches 43// then the paired mode is used. Entries are in priority order so the 44// first match wins. 45const COUNTER_REGEX: [RegExp, Modes][] = [ 46 // Power counters make more sense in rate mode since you're typically 47 // interested in the slope of the graph rather than the absolute 48 // value. 49 [new RegExp('^power..*$'), 'rate'], 50 // Same for cumulative PSI stall time counters, e.g., psi.cpu.some. 51 [new RegExp('^psi..*$'), 'rate'], 52 // Same for network counters. 53 [NETWORK_TRACK_REGEX, 'rate'], 54 // Entity residency 55 [ENTITY_RESIDENCY_REGEX, 'rate'], 56]; 57 58function getCounterMode(name: string): Modes | undefined { 59 for (const [re, mode] of COUNTER_REGEX) { 60 if (name.match(re)) { 61 return mode; 62 } 63 } 64 return undefined; 65} 66 67function getDefaultCounterOptions(name: string): Partial<CounterOptions> { 68 const options: Partial<CounterOptions> = {}; 69 options.yMode = getCounterMode(name); 70 71 if (name.endsWith('_pct')) { 72 options.yOverrideMinimum = 0; 73 options.yOverrideMaximum = 100; 74 options.unit = '%'; 75 } 76 77 if (name.startsWith('power.')) { 78 options.yRangeSharingKey = 'power'; 79 } 80 81 if (name.startsWith('mem.')) { 82 options.yRangeSharingKey = 'mem'; 83 } 84 85 if (name.startsWith('battery_stats.')) { 86 options.yRangeSharingKey = 'battery_stats'; 87 } 88 89 // All 'Entity residency: foo bar1234' tracks should share a y-axis 90 // with 'Entity residency: foo baz5678' etc tracks: 91 { 92 const r = new RegExp('Entity residency: ([^ ]+) '); 93 const m = r.exec(name); 94 if (m) { 95 options.yRangeSharingKey = `entity-residency-${m[1]}`; 96 } 97 } 98 99 { 100 const r = new RegExp('GPU .* Frequency'); 101 const m = r.exec(name); 102 if (m) { 103 options.yRangeSharingKey = 'gpu-frequency'; 104 } 105 } 106 107 return options; 108} 109 110async function getCounterEventBounds( 111 engine: Engine, 112 trackId: number, 113 id: number, 114): Promise<Optional<{ts: time; dur: duration}>> { 115 const query = ` 116 WITH CTE AS ( 117 SELECT 118 id, 119 ts as leftTs, 120 LEAD(ts) OVER (ORDER BY ts) AS rightTs 121 FROM counter 122 WHERE track_id = ${trackId} 123 ) 124 SELECT * FROM CTE WHERE id = ${id} 125 `; 126 127 const counter = await engine.query(query); 128 const row = counter.iter({ 129 leftTs: LONG, 130 rightTs: LONG_NULL, 131 }); 132 const leftTs = Time.fromRaw(row.leftTs); 133 const rightTs = row.rightTs !== null ? Time.fromRaw(row.rightTs) : leftTs; 134 const duration = rightTs - leftTs; 135 return {ts: leftTs, dur: duration}; 136} 137 138class CounterPlugin implements Plugin { 139 async onTraceLoad(ctx: PluginContextTrace): Promise<void> { 140 await this.addCounterTracks(ctx); 141 await this.addGpuFrequencyTracks(ctx); 142 await this.addCpuFreqLimitCounterTracks(ctx); 143 await this.addCpuPerfCounterTracks(ctx); 144 await this.addThreadCounterTracks(ctx); 145 await this.addProcessCounterTracks(ctx); 146 } 147 148 private async addCounterTracks(ctx: PluginContextTrace) { 149 const result = await ctx.engine.query(` 150 select name, id, unit 151 from ( 152 select name, id, unit 153 from counter_track 154 join _counter_track_summary using (id) 155 where type = 'counter_track' 156 union 157 select name, id, unit 158 from gpu_counter_track 159 join _counter_track_summary using (id) 160 where name != 'gpufreq' 161 ) 162 order by name 163 `); 164 165 // Add global or GPU counter tracks that are not bound to any pid/tid. 166 const it = result.iter({ 167 name: STR, 168 unit: STR_NULL, 169 id: NUM, 170 }); 171 172 for (; it.valid(); it.next()) { 173 const trackId = it.id; 174 const displayName = it.name; 175 const unit = it.unit ?? undefined; 176 ctx.registerStaticTrack({ 177 uri: `perfetto.Counter#${trackId}`, 178 displayName, 179 kind: COUNTER_TRACK_KIND, 180 trackIds: [trackId], 181 trackFactory: (trackCtx) => { 182 return new TraceProcessorCounterTrack({ 183 engine: ctx.engine, 184 trackKey: trackCtx.trackKey, 185 trackId, 186 options: { 187 ...getDefaultCounterOptions(displayName), 188 unit, 189 }, 190 }); 191 }, 192 sortKey: PrimaryTrackSortKey.COUNTER_TRACK, 193 detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, displayName), 194 getEventBounds: async (id) => { 195 return await getCounterEventBounds(ctx.engine, trackId, id); 196 }, 197 }); 198 } 199 } 200 201 async addCpuFreqLimitCounterTracks(ctx: PluginContextTrace): Promise<void> { 202 const cpuFreqLimitCounterTracksSql = ` 203 select name, id 204 from cpu_counter_track 205 join _counter_track_summary using (id) 206 where name glob "Cpu * Freq Limit" 207 order by name asc 208 `; 209 210 this.addCpuCounterTracks(ctx, cpuFreqLimitCounterTracksSql); 211 } 212 213 async addCpuPerfCounterTracks(ctx: PluginContextTrace): Promise<void> { 214 // Perf counter tracks are bound to CPUs, follow the scheduling and 215 // frequency track naming convention ("Cpu N ..."). 216 // Note: we might not have a track for a given cpu if no data was seen from 217 // it. This might look surprising in the UI, but placeholder tracks are 218 // wasteful as there's no way of collapsing global counter tracks at the 219 // moment. 220 const addCpuPerfCounterTracksSql = ` 221 select printf("Cpu %u %s", cpu, name) as name, id 222 from perf_counter_track as pct 223 join _counter_track_summary using (id) 224 order by perf_session_id asc, pct.name asc, cpu asc 225 `; 226 this.addCpuCounterTracks(ctx, addCpuPerfCounterTracksSql); 227 } 228 229 async addCpuCounterTracks( 230 ctx: PluginContextTrace, 231 sql: string, 232 ): Promise<void> { 233 const result = await ctx.engine.query(sql); 234 235 const it = result.iter({ 236 name: STR, 237 id: NUM, 238 }); 239 240 for (; it.valid(); it.next()) { 241 const name = it.name; 242 const trackId = it.id; 243 ctx.registerTrack({ 244 uri: `perfetto.Counter#cpu${trackId}`, 245 displayName: name, 246 kind: COUNTER_TRACK_KIND, 247 trackIds: [trackId], 248 trackFactory: (trackCtx) => { 249 return new TraceProcessorCounterTrack({ 250 engine: ctx.engine, 251 trackKey: trackCtx.trackKey, 252 trackId: trackId, 253 options: getDefaultCounterOptions(name), 254 }); 255 }, 256 detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name), 257 getEventBounds: async (id) => { 258 return await getCounterEventBounds(ctx.engine, trackId, id); 259 }, 260 }); 261 } 262 } 263 264 async addThreadCounterTracks(ctx: PluginContextTrace): Promise<void> { 265 const result = await ctx.engine.query(` 266 select 267 thread_counter_track.name as trackName, 268 utid, 269 upid, 270 tid, 271 thread.name as threadName, 272 thread_counter_track.id as trackId, 273 thread.start_ts as startTs, 274 thread.end_ts as endTs 275 from thread_counter_track 276 join _counter_track_summary using (id) 277 join thread using(utid) 278 where thread_counter_track.name != 'thread_time' 279 `); 280 281 const it = result.iter({ 282 startTs: LONG_NULL, 283 trackId: NUM, 284 endTs: LONG_NULL, 285 trackName: STR_NULL, 286 utid: NUM, 287 upid: NUM_NULL, 288 tid: NUM_NULL, 289 threadName: STR_NULL, 290 }); 291 for (; it.valid(); it.next()) { 292 const utid = it.utid; 293 const tid = it.tid; 294 const trackId = it.trackId; 295 const trackName = it.trackName; 296 const threadName = it.threadName; 297 const kind = COUNTER_TRACK_KIND; 298 const name = getTrackName({ 299 name: trackName, 300 utid, 301 tid, 302 kind, 303 threadName, 304 threadTrack: true, 305 }); 306 ctx.registerTrack({ 307 uri: `perfetto.Counter#thread${trackId}`, 308 displayName: name, 309 kind, 310 trackIds: [trackId], 311 trackFactory: (trackCtx) => { 312 return new TraceProcessorCounterTrack({ 313 engine: ctx.engine, 314 trackKey: trackCtx.trackKey, 315 trackId: trackId, 316 options: getDefaultCounterOptions(name), 317 }); 318 }, 319 detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name), 320 getEventBounds: async (id) => { 321 return await getCounterEventBounds(ctx.engine, trackId, id); 322 }, 323 }); 324 } 325 } 326 327 async addProcessCounterTracks(ctx: PluginContextTrace): Promise<void> { 328 const result = await ctx.engine.query(` 329 select 330 process_counter_track.id as trackId, 331 process_counter_track.name as trackName, 332 upid, 333 process.pid, 334 process.name as processName 335 from process_counter_track 336 join _counter_track_summary using (id) 337 join process using(upid); 338 `); 339 const it = result.iter({ 340 trackId: NUM, 341 trackName: STR_NULL, 342 upid: NUM, 343 pid: NUM_NULL, 344 processName: STR_NULL, 345 }); 346 for (let i = 0; it.valid(); ++i, it.next()) { 347 const trackId = it.trackId; 348 const pid = it.pid; 349 const trackName = it.trackName; 350 const upid = it.upid; 351 const processName = it.processName; 352 const kind = COUNTER_TRACK_KIND; 353 const name = getTrackName({ 354 name: trackName, 355 upid, 356 pid, 357 kind, 358 processName, 359 }); 360 ctx.registerTrack({ 361 uri: `perfetto.Counter#process${trackId}`, 362 displayName: name, 363 kind: COUNTER_TRACK_KIND, 364 trackIds: [trackId], 365 trackFactory: (trackCtx) => { 366 return new TraceProcessorCounterTrack({ 367 engine: ctx.engine, 368 trackKey: trackCtx.trackKey, 369 trackId: trackId, 370 options: getDefaultCounterOptions(name), 371 }); 372 }, 373 detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name), 374 getEventBounds: async (id) => { 375 return await getCounterEventBounds(ctx.engine, trackId, id); 376 }, 377 }); 378 } 379 } 380 381 private async addGpuFrequencyTracks(ctx: PluginContextTrace) { 382 const engine = ctx.engine; 383 const numGpus = ctx.trace.gpuCount; 384 385 for (let gpu = 0; gpu < numGpus; gpu++) { 386 // Only add a gpu freq track if we have 387 // gpu freq data. 388 const freqExistsResult = await engine.query(` 389 select id 390 from gpu_counter_track 391 join _counter_track_summary using (id) 392 where name = 'gpufreq' and gpu_id = ${gpu} 393 limit 1; 394 `); 395 if (freqExistsResult.numRows() > 0) { 396 const trackId = freqExistsResult.firstRow({id: NUM}).id; 397 const uri = `perfetto.Counter#gpu_freq${gpu}`; 398 const name = `Gpu ${gpu} Frequency`; 399 ctx.registerTrack({ 400 uri, 401 displayName: name, 402 kind: COUNTER_TRACK_KIND, 403 trackIds: [trackId], 404 trackFactory: (trackCtx) => { 405 return new TraceProcessorCounterTrack({ 406 engine: ctx.engine, 407 trackKey: trackCtx.trackKey, 408 trackId: trackId, 409 options: getDefaultCounterOptions(name), 410 }); 411 }, 412 detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name), 413 getEventBounds: async (id) => { 414 return await getCounterEventBounds(ctx.engine, trackId, id); 415 }, 416 }); 417 } 418 } 419 } 420} 421 422export const plugin: PluginDescriptor = { 423 pluginId: 'perfetto.Counter', 424 plugin: CounterPlugin, 425}; 426