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 {Icons} from '../../base/semantic_icons';
17import {duration, Time, time} from '../../base/time';
18import {exists} from '../../base/utils';
19import {SliceSqlId} from '../../components/sql_utils/core_types';
20import {Engine} from '../../trace_processor/engine';
21import {LONG, NUM, STR} from '../../trace_processor/query_result';
22import {Anchor} from '../../widgets/anchor';
23import {
24  CauseProcess,
25  CauseThread,
26  ScrollJankCauseMap,
27} from './scroll_jank_cause_map';
28import {scrollTo} from '../../public/scroll_helper';
29import {Trace} from '../../public/trace';
30
31const UNKNOWN_NAME = 'Unknown';
32
33export interface EventLatencyStage {
34  name: string;
35  // Slice id of the top level EventLatency slice (not a stage).
36  eventLatencyId: SliceSqlId;
37  ts: time;
38  dur: duration;
39}
40
41export interface EventLatencyCauseThreadTracks {
42  // A thread may have multiple tracks associated with it (e.g. from ATrace
43  // events).
44  trackIds: number[];
45  thread: CauseThread;
46  causeDescription: string;
47}
48
49export async function getScrollJankCauseStage(
50  engine: Engine,
51  eventLatencyId: SliceSqlId,
52): Promise<EventLatencyStage | undefined> {
53  const queryResult = await engine.query(`
54    SELECT
55      IFNULL(cause_of_jank, '${UNKNOWN_NAME}') AS causeOfJank,
56      IFNULL(sub_cause_of_jank, '${UNKNOWN_NAME}') AS subCauseOfJank,
57      IFNULL(substage.ts, -1) AS ts,
58      IFNULL(substage.dur, -1) AS dur
59    FROM chrome_janky_frame_presentation_intervals
60      JOIN descendant_slice(event_latency_id) substage
61    WHERE event_latency_id = ${eventLatencyId}
62      AND substage.name = COALESCE(sub_cause_of_jank, cause_of_jank)
63  `);
64
65  const causeIt = queryResult.iter({
66    causeOfJank: STR,
67    subCauseOfJank: STR,
68    ts: LONG,
69    dur: LONG,
70  });
71
72  for (; causeIt.valid(); causeIt.next()) {
73    const causeOfJank = causeIt.causeOfJank;
74    const subCauseOfJank = causeIt.subCauseOfJank;
75
76    if (causeOfJank == '' || causeOfJank == UNKNOWN_NAME) return undefined;
77    const cause = subCauseOfJank == UNKNOWN_NAME ? causeOfJank : subCauseOfJank;
78    const stageDetails: EventLatencyStage = {
79      name: cause,
80      eventLatencyId: eventLatencyId,
81      ts: Time.fromRaw(causeIt.ts),
82      dur: causeIt.dur,
83    };
84
85    return stageDetails;
86  }
87
88  return undefined;
89}
90
91export async function getEventLatencyCauseTracks(
92  engine: Engine,
93  scrollJankCauseStage: EventLatencyStage,
94): Promise<EventLatencyCauseThreadTracks[]> {
95  const threadTracks: EventLatencyCauseThreadTracks[] = [];
96  const causeDetails = ScrollJankCauseMap.getEventLatencyDetails(
97    scrollJankCauseStage.name,
98  );
99  if (causeDetails === undefined) return threadTracks;
100
101  for (const cause of causeDetails.jankCauses) {
102    switch (cause.process) {
103      case CauseProcess.RENDERER:
104      case CauseProcess.BROWSER:
105      case CauseProcess.GPU:
106        const tracksForProcess = await getChromeCauseTracks(
107          engine,
108          scrollJankCauseStage.eventLatencyId,
109          cause.process,
110          cause.thread,
111        );
112        for (const track of tracksForProcess) {
113          track.causeDescription = cause.description;
114          threadTracks.push(track);
115        }
116        break;
117      case CauseProcess.UNKNOWN:
118      default:
119        break;
120    }
121  }
122
123  return threadTracks;
124}
125
126async function getChromeCauseTracks(
127  engine: Engine,
128  eventLatencySliceId: number,
129  processName: CauseProcess,
130  threadName: CauseThread,
131): Promise<EventLatencyCauseThreadTracks[]> {
132  const queryResult = await engine.query(`
133      INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_cause_utils;
134
135      SELECT DISTINCT
136        utid,
137        id AS trackId
138      FROM thread_track
139      WHERE utid IN (
140        SELECT DISTINCT
141          utid
142        FROM chrome_select_scroll_jank_cause_thread(
143          ${eventLatencySliceId},
144          '${processName}',
145          '${threadName}'
146        )
147      );
148  `);
149
150  const it = queryResult.iter({
151    utid: NUM,
152    trackId: NUM,
153  });
154
155  const threadsWithTrack: {[id: number]: EventLatencyCauseThreadTracks} = {};
156  const utids: number[] = [];
157  for (; it.valid(); it.next()) {
158    const utid = it.utid;
159    if (!(utid in threadsWithTrack)) {
160      threadsWithTrack[utid] = {
161        trackIds: [it.trackId],
162        thread: threadName,
163        causeDescription: '',
164      };
165      utids.push(utid);
166    } else {
167      threadsWithTrack[utid].trackIds.push(it.trackId);
168    }
169  }
170
171  return utids.map((each) => threadsWithTrack[each]);
172}
173
174export function getCauseLink(
175  trace: Trace,
176  threadTracks: EventLatencyCauseThreadTracks,
177  tracksByTrackId: Map<number, string>,
178  ts: time | undefined,
179  dur: duration | undefined,
180): m.Child {
181  const trackUris: string[] = [];
182  for (const trackId of threadTracks.trackIds) {
183    const track = tracksByTrackId.get(trackId);
184    if (track === undefined) {
185      return `Could not locate track ${trackId} for thread ${threadTracks.thread} in the global state`;
186    }
187    trackUris.push(track);
188  }
189
190  if (trackUris.length == 0) {
191    return `No valid tracks for thread ${threadTracks.thread}.`;
192  }
193
194  // Fixed length of a container to ensure that the icon does not overlap with
195  // the text due to table formatting.
196  return m(
197    `div[style='width:250px']`,
198    m(
199      Anchor,
200      {
201        icon: Icons.UpdateSelection,
202        onclick: () => {
203          scrollTo({
204            track: {uri: trackUris[0], expandGroup: true},
205          });
206          if (exists(ts) && exists(dur)) {
207            scrollTo({
208              time: {
209                start: ts,
210                end: Time.fromRaw(ts + dur),
211                viewPercentage: 0.3,
212              },
213            });
214            trace.selection.selectArea({
215              start: ts,
216              end: Time.fromRaw(ts + dur),
217              trackUris,
218            });
219          }
220        },
221      },
222      threadTracks.thread,
223    ),
224  );
225}
226