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 {removeFalsyValues} from '../../base/array_utils'; 16import {TrackNode} from '../../public/workspace'; 17import {SLICE_TRACK_KIND} from '../../public/track_kinds'; 18import {Trace} from '../../public/trace'; 19import {PerfettoPlugin} from '../../public/plugin'; 20import {getThreadUriPrefix, getTrackName} from '../../public/utils'; 21import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result'; 22import {AsyncSliceTrack} from './async_slice_track'; 23import {exists} from '../../base/utils'; 24import {assertExists, assertTrue} from '../../base/logging'; 25import {SliceSelectionAggregator} from './slice_selection_aggregator'; 26import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups'; 27 28export default class implements PerfettoPlugin { 29 static readonly id = 'dev.perfetto.AsyncSlices'; 30 static readonly dependencies = [ProcessThreadGroupsPlugin]; 31 32 async onTraceLoad(ctx: Trace): Promise<void> { 33 const trackIdsToUris = new Map<number, string>(); 34 35 await this.addGlobalAsyncTracks(ctx, trackIdsToUris); 36 await this.addProcessAsyncSliceTracks(ctx, trackIdsToUris); 37 await this.addThreadAsyncSliceTracks(ctx, trackIdsToUris); 38 39 ctx.selection.registerSqlSelectionResolver({ 40 sqlTableName: 'slice', 41 callback: async (id: number) => { 42 // Locate the track for a given id in the slice table 43 const result = await ctx.engine.query(` 44 select 45 track_id as trackId 46 from 47 slice 48 where slice.id = ${id} 49 `); 50 51 if (result.numRows() === 0) { 52 return undefined; 53 } 54 55 const {trackId} = result.firstRow({ 56 trackId: NUM, 57 }); 58 59 const trackUri = trackIdsToUris.get(trackId); 60 if (!trackUri) { 61 return undefined; 62 } 63 64 return { 65 trackUri, 66 eventId: id, 67 }; 68 }, 69 }); 70 71 ctx.selection.registerAreaSelectionAggregator( 72 new SliceSelectionAggregator(), 73 ); 74 } 75 76 async addGlobalAsyncTracks( 77 ctx: Trace, 78 trackIdsToUris: Map<number, string>, 79 ): Promise<void> { 80 const {engine} = ctx; 81 // TODO(stevegolton): The track exclusion logic is currently a hack. This will be replaced 82 // by a mechanism for more specific plugins to override tracks from more generic plugins. 83 const suspendResumeLatencyTrackName = 'Suspend/Resume Latency'; 84 const rawGlobalAsyncTracks = await engine.query(` 85 include perfetto module graphs.search; 86 include perfetto module viz.summary.tracks; 87 88 with global_tracks_grouped as ( 89 select 90 t.parent_id, 91 t.name, 92 group_concat(t.id) as trackIds, 93 count() as trackCount, 94 ifnull(min(a.order_id), 0) as order_id 95 from track t 96 join _slice_track_summary using (id) 97 left join _track_event_tracks_ordered a USING (id) 98 where 99 t.type in ('__intrinsic_track', 'gpu_track', '__intrinsic_cpu_track') 100 and (name != '${suspendResumeLatencyTrackName}' or name is null) 101 and classification not in ( 102 'linux_rpm', 103 'linux_device_frequency', 104 'irq_counter', 105 'softirq_counter', 106 'android_energy_estimation_breakdown', 107 'android_energy_estimation_breakdown_per_uid' 108 ) 109 group by parent_id, name 110 order by parent_id, order_id 111 ), 112 intermediate_groups as ( 113 select 114 t.name, 115 t.id, 116 t.parent_id, 117 ifnull(a.order_id, 0) as order_id 118 from graph_reachable_dfs!( 119 ( 120 select id as source_node_id, parent_id as dest_node_id 121 from track 122 where parent_id is not null 123 ), 124 ( 125 select distinct parent_id as node_id 126 from global_tracks_grouped 127 where parent_id is not null 128 ) 129 ) g 130 join track t on g.node_id = t.id 131 left join _track_event_tracks_ordered a USING (id) 132 ) 133 select 134 t.name as name, 135 t.parent_id as parentId, 136 t.trackIds as trackIds, 137 t.order_id as orderId, 138 __max_layout_depth(t.trackCount, t.trackIds) as maxDepth 139 from global_tracks_grouped t 140 union all 141 select 142 t.name as name, 143 t.parent_id as parentId, 144 cast_string!(t.id) as trackIds, 145 t.order_id as orderId, 146 NULL as maxDepth 147 from intermediate_groups t 148 left join _slice_track_summary s using (id) 149 where s.id is null 150 order by parentId, orderId 151 `); 152 const it = rawGlobalAsyncTracks.iter({ 153 name: STR_NULL, 154 parentId: NUM_NULL, 155 trackIds: STR, 156 orderId: NUM, 157 maxDepth: NUM_NULL, 158 }); 159 160 // Create a map of track nodes by id 161 const trackMap = new Map< 162 number, 163 {parentId: number | null; trackNode: TrackNode} 164 >(); 165 166 for (; it.valid(); it.next()) { 167 const rawName = it.name === null ? undefined : it.name; 168 const title = getTrackName({ 169 name: rawName, 170 kind: SLICE_TRACK_KIND, 171 }); 172 const rawTrackIds = it.trackIds; 173 const trackIds = rawTrackIds.split(',').map((v) => Number(v)); 174 const maxDepth = it.maxDepth; 175 176 if (maxDepth === null) { 177 assertTrue(trackIds.length == 1); 178 const trackNode = new TrackNode({title, sortOrder: -25}); 179 trackMap.set(trackIds[0], {parentId: it.parentId, trackNode}); 180 } else { 181 const uri = `/async_slices_${rawName}_${it.parentId}`; 182 ctx.tracks.registerTrack({ 183 uri, 184 title, 185 tags: { 186 trackIds, 187 kind: SLICE_TRACK_KIND, 188 scope: 'global', 189 }, 190 track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds), 191 }); 192 const trackNode = new TrackNode({ 193 uri, 194 title, 195 sortOrder: it.orderId, 196 }); 197 trackIds.forEach((id) => { 198 trackMap.set(id, {parentId: it.parentId, trackNode}); 199 trackIdsToUris.set(id, uri); 200 }); 201 } 202 } 203 204 // Attach track nodes to parents / or the workspace if they have no parent 205 trackMap.forEach(({parentId, trackNode}) => { 206 if (exists(parentId)) { 207 const parent = assertExists(trackMap.get(parentId)); 208 parent.trackNode.addChildInOrder(trackNode); 209 } else { 210 ctx.workspace.addChildInOrder(trackNode); 211 } 212 }); 213 } 214 215 async addProcessAsyncSliceTracks( 216 ctx: Trace, 217 trackIdsToUris: Map<number, string>, 218 ): Promise<void> { 219 const result = await ctx.engine.query(` 220 select 221 upid, 222 t.name as trackName, 223 t.track_ids as trackIds, 224 process.name as processName, 225 process.pid as pid, 226 t.parent_id as parentId, 227 __max_layout_depth(t.track_count, t.track_ids) as maxDepth 228 from _process_track_summary_by_upid_and_parent_id_and_name t 229 join process using (upid) 230 where t.name is null or t.name not glob "* Timeline" 231 `); 232 233 const it = result.iter({ 234 upid: NUM, 235 parentId: NUM_NULL, 236 trackName: STR_NULL, 237 trackIds: STR, 238 processName: STR_NULL, 239 pid: NUM_NULL, 240 maxDepth: NUM, 241 }); 242 243 const trackMap = new Map< 244 number, 245 {parentId: number | null; upid: number; trackNode: TrackNode} 246 >(); 247 248 for (; it.valid(); it.next()) { 249 const upid = it.upid; 250 const trackName = it.trackName; 251 const rawTrackIds = it.trackIds; 252 const trackIds = rawTrackIds.split(',').map((v) => Number(v)); 253 const processName = it.processName; 254 const pid = it.pid; 255 const maxDepth = it.maxDepth; 256 257 const kind = SLICE_TRACK_KIND; 258 const title = getTrackName({ 259 name: trackName, 260 upid, 261 pid, 262 processName, 263 kind, 264 }); 265 266 const uri = `/process_${upid}/async_slices_${rawTrackIds}`; 267 ctx.tracks.registerTrack({ 268 uri, 269 title, 270 tags: { 271 trackIds, 272 kind: SLICE_TRACK_KIND, 273 scope: 'process', 274 upid, 275 }, 276 track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds), 277 }); 278 const track = new TrackNode({uri, title, sortOrder: 30}); 279 trackIds.forEach((id) => { 280 trackMap.set(id, {trackNode: track, parentId: it.parentId, upid}); 281 trackIdsToUris.set(id, uri); 282 }); 283 } 284 285 // Attach track nodes to parents / or the workspace if they have no parent 286 trackMap.forEach((t) => { 287 const parent = exists(t.parentId) && trackMap.get(t.parentId); 288 if (parent !== false && parent !== undefined) { 289 parent.trackNode.addChildInOrder(t.trackNode); 290 } else { 291 const processGroup = ctx.plugins 292 .getPlugin(ProcessThreadGroupsPlugin) 293 .getGroupForProcess(t.upid); 294 processGroup?.addChildInOrder(t.trackNode); 295 } 296 }); 297 } 298 299 async addThreadAsyncSliceTracks( 300 ctx: Trace, 301 trackIdsToUris: Map<number, string>, 302 ): Promise<void> { 303 const result = await ctx.engine.query(` 304 include perfetto module viz.summary.slices; 305 include perfetto module viz.summary.threads; 306 include perfetto module viz.threads; 307 308 select 309 t.utid, 310 t.parent_id as parentId, 311 thread.upid, 312 t.name as trackName, 313 thread.name as threadName, 314 thread.tid as tid, 315 t.track_ids as trackIds, 316 __max_layout_depth(t.track_count, t.track_ids) as maxDepth, 317 k.is_main_thread as isMainThread, 318 k.is_kernel_thread AS isKernelThread 319 from _thread_track_summary_by_utid_and_name t 320 join _threads_with_kernel_flag k using(utid) 321 join thread using (utid) 322 `); 323 324 const it = result.iter({ 325 utid: NUM, 326 parentId: NUM_NULL, 327 upid: NUM_NULL, 328 trackName: STR_NULL, 329 trackIds: STR, 330 maxDepth: NUM, 331 isMainThread: NUM_NULL, 332 isKernelThread: NUM, 333 threadName: STR_NULL, 334 tid: NUM_NULL, 335 }); 336 337 const trackMap = new Map< 338 number, 339 {parentId: number | null; utid: number; trackNode: TrackNode} 340 >(); 341 342 for (; it.valid(); it.next()) { 343 const { 344 utid, 345 parentId, 346 upid, 347 trackName, 348 isMainThread, 349 isKernelThread, 350 maxDepth, 351 threadName, 352 tid, 353 } = it; 354 const rawTrackIds = it.trackIds; 355 const trackIds = rawTrackIds.split(',').map((v) => Number(v)); 356 const title = getTrackName({ 357 name: trackName, 358 utid, 359 tid, 360 threadName, 361 kind: 'Slices', 362 }); 363 364 const uri = `/${getThreadUriPrefix(upid, utid)}_slice_${rawTrackIds}`; 365 ctx.tracks.registerTrack({ 366 uri, 367 title, 368 tags: { 369 trackIds, 370 kind: SLICE_TRACK_KIND, 371 scope: 'thread', 372 utid, 373 upid: upid ?? undefined, 374 ...(isKernelThread === 1 && {kernelThread: true}), 375 }, 376 chips: removeFalsyValues([ 377 isKernelThread === 0 && isMainThread === 1 && 'main thread', 378 ]), 379 track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds), 380 }); 381 const track = new TrackNode({uri, title, sortOrder: 20}); 382 trackIds.forEach((id) => { 383 trackMap.set(id, {trackNode: track, parentId, utid}); 384 trackIdsToUris.set(id, uri); 385 }); 386 } 387 388 // Attach track nodes to parents / or the workspace if they have no parent 389 trackMap.forEach((t) => { 390 const parent = exists(t.parentId) && trackMap.get(t.parentId); 391 if (parent !== false && parent !== undefined) { 392 parent.trackNode.addChildInOrder(t.trackNode); 393 } else { 394 const group = ctx.plugins 395 .getPlugin(ProcessThreadGroupsPlugin) 396 .getGroupForThread(t.utid); 397 group?.addChildInOrder(t.trackNode); 398 } 399 }); 400 } 401} 402