1// Copyright (C) 2025 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 m from 'mithril'; 16import {TrackEventDetailsPanel} from '../../public/details_panel'; 17import {Trace} from '../../public/trace'; 18import { 19 LONG, 20 NUM_NULL, 21 SqlValue, 22 STR, 23} from '../../trace_processor/query_result'; 24import {DetailsShell} from '../../widgets/details_shell'; 25import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout'; 26import {Duration, duration, Time, time} from '../../base/time'; 27import {assertExists, assertTrue} from '../../base/logging'; 28import {Section} from '../../widgets/section'; 29import {Tree, TreeNode} from '../../widgets/tree'; 30import {Timestamp} from '../../components/widgets/timestamp'; 31import {DurationWidget} from '../../components/widgets/duration'; 32import {fromSqlBool, renderSliceRef, renderSqlRef} from './utils'; 33import SqlModulesPlugin from '../dev.perfetto.SqlModules'; 34import { 35 TableColumn, 36 TableManager, 37} from '../../components/widgets/sql/table/table_column'; 38import {renderStandardCell} from '../../components/widgets/sql/table/render_cell_utils'; 39import {ScrollTimelineModel} from './scroll_timeline_model'; 40import { 41 DurationColumn, 42 StandardColumn, 43 TimestampColumn, 44} from '../../components/widgets/sql/table/columns'; 45 46function createPluginSliceIdColumn( 47 trace: Trace, 48 trackUri: string, 49 name: string, 50): TableColumn { 51 const col = new StandardColumn(name); 52 col.renderCell = (value: SqlValue, tableManager: TableManager) => { 53 if (value === null || typeof value !== 'bigint') { 54 return renderStandardCell(value, name, tableManager); 55 } 56 return renderSliceRef({ 57 trace: trace, 58 id: Number(value), 59 trackUri: trackUri, 60 title: `${value}`, 61 }); 62 }; 63 return col; 64} 65 66function createScrollTimelineTableColumns( 67 trace: Trace, 68 trackUri: string, 69): TableColumn[] { 70 return [ 71 createPluginSliceIdColumn(trace, trackUri, 'id'), 72 new StandardColumn('scroll_update_id'), 73 new TimestampColumn('ts'), 74 new DurationColumn('dur'), 75 new StandardColumn('name'), 76 new StandardColumn('classification'), 77 ]; 78} 79 80export class ScrollTimelineDetailsPanel implements TrackEventDetailsPanel { 81 // Information about the scroll update *slice*, which was emitted by 82 // ScrollTimelineTrack. 83 // Source: this.tableName[id=this.id] 84 private sliceData?: { 85 name: string; 86 ts: time; 87 dur: duration; 88 // ID of the scroll update in chrome_scroll_update_info. 89 scrollUpdateId: bigint; 90 }; 91 92 // Information about the scroll *update*, which comes from the Chrome tracing 93 // stdlib. 94 // Source: chrome_scroll_update_info[id=this.sliceData.scrollUpdateId] 95 private scrollData?: { 96 vsyncInterval: duration | undefined; 97 isPresented: boolean | undefined; 98 isJanky: boolean | undefined; 99 isInertial: boolean | undefined; 100 isFirstScrollUpdateInScroll: boolean | undefined; 101 isFirstScrollUpdateInFrame: boolean | undefined; 102 }; 103 104 constructor( 105 private readonly trace: Trace, 106 private readonly model: ScrollTimelineModel, 107 // ID of the slice in tableName. 108 private readonly id: number, 109 ) {} 110 111 async load(): Promise<void> { 112 await this.querySliceData(); 113 await this.queryScrollData(); 114 } 115 116 private async querySliceData(): Promise<void> { 117 assertTrue(this.sliceData === undefined); 118 const queryResult = await this.trace.engine.query(` 119 SELECT 120 name, 121 ts, 122 dur, 123 scroll_update_id 124 FROM ${this.model.tableName} 125 WHERE id = ${this.id}`); 126 const row = queryResult.firstRow({ 127 name: STR, 128 ts: LONG, 129 dur: LONG, 130 scroll_update_id: LONG, 131 }); 132 this.sliceData = { 133 name: row.name, 134 ts: Time.fromRaw(row.ts), 135 dur: Duration.fromRaw(row.dur), 136 scrollUpdateId: row.scroll_update_id, 137 }; 138 } 139 140 private async queryScrollData(): Promise<void> { 141 assertExists(this.sliceData); 142 assertTrue(this.scrollData === undefined); 143 const queryResult = await this.trace.engine.query(` 144 INCLUDE PERFETTO MODULE chrome.chrome_scrolls; 145 SELECT 146 vsync_interval_ms, 147 is_presented, 148 is_janky, 149 is_inertial, 150 is_first_scroll_update_in_scroll, 151 is_first_scroll_update_in_frame 152 FROM chrome_scroll_update_info 153 WHERE id = ${this.sliceData!.scrollUpdateId}`); 154 const row = queryResult.firstRow({ 155 vsync_interval_ms: NUM_NULL, 156 is_presented: NUM_NULL, 157 is_janky: NUM_NULL, 158 is_inertial: NUM_NULL, 159 is_first_scroll_update_in_scroll: NUM_NULL, 160 is_first_scroll_update_in_frame: NUM_NULL, 161 }); 162 this.scrollData = { 163 vsyncInterval: 164 row.vsync_interval_ms === null 165 ? undefined 166 : Duration.fromMillis?.(row.vsync_interval_ms), 167 isPresented: fromSqlBool(row.is_presented), 168 isJanky: fromSqlBool(row.is_janky), 169 isInertial: fromSqlBool(row.is_inertial), 170 isFirstScrollUpdateInScroll: fromSqlBool( 171 row.is_first_scroll_update_in_scroll, 172 ), 173 isFirstScrollUpdateInFrame: fromSqlBool( 174 row.is_first_scroll_update_in_frame, 175 ), 176 }; 177 } 178 179 render(): m.Children { 180 return m( 181 DetailsShell, 182 { 183 title: 'Slice', 184 description: this.sliceData?.name ?? 'Loading...', 185 }, 186 m( 187 GridLayout, 188 m(GridLayoutColumn, this.renderSliceDetails()), 189 m(GridLayoutColumn, this.renderScrollDetails()), 190 ), 191 ); 192 } 193 194 private renderSliceDetails(): m.Child { 195 let child; 196 if (this.sliceData === undefined) { 197 child = 'Loading...'; 198 } else { 199 child = m( 200 Tree, 201 m(TreeNode, { 202 left: 'Name', 203 right: this.sliceData.name, 204 }), 205 m(TreeNode, { 206 left: 'Start time', 207 right: m(Timestamp, {ts: this.sliceData.ts}), 208 }), 209 m(TreeNode, { 210 left: 'Duration', 211 right: m(DurationWidget, {dur: this.sliceData.dur}), 212 }), 213 m(TreeNode, { 214 left: 'SQL ID', 215 right: renderSqlRef({ 216 trace: this.trace, 217 tableName: this.model.tableName, 218 tableDescription: { 219 name: this.model.tableName, 220 columns: createScrollTimelineTableColumns( 221 this.trace, 222 this.model.trackUri, 223 ), 224 }, 225 id: this.id, 226 }), 227 }), 228 ); 229 } 230 return m(Section, {title: 'Slice details'}, child); 231 } 232 233 private renderScrollDetails(): m.Child { 234 let child; 235 if (this.sliceData === undefined || this.scrollData === undefined) { 236 child = 'Loading...'; 237 } else { 238 const scrollTableDescription = this.trace.plugins 239 .getPlugin(SqlModulesPlugin) 240 .getSqlModules() 241 .getModuleForTable('chrome_scroll_update_info') 242 ?.getSqlTableDescription('chrome_scroll_update_info'); 243 child = m( 244 Tree, 245 m(TreeNode, { 246 left: 'Vsync interval', 247 right: 248 this.scrollData.vsyncInterval === undefined 249 ? `${this.scrollData.vsyncInterval}` 250 : m(DurationWidget, {dur: this.scrollData.vsyncInterval}), 251 }), 252 m(TreeNode, { 253 left: 'Is presented', 254 right: `${this.scrollData.isPresented}`, 255 }), 256 m(TreeNode, { 257 left: 'Is janky', 258 right: `${this.scrollData.isJanky}`, 259 }), 260 m(TreeNode, { 261 left: 'Is inertial', 262 right: `${this.scrollData.isInertial}`, 263 }), 264 m(TreeNode, { 265 left: 'Is first scroll update in scroll', 266 right: `${this.scrollData.isFirstScrollUpdateInScroll}`, 267 }), 268 m(TreeNode, { 269 left: 'Is first scroll update in frame', 270 right: `${this.scrollData.isFirstScrollUpdateInFrame}`, 271 }), 272 m(TreeNode, { 273 left: 'SQL ID', 274 right: renderSqlRef({ 275 trace: this.trace, 276 tableName: 'chrome_scroll_update_info', 277 id: this.sliceData.scrollUpdateId, 278 tableDescription: scrollTableDescription, 279 }), 280 }), 281 ); 282 } 283 return m(Section, {title: 'Scroll details'}, child); 284 } 285} 286