1// Copyright (C) 2022 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 {uuidv4Sql} from '../../base/uuid'; 16import {generateSqlWithInternalLayout} from '../../components/sql_utils/layout'; 17import {Trace} from '../../public/trace'; 18import {PerfettoPlugin} from '../../public/plugin'; 19import { 20 createEventLatencyTrack, 21 JANKY_LATENCY_NAME, 22} from './event_latency_track'; 23import {createScrollJankV3Track} from './scroll_jank_v3_track'; 24import {ScrollJankCauseMap} from './scroll_jank_cause_map'; 25import {TrackNode} from '../../public/workspace'; 26import SqlModulesPlugin from '../dev.perfetto.SqlModules'; 27import {createScrollTimelineModel} from './scroll_timeline_model'; 28import {createQuerySliceTrack} from '../../components/tracks/query_slice_track'; 29import {createFlatColoredDurationTrack} from './flat_colored_duration_track'; 30import {createTopLevelScrollTrack} from './scroll_track'; 31import {createScrollTimelineTrack} from './scroll_timeline_track'; 32import {LONG, NUM, STR} from '../../trace_processor/query_result'; 33import {SourceDataset} from '../../trace_processor/dataset'; 34import {DatasetSliceTrack} from '../../components/tracks/dataset_slice_track'; 35import {escapeQuery} from '../../trace_processor/query_utils'; 36import {ThreadSliceDetailsPanel} from '../../components/details/thread_slice_details_tab'; 37 38export default class implements PerfettoPlugin { 39 static readonly id = 'org.chromium.ChromeScrollJank'; 40 static readonly dependencies = [SqlModulesPlugin]; 41 42 async onTraceLoad(ctx: Trace): Promise<void> { 43 const group = new TrackNode({ 44 title: 'Chrome Scroll Jank', 45 sortOrder: -30, 46 isSummary: true, 47 }); 48 await this.addTopLevelScrollTrack(ctx, group); 49 await this.addEventLatencyTrack(ctx, group); 50 await this.addScrollJankV3ScrollTrack(ctx, group); 51 await ScrollJankCauseMap.initialize(ctx.engine); 52 await this.addScrollTimelineTrack(ctx, group); 53 await this.addVsyncTracks(ctx, group); 54 ctx.workspace.addChildInOrder(group); 55 group.expand(); 56 } 57 58 private async addTopLevelScrollTrack( 59 ctx: Trace, 60 group: TrackNode, 61 ): Promise<void> { 62 await ctx.engine.query(` 63 INCLUDE PERFETTO MODULE chrome.chrome_scrolls; 64 INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets; 65 INCLUDE PERFETTO MODULE chrome.event_latency; 66 `); 67 68 const uri = 'org.chromium.ChromeScrollJank#toplevelScrolls'; 69 const title = 'Chrome Scrolls'; 70 71 ctx.tracks.registerTrack({ 72 uri, 73 title, 74 track: createTopLevelScrollTrack(ctx, uri), 75 }); 76 77 const track = new TrackNode({uri, title}); 78 group.addChildInOrder(track); 79 } 80 81 private async addEventLatencyTrack( 82 ctx: Trace, 83 group: TrackNode, 84 ): Promise<void> { 85 const subTableSql = generateSqlWithInternalLayout({ 86 columns: ['id', 'ts', 'dur', 'track_id', 'name'], 87 source: 'chrome_event_latencies', 88 ts: 'ts', 89 dur: 'dur', 90 whereClause: ` 91 event_type IN ( 92 'FIRST_GESTURE_SCROLL_UPDATE', 93 'GESTURE_SCROLL_UPDATE', 94 'INERTIAL_GESTURE_SCROLL_UPDATE') 95 AND is_presented`, 96 }); 97 98 // Table name must be unique - it cannot include '-' characters or begin 99 // with a numeric value. 100 const baseTable = `table_${uuidv4Sql()}_janky_event_latencies_v3`; 101 const tableDefSql = `CREATE TABLE ${baseTable} AS 102 WITH 103 event_latencies AS MATERIALIZED ( 104 ${subTableSql} 105 ), 106 latency_stages AS ( 107 SELECT 108 stage.id, 109 stage.ts, 110 stage.dur, 111 stage.track_id, 112 stage.name, 113 stage.depth, 114 event.id as event_latency_id, 115 event.depth as event_latency_depth 116 FROM event_latencies event 117 JOIN descendant_slice(event.id) stage 118 UNION ALL 119 SELECT 120 event.id, 121 event.ts, 122 event.dur, 123 event.track_id, 124 IIF( 125 id IN (SELECT id FROM chrome_janky_event_latencies_v3), 126 '${JANKY_LATENCY_NAME}', 127 name 128 ) as name, 129 0 as depth, 130 event.id as event_latency_id, 131 event.depth as event_latency_depth 132 FROM event_latencies event 133 ), 134 -- Event latencies have already had layout computed, but the width of event latency can vary (3 or 4), 135 -- so we have to compute the max stage depth for each event latency depth to compute offset for each 136 -- event latency row. 137 event_latency_height_per_row AS ( 138 SELECT 139 event_latency_depth, 140 MAX(depth) AS max_depth 141 FROM latency_stages 142 GROUP BY event_latency_depth 143 ), 144 -- Compute the offset for each event latency depth using max depth info for each depth. 145 event_latency_layout_offset AS ( 146 SELECT 147 event_latency_depth, 148 -- As the sum is exclusive, it will return NULL for the first row — we need to set it to 0 explicitly. 149 IFNULL( 150 SUM(max_depth + 1) OVER ( 151 ORDER BY event_latency_depth 152 ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING 153 ), 154 0) as offset 155 FROM event_latency_height_per_row 156 ) 157 SELECT 158 stage.id, 159 stage.ts, 160 stage.dur, 161 stage.name, 162 stage.depth + ( 163 ( 164 SELECT offset.offset 165 FROM event_latencies event 166 JOIN event_latency_layout_offset offset ON event.depth = offset.event_latency_depth 167 WHERE id = stage.event_latency_id 168 ) 169 ) AS depth 170 FROM latency_stages stage;`; 171 172 await ctx.engine.query( 173 `INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals`, 174 ); 175 await ctx.engine.query(tableDefSql); 176 177 const uri = 'org.chromium.ChromeScrollJank#eventLatency'; 178 const title = 'Chrome Scroll Input Latencies'; 179 180 ctx.tracks.registerTrack({ 181 uri, 182 title, 183 track: createEventLatencyTrack(ctx, uri, baseTable), 184 }); 185 186 const track = new TrackNode({uri, title}); 187 group.addChildInOrder(track); 188 } 189 190 private async addScrollJankV3ScrollTrack( 191 ctx: Trace, 192 group: TrackNode, 193 ): Promise<void> { 194 await ctx.engine.query( 195 `INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals`, 196 ); 197 198 const uri = 'org.chromium.ChromeScrollJank#scrollJankV3'; 199 const title = 'Chrome Scroll Janks'; 200 201 ctx.tracks.registerTrack({ 202 uri, 203 title, 204 track: createScrollJankV3Track(ctx, uri), 205 }); 206 207 const track = new TrackNode({uri, title}); 208 group.addChildInOrder(track); 209 } 210 211 private async addScrollTimelineTrack( 212 ctx: Trace, 213 group: TrackNode, 214 ): Promise<void> { 215 const uri = 'org.chromium.ChromeScrollJank#scrollTimeline'; 216 const title = 'Chrome Scroll Timeline'; 217 218 const tableName = 219 'scrolltimelinetrack_org_chromium_ChromeScrollJank_scrollTimeline'; 220 const model = await createScrollTimelineModel(ctx.engine, tableName, uri); 221 222 ctx.tracks.registerTrack({ 223 uri, 224 title, 225 track: createScrollTimelineTrack(ctx, model), 226 }); 227 228 const track = new TrackNode({uri, title}); 229 group.addChildInOrder(track); 230 } 231 232 private async addVsyncTracks(ctx: Trace, group: TrackNode) { 233 const vsyncTable = '_chrome_scroll_jank_plugin_vsyncs'; 234 await ctx.engine.query(` 235 INCLUDE PERFETTO MODULE chrome.chrome_scrolls; 236 237 CREATE PERFETTO TABLE ${vsyncTable} AS 238 SELECT 239 id, 240 ts, 241 dur, 242 track_id, 243 name 244 FROM slice 245 WHERE name = 'Extend_VSync'`); 246 247 { 248 // Add a track for the VSync slices. 249 const uri = 'org.chromium.ChromeScrollJank#ChromeVsync'; 250 const track = await createQuerySliceTrack({ 251 trace: ctx, 252 data: {sqlSource: `SELECT * FROM ${vsyncTable}`}, 253 argColumns: ['id', 'track_id', 'ts', 'dur'], 254 uri, 255 }); 256 const title = 'Chrome VSync'; 257 ctx.tracks.registerTrack({uri, title, track}); 258 group.addChildInOrder(new TrackNode({uri, title})); 259 } 260 261 { 262 // Add a track which tracks the differences between VSyncs. 263 const uri = 'org.chromium.ChromeScrollJank#ChromeVsyncDelta'; 264 const track = createFlatColoredDurationTrack( 265 ctx, 266 uri, 267 `(SELECT id, ts, LEAD(ts) OVER (ORDER BY ts) - ts as dur FROM ${vsyncTable})`, 268 ); 269 const title = 'Chrome VSync delta'; 270 ctx.tracks.registerTrack({uri, title, track}); 271 group.addChildInOrder(new TrackNode({uri, title})); 272 } 273 274 { 275 // Add a track which tracks the differences between inputs. 276 const uri = 'org.chromium.ChromeScrollJank#ChromeInputDelta'; 277 const track = createFlatColoredDurationTrack( 278 ctx, 279 uri, 280 `(SELECT 281 ROW_NUMBER() OVER () AS id, 282 generation_ts AS ts, 283 LEAD(generation_ts) OVER (ORDER BY generation_ts) - generation_ts as dur 284 FROM chrome_scroll_update_info 285 WHERE generation_ts IS NOT NULL)`, 286 ); 287 const title = 'Chrome input delta'; 288 ctx.tracks.registerTrack({uri, title, track}); 289 group.addChildInOrder(new TrackNode({uri, title})); 290 } 291 292 { 293 const steps = [ 294 { 295 column: 'scroll_update_created_slice_id', 296 name: 'Step: send event (UI, ScrollUpdate)', 297 }, 298 { 299 column: 'compositor_dispatch_slice_id', 300 name: 'Step: compositor dispatch (ScrollUpdate)', 301 }, 302 { 303 column: 'compositor_resample_slice_id', 304 name: 'Step: resample input', 305 }, 306 { 307 column: 'compositor_generate_compositor_frame_slice_id', 308 name: 'Step: generate frame (compositor)', 309 }, 310 { 311 column: 'viz_receive_compositor_frame_slice_id', 312 name: 'Step: receive frame (viz)', 313 }, 314 ]; 315 316 for (const step of steps) { 317 const uri = `org.chromium.ChromeScrollJank#chrome_scroll_update_info.${step.column}`; 318 const track = new DatasetSliceTrack({ 319 trace: ctx, 320 uri, 321 dataset: new SourceDataset({ 322 schema: { 323 id: NUM, 324 ts: LONG, 325 dur: LONG, 326 name: STR, 327 }, 328 src: ` 329 WITH slice_ids AS MATERIALIZED ( 330 SELECT DISTINCT ${step.column} AS slice_id 331 FROM chrome_scroll_update_info 332 WHERE ${step.column} IS NOT NULL 333 ) 334 SELECT 335 slice.id, 336 slice.ts, 337 slice.dur, 338 ${escapeQuery(step.name)} AS name 339 FROM slice_ids 340 JOIN slice USING (slice_id) 341 `, 342 }), 343 detailsPanel: () => new ThreadSliceDetailsPanel(ctx), 344 }); 345 ctx.tracks.registerTrack({uri, title: step.name, track}); 346 group.addChildInOrder(new TrackNode({uri, title: step.name})); 347 } 348 } 349 } 350} 351