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