1// Copyright (C) 2024 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 {Trace} from '../../public/trace'; 16import {maybeMachineLabel} from '../../base/multi_machine_trace'; 17import {PerfettoPlugin} from '../../public/plugin'; 18import {TrackNode} from '../../public/workspace'; 19import {NUM, STR, STR_NULL} from '../../trace_processor/query_result'; 20 21function stripPathFromExecutable(path: string) { 22 if (path[0] === '/') { 23 return path.split('/').slice(-1)[0]; 24 } else { 25 return path; 26 } 27} 28 29function getThreadDisplayName(threadName: string | undefined, tid: number) { 30 if (threadName) { 31 return `${stripPathFromExecutable(threadName)} ${tid}`; 32 } else { 33 return `Thread ${tid}`; 34 } 35} 36 37// This plugin is responsible for organizing all process and thread groups 38// including the kernel groups, sorting, and adding summary tracks. 39export default class implements PerfettoPlugin { 40 static readonly id = 'dev.perfetto.ProcessThreadGroups'; 41 42 private readonly processGroups = new Map<number, TrackNode>(); 43 private readonly threadGroups = new Map<number, TrackNode>(); 44 45 constructor(private readonly ctx: Trace) {} 46 47 getGroupForProcess(upid: number): TrackNode | undefined { 48 return this.processGroups.get(upid); 49 } 50 51 getGroupForThread(utid: number): TrackNode | undefined { 52 return this.threadGroups.get(utid); 53 } 54 55 async onTraceLoad(ctx: Trace): Promise<void> { 56 // Pre-group all kernel "threads" (actually processes) if this is a linux 57 // system trace. Below, addProcessTrackGroups will skip them due to an 58 // existing group uuid, and addThreadStateTracks will fill in the 59 // per-thread tracks. Quirk: since all threads will appear to be 60 // TrackKindPriority.MAIN_THREAD, any process-level tracks will end up 61 // pushed to the bottom of the group in the UI. 62 await this.addKernelThreadGrouping(); 63 64 // Create the per-process track groups. Note that this won't necessarily 65 // create a track per process. If a process has been completely idle and has 66 // no sched events, no track group will be emitted. 67 // Will populate this.addTrackGroupActions 68 await this.addProcessGroups(); 69 await this.addThreadGroups(); 70 71 ctx.onTraceReady.addListener(() => { 72 // If, by the time the trace has finished loading, some of the process or 73 // thread group tracks nodes have no children, just remove them. 74 const removeIfEmpty = (g: TrackNode) => { 75 if (!g.hasChildren) { 76 g.remove(); 77 } 78 }; 79 this.processGroups.forEach(removeIfEmpty); 80 this.threadGroups.forEach(removeIfEmpty); 81 }); 82 } 83 84 private async addKernelThreadGrouping(): Promise<void> { 85 // Identify kernel threads if this is a linux system trace, and sufficient 86 // process information is available. Kernel threads are identified by being 87 // children of kthreadd (always pid 2). 88 // The query will return the kthreadd process row first, which must exist 89 // for any other kthreads to be returned by the query. 90 // TODO(rsavitski): figure out how to handle the idle process (swapper), 91 // which has pid 0 but appears as a distinct process (with its own comm) on 92 // each cpu. It'd make sense to exclude its thread state track, but still 93 // put process-scoped tracks in this group. 94 const result = await this.ctx.engine.query(` 95 select 96 t.utid, p.upid, (case p.pid when 2 then 1 else 0 end) isKthreadd 97 from 98 thread t 99 join process p using (upid) 100 left join process parent on (p.parent_upid = parent.upid) 101 join 102 (select true from metadata m 103 where (m.name = 'system_name' and m.str_value = 'Linux') 104 union 105 select 1 from (select true from sched limit 1)) 106 where 107 p.pid = 2 or parent.pid = 2 108 order by isKthreadd desc 109 `); 110 111 const it = result.iter({ 112 utid: NUM, 113 upid: NUM, 114 }); 115 116 // Not applying kernel thread grouping. 117 if (!it.valid()) { 118 return; 119 } 120 121 // Create the track group. Use kthreadd's PROCESS_SUMMARY_TRACK for the 122 // main track. It doesn't summarise the kernel threads within the group, 123 // but creating a dedicated track type is out of scope at the time of 124 // writing. 125 const kernelThreadsGroup = new TrackNode({ 126 title: 'Kernel threads', 127 uri: '/kernel', 128 sortOrder: 50, 129 isSummary: true, 130 }); 131 this.ctx.workspace.addChildInOrder(kernelThreadsGroup); 132 133 // Set the group for all kernel threads (including kthreadd itself). 134 for (; it.valid(); it.next()) { 135 const {utid} = it; 136 137 const threadGroup = new TrackNode({ 138 uri: `thread${utid}`, 139 title: `Thread ${utid}`, 140 isSummary: true, 141 headless: true, 142 }); 143 kernelThreadsGroup.addChildInOrder(threadGroup); 144 this.threadGroups.set(utid, threadGroup); 145 } 146 } 147 148 // Adds top level groups for processes and thread that don't belong to a 149 // process. 150 private async addProcessGroups(): Promise<void> { 151 const result = await this.ctx.engine.query(` 152 with processGroups as ( 153 select 154 upid, 155 process.pid as pid, 156 process.name as processName, 157 sum_running_dur as sumRunningDur, 158 thread_slice_count + process_slice_count as sliceCount, 159 perf_sample_count as perfSampleCount, 160 instruments_sample_count as instrumentsSampleCount, 161 allocation_count as heapProfileAllocationCount, 162 graph_object_count as heapGraphObjectCount, 163 ( 164 select group_concat(string_value) 165 from args 166 where 167 process.arg_set_id is not null and 168 arg_set_id = process.arg_set_id and 169 flat_key = 'chrome.process_label' 170 ) chromeProcessLabels, 171 case process.name 172 when 'Browser' then 3 173 when 'Gpu' then 2 174 when 'Renderer' then 1 175 else 0 176 end as chromeProcessRank, 177 ifnull(machine_id, 0) as machine 178 from _process_available_info_summary 179 join process using(upid) 180 ), 181 threadGroups as ( 182 select 183 utid, 184 tid, 185 thread.name as threadName, 186 sum_running_dur as sumRunningDur, 187 slice_count as sliceCount, 188 perf_sample_count as perfSampleCount, 189 instruments_sample_count as instrumentsSampleCount, 190 ifnull(machine_id, 0) as machine 191 from _thread_available_info_summary 192 join thread using (utid) 193 where upid is null 194 ) 195 select * 196 from ( 197 select 198 'process' as kind, 199 upid as uid, 200 pid as id, 201 processName as name, 202 machine 203 from processGroups 204 order by 205 chromeProcessRank desc, 206 heapProfileAllocationCount desc, 207 heapGraphObjectCount desc, 208 perfSampleCount desc, 209 instrumentsSampleCount desc, 210 sumRunningDur desc, 211 sliceCount desc, 212 processName asc, 213 upid asc 214 ) 215 union all 216 select * 217 from ( 218 select 219 'thread' as kind, 220 utid as uid, 221 tid as id, 222 threadName as name, 223 machine 224 from threadGroups 225 order by 226 perfSampleCount desc, 227 instrumentsSampleCount desc, 228 sumRunningDur desc, 229 sliceCount desc, 230 threadName asc, 231 utid asc 232 ) 233 `); 234 235 const it = result.iter({ 236 kind: STR, 237 uid: NUM, 238 id: NUM, 239 name: STR_NULL, 240 machine: NUM, 241 }); 242 for (; it.valid(); it.next()) { 243 const {kind, uid, id, name} = it; 244 245 if (kind === 'process') { 246 // Ignore kernel process groups 247 if (this.processGroups.has(uid)) { 248 continue; 249 } 250 251 const machineLabel = maybeMachineLabel(it.machine); 252 function getProcessDisplayName( 253 processName: string | undefined, 254 pid: number, 255 ) { 256 if (processName) { 257 return `${stripPathFromExecutable(processName)} ${pid}${ 258 machineLabel 259 }`; 260 } else { 261 return `Process ${pid}${machineLabel}`; 262 } 263 } 264 265 const displayName = getProcessDisplayName(name ?? undefined, id); 266 const group = new TrackNode({ 267 uri: `/process_${uid}`, 268 title: displayName, 269 isSummary: true, 270 sortOrder: 50, 271 }); 272 273 // Re-insert the child node to sort it 274 this.ctx.workspace.addChildInOrder(group); 275 this.processGroups.set(uid, group); 276 } else { 277 // Ignore kernel process groups 278 if (this.threadGroups.has(uid)) { 279 continue; 280 } 281 282 const displayName = getThreadDisplayName(name ?? undefined, id); 283 const group = new TrackNode({ 284 uri: `/thread_${uid}`, 285 title: displayName, 286 isSummary: true, 287 sortOrder: 50, 288 }); 289 290 // Re-insert the child node to sort it 291 this.ctx.workspace.addChildInOrder(group); 292 this.threadGroups.set(uid, group); 293 } 294 } 295 } 296 297 // Create all the nested & headless thread groups that live inside existing 298 // process groups. 299 private async addThreadGroups(): Promise<void> { 300 const result = await this.ctx.engine.query(` 301 with threadGroups as ( 302 select 303 utid, 304 upid, 305 tid, 306 thread.name as threadName, 307 CASE 308 WHEN thread.is_main_thread = 1 THEN 10 309 WHEN thread.name = 'CrBrowserMain' THEN 10 310 WHEN thread.name = 'CrRendererMain' THEN 10 311 WHEN thread.name = 'CrGpuMain' THEN 10 312 WHEN thread.name glob '*RenderThread*' THEN 9 313 WHEN thread.name glob '*GPU completion*' THEN 8 314 WHEN thread.name = 'Chrome_ChildIOThread' THEN 7 315 WHEN thread.name = 'Chrome_IOThread' THEN 7 316 WHEN thread.name = 'Compositor' THEN 6 317 WHEN thread.name = 'VizCompositorThread' THEN 6 318 ELSE 5 319 END as priority 320 from _thread_available_info_summary 321 join thread using (utid) 322 where upid is not null 323 ) 324 select * 325 from ( 326 select 327 utid, 328 upid, 329 tid, 330 threadName 331 from threadGroups 332 order by 333 priority desc, 334 tid asc 335 ) 336 `); 337 338 const it = result.iter({ 339 utid: NUM, 340 tid: NUM, 341 upid: NUM, 342 threadName: STR_NULL, 343 }); 344 for (; it.valid(); it.next()) { 345 const {utid, tid, upid, threadName} = it; 346 347 // Ignore kernel thread groups 348 if (this.threadGroups.has(utid)) { 349 continue; 350 } 351 352 const group = new TrackNode({ 353 uri: `/thread_${utid}`, 354 title: getThreadDisplayName(threadName ?? undefined, tid), 355 isSummary: true, 356 headless: true, 357 }); 358 this.threadGroups.set(utid, group); 359 this.processGroups.get(upid)?.addChildInOrder(group); 360 } 361 } 362} 363