blob: 34df4e1efa6bda91087032a6f377c4c993edce99 [file] [log] [blame]
// Copyright (C) 2022 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 {v4 as uuidv4} from 'uuid';
import {Actions, AddTrackArgs, DeferredAction} from '../../common/actions';
import {
generateSqlWithInternalLayout,
} from '../../common/internal_layout_utils';
import {ObjectByKey} from '../../common/state';
import {featureFlags} from '../../core/feature_flags';
import {
NUM,
Plugin,
PluginContext,
PluginContextTrace,
PluginDescriptor,
PrimaryTrackSortKey,
} from '../../public';
import {Engine, EngineProxy} from '../../trace_processor/engine';
import {CustomSqlDetailsPanelConfig} from '../custom_sql_table_slices';
import {NULL_TRACK_URI} from '../null_track';
import {ChromeTasksScrollJankTrack} from './chrome_tasks_scroll_jank_track';
import {
addLatencyTracks,
EventLatencyTrack,
JANKY_LATENCY_NAME,
} from './event_latency_track';
import {ScrollJankCauseMap} from './scroll_jank_cause_map';
import {
addScrollJankV3ScrollTrack,
ScrollJankV3Track,
} from './scroll_jank_v3_track';
import {
addTopLevelScrollTrack,
CHROME_TOPLEVEL_SCROLLS_KIND,
TopLevelScrollTrack,
} from './scroll_track';
export const ENABLE_CHROME_SCROLL_JANK_PLUGIN = featureFlags.register({
id: 'enableChromeScrollJankPlugin',
name: 'Enable Chrome Scroll Jank plugin',
description: 'Adds new tracks for scroll jank in Chrome',
defaultValue: false,
});
export const ENABLE_SCROLL_JANK_PLUGIN_V2 = featureFlags.register({
id: 'enableScrollJankPluginV2',
name: 'Enable Scroll Jank plugin V2',
description: 'Adds new tracks and visualizations for scroll jank.',
defaultValue: false,
});
export const SCROLL_JANK_GROUP_ID = 'chrome-scroll-jank-track-group';
export type ScrollJankTracks = {
tracksToAdd: AddTrackArgs[],
};
export type ScrollJankTrackGroup = {
tracks: ScrollJankTracks; addTrackGroup: DeferredAction
}
export interface ScrollJankTrackSpec {
key: string;
sqlTableName: string;
detailsPanelConfig: CustomSqlDetailsPanelConfig;
}
// Global state for the scroll jank plugin.
export class ScrollJankPluginState {
private static instance: ScrollJankPluginState;
private tracks: ObjectByKey<ScrollJankTrackSpec>;
private constructor() {
this.tracks = {};
}
public static getInstance(): ScrollJankPluginState {
if (!ScrollJankPluginState.instance) {
ScrollJankPluginState.instance = new ScrollJankPluginState();
}
return ScrollJankPluginState.instance;
}
public registerTrack(args: {
kind: string,
trackKey: string,
tableName: string,
detailsPanelConfig: CustomSqlDetailsPanelConfig,
}): void {
this.tracks[args.kind] = {
key: args.trackKey,
sqlTableName: args.tableName,
detailsPanelConfig: args.detailsPanelConfig,
};
}
public unregisterTrack(kind: string): void {
delete this.tracks[kind];
}
public getTrack(kind: string): ScrollJankTrackSpec|undefined {
return this.tracks[kind];
}
}
export async function getScrollJankTracks(engine: Engine):
Promise<ScrollJankTrackGroup> {
const result: ScrollJankTracks = {
tracksToAdd: [],
};
const scrolls = await addTopLevelScrollTrack();
result.tracksToAdd = result.tracksToAdd.concat(scrolls.tracksToAdd);
const janks = await addScrollJankV3ScrollTrack();
result.tracksToAdd = result.tracksToAdd.concat(janks.tracksToAdd);
const eventLatencies = await addLatencyTracks();
result.tracksToAdd = result.tracksToAdd.concat(eventLatencies.tracksToAdd);
const summaryTrackKey = uuidv4();
result.tracksToAdd.push({
uri: NULL_TRACK_URI,
trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
name: '', // TODO(stevegolton): We should probably put some name here.
trackGroup: undefined,
key: summaryTrackKey,
});
const addTrackGroup = Actions.addTrackGroup({
name: 'Chrome Scroll Jank',
id: SCROLL_JANK_GROUP_ID,
collapsed: false,
summaryTrackKey,
fixedOrdering: true,
});
await ScrollJankCauseMap.initialize(engine);
return {tracks: result, addTrackGroup};
}
class ChromeScrollJankPlugin implements Plugin {
onActivate(_ctx: PluginContext): void {}
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
await this.addChromeScrollJankTrack(ctx);
await this.addTopLevelScrollTrack(ctx);
await this.addEventLatencyTrack(ctx);
await this.addScrollJankV3ScrollTrack(ctx);
if (!ENABLE_CHROME_SCROLL_JANK_PLUGIN.get()) {
return;
}
if (!await isChromeTrace(ctx.engine)) {
return;
}
// Initialise the chrome_tasks_delaying_input_processing table. It will be
// used in the tracks above.
await ctx.engine.query(`
select RUN_METRIC(
'chrome/chrome_tasks_delaying_input_processing.sql',
'duration_causing_jank_ms',
/* duration_causing_jank_ms = */ '8');`);
const query = `
select
s1.full_name,
s1.duration_ms,
s1.slice_id,
s1.thread_dur_ms,
s2.id,
s2.ts,
s2.dur,
s2.track_id
from chrome_tasks_delaying_input_processing s1
join slice s2 on s1.slice_id=s2.id
`;
ctx.tabs.openQuery(query, 'Scroll Jank: long tasks');
}
private async addChromeScrollJankTrack(ctx: PluginContextTrace):
Promise<void> {
ctx.registerStaticTrack({
uri: 'perfetto.ChromeScrollJank',
displayName: 'Scroll Jank causes - long tasks',
kind: ChromeTasksScrollJankTrack.kind,
track: ({trackKey}) => {
return new ChromeTasksScrollJankTrack({
engine: ctx.engine,
trackKey,
});
},
});
}
private async addTopLevelScrollTrack(ctx: PluginContextTrace): Promise<void> {
await ctx.engine.query(`
INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets;
`);
ctx.registerStaticTrack({
uri: 'perfetto.ChromeScrollJank#toplevelScrolls',
displayName: 'Chrome Scrolls',
kind: CHROME_TOPLEVEL_SCROLLS_KIND,
track: ({trackKey}) => {
return new TopLevelScrollTrack({
engine: ctx.engine,
trackKey,
});
},
});
}
private async addEventLatencyTrack(ctx: PluginContextTrace): Promise<void> {
const subTableSql = generateSqlWithInternalLayout({
columns: ['id', 'ts', 'dur', 'track_id', 'name'],
sourceTable: 'slice',
ts: 'ts',
dur: 'dur',
whereClause: `
EXTRACT_ARG(arg_set_id, 'event_latency.event_type') IN (
'FIRST_GESTURE_SCROLL_UPDATE',
'GESTURE_SCROLL_UPDATE',
'INERTIAL_GESTURE_SCROLL_UPDATE')
AND has_descendant_slice_with_name(
id,
'SubmitCompositorFrameToPresentationCompositorFrame')
AND name = 'EventLatency'
AND depth = 0`,
});
// Table name must be unique - it cannot include '-' characters or begin
// with a numeric value.
const baseTable =
`table_${uuidv4().split('-').join('_')}_janky_event_latencies_v3`;
const tableDefSql = `CREATE TABLE ${baseTable} AS
WITH
event_latencies AS MATERIALIZED (
${subTableSql}
),
latency_stages AS (
SELECT
stage.id,
stage.ts,
stage.dur,
stage.track_id,
stage.name,
stage.depth,
event.id as event_latency_id
FROM event_latencies event
JOIN descendant_slice(event.id) stage
)
SELECT
id,
ts,
dur,
CASE
WHEN id IN (
SELECT id FROM chrome_janky_event_latencies_v3)
THEN '${JANKY_LATENCY_NAME}'
ELSE name
END
AS name,
depth * 3 AS depth
FROM event_latencies
UNION ALL
SELECT
stage.id,
stage.ts,
stage.dur,
stage.name,
stage.depth + (
(
SELECT depth FROM event_latencies
WHERE id = stage.event_latency_id
) * 3
) AS depth
FROM latency_stages stage;`;
await ctx.engine.query(
`INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals`);
await ctx.engine.query(tableDefSql);
ctx.registerStaticTrack({
uri: 'perfetto.ChromeScrollJank#eventLatency',
displayName: 'Chrome Scroll Input Latencies',
kind: EventLatencyTrack.kind,
track: ({trackKey}) => {
return new EventLatencyTrack({engine: ctx.engine, trackKey}, baseTable);
},
});
}
private async addScrollJankV3ScrollTrack(ctx: PluginContextTrace):
Promise<void> {
await ctx.engine.query(
`INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals`);
ctx.registerStaticTrack({
uri: 'perfetto.ChromeScrollJank#scrollJankV3',
displayName: 'Chrome Scroll Janks',
kind: ScrollJankV3Track.kind,
track: ({trackKey}) => {
return new ScrollJankV3Track({
engine: ctx.engine,
trackKey,
});
},
});
}
}
async function isChromeTrace(engine: EngineProxy) {
const queryResult = await engine.query(`
select utid, upid
from thread
where name='CrBrowserMain'
`);
const it = queryResult.iter({
utid: NUM,
upid: NUM,
});
return it.valid();
}
export const plugin: PluginDescriptor = {
pluginId: 'perfetto.ChromeScrollJank',
plugin: ChromeScrollJankPlugin,
};