• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2025 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 {TrackEventDetailsPanel} from '../../public/details_panel';
17import {Trace} from '../../public/trace';
18import {
19  LONG,
20  NUM_NULL,
21  SqlValue,
22  STR,
23} from '../../trace_processor/query_result';
24import {DetailsShell} from '../../widgets/details_shell';
25import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
26import {Duration, duration, Time, time} from '../../base/time';
27import {assertExists, assertTrue} from '../../base/logging';
28import {Section} from '../../widgets/section';
29import {Tree, TreeNode} from '../../widgets/tree';
30import {Timestamp} from '../../components/widgets/timestamp';
31import {DurationWidget} from '../../components/widgets/duration';
32import {fromSqlBool, renderSliceRef, renderSqlRef} from './utils';
33import SqlModulesPlugin from '../dev.perfetto.SqlModules';
34import {
35  TableColumn,
36  TableManager,
37} from '../../components/widgets/sql/table/table_column';
38import {renderStandardCell} from '../../components/widgets/sql/table/render_cell_utils';
39import {ScrollTimelineModel} from './scroll_timeline_model';
40import {
41  DurationColumn,
42  StandardColumn,
43  TimestampColumn,
44} from '../../components/widgets/sql/table/columns';
45
46function createPluginSliceIdColumn(
47  trace: Trace,
48  trackUri: string,
49  name: string,
50): TableColumn {
51  const col = new StandardColumn(name);
52  col.renderCell = (value: SqlValue, tableManager: TableManager) => {
53    if (value === null || typeof value !== 'bigint') {
54      return renderStandardCell(value, name, tableManager);
55    }
56    return renderSliceRef({
57      trace: trace,
58      id: Number(value),
59      trackUri: trackUri,
60      title: `${value}`,
61    });
62  };
63  return col;
64}
65
66function createScrollTimelineTableColumns(
67  trace: Trace,
68  trackUri: string,
69): TableColumn[] {
70  return [
71    createPluginSliceIdColumn(trace, trackUri, 'id'),
72    new StandardColumn('scroll_update_id'),
73    new TimestampColumn('ts'),
74    new DurationColumn('dur'),
75    new StandardColumn('name'),
76    new StandardColumn('classification'),
77  ];
78}
79
80export class ScrollTimelineDetailsPanel implements TrackEventDetailsPanel {
81  // Information about the scroll update *slice*, which was emitted by
82  // ScrollTimelineTrack.
83  // Source: this.tableName[id=this.id]
84  private sliceData?: {
85    name: string;
86    ts: time;
87    dur: duration;
88    // ID of the scroll update in chrome_scroll_update_info.
89    scrollUpdateId: bigint;
90  };
91
92  // Information about the scroll *update*, which comes from the Chrome tracing
93  // stdlib.
94  // Source: chrome_scroll_update_info[id=this.sliceData.scrollUpdateId]
95  private scrollData?: {
96    vsyncInterval: duration | undefined;
97    isPresented: boolean | undefined;
98    isJanky: boolean | undefined;
99    isInertial: boolean | undefined;
100    isFirstScrollUpdateInScroll: boolean | undefined;
101    isFirstScrollUpdateInFrame: boolean | undefined;
102  };
103
104  constructor(
105    private readonly trace: Trace,
106    private readonly model: ScrollTimelineModel,
107    // ID of the slice in tableName.
108    private readonly id: number,
109  ) {}
110
111  async load(): Promise<void> {
112    await this.querySliceData();
113    await this.queryScrollData();
114  }
115
116  private async querySliceData(): Promise<void> {
117    assertTrue(this.sliceData === undefined);
118    const queryResult = await this.trace.engine.query(`
119      SELECT
120        name,
121        ts,
122        dur,
123        scroll_update_id
124      FROM ${this.model.tableName}
125      WHERE id = ${this.id}`);
126    const row = queryResult.firstRow({
127      name: STR,
128      ts: LONG,
129      dur: LONG,
130      scroll_update_id: LONG,
131    });
132    this.sliceData = {
133      name: row.name,
134      ts: Time.fromRaw(row.ts),
135      dur: Duration.fromRaw(row.dur),
136      scrollUpdateId: row.scroll_update_id,
137    };
138  }
139
140  private async queryScrollData(): Promise<void> {
141    assertExists(this.sliceData);
142    assertTrue(this.scrollData === undefined);
143    const queryResult = await this.trace.engine.query(`
144      INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
145      SELECT
146        vsync_interval_ms,
147        is_presented,
148        is_janky,
149        is_inertial,
150        is_first_scroll_update_in_scroll,
151        is_first_scroll_update_in_frame
152      FROM chrome_scroll_update_info
153      WHERE id = ${this.sliceData!.scrollUpdateId}`);
154    const row = queryResult.firstRow({
155      vsync_interval_ms: NUM_NULL,
156      is_presented: NUM_NULL,
157      is_janky: NUM_NULL,
158      is_inertial: NUM_NULL,
159      is_first_scroll_update_in_scroll: NUM_NULL,
160      is_first_scroll_update_in_frame: NUM_NULL,
161    });
162    this.scrollData = {
163      vsyncInterval:
164        row.vsync_interval_ms === null
165          ? undefined
166          : Duration.fromMillis?.(row.vsync_interval_ms),
167      isPresented: fromSqlBool(row.is_presented),
168      isJanky: fromSqlBool(row.is_janky),
169      isInertial: fromSqlBool(row.is_inertial),
170      isFirstScrollUpdateInScroll: fromSqlBool(
171        row.is_first_scroll_update_in_scroll,
172      ),
173      isFirstScrollUpdateInFrame: fromSqlBool(
174        row.is_first_scroll_update_in_frame,
175      ),
176    };
177  }
178
179  render(): m.Children {
180    return m(
181      DetailsShell,
182      {
183        title: 'Slice',
184        description: this.sliceData?.name ?? 'Loading...',
185      },
186      m(
187        GridLayout,
188        m(GridLayoutColumn, this.renderSliceDetails()),
189        m(GridLayoutColumn, this.renderScrollDetails()),
190      ),
191    );
192  }
193
194  private renderSliceDetails(): m.Child {
195    let child;
196    if (this.sliceData === undefined) {
197      child = 'Loading...';
198    } else {
199      child = m(
200        Tree,
201        m(TreeNode, {
202          left: 'Name',
203          right: this.sliceData.name,
204        }),
205        m(TreeNode, {
206          left: 'Start time',
207          right: m(Timestamp, {ts: this.sliceData.ts}),
208        }),
209        m(TreeNode, {
210          left: 'Duration',
211          right: m(DurationWidget, {dur: this.sliceData.dur}),
212        }),
213        m(TreeNode, {
214          left: 'SQL ID',
215          right: renderSqlRef({
216            trace: this.trace,
217            tableName: this.model.tableName,
218            tableDescription: {
219              name: this.model.tableName,
220              columns: createScrollTimelineTableColumns(
221                this.trace,
222                this.model.trackUri,
223              ),
224            },
225            id: this.id,
226          }),
227        }),
228      );
229    }
230    return m(Section, {title: 'Slice details'}, child);
231  }
232
233  private renderScrollDetails(): m.Child {
234    let child;
235    if (this.sliceData === undefined || this.scrollData === undefined) {
236      child = 'Loading...';
237    } else {
238      const scrollTableDescription = this.trace.plugins
239        .getPlugin(SqlModulesPlugin)
240        .getSqlModules()
241        .getModuleForTable('chrome_scroll_update_info')
242        ?.getSqlTableDescription('chrome_scroll_update_info');
243      child = m(
244        Tree,
245        m(TreeNode, {
246          left: 'Vsync interval',
247          right:
248            this.scrollData.vsyncInterval === undefined
249              ? `${this.scrollData.vsyncInterval}`
250              : m(DurationWidget, {dur: this.scrollData.vsyncInterval}),
251        }),
252        m(TreeNode, {
253          left: 'Is presented',
254          right: `${this.scrollData.isPresented}`,
255        }),
256        m(TreeNode, {
257          left: 'Is janky',
258          right: `${this.scrollData.isJanky}`,
259        }),
260        m(TreeNode, {
261          left: 'Is inertial',
262          right: `${this.scrollData.isInertial}`,
263        }),
264        m(TreeNode, {
265          left: 'Is first scroll update in scroll',
266          right: `${this.scrollData.isFirstScrollUpdateInScroll}`,
267        }),
268        m(TreeNode, {
269          left: 'Is first scroll update in frame',
270          right: `${this.scrollData.isFirstScrollUpdateInFrame}`,
271        }),
272        m(TreeNode, {
273          left: 'SQL ID',
274          right: renderSqlRef({
275            trace: this.trace,
276            tableName: 'chrome_scroll_update_info',
277            id: this.sliceData.scrollUpdateId,
278            tableDescription: scrollTableDescription,
279          }),
280        }),
281      );
282    }
283    return m(Section, {title: 'Scroll details'}, child);
284  }
285}
286