Skip to content

Commit

Permalink
Add links to Scroll Jank V3 track.
Browse files Browse the repository at this point in the history
Now Event latencies that are marked as janky can link back to the
associated janky interval (Chrome Scroll Jank track).

https://screenshot.googleplex.com/44SXyhsyadAT8cx

Scroll slices now also link to the maximum delay/jank slice.

https://screenshot.googleplex.com/5YM8nfFZBenR4pt

Bug: b/278844325
Change-Id: I3a718f6029ebab0ca0e12319142859b202f7e2ed
  • Loading branch information
Harkiran Bolaria committed Aug 23, 2023
1 parent be64e1c commit ef45a17
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 48 deletions.
130 changes: 108 additions & 22 deletions ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import m from 'mithril';

import {exists} from '../../base/utils';
import {NUM} from '../../common/query_result';
import {raf} from '../../core/raf_scheduler';
import {
BottomTab,
Expand All @@ -33,11 +34,21 @@ import {GridLayout, GridLayoutColumn} from '../../frontend/widgets/grid_layout';
import {Section} from '../../frontend/widgets/section';
import {Tree, TreeNode} from '../../frontend/widgets/tree';

import {
getScrollJankSlices,
getSliceForTrack,
ScrollJankSlice,
} from './scroll_jank_slice';
import {ScrollJankV3Track} from './scroll_jank_v3_track';

export class EventLatencySliceDetailsPanel extends
BottomTab<GenericSliceDetailsTabConfig> {
static readonly kind = 'dev.perfetto.EventLatencySliceDetailsPanel';

private loaded = false;

private sliceDetails?: SliceDetails;
private jankySlice?: ScrollJankSlice;

static create(args: NewBottomTabArgs): EventLatencySliceDetailsPanel {
return new EventLatencySliceDetailsPanel(args);
Expand All @@ -46,8 +57,13 @@ export class EventLatencySliceDetailsPanel extends
constructor(args: NewBottomTabArgs) {
super(args);

// Start loading the slice details
this.loadSlice();
this.loadData();
}

async loadData() {
await this.loadSlice();
await this.loadJankSlice();
this.loaded = true;
}

async loadSlice() {
Expand All @@ -56,6 +72,88 @@ export class EventLatencySliceDetailsPanel extends
raf.scheduleRedraw();
}

async loadJankSlice() {
if (exists(this.sliceDetails)) {
// Get the id for the top-level EventLatency slice (this or parent), as
// this id is used in the ScrollJankV3 track to identify the corresponding
// janky interval.
let eventLatencyId = -1;
if (this.sliceDetails.name == 'EventLatency') {
eventLatencyId = this.sliceDetails.id;
} else {
const queryResult = await this.engine.query(`
SELECT
id
FROM ancestor_slice(${this.sliceDetails.id})
WHERE name = 'EventLatency'
`);
const it = queryResult.iter({
id: NUM,
});
for (; it.valid(); it.next()) {
eventLatencyId = it.id;
break;
}
}

const possibleSlices =
await getScrollJankSlices(this.engine, eventLatencyId);
// We may not get any slices if the EventLatency doesn't indicate any
// jank occurred.
if (possibleSlices.length > 0) {
this.jankySlice = possibleSlices[0];
}
}
}

private getLinksSection(): m.Child {
return m(
Section,
{title: 'Quick links'},
m(
Tree,
m(TreeNode, {
left: exists(this.sliceDetails) ?
sliceRef(
this.sliceDetails,
'EventLatency in context of other Input events') :
'EventLatency in context of other Input events',
right: exists(this.sliceDetails) ? '' : 'N/A',
}),
m(TreeNode, {
left: exists(this.jankySlice) ? getSliceForTrack(
this.jankySlice,
ScrollJankV3Track.kind,
'Jank Interval') :
'Jank Interval',
right: exists(this.jankySlice) ? '' : 'N/A',
}),
),
);
}

private getDescriptionText(): m.Child {
return m(
`div[style='white-space:pre-wrap']`,
`EventLatency tracks the latency of handling a given input event
(Scrolls, Touchs, Taps, etc). Ideally from when the input was read by
the hardware to when it was reflected on the screen.{new_lines}
Note however the concept of coalescing or terminating early. This occurs
when we receive multiple events or handle them quickly by converting
the into a different event. Such as a TOUCH_MOVE being converted into a
GESTURE_SCROLL_UPDATE type, or a multiple GESTURE_SCROLL_UPDATE events
being formed into a single frame at the end of the
RendererCompositorQueuingDelay.{new_lines}
*Important:* On some platforms (MacOS) we do not get feedback on when
something is presented on the screen so the timings are only accurate
for what we know on a given platform.`.replace(/\s\s+/g, ' ')
.replace(/{new_lines}/g, '\n\n')
.replace(/ Note:/g, 'Note:'),
);
}

viewTab() {
if (exists(this.sliceDetails)) {
const slice = this.sliceDetails;
Expand All @@ -66,34 +164,22 @@ export class EventLatencySliceDetailsPanel extends
description: slice.name,
},
m(GridLayout,
m(
GridLayoutColumn,
renderDetails(slice),
),
m(GridLayoutColumn,
renderArguments(this.engine, slice),
m(
Section,
{title: 'Quick links'},
// TODO(hbolaria): add a link to the jank interval if this
// slice is a janky latency.
m(
Tree,
m(TreeNode, {
left: sliceRef(
this.sliceDetails, 'Original EventLatency'),
right: '',
}),
),
))),
renderDetails(slice),
renderArguments(this.engine, slice)),
m(GridLayoutColumn,
m(Section,
{title: 'Description'},
m('.div', this.getDescriptionText())),
this.getLinksSection())),
);
} else {
return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
}
}

isLoading() {
return !exists(this.sliceDetails);
return !this.loaded;
}

getTitle(): string {
Expand Down
120 changes: 95 additions & 25 deletions ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import m from 'mithril';

import {exists} from '../../base/utils';
import {LONG, NUM} from '../../common/query_result';
import {LONG, NUM, STR} from '../../common/query_result';
import {duration, Time, time} from '../../common/time';
import {raf} from '../../core/raf_scheduler';
import {
Expand All @@ -27,13 +27,31 @@ import {
GenericSliceDetailsTabConfig,
} from '../../frontend/generic_slice_details_tab';
import {sqlValueToString} from '../../frontend/sql_utils';
import {
ColumnDescriptor,
numberColumn,
Table,
TableData,
} from '../../frontend/tables/table';
import {DetailsShell} from '../../frontend/widgets/details_shell';
import {DurationWidget} from '../../frontend/widgets/duration';
import {GridLayout, GridLayoutColumn} from '../../frontend/widgets/grid_layout';
import {Section} from '../../frontend/widgets/section';
import {SqlRef} from '../../frontend/widgets/sql_ref';
import {Timestamp} from '../../frontend/widgets/timestamp';
import {dictToTreeNodes, Tree, TreeNode} from '../../frontend/widgets/tree';
import {dictToTreeNodes, Tree} from '../../frontend/widgets/tree';

import {
getScrollJankSlices,
getSliceForTrack,
ScrollJankSlice,
} from './scroll_jank_slice';
import {ScrollJankV3Track} from './scroll_jank_v3_track';

function widgetColumn<T>(
name: string, getter: (t: T) => m.Child): ColumnDescriptor<T> {
return new ColumnDescriptor<T>(name, getter);
}

interface Data {
// Scroll ID.
Expand All @@ -51,17 +69,23 @@ interface Metrics {
jankyFrameCount?: number;
jankyFramePercent?: number;
missedVsyncs?: number;
maxDelayDur?: duration;
maxDelayVsync?: number;
// TODO(b/279581028): add pixels scrolled.
}

interface JankSliceDetails {
cause: string;
jankSlice: ScrollJankSlice;
delayDur: duration;
delayVsync: number;
}

export class ScrollDetailsPanel extends
BottomTab<GenericSliceDetailsTabConfig> {
static readonly kind = 'org.perfetto.ScrollDetailsPanel';
loaded = false;
data: Data|undefined;
metrics: Metrics = {};
maxJankSlices: JankSliceDetails[] = [];

static create(args: NewBottomTabArgs): ScrollDetailsPanel {
return new ScrollDetailsPanel(args);
Expand Down Expand Up @@ -166,22 +190,45 @@ export class ScrollDetailsPanel extends
private async loadMaxDelay() {
if (exists(this.data)) {
const queryResult = await this.engine.query(`
WITH max_delay_tbl AS (
SELECT
MAX(dur) AS max_dur
FROM chrome_janky_frame_presentation_intervals s
WHERE s.ts >= ${this.data.ts}
AND s.ts + s.dur <= ${this.data.ts + this.data.dur}
)
SELECT
IFNULL(sub_cause_of_jank, IFNULL(cause_of_jank, 'Unknown')) AS cause,
IFNULL(event_latency_id, 0) AS eventLatencyId,
IFNULL(MAX(dur), 0) AS maxDelayDur,
IFNULL(delayed_frame_count, 0) AS maxDelayVsync
FROM chrome_janky_frame_presentation_intervals s
WHERE s.ts >= ${this.data.ts}
AND s.ts + s.dur <= ${this.data.ts + this.data.dur}
AND dur IN (SELECT max_dur FROM max_delay_tbl)
GROUP BY eventLatencyId, cause;
`);

const iter = queryResult.firstRow({
const iter = queryResult.iter({
cause: STR,
eventLatencyId: NUM,
maxDelayDur: LONG,
maxDelayVsync: NUM,
});

if (iter.maxDelayDur > 0) {
this.metrics.maxDelayDur = iter.maxDelayDur;
this.metrics.maxDelayVsync = iter.maxDelayVsync;
for (; iter.valid(); iter.next()) {
if (iter.maxDelayDur <= 0) {
break;
}
const jankSlices =
await getScrollJankSlices(this.engine, iter.eventLatencyId);

this.maxJankSlices.push({
cause: iter.cause,
jankSlice: jankSlices[0],
delayDur: iter.maxDelayDur,
delayVsync: iter.maxDelayVsync,
});
}
}
}
Expand All @@ -200,24 +247,42 @@ export class ScrollDetailsPanel extends
sqlValueToString(`${this.metrics.jankyFramePercent}%`);
}

if (this.metrics.maxDelayDur !== undefined &&
this.metrics.maxDelayVsync !== undefined) {
// TODO(b/278844325): replace this with a link to the actual scroll slice.
metrics['Max Frame Presentation Delay'] =
m(Tree,
m(TreeNode, {
left: 'Duration',
right: m(DurationWidget, {dur: this.metrics.maxDelayDur}),
}),
m(TreeNode, {
left: 'Vsyncs Missed',
right: this.metrics.maxDelayVsync,
}));
return dictToTreeNodes(metrics);
}

private getMaxDelayTable(): m.Child {
if (this.maxJankSlices.length > 0) {
interface DelayData {
jankLink: m.Child;
dur: m.Child;
delayedVSyncs: number;
}
;

const columns: ColumnDescriptor<DelayData>[] = [
widgetColumn<DelayData>('Cause', (x) => x.jankLink),
widgetColumn<DelayData>('Duration', (x) => x.dur),
numberColumn<DelayData>('Delayed Vsyncs', (x) => x.delayedVSyncs),
];
const data: DelayData[] = [];
for (const jankSlice of this.maxJankSlices) {
data.push({
jankLink: getSliceForTrack(
jankSlice.jankSlice, ScrollJankV3Track.kind, jankSlice.cause),
dur: m(DurationWidget, {dur: jankSlice.delayDur}),
delayedVSyncs: jankSlice.delayVsync,
});
}

const tableData = new TableData(data);

return m(Table, {
data: tableData,
columns: columns,
});
} else {
metrics['Max Frame Presentation Delay'] = sqlValueToString('None');
return sqlValueToString('None');
}

return dictToTreeNodes(metrics);
}

private getDescriptionText(): m.Child {
Expand Down Expand Up @@ -267,7 +332,12 @@ export class ScrollDetailsPanel extends
),
m(Section,
{title: 'Slice Metrics'},
m(Tree, this.renderMetricsDictionary()))),
m(Tree, this.renderMetricsDictionary())),
m(
Section,
{title: 'Max Frame Presentation Delay'},
this.getMaxDelayTable(),
)),
m(
GridLayoutColumn,
m(
Expand Down
Loading

0 comments on commit ef45a17

Please sign in to comment.