• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2024 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 {Trace} from '../../public/trace';
16import {TrackNode} from '../../public/workspace';
17import {SourceDataset} from '../../trace_processor/dataset';
18import {Engine} from '../../trace_processor/engine';
19import {LONG, NUM, STR} from '../../trace_processor/query_result';
20import {
21  createPerfettoTable,
22  sqlValueToReadableString,
23  sqlValueToSqliteString,
24} from '../../trace_processor/sql_utils';
25import {DatasetSliceTrack} from './dataset_slice_track';
26import {
27  ARG_PREFIX,
28  DebugSliceTrackDetailsPanel,
29} from './debug_slice_track_details_panel';
30import {
31  CounterColumnMapping,
32  SqlTableCounterTrack,
33} from './query_counter_track';
34import {SliceColumnMapping, SqlDataSource} from './query_slice_track';
35
36let trackCounter = 0; // For reproducible ids.
37
38function getUniqueTrackCounter() {
39  return trackCounter++;
40}
41
42export interface DebugSliceTrackArgs {
43  readonly trace: Trace;
44  readonly data: SqlDataSource;
45  readonly title?: string;
46  readonly columns?: Partial<SliceColumnMapping>;
47  readonly argColumns?: string[];
48  readonly pivotOn?: string;
49}
50
51/**
52 * Adds a new debug slice track to the workspace.
53 *
54 * A debug slice track is a track based on a query which is:
55 * - Based on a query.
56 * - Uses automatic slice layout.
57 * - Automatically added to the top of the current workspace.
58 * - Pinned.
59 * - Has a close button to remove it.
60 *
61 * @param args - Args to pass to the trace.
62 * @param args.trace - The trace to use.
63 * @param args.data.sqlSource - The query to run.
64 * @param args.data.columns - Optional: Override columns.
65 * @param args.title - Optional: Title for the track. If pivotOn is supplied,
66 * this will be used as the root title for each track, but each title will have
67 * the value appended.
68 * @param args.columns - Optional: The columns names to use for the various
69 * essential column names.
70 * @param args.argColumns - Optional: A list of columns which are passed to the
71 * details panel.
72 * @param args.pivotOn - Optional: The name of a column on which to pivot. If
73 * provided, we will create N tracks, one for each distinct value of the pivotOn
74 * column. Each track will only show the slices which have the corresponding
75 * value in their pivotOn column.
76 */
77export async function addDebugSliceTrack(args: DebugSliceTrackArgs) {
78  const tableId = getUniqueTrackCounter();
79  const tableName = `__debug_track_${tableId}`;
80  const titleBase = args.title?.trim() || `Debug Slice Track ${tableId}`;
81  const uriBase = `debug.track${tableId}`;
82
83  // Create a table for this query before doing anything
84  await createTableForSliceTrack(
85    args.trace.engine,
86    tableName,
87    args.data,
88    args.columns,
89    args.argColumns,
90    args.pivotOn,
91  );
92
93  if (args.pivotOn) {
94    await addPivotedSliceTracks(
95      args.trace,
96      tableName,
97      titleBase,
98      uriBase,
99      args.pivotOn,
100    );
101  } else {
102    addSingleSliceTrack(args.trace, tableName, titleBase, uriBase);
103  }
104}
105
106async function createTableForSliceTrack(
107  engine: Engine,
108  tableName: string,
109  data: SqlDataSource,
110  columns: Partial<SliceColumnMapping> = {},
111  argColumns?: string[],
112  pivotCol?: string,
113) {
114  const {ts = 'ts', dur = 'dur', name = 'name'} = columns;
115
116  // If the view has clashing names (e.g. "name" coming from joining two
117  // different tables, we will see names like "name_1", "name_2", but they
118  // won't be addressable from the SQL. So we explicitly name them through a
119  // list of columns passed to CTE.
120  const dataColumns =
121    data.columns !== undefined ? `(${data.columns.join(', ')})` : '';
122
123  const cols = [
124    `${ts} as ts`,
125    `ifnull(cast(${dur} as int), -1) as dur`,
126    `printf('%s', ${name}) as name`,
127    argColumns && argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`),
128    pivotCol && `${pivotCol} as pivot`,
129  ]
130    .flat() // Convert to flattened list
131    .filter(Boolean) // Remove falsy values
132    .join(',');
133
134  const query = `
135    with data${dataColumns} as (
136      ${data.sqlSource}
137    ),
138    prepared_data as (
139      select ${cols}
140      from data
141    )
142    select
143      row_number() over (order by ts) as id,
144      *
145    from prepared_data
146    order by ts
147  `;
148
149  return await createPerfettoTable(engine, tableName, query);
150}
151
152async function addPivotedSliceTracks(
153  trace: Trace,
154  tableName: string,
155  titleBase: string,
156  uriBase: string,
157  pivotColName: string,
158) {
159  const result = await trace.engine.query(`
160    SELECT DISTINCT pivot
161    FROM ${tableName}
162    ORDER BY pivot
163  `);
164
165  let trackCount = 0;
166  for (const iter = result.iter({}); iter.valid(); iter.next()) {
167    const uri = `${uriBase}_${trackCount++}`;
168    const pivotValue = iter.get('pivot');
169    const title = `${titleBase}: ${pivotColName} = ${sqlValueToReadableString(pivotValue)}`;
170
171    trace.tracks.registerTrack({
172      uri,
173      title,
174      track: new DatasetSliceTrack({
175        trace,
176        uri,
177        dataset: new SourceDataset({
178          schema: {
179            id: NUM,
180            ts: LONG,
181            dur: LONG,
182            name: STR,
183          },
184          src: tableName,
185          filter: {
186            col: 'pivot',
187            eq: pivotValue,
188          },
189        }),
190        detailsPanel: (row) => {
191          return new DebugSliceTrackDetailsPanel(trace, tableName, row.id);
192        },
193      }),
194    });
195
196    const trackNode = new TrackNode({uri, title, removable: true});
197    trace.workspace.pinnedTracksNode.addChildLast(trackNode);
198  }
199}
200
201function addSingleSliceTrack(
202  trace: Trace,
203  tableName: string,
204  title: string,
205  uri: string,
206) {
207  trace.tracks.registerTrack({
208    uri,
209    title,
210    track: new DatasetSliceTrack({
211      trace,
212      uri,
213      dataset: new SourceDataset({
214        schema: {
215          id: NUM,
216          ts: LONG,
217          dur: LONG,
218          name: STR,
219        },
220        src: tableName,
221      }),
222      detailsPanel: (row) => {
223        return new DebugSliceTrackDetailsPanel(trace, tableName, row.id);
224      },
225    }),
226  });
227
228  const trackNode = new TrackNode({uri, title, removable: true});
229  trace.workspace.pinnedTracksNode.addChildLast(trackNode);
230}
231
232export interface DebugCounterTrackArgs {
233  readonly trace: Trace;
234  readonly data: SqlDataSource;
235  readonly title?: string;
236  readonly columns?: Partial<CounterColumnMapping>;
237  readonly pivotOn?: string;
238}
239
240/**
241 * Adds a new debug counter track to the workspace.
242 *
243 * A debug slice track is a track based on a query which is:
244 * - Based on a query.
245 * - Automatically added to the top of the current workspace.
246 * - Pinned.
247 * - Has a close button to remove it.
248 *
249 * @param args - Args to pass to the trace.
250 * @param args.trace - The trace to use.
251 * @param args.data.sqlSource - The query to run.
252 * @param args.data.columns - Optional: Override columns.
253 * @param args.title - Optional: Title for the track. If pivotOn is supplied,
254 * this will be used as the root title for each track, but each title will have
255 * the value appended.
256 * @param args.columns - Optional: The columns names to use for the various
257 * essential column names.
258 * @param args.pivotOn - Optional: The name of a column on which to pivot. If
259 * provided, we will create N tracks, one for each distinct value of the pivotOn
260 * column. Each track will only show the slices which have the corresponding
261 * value in their pivotOn column.
262 */
263export async function addDebugCounterTrack(args: DebugCounterTrackArgs) {
264  const tableId = getUniqueTrackCounter();
265  const tableName = `__debug_track_${tableId}`;
266  const titleBase = args.title?.trim() || `Debug Slice Track ${tableId}`;
267  const uriBase = `debug.track${tableId}`;
268
269  // Create a table for this query before doing anything
270  await createTableForCounterTrack(
271    args.trace.engine,
272    tableName,
273    args.data,
274    args.columns,
275    args.pivotOn,
276  );
277
278  if (args.pivotOn) {
279    await addPivotedCounterTracks(
280      args.trace,
281      tableName,
282      titleBase,
283      uriBase,
284      args.pivotOn,
285    );
286  } else {
287    addSingleCounterTrack(args.trace, tableName, titleBase, uriBase);
288  }
289}
290
291async function createTableForCounterTrack(
292  engine: Engine,
293  tableName: string,
294  data: SqlDataSource,
295  columnMapping: Partial<CounterColumnMapping> = {},
296  pivotCol?: string,
297) {
298  const {ts = 'ts', value = 'value'} = columnMapping;
299  const cols = [
300    `${ts} as ts`,
301    `${value} as value`,
302    pivotCol && `${pivotCol} as pivot`,
303  ]
304    .flat() // Convert to flattened list
305    .filter(Boolean) // Remove falsy values
306    .join(',');
307
308  const query = `
309    with data as (
310      ${data.sqlSource}
311    )
312    select ${cols}
313    from data
314    order by ts
315  `;
316
317  return await createPerfettoTable(engine, tableName, query);
318}
319
320async function addPivotedCounterTracks(
321  trace: Trace,
322  tableName: string,
323  titleBase: string,
324  uriBase: string,
325  pivotColName: string,
326) {
327  const result = await trace.engine.query(`
328    SELECT DISTINCT pivot
329    FROM ${tableName}
330    ORDER BY pivot
331  `);
332
333  let trackCount = 0;
334  for (const iter = result.iter({}); iter.valid(); iter.next()) {
335    const uri = `${uriBase}_${trackCount++}`;
336    const pivotValue = iter.get('pivot');
337    const title = `${titleBase}: ${pivotColName} = ${sqlValueToReadableString(pivotValue)}`;
338
339    trace.tracks.registerTrack({
340      uri,
341      title,
342      track: new SqlTableCounterTrack(
343        trace,
344        uri,
345        `
346          SELECT *
347          FROM ${tableName}
348          WHERE pivot = ${sqlValueToSqliteString(pivotValue)}
349        `,
350      ),
351    });
352
353    const trackNode = new TrackNode({uri, title, removable: true});
354    trace.workspace.pinnedTracksNode.addChildLast(trackNode);
355  }
356}
357
358function addSingleCounterTrack(
359  trace: Trace,
360  tableName: string,
361  title: string,
362  uri: string,
363) {
364  trace.tracks.registerTrack({
365    uri,
366    title,
367    track: new SqlTableCounterTrack(trace, uri, tableName),
368  });
369
370  const trackNode = new TrackNode({uri, title, removable: true});
371  trace.workspace.pinnedTracksNode.addChildLast(trackNode);
372}
373