// Copyright (C) 2023 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import m from 'mithril'; import {duration, Time, time} from '../../base/time'; import {Engine} from '../../trace_processor/engine'; import {LONG, NUM} from '../../trace_processor/query_result'; import {VegaView} from '../../components/widgets/vega_view'; const INPUT_CATEGORY = 'Input'; const PRESENTED_CATEGORY = 'Presented'; const PRESENTED_JANKY_CATEGORY = 'Presented with Predictor Jank'; interface ScrollDeltaPlotDatum { // What type of data this is - input scroll or presented scroll. This is used // to denote the color of the data point. category: string; offset: number; scrollUpdateId: number; ts: number; delta: number; predictorJank: string; } export interface ScrollDeltaDetails { ts: time; scrollUpdateId: number; scrollDelta: number; scrollOffset: number; predictorJank: number; } export interface JankIntervalPlotDetails { start_ts: number; end_ts: number; } export async function getInputScrollDeltas( engine: Engine, scrollId: bigint, ): Promise { const queryResult = await engine.query(` INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets; SELECT ts, IFNULL(scroll_update_id, 0) AS scrollUpdateId, delta_y AS deltaY, relative_offset_y AS offsetY FROM chrome_scroll_input_offsets WHERE scroll_id = ${scrollId}; `); const it = queryResult.iter({ ts: LONG, scrollUpdateId: NUM, deltaY: NUM, offsetY: NUM, }); const deltas: ScrollDeltaDetails[] = []; for (; it.valid(); it.next()) { deltas.push({ ts: Time.fromRaw(it.ts), scrollUpdateId: it.scrollUpdateId, scrollOffset: it.offsetY, scrollDelta: it.deltaY, predictorJank: 0, }); } return deltas; } export async function getPresentedScrollDeltas( engine: Engine, scrollId: bigint, ): Promise { const queryResult = await engine.query(` INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets; SELECT ts, IFNULL(scroll_update_id, 0) AS scrollUpdateId, delta_y AS deltaY, relative_offset_y AS offsetY FROM chrome_presented_scroll_offsets WHERE scroll_id = ${scrollId} -- Filter out the deltas which do not have a presented timestamp. -- This is needed for now as we don't perfectly all EventLatencies to -- presentation, e.g. for dropped frames (crbug.com/380286381). AND ts IS NOT NULL AND delta_y IS NOT NULL; `); const it = queryResult.iter({ ts: LONG, scrollUpdateId: NUM, deltaY: NUM, offsetY: NUM, }); const deltas: ScrollDeltaDetails[] = []; let offset = 0; for (; it.valid(); it.next()) { offset = it.offsetY; deltas.push({ ts: Time.fromRaw(it.ts), scrollUpdateId: it.scrollUpdateId, scrollOffset: offset, scrollDelta: it.deltaY, predictorJank: 0, }); } return deltas; } export async function getPredictorJankDeltas( engine: Engine, scrollId: bigint, ): Promise { const queryResult = await engine.query(` INCLUDE PERFETTO MODULE chrome.scroll_jank.predictor_error; SELECT present_ts AS ts, IFNULL(scroll_update_id, 0) AS scrollUpdateId, delta_y AS deltaY, relative_offset_y AS offsetY, predictor_jank AS predictorJank FROM chrome_predictor_error WHERE scroll_id = ${scrollId} AND predictor_jank != 0 AND predictor_jank IS NOT NULL; `); const it = queryResult.iter({ ts: LONG, scrollUpdateId: NUM, deltaY: NUM, offsetY: NUM, predictorJank: NUM, }); const deltas: ScrollDeltaDetails[] = []; let offset = 0; for (; it.valid(); it.next()) { offset = it.offsetY; deltas.push({ ts: Time.fromRaw(it.ts), scrollUpdateId: it.scrollUpdateId, scrollOffset: offset, scrollDelta: it.deltaY, predictorJank: it.predictorJank, }); } return deltas; } export async function getJankIntervals( engine: Engine, startTs: time, dur: duration, ): Promise { const queryResult = await engine.query(` INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals; SELECT ts, dur FROM chrome_janky_frame_presentation_intervals WHERE ts >= ${startTs} AND ts <= ${startTs + dur}; `); const it = queryResult.iter({ ts: LONG, dur: LONG, }); const details: JankIntervalPlotDetails[] = []; for (; it.valid(); it.next()) { details.push({ start_ts: Number(it.ts), end_ts: Number(it.ts + it.dur), }); } return details; } // TODO(b/352038635): Show the error margin on the graph - what the pixel offset // should have been if there were no predictor jank. export function buildScrollOffsetsGraph( inputDeltas: ScrollDeltaDetails[], presentedDeltas: ScrollDeltaDetails[], predictorDeltas: ScrollDeltaDetails[], jankIntervals: JankIntervalPlotDetails[], ): m.Child { const inputData = buildOffsetData(inputDeltas, INPUT_CATEGORY); // Filter out the predictor deltas from the presented deltas, as these will be // rendered in a new layer, with new tooltip/color/etc. const filteredPresentedDeltas = presentedDeltas.filter((item) => { for (let i = 0; i < predictorDeltas.length; i++) { const predictorDelta: ScrollDeltaDetails = predictorDeltas[i]; if ( predictorDelta.ts == item.ts && predictorDelta.scrollUpdateId == item.scrollUpdateId ) { return false; } } return true; }); const presentedData = buildOffsetData( filteredPresentedDeltas, PRESENTED_CATEGORY, ); const predictorData = buildOffsetData( predictorDeltas, PRESENTED_JANKY_CATEGORY, ); const jankData = buildJankLayerData(jankIntervals); return m(VegaView, { spec: ` { "$schema": "https://vega.github.io/schema/vega-lite/v5.json", "description": "Scatter plot showcasing the pixel offset deltas between input frames and presented frames.", "width": "container", "height": 200, "padding": 5, "data": { "name": "table" }, "layer": [ { "mark": "rect", "data": { "values": [ ${jankData} ] }, "encoding": { "x": { "field": "start", "type": "quantitative" }, "x2": { "field": "end", "type": "quantitative" }, "color": { "value": "#D3D3D3" } } }, { "mark": { "type": "point", "filled": true }, "encoding": { "x": { "field": "ts", "type": "quantitative", "title": "Raw Timestamp", "axis" : { "labels": true }, "scale": {"zero":false} }, "y": { "field": "offset", "type": "quantitative", "title": "Offset (pixels)", "scale": {"zero":false} }, "color": { "field": "category", "type": "nominal", "scale": { "domain": [ "${INPUT_CATEGORY}", "${PRESENTED_CATEGORY}", "${PRESENTED_JANKY_CATEGORY}" ], "range": ["blue", "red", "orange"] }, "legend": { "title":null } }, "tooltip": [ { "field": "delta", "type": "quantitative", "title": "Delta", "format": ".2f" }, { "field": "scrollUpdateId", "type": "quantititive", "title": "Trace Id" }, { "field": "predictorJank", "type": "nominal", "title": "Predictor Jank" } ] } } ] } `, data: {table: inputData.concat(presentedData).concat(predictorData)}, }); } function buildOffsetData( deltas: ScrollDeltaDetails[], category: string, ): ScrollDeltaPlotDatum[] { const plotData: ScrollDeltaPlotDatum[] = []; for (const delta of deltas) { let predictorJank = 'N/A'; if (delta.predictorJank > 0) { predictorJank = parseFloat(delta.predictorJank.toString()).toFixed(2); predictorJank += " (times delta compared to the next/previous frame's delta)"; } plotData.push({ category: category, ts: Number(delta.ts) / 10e8, scrollUpdateId: delta.scrollUpdateId, offset: delta.scrollOffset, delta: delta.scrollDelta, predictorJank: predictorJank, }); } return plotData; } function buildJankLayerData(janks: JankIntervalPlotDetails[]): string { let dataJsonString = ''; for (let i = 0; i < janks.length; i++) { if (i != 0) { dataJsonString += ','; } const jank = janks[i]; dataJsonString += ` { "start": ${jank.start_ts / 10e8}, "end": ${jank.end_ts / 10e8} } `; } return dataJsonString; }