1// Copyright (C) 2023 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import m from 'mithril';
16import {Duration, duration, Time, time} from '../../base/time';
17import {hasArgs, renderArguments} from '../../components/details/slice_args';
18import {renderDetails} from '../../components/details/slice_details';
19import {
20  getDescendantSliceTree,
21  getSlice,
22  SliceDetails,
23  SliceTreeNode,
24} from '../../components/sql_utils/slice';
25import {asSliceSqlId, SliceSqlId} from '../../components/sql_utils/core_types';
26import {
27  ColumnDescriptor,
28  Table,
29  TableData,
30  widgetColumn,
31} from '../../widgets/table';
32import {TreeTable, TreeTableAttrs} from '../../components/widgets/treetable';
33import {LONG, NUM, STR} from '../../trace_processor/query_result';
34import {DetailsShell} from '../../widgets/details_shell';
35import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
36import {Section} from '../../widgets/section';
37import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph';
38import {Tree, TreeNode} from '../../widgets/tree';
39import {
40  EventLatencyCauseThreadTracks,
41  EventLatencyStage,
42  getCauseLink,
43  getEventLatencyCauseTracks,
44  getScrollJankCauseStage,
45} from './scroll_jank_cause_link_utils';
46import {ScrollJankCauseMap} from './scroll_jank_cause_map';
47import {sliceRef} from '../../components/widgets/slice';
48import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils';
49import {TrackEventDetailsPanel} from '../../public/details_panel';
50import {Trace} from '../../public/trace';
51
52// Given a node in the slice tree, return a path from root to it.
53function getPath(slice: SliceTreeNode): string[] {
54  const result: string[] = [];
55  let node: SliceTreeNode | undefined = slice;
56  while (node.parent !== undefined) {
57    result.push(node.name);
58    node = node.parent;
59  }
60  return result.reverse();
61}
62
63// Given a slice tree node and a path, find the node following the path from
64// the given slice, or `undefined` if not found.
65function findSliceInTreeByPath(
66  slice: SliceTreeNode | undefined,
67  path: string[],
68): SliceTreeNode | undefined {
69  if (slice === undefined) {
70    return undefined;
71  }
72  let result = slice;
73  for (const segment of path) {
74    let found = false;
75    for (const child of result.children) {
76      if (child.name === segment) {
77        found = true;
78        result = child;
79        break;
80      }
81    }
82    if (!found) {
83      return undefined;
84    }
85  }
86  return result;
87}
88
89function durationDelta(value: duration, base?: duration): string {
90  if (base === undefined) {
91    return 'NULL';
92  }
93  const delta = value - base;
94  return `${delta > 0 ? '+' : ''}${Duration.humanise(delta)}`;
95}
96
97export class EventLatencySliceDetailsPanel implements TrackEventDetailsPanel {
98  private name = '';
99  private topEventLatencyId: SliceSqlId | undefined = undefined;
100
101  private sliceDetails?: SliceDetails;
102  private jankySlice?: {
103    ts: time;
104    dur: duration;
105    id: number;
106    causeOfJank: string;
107  };
108
109  // Whether this stage has caused jank. This is also true for top level
110  // EventLatency slices where a descendant is a cause of jank.
111  private isJankStage = false;
112
113  // For top level EventLatency slices - if any descendant is a cause of jank,
114  // this field stores information about that descendant slice. Otherwise, this
115  // is stores information about the current stage;
116  private relevantThreadStage: EventLatencyStage | undefined;
117  private relevantThreadTracks: EventLatencyCauseThreadTracks[] = [];
118  // Stages tree for the current EventLatency.
119  private eventLatencyBreakdown?: SliceTreeNode;
120  // Stages tree for the next EventLatency.
121  private nextEventLatencyBreakdown?: SliceTreeNode;
122  // Stages tree for the prev EventLatency.
123  private prevEventLatencyBreakdown?: SliceTreeNode;
124
125  private tracksByTrackId: Map<number, string>;
126
127  constructor(
128    private readonly trace: Trace,
129    private readonly id: number,
130  ) {
131    this.tracksByTrackId = new Map<number, string>();
132    this.trace.tracks.getAllTracks().forEach((td) => {
133      td.tags?.trackIds?.forEach((trackId) => {
134        this.tracksByTrackId.set(trackId, td.uri);
135      });
136    });
137  }
138
139  async load() {
140    const queryResult = await this.trace.engine.query(`
141      SELECT
142        name
143      FROM slice
144      WHERE id = ${this.id}
145      `);
146
147    const iter = queryResult.firstRow({
148      name: STR,
149    });
150
151    this.name = iter.name;
152
153    await this.loadSlice();
154    await this.loadJankSlice();
155    await this.loadRelevantThreads();
156    await this.loadEventLatencyBreakdown();
157  }
158
159  async loadSlice() {
160    this.sliceDetails = await getSlice(
161      this.trace.engine,
162      asSliceSqlId(this.id),
163    );
164    this.trace.scheduleFullRedraw();
165  }
166
167  async loadJankSlice() {
168    if (!this.sliceDetails) return;
169    // Get the id for the top-level EventLatency slice (this or parent), as
170    // this id is used in the ScrollJankV3 track to identify the corresponding
171    // janky interval.
172    if (this.sliceDetails.name === 'EventLatency') {
173      this.topEventLatencyId = this.sliceDetails.id;
174    } else {
175      this.topEventLatencyId = asSliceSqlId(
176        await this.getOldestAncestorSliceId(),
177      );
178    }
179
180    const it = (
181      await this.trace.engine.query(`
182      SELECT ts, dur, id, cause_of_jank as causeOfJank
183      FROM chrome_janky_frame_presentation_intervals
184      WHERE event_latency_id = ${this.topEventLatencyId}`)
185    ).iter({
186      id: NUM,
187      ts: LONG,
188      dur: LONG,
189      causeOfJank: STR,
190    });
191
192    if (it.valid()) {
193      this.jankySlice = {
194        id: it.id,
195        ts: Time.fromRaw(it.ts),
196        dur: Duration.fromRaw(it.dur),
197        causeOfJank: it.causeOfJank,
198      };
199    }
200  }
201
202  async loadRelevantThreads() {
203    if (!this.sliceDetails) return;
204    if (!this.topEventLatencyId) return;
205
206    // Relevant threads should only be available on a "Janky" EventLatency
207    // slice to allow the user to jump to the possible cause of jank.
208    if (this.sliceDetails.name === 'EventLatency' && !this.jankySlice) return;
209
210    const possibleScrollJankStage = await getScrollJankCauseStage(
211      this.trace.engine,
212      this.topEventLatencyId,
213    );
214    if (this.sliceDetails.name === 'EventLatency') {
215      this.isJankStage = true;
216      this.relevantThreadStage = possibleScrollJankStage;
217    } else {
218      if (
219        possibleScrollJankStage &&
220        this.sliceDetails.name === possibleScrollJankStage.name
221      ) {
222        this.isJankStage = true;
223      }
224      this.relevantThreadStage = {
225        name: this.sliceDetails.name,
226        eventLatencyId: this.topEventLatencyId,
227        ts: this.sliceDetails.ts,
228        dur: this.sliceDetails.dur,
229      };
230    }
231
232    if (this.relevantThreadStage) {
233      this.relevantThreadTracks = await getEventLatencyCauseTracks(
234        this.trace.engine,
235        this.relevantThreadStage,
236      );
237    }
238  }
239
240  async loadEventLatencyBreakdown() {
241    if (this.topEventLatencyId === undefined) {
242      return;
243    }
244    this.eventLatencyBreakdown = await getDescendantSliceTree(
245      this.trace.engine,
246      this.topEventLatencyId,
247    );
248
249    // TODO(altimin): this should only consider EventLatencies within the same scroll.
250    const prevEventLatency = (
251      await this.trace.engine.query(`
252      INCLUDE PERFETTO MODULE chrome.event_latency;
253      SELECT
254        id
255      FROM chrome_event_latencies
256      WHERE event_type IN (
257        'FIRST_GESTURE_SCROLL_UPDATE',
258        'GESTURE_SCROLL_UPDATE',
259        'INERTIAL_GESTURE_SCROLL_UPDATE')
260      AND is_presented
261      AND id < ${this.topEventLatencyId}
262      ORDER BY id DESC
263      LIMIT 1
264      ;
265    `)
266    ).maybeFirstRow({id: NUM});
267    if (prevEventLatency !== undefined) {
268      this.prevEventLatencyBreakdown = await getDescendantSliceTree(
269        this.trace.engine,
270        asSliceSqlId(prevEventLatency.id),
271      );
272    }
273
274    const nextEventLatency = (
275      await this.trace.engine.query(`
276      INCLUDE PERFETTO MODULE chrome.event_latency;
277      SELECT
278        id
279      FROM chrome_event_latencies
280      WHERE event_type IN (
281        'FIRST_GESTURE_SCROLL_UPDATE',
282        'GESTURE_SCROLL_UPDATE',
283        'INERTIAL_GESTURE_SCROLL_UPDATE')
284      AND is_presented
285      AND id > ${this.topEventLatencyId}
286      ORDER BY id DESC
287      LIMIT 1;
288    `)
289    ).maybeFirstRow({id: NUM});
290    if (nextEventLatency !== undefined) {
291      this.nextEventLatencyBreakdown = await getDescendantSliceTree(
292        this.trace.engine,
293        asSliceSqlId(nextEventLatency.id),
294      );
295    }
296  }
297
298  private getRelevantLinks(): m.Child {
299    if (!this.sliceDetails) return undefined;
300
301    // Relevant threads should only be available on a "Janky" EventLatency
302    // slice to allow the user to jump to the possible cause of jank.
303    if (
304      this.sliceDetails.name === 'EventLatency' &&
305      !this.relevantThreadStage
306    ) {
307      return undefined;
308    }
309
310    const name = this.relevantThreadStage
311      ? this.relevantThreadStage.name
312      : this.sliceDetails.name;
313    const ts = this.relevantThreadStage
314      ? this.relevantThreadStage.ts
315      : this.sliceDetails.ts;
316    const dur = this.relevantThreadStage
317      ? this.relevantThreadStage.dur
318      : this.sliceDetails.dur;
319    const stageDetails = ScrollJankCauseMap.getEventLatencyDetails(name);
320    if (stageDetails === undefined) return undefined;
321
322    const childWidgets: m.Child[] = [];
323    childWidgets.push(m(TextParagraph, {text: stageDetails.description}));
324
325    interface RelevantThreadRow {
326      description: string;
327      tracks: EventLatencyCauseThreadTracks;
328      ts: time;
329      dur: duration;
330    }
331
332    const columns: ColumnDescriptor<RelevantThreadRow>[] = [
333      widgetColumn<RelevantThreadRow>('Relevant Thread', (x) =>
334        getCauseLink(this.trace, x.tracks, this.tracksByTrackId, x.ts, x.dur),
335      ),
336      widgetColumn<RelevantThreadRow>('Description', (x) => {
337        if (x.description === '') {
338          return x.description;
339        } else {
340          return m(TextParagraph, {text: x.description});
341        }
342      }),
343    ];
344
345    const trackLinks: RelevantThreadRow[] = [];
346
347    for (let i = 0; i < this.relevantThreadTracks.length; i++) {
348      const track = this.relevantThreadTracks[i];
349      let description = '';
350      if (i == 0 || track.thread != this.relevantThreadTracks[i - 1].thread) {
351        description = track.causeDescription;
352      }
353      trackLinks.push({
354        description: description,
355        tracks: this.relevantThreadTracks[i],
356        ts: ts,
357        dur: dur,
358      });
359    }
360
361    const tableData = new TableData(trackLinks);
362
363    if (trackLinks.length > 0) {
364      childWidgets.push(
365        m(Table, {
366          data: tableData,
367          columns: columns,
368        }),
369      );
370    }
371
372    return m(
373      Section,
374      {title: this.isJankStage ? `Jank Cause: ${name}` : name},
375      childWidgets,
376    );
377  }
378
379  private async getOldestAncestorSliceId(): Promise<number> {
380    let eventLatencyId = -1;
381    if (!this.sliceDetails) return eventLatencyId;
382    const queryResult = await this.trace.engine.query(`
383      SELECT
384        id
385      FROM ancestor_slice(${this.sliceDetails.id})
386      WHERE name = 'EventLatency'
387    `);
388
389    const it = queryResult.iter({
390      id: NUM,
391    });
392
393    for (; it.valid(); it.next()) {
394      eventLatencyId = it.id;
395      break;
396    }
397
398    return eventLatencyId;
399  }
400
401  private getLinksSection(): m.Child {
402    return m(
403      Section,
404      {title: 'Quick links'},
405      m(
406        Tree,
407        m(TreeNode, {
408          left: this.sliceDetails
409            ? sliceRef(
410                this.sliceDetails,
411                'EventLatency in context of other Input events',
412              )
413            : 'EventLatency in context of other Input events',
414          right: this.sliceDetails ? '' : 'N/A',
415        }),
416        this.jankySlice &&
417          m(TreeNode, {
418            left: renderSliceRef({
419              trace: this.trace,
420              id: this.jankySlice.id,
421              trackUri: JANKS_TRACK_URI,
422              title: this.jankySlice.causeOfJank,
423            }),
424          }),
425      ),
426    );
427  }
428
429  private getBreakdownSection(): m.Child {
430    if (this.eventLatencyBreakdown === undefined) {
431      return undefined;
432    }
433
434    const attrs: TreeTableAttrs<SliceTreeNode> = {
435      rows: [this.eventLatencyBreakdown],
436      getChildren: (slice) => slice.children,
437      columns: [
438        {name: 'Name', getData: (slice) => slice.name},
439        {name: 'Duration', getData: (slice) => Duration.humanise(slice.dur)},
440        {
441          name: 'vs prev',
442          getData: (slice) =>
443            durationDelta(
444              slice.dur,
445              findSliceInTreeByPath(
446                this.prevEventLatencyBreakdown,
447                getPath(slice),
448              )?.dur,
449            ),
450        },
451        {
452          name: 'vs next',
453          getData: (slice) =>
454            durationDelta(
455              slice.dur,
456              findSliceInTreeByPath(
457                this.nextEventLatencyBreakdown,
458                getPath(slice),
459              )?.dur,
460            ),
461        },
462      ],
463    };
464
465    return m(
466      Section,
467      {
468        title: 'EventLatency Stage Breakdown',
469      },
470      m(TreeTable<SliceTreeNode>, attrs),
471    );
472  }
473
474  private getDescriptionText(): m.Child {
475    return m(
476      MultiParagraphText,
477      m(TextParagraph, {
478        text: `EventLatency tracks the latency of handling a given input event
479                 (Scrolls, Touches, Taps, etc). Ideally from when the input was
480                 read by the hardware to when it was reflected on the screen.`,
481      }),
482      m(TextParagraph, {
483        text: `Note however the concept of coalescing or terminating early. This
484               occurs when we receive multiple events or handle them quickly by
485               converting them into a different event. Such as a TOUCH_MOVE
486               being converted into a GESTURE_SCROLL_UPDATE type, or a multiple
487               GESTURE_SCROLL_UPDATE events being formed into a single frame at
488               the end of the RendererCompositorQueuingDelay.`,
489      }),
490      m(TextParagraph, {
491        text: `*Important:* On some platforms (MacOS) we do not get feedback on
492               when something is presented on the screen so the timings are only
493               accurate for what we know on a given platform.`,
494      }),
495    );
496  }
497
498  render() {
499    if (this.sliceDetails) {
500      const slice = this.sliceDetails;
501
502      const rightSideWidgets: m.Child[] = [];
503      rightSideWidgets.push(
504        m(
505          Section,
506          {title: 'Description'},
507          m('.div', this.getDescriptionText()),
508        ),
509      );
510
511      const stageWidget = this.getRelevantLinks();
512      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
513      if (stageWidget) {
514        rightSideWidgets.push(stageWidget);
515      }
516      rightSideWidgets.push(this.getLinksSection());
517      rightSideWidgets.push(this.getBreakdownSection());
518
519      return m(
520        DetailsShell,
521        {
522          title: 'Slice',
523          description: this.name,
524        },
525        m(
526          GridLayout,
527          m(
528            GridLayoutColumn,
529            renderDetails(this.trace, slice),
530            hasArgs(slice.args) &&
531              m(
532                Section,
533                {title: 'Arguments'},
534                m(Tree, renderArguments(this.trace, slice.args)),
535              ),
536          ),
537          m(GridLayoutColumn, rightSideWidgets),
538        ),
539      );
540    } else {
541      return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
542    }
543  }
544}
545