| // 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 {test, type Page} from '@playwright/test'; |
| import {PerfettoTestHelper} from './perfetto_ui_test_helper'; |
| import { |
| EVENT_LATENCY_TRACK, |
| SCROLL_TIMELINE_TRACK, |
| SCROLL_TIMELINE_V4_TRACK, |
| type TrackSpec, |
| } from '../plugins/org.chromium.ChromeScrollJank/tracks'; |
| import {Time, type time} from '../base/time'; |
| |
| let pth: PerfettoTestHelper; |
| let page: Page; |
| |
| test.beforeEach(async ({browser}, _testInfo) => { |
| page = await browser.newPage(); |
| pth = new PerfettoTestHelper(page); |
| await pth.openTraceFile('chrome/scroll_m144.pftrace', { |
| enablePlugins: 'org.chromium.ChromeScrollJank', |
| }); |
| }); |
| |
| test.afterEach(async () => await page.close()); |
| |
| async function selectPluginSlice( |
| trackSpec: TrackSpec, |
| name: string, |
| ts: time, |
| ): Promise<void> { |
| // We cannot use `PerfettoTestHelper.searchSlice()` because omnibox search |
| // results don't include track events (slices) created by plugins. |
| // |
| // We could theoretically emulate clicking on the relevant plugin slice with |
| // the following code: |
| // |
| // ``` |
| // const coords = assertExists(await trk.boundingBox()); |
| // await page.mouse.click(coords.x + 823, coords.y + 120); |
| // ``` |
| // |
| // but it would break easily, in which case updating the coordinates manually |
| // would be very tedious. So we do the following instead. |
| await page.evaluate( |
| async ({tableName, trackUri, name, ts}) => { |
| const trace = self.app.trace!; |
| |
| // Step 1: Find the ID of the slice. |
| const result = await trace.engine.query(` |
| SELECT id |
| FROM ${tableName} |
| WHERE name = '${name}' AND ts = ${ts} |
| `); |
| if (result.numRows() > 1) { |
| throw new Error('Multiple slices match'); |
| } |
| const id = result.firstRow({id: Number()}).id; |
| |
| // Step 2: Select the slice. |
| trace.selection.selectTrackEvent(trackUri, id); |
| }, |
| {tableName: trackSpec.tableName, trackUri: trackSpec.uri, name, ts}, |
| ); |
| } |
| |
| test('event_latency_track', async () => { |
| const trk = pth.locateTrack( |
| 'Chrome Scroll Jank/Chrome Scroll Input Latencies', |
| ); |
| await trk.scrollIntoViewIfNeeded(); |
| await pth.waitForIdleAndScreenshot('track.png', { |
| locator: page.locator('.pf-timeline-page__timeline'), |
| }); |
| |
| // Select the 'RendererCompositorQueueingDelay' stage within the first |
| // janky EventLatency. |
| await selectPluginSlice( |
| EVENT_LATENCY_TRACK, |
| 'RendererCompositorQueueingDelay', |
| Time.fromRaw(16784825798017n), |
| ); |
| await trk.scrollIntoViewIfNeeded(); |
| await pth.waitForIdleAndScreenshot('details_panel_stage.png', { |
| locator: page.locator('.pf-timeline-page__timeline'), |
| }); |
| |
| // Jump from the stage to the first janky EventLatency. |
| await page.getByText('Parent EventLatency').click(); |
| await pth.waitForIdleAndScreenshot('details_panel_event_latency.png', { |
| locator: page.locator('.pf-timeline-page__timeline'), |
| }); |
| |
| // Jump from the first janky EventLatency to the corresponding scroll update. |
| await page.getByText('Corresponding scroll update').click(); |
| await pth.waitForIdleAndScreenshot( |
| 'details_panel_link_to_scroll_timeline.png', |
| {locator: page.locator('.pf-timeline-page__timeline')}, |
| ); |
| |
| // Go back to the first janky EventLatency and then jump the corresponding frame. |
| await selectPluginSlice( |
| EVENT_LATENCY_TRACK, |
| 'Janky EventLatency', |
| Time.fromRaw(16784822412017n), |
| ); |
| await page |
| .getByText('Frame where this was the first presented EventLatency') |
| .click(); |
| await pth.waitForIdleAndScreenshot( |
| 'details_panel_link_to_scroll_timeline_v4.png', |
| {locator: page.locator('.pf-timeline-page__timeline')}, |
| ); |
| }); |
| |
| test('scroll_timeline_track', async () => { |
| const trk = pth.locateTrack('Chrome Scroll Jank/Chrome Scroll Timeline'); |
| await trk.scrollIntoViewIfNeeded(); |
| await pth.waitForIdleAndScreenshot('track.png', { |
| locator: page.locator('.pf-timeline-page__timeline'), |
| }); |
| |
| // Select the 'GenerationToBrowserMain' stage within the first inertial scroll |
| // update. |
| await selectPluginSlice( |
| SCROLL_TIMELINE_TRACK, |
| 'GenerationToBrowserMain', |
| Time.fromRaw(16784307235017n), |
| ); |
| await trk.scrollIntoViewIfNeeded(); |
| await pth.waitForIdleAndScreenshot('details_panel_stage.png', { |
| locator: page.locator('.pf-timeline-page__timeline'), |
| }); |
| |
| // Jump from the stage to the first inertial scroll update. |
| await page.getByText('Parent scroll update').click(); |
| await pth.waitForIdleAndScreenshot('details_panel_scroll_update.png', { |
| locator: page.locator('.pf-timeline-page__timeline'), |
| }); |
| |
| // Jump from the first inertial scroll update to the corresponding EventLatency. |
| await page.getByText('Corresponding EventLatency').click(); |
| await pth.waitForIdleAndScreenshot( |
| 'details_panel_link_to_event_latency.png', |
| { |
| locator: page.locator('.pf-timeline-page__timeline'), |
| }, |
| ); |
| |
| // Go back to the first inertial scroll update and then jump the corresponding |
| // frame. |
| await selectPluginSlice( |
| SCROLL_TIMELINE_TRACK, |
| 'Inertial Scroll Update', |
| Time.fromRaw(16784307235017n), |
| ); |
| await page |
| .getByText('Frame where this was the first presented scroll update') |
| .click(); |
| await pth.waitForIdleAndScreenshot( |
| 'details_panel_link_to_scroll_timeline_v4.png', |
| {locator: page.locator('.pf-timeline-page__timeline')}, |
| ); |
| }); |
| |
| test('scroll_timeline_v4_track', async () => { |
| const trk = pth.locateTrack('Chrome Scroll Jank/Chrome Scroll Timeline v4'); |
| await trk.scrollIntoViewIfNeeded(); |
| await pth.waitForIdleAndScreenshot('scroll_timeline_v4_track.png', { |
| locator: page.locator('.pf-timeline-page__timeline'), |
| }); |
| |
| // Select the 'Real scroll update input generation' stage within the second |
| // janky frame. |
| await selectPluginSlice( |
| SCROLL_TIMELINE_V4_TRACK, |
| 'Real scroll update input generation', |
| Time.fromRaw(16784838286017n), |
| ); |
| await trk.scrollIntoViewIfNeeded(); |
| await pth.waitForIdleAndScreenshot( |
| 'scroll_timeline_v4_details_panel_stage.png', |
| {locator: page.locator('.pf-timeline-page__timeline')}, |
| ); |
| |
| // Jump from the stage to the second janky frame. |
| await page.getByText('Parent frame').click(); |
| await pth.waitForIdleAndScreenshot( |
| 'scroll_timeline_v4_details_panel_frame.png', |
| {locator: page.locator('.pf-timeline-page__timeline')}, |
| ); |
| |
| // Jump from the second janky frame to the corresponding EventLatency. |
| await page.getByText('First EventLatency in this frame').click(); |
| await pth.waitForIdleAndScreenshot( |
| 'scroll_timeline_v4_details_panel_link_to_event_latency.png', |
| {locator: page.locator('.pf-timeline-page__timeline')}, |
| ); |
| |
| // Go back to the second janky frame and then jump the corresponding frame. |
| await selectPluginSlice( |
| SCROLL_TIMELINE_V4_TRACK, |
| 'Janky Frame', |
| Time.fromRaw(16784838286017n), |
| ); |
| await page.getByText('First scroll update in this frame').click(); |
| await pth.waitForIdleAndScreenshot( |
| 'scroll_timeline_v4_details_panel_link_to_scroll_timeline.png', |
| {locator: page.locator('.pf-timeline-page__timeline')}, |
| ); |
| }); |