diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts index 1b425fb392..b77beaae60 100644 --- a/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts +++ b/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts @@ -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, @@ -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 { static readonly kind = 'dev.perfetto.EventLatencySliceDetailsPanel'; + private loaded = false; + private sliceDetails?: SliceDetails; + private jankySlice?: ScrollJankSlice; static create(args: NewBottomTabArgs): EventLatencySliceDetailsPanel { return new EventLatencySliceDetailsPanel(args); @@ -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() { @@ -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; @@ -66,26 +164,14 @@ 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...'}); @@ -93,7 +179,7 @@ export class EventLatencySliceDetailsPanel extends } isLoading() { - return !exists(this.sliceDetails); + return !this.loaded; } getTitle(): string { diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts b/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts index 3e848ec0f5..b97c46f7a6 100644 --- a/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts +++ b/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts @@ -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 { @@ -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( + name: string, getter: (t: T) => m.Child): ColumnDescriptor { + return new ColumnDescriptor(name, getter); +} interface Data { // Scroll ID. @@ -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 { static readonly kind = 'org.perfetto.ScrollDetailsPanel'; loaded = false; data: Data|undefined; metrics: Metrics = {}; + maxJankSlices: JankSliceDetails[] = []; static create(args: NewBottomTabArgs): ScrollDetailsPanel { return new ScrollDetailsPanel(args); @@ -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, + }); } } } @@ -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[] = [ + widgetColumn('Cause', (x) => x.jankLink), + widgetColumn('Duration', (x) => x.dur), + numberColumn('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 { @@ -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( diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_slice.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_slice.ts index 3ac8d63b57..9b0d1554e4 100644 --- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_slice.ts +++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_slice.ts @@ -30,6 +30,7 @@ import { import {EventLatencyTrack} from './event_latency_track'; import {ScrollJankPluginState, ScrollJankTrackSpec} from './index'; +import {ScrollJankV3Track} from './scroll_jank_v3_track'; interface BasicSlice { // ID of slice. @@ -39,6 +40,7 @@ interface BasicSlice { // Duration of this slice in nanoseconds. dur: duration; } + async function getSlicesFromTrack( engine: EngineProxy, track: ScrollJankTrackSpec, @@ -67,6 +69,21 @@ async function getSlicesFromTrack( return result; } +export type ScrollJankSlice = BasicSlice; +export async function getScrollJankSlices( + engine: EngineProxy, id: number): Promise { + const track = + ScrollJankPluginState.getInstance().getTrack(ScrollJankV3Track.kind); + if (track == undefined) { + throw new Error(`${ScrollJankV3Track.kind} track is not registered.`); + } + + const slices = await getSlicesFromTrack(engine, track, { + filters: [`event_latency_id=${id}`], + }); + return slices; +} + export type EventLatencySlice = BasicSlice; export async function getEventLatencySlice( engine: EngineProxy, id: number): Promise { diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_details_panel.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_details_panel.ts index cf13a51412..ee2b5548e9 100644 --- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_details_panel.ts +++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_details_panel.ts @@ -276,7 +276,7 @@ export class ScrollJankV3DetailsPanel extends left: getSliceForTrack( this.eventLatencySliceDetails, EventLatencyTrack.kind, - 'Event Latency'), + 'Input EventLatency in context of ScrollUpdates'), right: '', })); } else { diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts index a065e2982d..a2f83f7965 100644 --- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts +++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts @@ -73,6 +73,7 @@ export class ScrollJankV3Track extends 'id', 'ts', 'dur', + 'event_latency_id', ], sqlTableName: 'chrome_janky_frame_presentation_intervals', };