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'; 16import {Icons} from '../../base/semantic_icons'; 17import {TimeSpan} from '../../base/time'; 18import {exists} from '../../base/utils'; 19import {Engine} from '../../trace_processor/engine'; 20import {Button} from '../../widgets/button'; 21import {DetailsShell} from '../../widgets/details_shell'; 22import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout'; 23import {MenuItem, PopupMenu} from '../../widgets/menu'; 24import {Section} from '../../widgets/section'; 25import {Tree} from '../../widgets/tree'; 26import {Flow, FlowPoint} from '../../core/flow_types'; 27import {hasArgs, renderArguments} from './slice_args'; 28import {renderDetails} from './slice_details'; 29import {getSlice, SliceDetails} from '../sql_utils/slice'; 30import { 31 BreakdownByThreadState, 32 breakDownIntervalByThreadState, 33} from './thread_state'; 34import {asSliceSqlId} from '../sql_utils/core_types'; 35import {DurationWidget} from '../widgets/duration'; 36import {SliceRef} from '../widgets/slice'; 37import {BasicTable} from '../../widgets/basic_table'; 38import {getSqlTableDescription} from '../widgets/sql/table/sql_table_registry'; 39import {assertExists, assertIsInstance} from '../../base/logging'; 40import {Trace} from '../../public/trace'; 41import {TrackEventDetailsPanel} from '../../public/details_panel'; 42import {TrackEventSelection} from '../../public/selection'; 43import {extensions} from '../extensions'; 44import {TraceImpl} from '../../core/trace_impl'; 45 46interface ContextMenuItem { 47 name: string; 48 shouldDisplay(slice: SliceDetails): boolean; 49 run(slice: SliceDetails, trace: Trace): void; 50} 51 52function getTidFromSlice(slice: SliceDetails): number | undefined { 53 return slice.thread?.tid; 54} 55 56function getPidFromSlice(slice: SliceDetails): number | undefined { 57 return slice.process?.pid; 58} 59 60function getProcessNameFromSlice(slice: SliceDetails): string | undefined { 61 return slice.process?.name; 62} 63 64function getThreadNameFromSlice(slice: SliceDetails): string | undefined { 65 return slice.thread?.name; 66} 67 68function hasName(slice: SliceDetails): boolean { 69 return slice.name !== undefined; 70} 71 72function hasTid(slice: SliceDetails): boolean { 73 return getTidFromSlice(slice) !== undefined; 74} 75 76function hasPid(slice: SliceDetails): boolean { 77 return getPidFromSlice(slice) !== undefined; 78} 79 80function hasProcessName(slice: SliceDetails): boolean { 81 return getProcessNameFromSlice(slice) !== undefined; 82} 83 84function hasThreadName(slice: SliceDetails): boolean { 85 return getThreadNameFromSlice(slice) !== undefined; 86} 87 88const ITEMS: ContextMenuItem[] = [ 89 { 90 name: 'Ancestor slices', 91 shouldDisplay: (slice: SliceDetails) => slice.parentId !== undefined, 92 run: (slice: SliceDetails, trace: Trace) => 93 extensions.addLegacySqlTableTab(trace, { 94 table: assertExists(getSqlTableDescription('slice')), 95 filters: [ 96 { 97 op: (cols) => 98 `${cols[0]} IN (SELECT id FROM _slice_ancestor_and_self(${slice.id}))`, 99 columns: ['id'], 100 }, 101 ], 102 imports: ['slices.hierarchy'], 103 }), 104 }, 105 { 106 name: 'Descendant slices', 107 shouldDisplay: () => true, 108 run: (slice: SliceDetails, trace: Trace) => 109 extensions.addLegacySqlTableTab(trace, { 110 table: assertExists(getSqlTableDescription('slice')), 111 filters: [ 112 { 113 op: (cols) => 114 `${cols[0]} IN (SELECT id FROM _slice_descendant_and_self(${slice.id}))`, 115 columns: ['id'], 116 }, 117 ], 118 imports: ['slices.hierarchy'], 119 }), 120 }, 121 { 122 name: 'Average duration of slice name', 123 shouldDisplay: (slice: SliceDetails) => hasName(slice), 124 run: (slice: SliceDetails, trace: Trace) => 125 extensions.addQueryResultsTab(trace, { 126 query: `SELECT AVG(dur) / 1e9 FROM slice WHERE name = '${slice.name!}'`, 127 title: `${slice.name} average dur`, 128 }), 129 }, 130 { 131 name: 'Binder txn names + monitor contention on thread', 132 shouldDisplay: (slice) => 133 hasProcessName(slice) && 134 hasThreadName(slice) && 135 hasTid(slice) && 136 hasPid(slice), 137 run: (slice: SliceDetails, trace: Trace) => { 138 trace.engine 139 .query( 140 `INCLUDE PERFETTO MODULE android.binder; 141 INCLUDE PERFETTO MODULE android.monitor_contention;`, 142 ) 143 .then(() => 144 extensions.addDebugSliceTrack({ 145 trace, 146 data: { 147 sqlSource: ` 148 WITH merged AS ( 149 SELECT s.ts, s.dur, tx.aidl_name AS name, 0 AS depth 150 FROM android_binder_txns tx 151 JOIN slice s 152 ON tx.binder_txn_id = s.id 153 JOIN thread_track 154 ON s.track_id = thread_track.id 155 JOIN thread 156 USING (utid) 157 JOIN process 158 USING (upid) 159 WHERE pid = ${getPidFromSlice(slice)} 160 AND tid = ${getTidFromSlice(slice)} 161 AND aidl_name IS NOT NULL 162 UNION ALL 163 SELECT 164 s.ts, 165 s.dur, 166 short_blocked_method || ' -> ' || blocking_thread_name || ':' || short_blocking_method AS name, 167 1 AS depth 168 FROM android_binder_txns tx 169 JOIN android_monitor_contention m 170 ON m.binder_reply_tid = tx.server_tid AND m.binder_reply_ts = tx.server_ts 171 JOIN slice s 172 ON tx.binder_txn_id = s.id 173 JOIN thread_track 174 ON s.track_id = thread_track.id 175 JOIN thread ON thread.utid = thread_track.utid 176 JOIN process ON process.upid = thread.upid 177 WHERE process.pid = ${getPidFromSlice(slice)} 178 AND thread.tid = ${getTidFromSlice( 179 slice, 180 )} 181 AND short_blocked_method IS NOT NULL 182 ORDER BY depth 183 ) SELECT ts, dur, name FROM merged`, 184 }, 185 title: `Binder names (${getProcessNameFromSlice( 186 slice, 187 )}:${getThreadNameFromSlice(slice)})`, 188 }), 189 ); 190 }, 191 }, 192]; 193 194function getSliceContextMenuItems(slice: SliceDetails) { 195 return ITEMS.filter((item) => item.shouldDisplay(slice)); 196} 197 198async function getSliceDetails( 199 engine: Engine, 200 id: number, 201): Promise<SliceDetails | undefined> { 202 return getSlice(engine, asSliceSqlId(id)); 203} 204 205export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel { 206 private sliceDetails?: SliceDetails; 207 private breakdownByThreadState?: BreakdownByThreadState; 208 private readonly trace: TraceImpl; 209 210 constructor(trace: Trace) { 211 // Rationale for the assertIsInstance: ThreadSliceDetailsPanel requires a 212 // TraceImpl (because of flows) but here we must take a Trace interface, 213 // because this track is exposed to plugins (which see only Trace). 214 this.trace = assertIsInstance(trace, TraceImpl); 215 } 216 217 async load({eventId}: TrackEventSelection) { 218 const {trace} = this; 219 const details = await getSliceDetails(trace.engine, eventId); 220 221 if ( 222 details !== undefined && 223 details.thread !== undefined && 224 details.dur > 0 225 ) { 226 this.breakdownByThreadState = await breakDownIntervalByThreadState( 227 trace.engine, 228 TimeSpan.fromTimeAndDuration(details.ts, details.dur), 229 details.thread.utid, 230 ); 231 } 232 233 this.sliceDetails = details; 234 } 235 236 render() { 237 if (!exists(this.sliceDetails)) { 238 return m(DetailsShell, {title: 'Slice', description: 'Loading...'}); 239 } 240 const slice = this.sliceDetails; 241 return m( 242 DetailsShell, 243 { 244 title: 'Slice', 245 description: slice.name, 246 buttons: this.renderContextButton(slice), 247 }, 248 m( 249 GridLayout, 250 renderDetails(this.trace, slice, this.breakdownByThreadState), 251 this.renderRhs(this.trace, slice), 252 ), 253 ); 254 } 255 256 private renderRhs(trace: Trace, slice: SliceDetails): m.Children { 257 const precFlows = this.renderPrecedingFlows(slice); 258 const followingFlows = this.renderFollowingFlows(slice); 259 const args = 260 hasArgs(slice.args) && 261 m( 262 Section, 263 {title: 'Arguments'}, 264 m(Tree, renderArguments(trace, slice.args)), 265 ); 266 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 267 if (precFlows ?? followingFlows ?? args) { 268 return m(GridLayoutColumn, precFlows, followingFlows, args); 269 } else { 270 return undefined; 271 } 272 } 273 274 private renderPrecedingFlows(slice: SliceDetails): m.Children { 275 const flows = this.trace.flows.connectedFlows; 276 const inFlows = flows.filter(({end}) => end.sliceId === slice.id); 277 278 if (inFlows.length > 0) { 279 const isRunTask = 280 slice.name === 'ThreadControllerImpl::RunTask' || 281 slice.name === 'ThreadPool_RunTask'; 282 283 return m( 284 Section, 285 {title: 'Preceding Flows'}, 286 m(BasicTable<Flow>, { 287 columns: [ 288 { 289 title: 'Slice', 290 render: (flow: Flow) => 291 m(SliceRef, { 292 id: asSliceSqlId(flow.begin.sliceId), 293 name: 294 flow.begin.sliceChromeCustomName ?? flow.begin.sliceName, 295 }), 296 }, 297 { 298 title: 'Delay', 299 render: (flow: Flow) => 300 m(DurationWidget, { 301 dur: flow.end.sliceStartTs - flow.begin.sliceEndTs, 302 }), 303 }, 304 { 305 title: 'Thread', 306 render: (flow: Flow) => 307 this.getThreadNameForFlow(flow.begin, !isRunTask), 308 }, 309 ], 310 data: inFlows, 311 }), 312 ); 313 } else { 314 return null; 315 } 316 } 317 318 private renderFollowingFlows(slice: SliceDetails): m.Children { 319 const flows = this.trace.flows.connectedFlows; 320 const outFlows = flows.filter(({begin}) => begin.sliceId === slice.id); 321 322 if (outFlows.length > 0) { 323 const isPostTask = 324 slice.name === 'ThreadPool_PostTask' || 325 slice.name === 'SequenceManager PostTask'; 326 327 return m( 328 Section, 329 {title: 'Following Flows'}, 330 m(BasicTable<Flow>, { 331 columns: [ 332 { 333 title: 'Slice', 334 render: (flow: Flow) => 335 m(SliceRef, { 336 id: asSliceSqlId(flow.end.sliceId), 337 name: flow.end.sliceChromeCustomName ?? flow.end.sliceName, 338 }), 339 }, 340 { 341 title: 'Delay', 342 render: (flow: Flow) => 343 m(DurationWidget, { 344 dur: flow.end.sliceStartTs - flow.begin.sliceEndTs, 345 }), 346 }, 347 { 348 title: 'Thread', 349 render: (flow: Flow) => 350 this.getThreadNameForFlow(flow.end, !isPostTask), 351 }, 352 ], 353 data: outFlows, 354 }), 355 ); 356 } else { 357 return null; 358 } 359 } 360 361 private getThreadNameForFlow( 362 flow: FlowPoint, 363 includeProcessName: boolean, 364 ): string { 365 return includeProcessName 366 ? `${flow.threadName} (${flow.processName})` 367 : flow.threadName; 368 } 369 370 private renderContextButton(sliceInfo: SliceDetails): m.Children { 371 const contextMenuItems = getSliceContextMenuItems(sliceInfo); 372 if (contextMenuItems.length > 0) { 373 const trigger = m(Button, { 374 compact: true, 375 label: 'Contextual Options', 376 rightIcon: Icons.ContextMenu, 377 }); 378 return m( 379 PopupMenu, 380 {trigger}, 381 contextMenuItems.map(({name, run}) => 382 m(MenuItem, {label: name, onclick: () => run(sliceInfo, this.trace)}), 383 ), 384 ); 385 } else { 386 return undefined; 387 } 388 } 389} 390