1// Copyright (C) 2023 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 {duration, Time, time} from '../../base/time'; 17import {Engine} from '../../trace_processor/engine'; 18import {LONG, NUM} from '../../trace_processor/query_result'; 19import {VegaView} from '../../components/widgets/vega_view'; 20 21const INPUT_CATEGORY = 'Input'; 22const PRESENTED_CATEGORY = 'Presented'; 23const PRESENTED_JANKY_CATEGORY = 'Presented with Predictor Jank'; 24 25interface ScrollDeltaPlotDatum { 26 // What type of data this is - input scroll or presented scroll. This is used 27 // to denote the color of the data point. 28 category: string; 29 offset: number; 30 scrollUpdateId: number; 31 ts: number; 32 delta: number; 33 predictorJank: string; 34} 35 36export interface ScrollDeltaDetails { 37 ts: time; 38 scrollUpdateId: number; 39 scrollDelta: number; 40 scrollOffset: number; 41 predictorJank: number; 42} 43 44export interface JankIntervalPlotDetails { 45 start_ts: number; 46 end_ts: number; 47} 48 49export async function getInputScrollDeltas( 50 engine: Engine, 51 scrollId: bigint, 52): Promise<ScrollDeltaDetails[]> { 53 const queryResult = await engine.query(` 54 INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets; 55 56 SELECT 57 ts, 58 IFNULL(scroll_update_id, 0) AS scrollUpdateId, 59 delta_y AS deltaY, 60 relative_offset_y AS offsetY 61 FROM chrome_scroll_input_offsets 62 WHERE scroll_id = ${scrollId}; 63 `); 64 65 const it = queryResult.iter({ 66 ts: LONG, 67 scrollUpdateId: NUM, 68 deltaY: NUM, 69 offsetY: NUM, 70 }); 71 const deltas: ScrollDeltaDetails[] = []; 72 73 for (; it.valid(); it.next()) { 74 deltas.push({ 75 ts: Time.fromRaw(it.ts), 76 scrollUpdateId: it.scrollUpdateId, 77 scrollOffset: it.offsetY, 78 scrollDelta: it.deltaY, 79 predictorJank: 0, 80 }); 81 } 82 83 return deltas; 84} 85 86export async function getPresentedScrollDeltas( 87 engine: Engine, 88 scrollId: bigint, 89): Promise<ScrollDeltaDetails[]> { 90 const queryResult = await engine.query(` 91 INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets; 92 93 SELECT 94 ts, 95 IFNULL(scroll_update_id, 0) AS scrollUpdateId, 96 delta_y AS deltaY, 97 relative_offset_y AS offsetY 98 FROM chrome_presented_scroll_offsets 99 WHERE scroll_id = ${scrollId} 100 -- Filter out the deltas which do not have a presented timestamp. 101 -- This is needed for now as we don't perfectly all EventLatencies to 102 -- presentation, e.g. for dropped frames (crbug.com/380286381). 103 AND ts IS NOT NULL 104 AND delta_y IS NOT NULL; 105 `); 106 107 const it = queryResult.iter({ 108 ts: LONG, 109 scrollUpdateId: NUM, 110 deltaY: NUM, 111 offsetY: NUM, 112 }); 113 const deltas: ScrollDeltaDetails[] = []; 114 let offset = 0; 115 116 for (; it.valid(); it.next()) { 117 offset = it.offsetY; 118 119 deltas.push({ 120 ts: Time.fromRaw(it.ts), 121 scrollUpdateId: it.scrollUpdateId, 122 scrollOffset: offset, 123 scrollDelta: it.deltaY, 124 predictorJank: 0, 125 }); 126 } 127 128 return deltas; 129} 130 131export async function getPredictorJankDeltas( 132 engine: Engine, 133 scrollId: bigint, 134): Promise<ScrollDeltaDetails[]> { 135 const queryResult = await engine.query(` 136 INCLUDE PERFETTO MODULE chrome.scroll_jank.predictor_error; 137 138 SELECT 139 present_ts AS ts, 140 IFNULL(scroll_update_id, 0) AS scrollUpdateId, 141 delta_y AS deltaY, 142 relative_offset_y AS offsetY, 143 predictor_jank AS predictorJank 144 FROM chrome_predictor_error 145 WHERE scroll_id = ${scrollId} 146 AND predictor_jank != 0 AND predictor_jank IS NOT NULL; 147 `); 148 149 const it = queryResult.iter({ 150 ts: LONG, 151 scrollUpdateId: NUM, 152 deltaY: NUM, 153 offsetY: NUM, 154 predictorJank: NUM, 155 }); 156 const deltas: ScrollDeltaDetails[] = []; 157 let offset = 0; 158 159 for (; it.valid(); it.next()) { 160 offset = it.offsetY; 161 162 deltas.push({ 163 ts: Time.fromRaw(it.ts), 164 scrollUpdateId: it.scrollUpdateId, 165 scrollOffset: offset, 166 scrollDelta: it.deltaY, 167 predictorJank: it.predictorJank, 168 }); 169 } 170 171 return deltas; 172} 173 174export async function getJankIntervals( 175 engine: Engine, 176 startTs: time, 177 dur: duration, 178): Promise<JankIntervalPlotDetails[]> { 179 const queryResult = await engine.query(` 180 INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals; 181 182 SELECT 183 ts, 184 dur 185 FROM chrome_janky_frame_presentation_intervals 186 WHERE ts >= ${startTs} AND ts <= ${startTs + dur}; 187 `); 188 189 const it = queryResult.iter({ 190 ts: LONG, 191 dur: LONG, 192 }); 193 194 const details: JankIntervalPlotDetails[] = []; 195 196 for (; it.valid(); it.next()) { 197 details.push({ 198 start_ts: Number(it.ts), 199 end_ts: Number(it.ts + it.dur), 200 }); 201 } 202 203 return details; 204} 205 206// TODO(b/352038635): Show the error margin on the graph - what the pixel offset 207// should have been if there were no predictor jank. 208export function buildScrollOffsetsGraph( 209 inputDeltas: ScrollDeltaDetails[], 210 presentedDeltas: ScrollDeltaDetails[], 211 predictorDeltas: ScrollDeltaDetails[], 212 jankIntervals: JankIntervalPlotDetails[], 213): m.Child { 214 const inputData = buildOffsetData(inputDeltas, INPUT_CATEGORY); 215 // Filter out the predictor deltas from the presented deltas, as these will be 216 // rendered in a new layer, with new tooltip/color/etc. 217 const filteredPresentedDeltas = presentedDeltas.filter((item) => { 218 for (let i = 0; i < predictorDeltas.length; i++) { 219 const predictorDelta: ScrollDeltaDetails = predictorDeltas[i]; 220 if ( 221 predictorDelta.ts == item.ts && 222 predictorDelta.scrollUpdateId == item.scrollUpdateId 223 ) { 224 return false; 225 } 226 } 227 return true; 228 }); 229 230 const presentedData = buildOffsetData( 231 filteredPresentedDeltas, 232 PRESENTED_CATEGORY, 233 ); 234 const predictorData = buildOffsetData( 235 predictorDeltas, 236 PRESENTED_JANKY_CATEGORY, 237 ); 238 const jankData = buildJankLayerData(jankIntervals); 239 240 return m(VegaView, { 241 spec: ` 242{ 243 "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 244 "description": "Scatter plot showcasing the pixel offset deltas between input frames and presented frames.", 245 "width": "container", 246 "height": 200, 247 "padding": 5, 248 249 "data": { 250 "name": "table" 251 }, 252 253 "layer": [ 254 { 255 "mark": "rect", 256 "data": { 257 "values": [ 258 ${jankData} 259 ] 260 }, 261 "encoding": { 262 "x": { 263 "field": "start", 264 "type": "quantitative" 265 }, 266 "x2": { 267 "field": "end", 268 "type": "quantitative" 269 }, 270 "color": { 271 "value": "#D3D3D3" 272 } 273 } 274 }, 275 { 276 "mark": { 277 "type": "point", 278 "filled": true 279 }, 280 281 "encoding": { 282 "x": { 283 "field": "ts", 284 "type": "quantitative", 285 "title": "Raw Timestamp", 286 "axis" : { 287 "labels": true 288 }, 289 "scale": {"zero":false} 290 }, 291 "y": { 292 "field": "offset", 293 "type": "quantitative", 294 "title": "Offset (pixels)", 295 "scale": {"zero":false} 296 }, 297 "color": { 298 "field": "category", 299 "type": "nominal", 300 "scale": { 301 "domain": [ 302 "${INPUT_CATEGORY}", 303 "${PRESENTED_CATEGORY}", 304 "${PRESENTED_JANKY_CATEGORY}" 305 ], 306 "range": ["blue", "red", "orange"] 307 }, 308 "legend": { 309 "title":null 310 } 311 }, 312 "tooltip": [ 313 { 314 "field": "delta", 315 "type": "quantitative", 316 "title": "Delta", 317 "format": ".2f" 318 }, 319 { 320 "field": "scrollUpdateId", 321 "type": "quantititive", 322 "title": "Trace Id" 323 }, 324 { 325 "field": "predictorJank", 326 "type": "nominal", 327 "title": "Predictor Jank" 328 } 329 ] 330 } 331 } 332 ] 333} 334`, 335 data: {table: inputData.concat(presentedData).concat(predictorData)}, 336 }); 337} 338 339function buildOffsetData( 340 deltas: ScrollDeltaDetails[], 341 category: string, 342): ScrollDeltaPlotDatum[] { 343 const plotData: ScrollDeltaPlotDatum[] = []; 344 for (const delta of deltas) { 345 let predictorJank = 'N/A'; 346 if (delta.predictorJank > 0) { 347 predictorJank = parseFloat(delta.predictorJank.toString()).toFixed(2); 348 predictorJank += 349 " (times delta compared to the next/previous frame's delta)"; 350 } 351 plotData.push({ 352 category: category, 353 ts: Number(delta.ts) / 10e8, 354 scrollUpdateId: delta.scrollUpdateId, 355 offset: delta.scrollOffset, 356 delta: delta.scrollDelta, 357 predictorJank: predictorJank, 358 }); 359 } 360 361 return plotData; 362} 363 364function buildJankLayerData(janks: JankIntervalPlotDetails[]): string { 365 let dataJsonString = ''; 366 for (let i = 0; i < janks.length; i++) { 367 if (i != 0) { 368 dataJsonString += ','; 369 } 370 const jank = janks[i]; 371 dataJsonString += ` 372 { 373 "start": ${jank.start_ts / 10e8}, 374 "end": ${jank.end_ts / 10e8} 375 } 376 `; 377 } 378 return dataJsonString; 379} 380