blob: 13c908baceeeaaf98a0eb6ecf163db19c872fcce [file] [log] [blame]
// 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 {exists} from '../../base/utils';
import {raf} from '../../core/raf_scheduler';
import {
BottomTab,
bottomTabRegistry,
NewBottomTabArgs,
} from '../../frontend/bottom_tab';
import {
GenericSliceDetailsTabConfig,
} from '../../frontend/generic_slice_details_tab';
import {getSlice, SliceDetails} from '../../frontend/sql/slice';
import {asSliceSqlId} from '../../frontend/sql_types';
import {sqlValueToString} from '../../frontend/sql_utils';
import {Timestamp} from '../../frontend/widgets/timestamp';
import {EngineProxy} from '../../trace_processor/engine';
import {LONG, NUM, STR} from '../../trace_processor/query_result';
import {DetailsShell} from '../../widgets/details_shell';
import {DurationWidget} from '../../widgets/duration';
import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
import {Section} from '../../widgets/section';
import {SqlRef} from '../../widgets/sql_ref';
import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph';
import {dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree';
import {EventLatencyTrack} from './event_latency_track';
import {
EventLatencySlice,
getEventLatencyDescendantSlice,
getEventLatencySlice,
getSliceForTrack,
} from './scroll_jank_slice';
interface Data {
name: string;
// Jank ID.
id: number;
// Timestamp of the beginning of this slice in nanoseconds.
ts: time;
// Duration of this slice in nanoseconds.
dur: duration;
// The number of frames that were delayed due to the jank.
delayedVsyncCount: number;
// Slice ID of the corresponding EventLatency slice.
eventLatencyId: number;
// The stage of EventLatency that is the cause of jank.
jankCause: string;
// Where possible, the subcause of jank.
jankSubcause: string;
jankyFrames?: number;
}
async function getSliceDetails(
engine: EngineProxy, id: number): Promise<SliceDetails|undefined> {
return getSlice(engine, asSliceSqlId(id));
}
export class ScrollJankV3DetailsPanel extends
BottomTab<GenericSliceDetailsTabConfig> {
static readonly kind = 'org.perfetto.ScrollJankV3DetailsPanel';
data: Data|undefined;
loaded = false;
//
// Linking to associated slices
//
// Link to the original Event Latency in the Slice table.
// TODO(b/278844325): once the EventLatencyTrack has a custom details panel,
// move this link there.
private sliceDetails?: SliceDetails;
// Link to the Event Latency in the EventLatencyTrack (subset of event
// latencies associated with input events).
private eventLatencySliceDetails?: EventLatencySlice;
// Link to the scroll jank cause stage of the associated EventLatencyTrack
// slice. May be unknown.
private causeSliceDetails?: EventLatencySlice;
// Link to the scroll jank sub-cause stage of the associated EventLatencyTrack
// slice. Does not apply to all causes.
private subcauseSliceDetails?: EventLatencySlice;
static create(args: NewBottomTabArgs): ScrollJankV3DetailsPanel {
return new ScrollJankV3DetailsPanel(args);
}
constructor(args: NewBottomTabArgs) {
super(args);
this.loadData();
}
private async loadData() {
const queryResult = await this.engine.query(`
SELECT
IIF(
cause_of_jank IS NOT NULL,
cause_of_jank || IIF(
sub_cause_of_jank IS NOT NULL, "::" || sub_cause_of_jank, ""
), "Unknown") || " Jank" AS name,
id,
ts,
dur,
delayed_frame_count AS delayedVsyncCount,
event_latency_id AS eventLatencyId,
IFNULL(cause_of_jank, "UNKNOWN") AS causeOfJank,
IFNULL(sub_cause_of_jank, "UNKNOWN") AS subcauseOfJank
FROM chrome_janky_frame_presentation_intervals
WHERE id = ${this.config.id}`);
const iter = queryResult.firstRow({
name: STR,
id: NUM,
ts: LONG,
dur: LONG,
delayedVsyncCount: NUM,
eventLatencyId: NUM,
causeOfJank: STR,
subcauseOfJank: STR,
});
this.data = {
name: iter.name,
id: iter.id,
ts: Time.fromRaw(iter.ts),
dur: iter.dur,
delayedVsyncCount: iter.delayedVsyncCount,
eventLatencyId: iter.eventLatencyId,
jankCause: iter.causeOfJank,
jankSubcause: iter.subcauseOfJank,
};
await this.loadJankyFrames();
await this.loadSlices();
this.loaded = true;
raf.scheduleFullRedraw();
}
private hasCause(): boolean {
if (this.data === undefined) {
return false;
}
return this.data.jankCause !== 'UNKNOWN';
}
private hasSubcause(): boolean {
if (this.data === undefined) {
return false;
}
return this.hasCause() && this.data.jankSubcause !== 'UNKNOWN';
}
private async loadSlices() {
if (exists(this.data)) {
this.sliceDetails =
await getSliceDetails(this.engine, this.data.eventLatencyId);
this.eventLatencySliceDetails =
await getEventLatencySlice(this.engine, this.data.eventLatencyId);
if (this.hasCause()) {
this.causeSliceDetails = await getEventLatencyDescendantSlice(
this.engine, this.data.eventLatencyId, this.data.jankCause);
}
if (this.hasSubcause()) {
this.subcauseSliceDetails = await getEventLatencyDescendantSlice(
this.engine, this.data.eventLatencyId, this.data.jankSubcause);
}
}
}
private async loadJankyFrames() {
if (exists(this.data)) {
const queryResult = await this.engine.query(`
SELECT
COUNT(*) AS jankyFrames
FROM chrome_frame_info_with_delay
WHERE delay_since_last_frame >
(
SELECT
vsync_interval + vsync_interval / 2
FROM chrome_vsyncs)
AND delay_since_last_input <
(
SELECT
vsync_interval + vsync_interval / 2
FROM chrome_vsyncs)
AND presentation_timestamp >= ${this.data.ts}
AND presentation_timestamp <= ${this.data.ts + this.data.dur};
`);
const iter = queryResult.firstRow({
jankyFrames: NUM,
});
this.data.jankyFrames = iter.jankyFrames;
}
}
private renderDetailsDictionary(): m.Child[] {
const details: {[key: string]: m.Child} = {};
if (exists(this.data)) {
details['Name'] = sqlValueToString(this.data.name);
details['Expected Frame Presentation Timestamp'] =
m(Timestamp, {ts: this.data.ts});
details['Actual Frame Presentation Timestamp'] =
m(Timestamp, {ts: Time.add(this.data.ts, this.data.dur)});
details['Frame Presentation Delay'] =
m(DurationWidget, {dur: this.data.dur});
details['Vsyncs Delayed'] = this.data.delayedVsyncCount;
if (exists(this.data.jankyFrames)) {
details['Janky Frame Count'] = this.data.jankyFrames;
}
details['Original Event Latency'] = this.data.eventLatencyId;
details['SQL ID'] = m(SqlRef, {
table: 'chrome_janky_frame_presentation_intervals',
id: this.data.id,
});
}
return dictToTreeNodes(details);
}
private getDescriptionText(): m.Child {
return m(
MultiParagraphText,
m(TextParagraph, {
text: `Delay between when the frame was expected to be presented and
when it was actually presented.`,
}),
m(TextParagraph, {
text: `This is the period of time during which the user is viewing a
frame that isn't correct.`,
}));
}
private getLinksSection(): m.Child[] {
const result: {[key: string]: m.Child} = {};
if (exists(this.sliceDetails) && exists(this.data)) {
result['Janked Event Latency stage'] = exists(this.causeSliceDetails) ?
getSliceForTrack(
this.causeSliceDetails,
EventLatencyTrack.kind,
this.data.jankCause) :
sqlValueToString(this.data.jankCause);
if (this.hasSubcause()) {
result['Sub-cause of Jank'] = exists(this.subcauseSliceDetails) ?
getSliceForTrack(
this.subcauseSliceDetails,
EventLatencyTrack.kind,
this.data.jankSubcause) :
sqlValueToString(this.data.jankSubcause);
}
const children = dictToTreeNodes(result);
if (exists(this.eventLatencySliceDetails)) {
children.unshift(m(TreeNode, {
left: getSliceForTrack(
this.eventLatencySliceDetails,
EventLatencyTrack.kind,
'Input EventLatency in context of ScrollUpdates'),
right: '',
}));
} else {
children.unshift(sqlValueToString('Event Latency'));
}
return children;
}
return dictToTreeNodes(result);
}
viewTab() {
if (this.data === undefined) {
return m('h2', 'Loading');
}
const details = this.renderDetailsDictionary();
return m(
DetailsShell,
{
title: this.getTitle(),
},
m(GridLayout,
m(
GridLayoutColumn,
m(
Section,
{title: 'Details'},
m(Tree, details),
),
),
m(GridLayoutColumn,
m(
Section,
{title: 'Description'},
this.getDescriptionText(),
),
m(
Section,
{title: 'Jank Cause'},
m(Tree, this.getLinksSection()),
))),
);
}
getTitle(): string {
return this.config.title;
}
isLoading() {
return !this.loaded;
}
}
bottomTabRegistry.register(ScrollJankV3DetailsPanel);