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 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 {AggregationPanel} from './aggregation_panel'; 17import {isEmptyData} from '../public/aggregation'; 18import {DetailsShell} from '../widgets/details_shell'; 19import {Button, ButtonBar} from '../widgets/button'; 20import {raf} from '../core/raf_scheduler'; 21import {EmptyState} from '../widgets/empty_state'; 22import {FlowEventsAreaSelectedPanel} from './flow_events_panel'; 23import {PivotTable} from './pivot_table'; 24import {AreaSelection} from '../public/selection'; 25import {Monitor} from '../base/monitor'; 26import { 27 CPU_PROFILE_TRACK_KIND, 28 PERF_SAMPLES_PROFILE_TRACK_KIND, 29 SLICE_TRACK_KIND, 30} from '../public/track_kinds'; 31import { 32 QueryFlamegraph, 33 metricsFromTableOrSubquery, 34} from '../components/query_flamegraph'; 35import {DisposableStack} from '../base/disposable_stack'; 36import {assertExists} from '../base/logging'; 37import {TraceImpl} from '../core/trace_impl'; 38import {Trace} from '../public/trace'; 39import {Flamegraph} from '../widgets/flamegraph'; 40 41interface View { 42 key: string; 43 name: string; 44 content: m.Children; 45} 46 47export type AreaDetailsPanelAttrs = {trace: TraceImpl}; 48 49class AreaDetailsPanel implements m.ClassComponent<AreaDetailsPanelAttrs> { 50 private trace: TraceImpl; 51 private monitor: Monitor; 52 private currentTab: string | undefined = undefined; 53 private cpuProfileFlamegraph?: QueryFlamegraph; 54 private perfSampleFlamegraph?: QueryFlamegraph; 55 private sliceFlamegraph?: QueryFlamegraph; 56 57 constructor({attrs}: m.CVnode<AreaDetailsPanelAttrs>) { 58 this.trace = attrs.trace; 59 this.monitor = new Monitor([() => this.trace.selection.selection]); 60 } 61 62 private getCurrentView(): string | undefined { 63 const types = this.getViews().map(({key}) => key); 64 65 if (types.length === 0) { 66 return undefined; 67 } 68 69 if (this.currentTab === undefined) { 70 return types[0]; 71 } 72 73 if (!types.includes(this.currentTab)) { 74 return types[0]; 75 } 76 77 return this.currentTab; 78 } 79 80 private getViews(): View[] { 81 const views: View[] = []; 82 83 for (const aggregator of this.trace.selection.aggregation.aggregators) { 84 const aggregatorId = aggregator.id; 85 const value = 86 this.trace.selection.aggregation.getAggregatedData(aggregatorId); 87 if (value !== undefined && !isEmptyData(value)) { 88 views.push({ 89 key: value.tabName, 90 name: value.tabName, 91 content: m(AggregationPanel, { 92 aggregatorId, 93 data: value, 94 trace: this.trace, 95 }), 96 }); 97 } 98 } 99 100 const pivotTableState = this.trace.pivotTable.state; 101 const tree = pivotTableState.queryResult?.tree; 102 if ( 103 pivotTableState.selectionArea != undefined && 104 (tree === undefined || tree.children.size > 0 || tree?.rows.length > 0) 105 ) { 106 views.push({ 107 key: 'pivot_table', 108 name: 'Pivot Table', 109 content: m(PivotTable, { 110 trace: this.trace, 111 selectionArea: pivotTableState.selectionArea, 112 }), 113 }); 114 } 115 116 this.addFlamegraphView(this.trace, this.monitor.ifStateChanged(), views); 117 118 // Add this after all aggregation panels, to make it appear after 'Slices' 119 if (this.trace.flows.selectedFlows.length > 0) { 120 views.push({ 121 key: 'selected_flows', 122 name: 'Flow Events', 123 content: m(FlowEventsAreaSelectedPanel, {trace: this.trace}), 124 }); 125 } 126 127 return views; 128 } 129 130 view(): m.Children { 131 const views = this.getViews(); 132 const currentViewKey = this.getCurrentView(); 133 134 const aggregationButtons = views.map(({key, name}) => { 135 return m(Button, { 136 onclick: () => { 137 this.currentTab = key; 138 raf.scheduleFullRedraw(); 139 }, 140 key, 141 label: name, 142 active: currentViewKey === key, 143 }); 144 }); 145 146 if (currentViewKey === undefined) { 147 return this.renderEmptyState(); 148 } 149 150 const content = views.find(({key}) => key === currentViewKey)?.content; 151 if (content === undefined) { 152 return this.renderEmptyState(); 153 } 154 155 return m( 156 DetailsShell, 157 { 158 title: 'Area Selection', 159 description: m(ButtonBar, aggregationButtons), 160 }, 161 content, 162 ); 163 } 164 165 private renderEmptyState(): m.Children { 166 return m( 167 EmptyState, 168 { 169 className: 'pf-noselection', 170 title: 'Unsupported area selection', 171 }, 172 'No details available for this area selection', 173 ); 174 } 175 176 private addFlamegraphView(trace: Trace, isChanged: boolean, views: View[]) { 177 this.cpuProfileFlamegraph = this.computeCpuProfileFlamegraph( 178 trace, 179 isChanged, 180 ); 181 if (this.cpuProfileFlamegraph !== undefined) { 182 views.push({ 183 key: 'cpu_profile_flamegraph_selection', 184 name: 'CPU Profile Sample Flamegraph', 185 content: this.cpuProfileFlamegraph.render(), 186 }); 187 } 188 this.perfSampleFlamegraph = this.computePerfSampleFlamegraph( 189 trace, 190 isChanged, 191 ); 192 if (this.perfSampleFlamegraph !== undefined) { 193 views.push({ 194 key: 'perf_sample_flamegraph_selection', 195 name: 'Perf Sample Flamegraph', 196 content: this.perfSampleFlamegraph.render(), 197 }); 198 } 199 this.sliceFlamegraph = this.computeSliceFlamegraph(trace, isChanged); 200 if (this.sliceFlamegraph !== undefined) { 201 views.push({ 202 key: 'slice_flamegraph_selection', 203 name: 'Slice Flamegraph', 204 content: this.sliceFlamegraph.render(), 205 }); 206 } 207 } 208 209 private computeCpuProfileFlamegraph(trace: Trace, isChanged: boolean) { 210 const currentSelection = trace.selection.selection; 211 if (currentSelection.kind !== 'area') { 212 return undefined; 213 } 214 if (!isChanged) { 215 // If the selection has not changed, just return a copy of the last seen 216 // attrs. 217 return this.cpuProfileFlamegraph; 218 } 219 const utids = []; 220 for (const trackInfo of currentSelection.tracks) { 221 if (trackInfo?.tags?.kind === CPU_PROFILE_TRACK_KIND) { 222 utids.push(trackInfo.tags?.utid); 223 } 224 } 225 if (utids.length === 0) { 226 return undefined; 227 } 228 const metrics = metricsFromTableOrSubquery( 229 ` 230 ( 231 select 232 id, 233 parent_id as parentId, 234 name, 235 mapping_name, 236 source_file, 237 cast(line_number AS text) as line_number, 238 self_count 239 from _callstacks_for_callsites!(( 240 select p.callsite_id 241 from cpu_profile_stack_sample p 242 where p.ts >= ${currentSelection.start} 243 and p.ts <= ${currentSelection.end} 244 and p.utid in (${utids.join(',')}) 245 )) 246 ) 247 `, 248 [ 249 { 250 name: 'CPU Profile Samples', 251 unit: '', 252 columnName: 'self_count', 253 }, 254 ], 255 'include perfetto module callstacks.stack_profile', 256 [{name: 'mapping_name', displayName: 'Mapping'}], 257 [ 258 { 259 name: 'source_file', 260 displayName: 'Source File', 261 mergeAggregation: 'ONE_OR_NULL', 262 }, 263 { 264 name: 'line_number', 265 displayName: 'Line Number', 266 mergeAggregation: 'ONE_OR_NULL', 267 }, 268 ], 269 ); 270 return new QueryFlamegraph(trace, metrics, { 271 state: Flamegraph.createDefaultState(metrics), 272 }); 273 } 274 275 private computePerfSampleFlamegraph(trace: Trace, isChanged: boolean) { 276 const currentSelection = trace.selection.selection; 277 if (currentSelection.kind !== 'area') { 278 return undefined; 279 } 280 if (!isChanged) { 281 // If the selection has not changed, just return a copy of the last seen 282 // attrs. 283 return this.perfSampleFlamegraph; 284 } 285 const upids = getUpidsFromPerfSampleAreaSelection(currentSelection); 286 const utids = getUtidsFromPerfSampleAreaSelection(currentSelection); 287 if (utids.length === 0 && upids.length === 0) { 288 return undefined; 289 } 290 const metrics = metricsFromTableOrSubquery( 291 ` 292 ( 293 select id, parent_id as parentId, name, self_count 294 from _callstacks_for_callsites!(( 295 select p.callsite_id 296 from perf_sample p 297 join thread t using (utid) 298 where p.ts >= ${currentSelection.start} 299 and p.ts <= ${currentSelection.end} 300 and ( 301 p.utid in (${utids.join(',')}) 302 or t.upid in (${upids.join(',')}) 303 ) 304 )) 305 ) 306 `, 307 [ 308 { 309 name: 'Perf Samples', 310 unit: '', 311 columnName: 'self_count', 312 }, 313 ], 314 'include perfetto module linux.perf.samples', 315 ); 316 return new QueryFlamegraph(trace, metrics, { 317 state: Flamegraph.createDefaultState(metrics), 318 }); 319 } 320 321 private computeSliceFlamegraph(trace: Trace, isChanged: boolean) { 322 const currentSelection = trace.selection.selection; 323 if (currentSelection.kind !== 'area') { 324 return undefined; 325 } 326 if (!isChanged) { 327 // If the selection has not changed, just return a copy of the last seen 328 // attrs. 329 return this.sliceFlamegraph; 330 } 331 const trackIds = []; 332 for (const trackInfo of currentSelection.tracks) { 333 if (trackInfo?.tags?.kind !== SLICE_TRACK_KIND) { 334 continue; 335 } 336 if (trackInfo.tags?.trackIds === undefined) { 337 continue; 338 } 339 trackIds.push(...trackInfo.tags.trackIds); 340 } 341 if (trackIds.length === 0) { 342 return undefined; 343 } 344 const metrics = metricsFromTableOrSubquery( 345 ` 346 ( 347 select * 348 from _viz_slice_ancestor_agg!(( 349 select s.id, s.dur 350 from slice s 351 left join slice t on t.parent_id = s.id 352 where s.ts >= ${currentSelection.start} 353 and s.ts <= ${currentSelection.end} 354 and s.track_id in (${trackIds.join(',')}) 355 and t.id is null 356 )) 357 ) 358 `, 359 [ 360 { 361 name: 'Duration', 362 unit: 'ns', 363 columnName: 'self_dur', 364 }, 365 { 366 name: 'Samples', 367 unit: '', 368 columnName: 'self_count', 369 }, 370 ], 371 'include perfetto module viz.slices;', 372 ); 373 return new QueryFlamegraph(trace, metrics, { 374 state: Flamegraph.createDefaultState(metrics), 375 }); 376 } 377} 378 379export class AggregationsTabs implements Disposable { 380 private trash = new DisposableStack(); 381 382 constructor(trace: TraceImpl) { 383 const unregister = trace.tabs.registerDetailsPanel({ 384 render(selection) { 385 if (selection.kind === 'area') { 386 return m(AreaDetailsPanel, {trace}); 387 } else { 388 return undefined; 389 } 390 }, 391 }); 392 393 this.trash.use(unregister); 394 } 395 396 [Symbol.dispose]() { 397 this.trash.dispose(); 398 } 399} 400 401function getUpidsFromPerfSampleAreaSelection(currentSelection: AreaSelection) { 402 const upids = []; 403 for (const trackInfo of currentSelection.tracks) { 404 if ( 405 trackInfo?.tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND && 406 trackInfo.tags?.utid === undefined 407 ) { 408 upids.push(assertExists(trackInfo.tags?.upid)); 409 } 410 } 411 return upids; 412} 413 414function getUtidsFromPerfSampleAreaSelection(currentSelection: AreaSelection) { 415 const utids = []; 416 for (const trackInfo of currentSelection.tracks) { 417 if ( 418 trackInfo?.tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND && 419 trackInfo.tags?.utid !== undefined 420 ) { 421 utids.push(trackInfo.tags?.utid); 422 } 423 } 424 return utids; 425} 426