• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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