1// Copyright (C) 2023 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use size 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 m from 'mithril'; 16 17import {Time, time} from '../base/time'; 18import {raf} from '../core/raf_scheduler'; 19import {Anchor} from '../widgets/anchor'; 20import {Button} from '../widgets/button'; 21import {DetailsShell} from '../widgets/details_shell'; 22import {GridLayout} from '../widgets/grid_layout'; 23import {Section} from '../widgets/section'; 24import {SqlRef} from '../widgets/sql_ref'; 25import {Tree, TreeNode} from '../widgets/tree'; 26import {Intent} from '../widgets/common'; 27 28import {BottomTab, NewBottomTabArgs} from './bottom_tab'; 29import {SchedSqlId, ThreadStateSqlId} from './sql_types'; 30import { 31 getFullThreadName, 32 getProcessName, 33 getThreadName, 34 ThreadInfo, 35} from './thread_and_process_info'; 36import { 37 getThreadState, 38 getThreadStateFromConstraints, 39 goToSchedSlice, 40 ThreadState, 41 ThreadStateRef, 42} from './thread_state'; 43import {DurationWidget, renderDuration} from './widgets/duration'; 44import {Timestamp} from './widgets/timestamp'; 45import {addDebugSliceTrack} from './debug_tracks/debug_tracks'; 46import {globals} from './globals'; 47 48interface ThreadStateTabConfig { 49 // Id into |thread_state| sql table. 50 readonly id: ThreadStateSqlId; 51} 52 53interface RelatedThreadStates { 54 prev?: ThreadState; 55 next?: ThreadState; 56 waker?: ThreadState; 57 wakee?: ThreadState[]; 58} 59 60export class ThreadStateTab extends BottomTab<ThreadStateTabConfig> { 61 static readonly kind = 'dev.perfetto.ThreadStateTab'; 62 63 state?: ThreadState; 64 relatedStates?: RelatedThreadStates; 65 loaded: boolean = false; 66 67 static create(args: NewBottomTabArgs<ThreadStateTabConfig>): ThreadStateTab { 68 return new ThreadStateTab(args); 69 } 70 71 constructor(args: NewBottomTabArgs<ThreadStateTabConfig>) { 72 super(args); 73 74 this.load().then(() => { 75 this.loaded = true; 76 raf.scheduleFullRedraw(); 77 }); 78 } 79 80 async load() { 81 this.state = await getThreadState(this.engine, this.config.id); 82 83 if (!this.state) { 84 return; 85 } 86 87 const relatedStates: RelatedThreadStates = {}; 88 relatedStates.prev = ( 89 await getThreadStateFromConstraints(this.engine, { 90 filters: [ 91 `ts + dur = ${this.state.ts}`, 92 `utid = ${this.state.thread?.utid}`, 93 ], 94 limit: 1, 95 }) 96 )[0]; 97 relatedStates.next = ( 98 await getThreadStateFromConstraints(this.engine, { 99 filters: [ 100 `ts = ${this.state.ts + this.state.dur}`, 101 `utid = ${this.state.thread?.utid}`, 102 ], 103 limit: 1, 104 }) 105 )[0]; 106 if (this.state.wakerThread?.utid !== undefined) { 107 relatedStates.waker = ( 108 await getThreadStateFromConstraints(this.engine, { 109 filters: [ 110 `utid = ${this.state.wakerThread?.utid}`, 111 `ts <= ${this.state.ts}`, 112 `ts + dur >= ${this.state.ts}`, 113 ], 114 }) 115 )[0]; 116 } 117 relatedStates.wakee = await getThreadStateFromConstraints(this.engine, { 118 filters: [ 119 `waker_utid = ${this.state.thread?.utid}`, 120 `state = 'R'`, 121 `ts >= ${this.state.ts}`, 122 `ts <= ${this.state.ts + this.state.dur}`, 123 ], 124 }); 125 126 this.relatedStates = relatedStates; 127 } 128 129 getTitle() { 130 // TODO(altimin): Support dynamic titles here. 131 return 'Current Selection'; 132 } 133 134 viewTab() { 135 // TODO(altimin/stevegolton): Differentiate between "Current Selection" and 136 // "Pinned" views in DetailsShell. 137 return m( 138 DetailsShell, 139 {title: 'Thread State', description: this.renderLoadingText()}, 140 m( 141 GridLayout, 142 m( 143 Section, 144 {title: 'Details'}, 145 this.state && this.renderTree(this.state), 146 ), 147 m( 148 Section, 149 {title: 'Related thread states'}, 150 this.renderRelatedThreadStates(), 151 ), 152 ), 153 ); 154 } 155 156 private renderLoadingText() { 157 if (!this.loaded) { 158 return 'Loading'; 159 } 160 if (!this.state) { 161 return `Thread state ${this.config.id} does not exist`; 162 } 163 // TODO(stevegolton): Return something intelligent here. 164 return this.config.id; 165 } 166 167 private renderTree(state: ThreadState) { 168 const thread = state.thread; 169 const process = state.thread?.process; 170 return m( 171 Tree, 172 m(TreeNode, { 173 left: 'Start time', 174 right: m(Timestamp, {ts: state.ts}), 175 }), 176 m(TreeNode, { 177 left: 'Duration', 178 right: m(DurationWidget, {dur: state.dur}), 179 }), 180 m(TreeNode, { 181 left: 'State', 182 right: this.renderState( 183 state.state, 184 state.cpu, 185 state.schedSqlId, 186 state.ts, 187 ), 188 }), 189 state.blockedFunction && 190 m(TreeNode, { 191 left: 'Blocked function', 192 right: state.blockedFunction, 193 }), 194 process && 195 m(TreeNode, { 196 left: 'Process', 197 right: getProcessName(process), 198 }), 199 thread && m(TreeNode, {left: 'Thread', right: getThreadName(thread)}), 200 state.wakerThread && this.renderWakerThread(state.wakerThread), 201 m(TreeNode, { 202 left: 'SQL ID', 203 right: m(SqlRef, {table: 'thread_state', id: state.threadStateSqlId}), 204 }), 205 ); 206 } 207 208 private renderState( 209 state: string, 210 cpu: number | undefined, 211 id: SchedSqlId | undefined, 212 ts: time, 213 ): m.Children { 214 if (!state) { 215 return null; 216 } 217 if (id === undefined || cpu === undefined) { 218 return state; 219 } 220 return m( 221 Anchor, 222 { 223 title: 'Go to CPU slice', 224 icon: 'call_made', 225 onclick: () => goToSchedSlice(cpu, id, ts), 226 }, 227 `${state} on CPU ${cpu}`, 228 ); 229 } 230 231 private renderWakerThread(wakerThread: ThreadInfo) { 232 return m( 233 TreeNode, 234 {left: 'Waker'}, 235 m(TreeNode, { 236 left: 'Process', 237 right: getProcessName(wakerThread.process), 238 }), 239 m(TreeNode, {left: 'Thread', right: getThreadName(wakerThread)}), 240 ); 241 } 242 243 private renderRelatedThreadStates(): m.Children { 244 if (this.state === undefined || this.relatedStates === undefined) { 245 return 'Loading'; 246 } 247 const startTs = this.state.ts; 248 const renderRef = (state: ThreadState, name?: string) => 249 m(ThreadStateRef, { 250 id: state.threadStateSqlId, 251 ts: state.ts, 252 dur: state.dur, 253 utid: state.thread!.utid, 254 name, 255 }); 256 257 const sliceColumns = {ts: 'ts', dur: 'dur', name: 'name'}; 258 const sliceColumnNames = ['id', 'utid', 'ts', 'dur', 'name', 'table_name']; 259 260 const sliceLiteColumns = {ts: 'ts', dur: 'dur', name: 'thread_name'}; 261 const sliceLiteColumnNames = [ 262 'id', 263 'utid', 264 'ts', 265 'dur', 266 'thread_name', 267 'process_name', 268 'table_name', 269 ]; 270 271 const nameForNextOrPrev = (state: ThreadState) => 272 `${state.state} for ${renderDuration(state.dur)}`; 273 return [ 274 m( 275 Tree, 276 this.relatedStates.waker && 277 m(TreeNode, { 278 left: 'Waker', 279 right: renderRef( 280 this.relatedStates.waker, 281 getFullThreadName(this.relatedStates.waker.thread), 282 ), 283 }), 284 this.relatedStates.prev && 285 m(TreeNode, { 286 left: 'Previous state', 287 right: renderRef( 288 this.relatedStates.prev, 289 nameForNextOrPrev(this.relatedStates.prev), 290 ), 291 }), 292 this.relatedStates.next && 293 m(TreeNode, { 294 left: 'Next state', 295 right: renderRef( 296 this.relatedStates.next, 297 nameForNextOrPrev(this.relatedStates.next), 298 ), 299 }), 300 this.relatedStates.wakee && 301 this.relatedStates.wakee.length > 0 && 302 m( 303 TreeNode, 304 { 305 left: 'Woken threads', 306 }, 307 this.relatedStates.wakee.map((state) => 308 m(TreeNode, { 309 left: m(Timestamp, { 310 ts: state.ts, 311 display: [ 312 'Start+', 313 m(DurationWidget, {dur: Time.sub(state.ts, startTs)}), 314 ], 315 }), 316 right: renderRef(state, getFullThreadName(state.thread)), 317 }), 318 ), 319 ), 320 ), 321 m(Button, { 322 label: 'Critical path lite', 323 intent: Intent.Primary, 324 onclick: () => 325 this.engine 326 .query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`) 327 .then(() => 328 addDebugSliceTrack( 329 // NOTE(stevegolton): This is a temporary patch, this menu 330 // should become part of a critical path plugin, at which point 331 // we can just use the plugin's context object. 332 { 333 engine: this.engine, 334 registerTrack: (x) => globals.trackManager.registerTrack(x), 335 }, 336 { 337 sqlSource: ` 338 SELECT 339 cr.id, 340 cr.utid, 341 cr.ts, 342 cr.dur, 343 thread.name AS thread_name, 344 process.name AS process_name, 345 'thread_state' AS table_name 346 FROM 347 _thread_executing_span_critical_path( 348 ${this.state?.thread?.utid}, 349 trace_bounds.start_ts, 350 trace_bounds.end_ts - trace_bounds.start_ts) cr, 351 trace_bounds 352 JOIN thread USING(utid) 353 JOIN process USING(upid) 354 `, 355 columns: sliceLiteColumnNames, 356 }, 357 `${this.state?.thread?.name}`, 358 sliceLiteColumns, 359 sliceLiteColumnNames, 360 ), 361 ), 362 }), 363 m(Button, { 364 label: 'Critical path', 365 intent: Intent.Primary, 366 onclick: () => 367 this.engine 368 .query( 369 `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`, 370 ) 371 .then(() => 372 addDebugSliceTrack( 373 // NOTE(stevegolton): This is a temporary patch, this menu 374 // should become part of a critical path plugin, at which point 375 // we can just use the plugin's context object. 376 { 377 engine: this.engine, 378 registerTrack: (x) => globals.trackManager.registerTrack(x), 379 }, 380 { 381 sqlSource: ` 382 SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name 383 FROM 384 _thread_executing_span_critical_path_stack( 385 ${this.state?.thread?.utid}, 386 trace_bounds.start_ts, 387 trace_bounds.end_ts - trace_bounds.start_ts) cr, 388 trace_bounds WHERE name IS NOT NULL 389 `, 390 columns: sliceColumnNames, 391 }, 392 `${this.state?.thread?.name}`, 393 sliceColumns, 394 sliceColumnNames, 395 ), 396 ), 397 }), 398 ]; 399 } 400 401 isLoading() { 402 return this.state === undefined || this.relatedStates === undefined; 403 } 404} 405