| // Copyright (C) 2026 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 {generateSqlWithInternalLayout} from '../../components/sql_utils/layout'; |
| import type {SqlTableDefinition} from '../../components/widgets/sql/table/table_description'; |
| import type {Engine} from '../../trace_processor/engine'; |
| import {PerfettoSqlTypes} from '../../trace_processor/perfetto_sql_type'; |
| import {EVENT_LATENCY_TRACK} from './tracks'; |
| |
| export const JANKY_LATENCY_NAME = 'Janky EventLatency'; |
| |
| /** |
| * Definition of the Perfetto table created by |
| * {@link createEventLatencyModel}, which underpins |
| * {@link tracks#EVENT_LATENCY_TRACK}. |
| * |
| * Note: The table contains both: |
| * |
| * 1. parent slices for entire scroll updates (e.g. 'Janky EventLatency') and |
| * 2. child slices for the individual stages of a scroll update (e.g. |
| * 'GenerationToBrowserMain'). Stages can be NESTED (i.e. the parent of |
| * each stage is either a scroll update or a stage). |
| */ |
| export const EVENT_LATENCY_TABLE_DEFINITION: SqlTableDefinition = { |
| name: EVENT_LATENCY_TRACK.tableName, |
| columns: [ |
| /** |
| * Unique ID of the slice. Copied from the original slices in the `slice` |
| * table. Can be joined with `chrome_event_latencies.id`. |
| */ |
| { |
| column: 'id', |
| type: { |
| kind: 'joinid', |
| source: {table: 'slice', column: 'id'}, |
| }, |
| }, |
| |
| /** Start timestamp of the slice. */ |
| {column: 'ts', type: PerfettoSqlTypes.TIMESTAMP}, |
| |
| /** Duration of the slice. */ |
| {column: 'dur', type: PerfettoSqlTypes.DURATION}, |
| |
| /** Title of the slice. */ |
| {column: 'name', type: PerfettoSqlTypes.STRING}, |
| |
| /** Depth of the slice on the track. */ |
| {column: 'depth', type: PerfettoSqlTypes.INT}, |
| |
| /** |
| * ID of the scroll update event (aka LatencyInfo.ID). Can be joined with |
| * `chrome_event_latencies.scroll_update_id`, `chrome_scroll_update_info.id` |
| * and many other tables in Chrome's tracing stdlib. |
| * |
| * In general, multiple rows in this table correspond to a single row in |
| * `chrome_event_latencies`. One for the scroll update (parent) slice and |
| * zero or more for the stage (child) slices. |
| */ |
| { |
| column: 'scroll_update_id', |
| type: { |
| kind: 'joinid', |
| source: {table: 'chrome_event_latencies', column: 'scroll_update_id'}, |
| }, |
| }, |
| |
| /** |
| * ID of the parent scroll update slice or parent stage slice if the row |
| * corresponds to a stage of a scroll update. NULL if the row corresponds to |
| * a scroll update. |
| */ |
| { |
| column: 'parent_id', |
| type: { |
| kind: 'joinid', |
| source: {table: 'slice', column: 'id'}, |
| }, |
| }, |
| ], |
| }; |
| |
| export async function createEventLatencyModel(engine: Engine): Promise<void> { |
| await engine.query(` |
| INCLUDE PERFETTO MODULE chrome.event_latency; |
| INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals; |
| |
| CREATE TABLE ${EVENT_LATENCY_TRACK.tableName} AS |
| WITH |
| event_latencies AS MATERIALIZED ( |
| ${generateSqlWithInternalLayout({ |
| columns: ['id', 'ts', 'dur', 'name', 'scroll_update_id'], |
| source: 'chrome_event_latencies', |
| ts: 'ts', |
| dur: 'dur', |
| whereClause: ` |
| event_type IN ( |
| 'FIRST_GESTURE_SCROLL_UPDATE', |
| 'GESTURE_SCROLL_UPDATE', |
| 'INERTIAL_GESTURE_SCROLL_UPDATE') |
| AND is_presented`, |
| })} |
| ), |
| latency_stages AS ( |
| SELECT |
| stage.id, |
| stage.ts, |
| stage.dur, |
| stage.name, |
| stage.depth, |
| event.id as event_latency_id, |
| event.depth as event_latency_depth, |
| event.scroll_update_id, |
| stage.parent_id |
| FROM event_latencies event |
| JOIN descendant_slice(event.id) stage |
| UNION ALL |
| SELECT |
| event.id, |
| event.ts, |
| event.dur, |
| IIF( |
| id IN (SELECT id FROM chrome_janky_event_latencies_v3), |
| '${JANKY_LATENCY_NAME}', |
| name |
| ) as name, |
| 0 as depth, |
| event.id as event_latency_id, |
| event.depth as event_latency_depth, |
| event.scroll_update_id, |
| NULL as parent_id |
| FROM event_latencies event |
| ), |
| -- Event latencies have already had layout computed, but the width of event latency can vary (3 or 4), |
| -- so we have to compute the max stage depth for each event latency depth to compute offset for each |
| -- event latency row. |
| event_latency_height_per_row AS ( |
| SELECT |
| event_latency_depth, |
| MAX(depth) AS max_depth |
| FROM latency_stages |
| GROUP BY event_latency_depth |
| ), |
| -- Compute the offset for each event latency depth using max depth info for each depth. |
| event_latency_layout_offset AS ( |
| SELECT |
| event_latency_depth, |
| -- As the sum is exclusive, it will return NULL for the first row — we need to set it to 0 explicitly. |
| IFNULL( |
| SUM(max_depth + 1) OVER ( |
| ORDER BY event_latency_depth |
| ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING |
| ), |
| 0) as offset |
| FROM event_latency_height_per_row |
| ) |
| SELECT |
| stage.id, |
| stage.ts, |
| stage.dur, |
| stage.name, |
| stage.depth + ( |
| ( |
| SELECT offset.offset |
| FROM event_latencies event |
| JOIN event_latency_layout_offset offset ON event.depth = offset.event_latency_depth |
| WHERE id = stage.event_latency_id |
| ) |
| ) AS depth, |
| stage.scroll_update_id, |
| stage.parent_id |
| FROM latency_stages stage;`); |
| } |