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