1// Copyright (C) 2019 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 {Icons} from '../base/semantic_icons'; 18import {duration, Time, TimeSpan} from '../base/time'; 19import {exists} from '../base/utils'; 20import {raf} from '../core/raf_scheduler'; 21import {Engine} from '../trace_processor/engine'; 22import {LONG, LONG_NULL, NUM, STR_NULL} from '../trace_processor/query_result'; 23import {Button} from '../widgets/button'; 24import {DetailsShell} from '../widgets/details_shell'; 25import {GridLayout, GridLayoutColumn} from '../widgets/grid_layout'; 26import {MenuItem, PopupMenu2} from '../widgets/menu'; 27import {Section} from '../widgets/section'; 28import {Tree, TreeNode} from '../widgets/tree'; 29 30import {BottomTab, NewBottomTabArgs} from './bottom_tab'; 31import {FlowPoint, globals} from './globals'; 32import {hasArgs, renderArguments} from './slice_args'; 33import {renderDetails} from './slice_details'; 34import {getSlice, SliceDetails, SliceRef} from './sql/slice'; 35import { 36 BreakdownByThreadState, 37 breakDownIntervalByThreadState, 38} from './sql/thread_state'; 39import {asSliceSqlId} from './sql_types'; 40import {DurationWidget} from './widgets/duration'; 41import {addDebugSliceTrack} from './debug_tracks/debug_tracks'; 42import {addQueryResultsTab} from './query_result_tab'; 43 44interface ContextMenuItem { 45 name: string; 46 shouldDisplay(slice: SliceDetails): boolean; 47 run(slice: SliceDetails): void; 48} 49 50function getTidFromSlice(slice: SliceDetails): number | undefined { 51 return slice.thread?.tid; 52} 53 54function getPidFromSlice(slice: SliceDetails): number | undefined { 55 return slice.process?.pid; 56} 57 58function getProcessNameFromSlice(slice: SliceDetails): string | undefined { 59 return slice.process?.name; 60} 61 62function getThreadNameFromSlice(slice: SliceDetails): string | undefined { 63 return slice.thread?.name; 64} 65 66function hasName(slice: SliceDetails): boolean { 67 return slice.name !== undefined; 68} 69 70function hasTid(slice: SliceDetails): boolean { 71 return getTidFromSlice(slice) !== undefined; 72} 73 74function hasPid(slice: SliceDetails): boolean { 75 return getPidFromSlice(slice) !== undefined; 76} 77 78function hasProcessName(slice: SliceDetails): boolean { 79 return getProcessNameFromSlice(slice) !== undefined; 80} 81 82function hasThreadName(slice: SliceDetails): boolean { 83 return getThreadNameFromSlice(slice) !== undefined; 84} 85 86const ITEMS: ContextMenuItem[] = [ 87 { 88 name: 'Average duration of slice name', 89 shouldDisplay: (slice: SliceDetails) => hasName(slice), 90 run: (slice: SliceDetails) => 91 addQueryResultsTab({ 92 query: `SELECT AVG(dur) / 1e9 FROM slice WHERE name = '${slice.name!}'`, 93 title: `${slice.name} average dur`, 94 }), 95 }, 96 { 97 name: 'Binder txn names + monitor contention on thread', 98 shouldDisplay: (slice) => 99 hasProcessName(slice) && 100 hasThreadName(slice) && 101 hasTid(slice) && 102 hasPid(slice), 103 run: (slice: SliceDetails) => { 104 const engine = getEngine(); 105 if (engine === undefined) return; 106 engine 107 .query( 108 ` 109 INCLUDE PERFETTO MODULE android.binder; 110 INCLUDE PERFETTO MODULE android.monitor_contention; 111 `, 112 ) 113 .then(() => 114 addDebugSliceTrack( 115 // NOTE(stevegolton): This is a temporary patch, this menu should 116 // become part of another plugin, at which point we can just use the 117 // plugin's context object. 118 { 119 engine, 120 registerTrack: (x) => globals.trackManager.registerTrack(x), 121 }, 122 { 123 sqlSource: ` 124 WITH merged AS ( 125 SELECT s.ts, s.dur, tx.aidl_name AS name, 0 AS depth 126 FROM android_binder_txns tx 127 JOIN slice s 128 ON tx.binder_txn_id = s.id 129 JOIN thread_track 130 ON s.track_id = thread_track.id 131 JOIN thread 132 USING (utid) 133 JOIN process 134 USING (upid) 135 WHERE pid = ${getPidFromSlice(slice)} 136 AND tid = ${getTidFromSlice(slice)} 137 AND aidl_name IS NOT NULL 138 UNION ALL 139 SELECT 140 s.ts, 141 s.dur, 142 short_blocked_method || ' -> ' || blocking_thread_name || ':' || short_blocking_method AS name, 143 1 AS depth 144 FROM android_binder_txns tx 145 JOIN android_monitor_contention m 146 ON m.binder_reply_tid = tx.server_tid AND m.binder_reply_ts = tx.server_ts 147 JOIN slice s 148 ON tx.binder_txn_id = s.id 149 JOIN thread_track 150 ON s.track_id = thread_track.id 151 JOIN thread ON thread.utid = thread_track.utid 152 JOIN process ON process.upid = thread.upid 153 WHERE process.pid = ${getPidFromSlice(slice)} 154 AND thread.tid = ${getTidFromSlice( 155 slice, 156 )} 157 AND short_blocked_method IS NOT NULL 158 ORDER BY depth 159 ) SELECT ts, dur, name FROM merged`, 160 }, 161 `Binder names (${getProcessNameFromSlice( 162 slice, 163 )}:${getThreadNameFromSlice(slice)})`, 164 {ts: 'ts', dur: 'dur', name: 'name'}, 165 [], 166 ), 167 ); 168 }, 169 }, 170]; 171 172function getSliceContextMenuItems(slice: SliceDetails) { 173 return ITEMS.filter((item) => item.shouldDisplay(slice)); 174} 175 176function getEngine(): Engine | undefined { 177 const engineId = globals.getCurrentEngine()?.id; 178 if (engineId === undefined) { 179 return undefined; 180 } 181 const engine = globals.engines.get(engineId)?.getProxy('SlicePanel'); 182 return engine; 183} 184 185async function getAnnotationSlice( 186 engine: Engine, 187 id: number, 188): Promise<SliceDetails | undefined> { 189 const query = await engine.query(` 190 SELECT 191 id, 192 name, 193 ts, 194 dur, 195 track_id as trackId, 196 thread_dur as threadDur, 197 cat, 198 ABS_TIME_STR(ts) as absTime 199 FROM annotation_slice 200 where id = ${id}`); 201 202 const it = query.firstRow({ 203 id: NUM, 204 name: STR_NULL, 205 ts: LONG, 206 dur: LONG, 207 trackId: NUM, 208 threadDur: LONG_NULL, 209 cat: STR_NULL, 210 absTime: STR_NULL, 211 }); 212 213 return { 214 id: asSliceSqlId(it.id), 215 name: it.name ?? 'null', 216 ts: Time.fromRaw(it.ts), 217 dur: it.dur, 218 depth: 0, 219 trackId: it.trackId, 220 threadDur: it.threadDur ?? undefined, 221 category: it.cat ?? undefined, 222 absTime: it.absTime ?? undefined, 223 }; 224} 225 226async function getSliceDetails( 227 engine: Engine, 228 id: number, 229 table: string, 230): Promise<SliceDetails | undefined> { 231 if (table === 'annotation_slice') { 232 return getAnnotationSlice(engine, id); 233 } else { 234 return getSlice(engine, asSliceSqlId(id)); 235 } 236} 237 238interface ThreadSliceDetailsTabConfig { 239 id: number; 240 table: string; 241} 242 243export class ThreadSliceDetailsTab extends BottomTab<ThreadSliceDetailsTabConfig> { 244 private sliceDetails?: SliceDetails; 245 private breakdownByThreadState?: BreakdownByThreadState; 246 247 static create( 248 args: NewBottomTabArgs<ThreadSliceDetailsTabConfig>, 249 ): ThreadSliceDetailsTab { 250 return new ThreadSliceDetailsTab(args); 251 } 252 253 constructor(args: NewBottomTabArgs<ThreadSliceDetailsTabConfig>) { 254 super(args); 255 this.load(); 256 } 257 258 async load() { 259 // Start loading the slice details 260 const {id, table} = this.config; 261 const details = await getSliceDetails(this.engine, id, table); 262 263 if ( 264 details !== undefined && 265 details.thread !== undefined && 266 details.dur > 0 267 ) { 268 this.breakdownByThreadState = await breakDownIntervalByThreadState( 269 this.engine, 270 TimeSpan.fromTimeAndDuration(details.ts, details.dur), 271 details.thread.utid, 272 ); 273 } 274 275 this.sliceDetails = details; 276 raf.scheduleFullRedraw(); 277 } 278 279 getTitle(): string { 280 return `Current Selection`; 281 } 282 283 viewTab() { 284 if (!exists(this.sliceDetails)) { 285 return m(DetailsShell, {title: 'Slice', description: 'Loading...'}); 286 } 287 const slice = this.sliceDetails; 288 return m( 289 DetailsShell, 290 { 291 title: 'Slice', 292 description: slice.name, 293 buttons: this.renderContextButton(slice), 294 }, 295 m( 296 GridLayout, 297 renderDetails(slice, this.breakdownByThreadState), 298 this.renderRhs(this.engine, slice), 299 ), 300 ); 301 } 302 303 isLoading() { 304 return !exists(this.sliceDetails); 305 } 306 307 private renderRhs(engine: Engine, slice: SliceDetails): m.Children { 308 const precFlows = this.renderPrecedingFlows(slice); 309 const followingFlows = this.renderFollowingFlows(slice); 310 const args = 311 hasArgs(slice.args) && 312 m( 313 Section, 314 {title: 'Arguments'}, 315 m(Tree, renderArguments(engine, slice.args)), 316 ); 317 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 318 if (precFlows ?? followingFlows ?? args) { 319 return m(GridLayoutColumn, precFlows, followingFlows, args); 320 } else { 321 return undefined; 322 } 323 } 324 325 private renderPrecedingFlows(slice: SliceDetails): m.Children { 326 const flows = globals.connectedFlows; 327 const inFlows = flows.filter(({end}) => end.sliceId === slice.id); 328 329 if (inFlows.length > 0) { 330 const isRunTask = 331 slice.name === 'ThreadControllerImpl::RunTask' || 332 slice.name === 'ThreadPool_RunTask'; 333 334 return m( 335 Section, 336 {title: 'Preceding Flows'}, 337 m( 338 Tree, 339 inFlows.map(({begin, dur}) => 340 this.renderFlow(begin, dur, !isRunTask), 341 ), 342 ), 343 ); 344 } else { 345 return null; 346 } 347 } 348 349 private renderFollowingFlows(slice: SliceDetails): m.Children { 350 const flows = globals.connectedFlows; 351 const outFlows = flows.filter(({begin}) => begin.sliceId === slice.id); 352 353 if (outFlows.length > 0) { 354 const isPostTask = 355 slice.name === 'ThreadPool_PostTask' || 356 slice.name === 'SequenceManager PostTask'; 357 358 return m( 359 Section, 360 {title: 'Following Flows'}, 361 m( 362 Tree, 363 outFlows.map(({end, dur}) => this.renderFlow(end, dur, !isPostTask)), 364 ), 365 ); 366 } else { 367 return null; 368 } 369 } 370 371 private renderFlow( 372 flow: FlowPoint, 373 dur: duration, 374 includeProcessName: boolean, 375 ): m.Children { 376 const description = 377 flow.sliceChromeCustomName === undefined 378 ? flow.sliceName 379 : flow.sliceChromeCustomName; 380 const threadName = includeProcessName 381 ? `${flow.threadName} (${flow.processName})` 382 : flow.threadName; 383 384 return m( 385 TreeNode, 386 {left: 'Flow'}, 387 m(TreeNode, { 388 left: 'Slice', 389 right: m(SliceRef, { 390 id: asSliceSqlId(flow.sliceId), 391 name: description, 392 ts: flow.sliceStartTs, 393 dur: flow.sliceEndTs - flow.sliceStartTs, 394 sqlTrackId: flow.trackId, 395 }), 396 }), 397 m(TreeNode, {left: 'Delay', right: m(DurationWidget, {dur})}), 398 m(TreeNode, {left: 'Thread', right: threadName}), 399 ); 400 } 401 402 private renderContextButton(sliceInfo: SliceDetails): m.Children { 403 const contextMenuItems = getSliceContextMenuItems(sliceInfo); 404 if (contextMenuItems.length > 0) { 405 const trigger = m(Button, { 406 compact: true, 407 label: 'Contextual Options', 408 rightIcon: Icons.ContextMenu, 409 }); 410 return m( 411 PopupMenu2, 412 {trigger}, 413 contextMenuItems.map(({name, run}) => 414 m(MenuItem, {label: name, onclick: () => run(sliceInfo)}), 415 ), 416 ); 417 } else { 418 return undefined; 419 } 420 } 421} 422