• 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 {uuidv4, uuidv4Sql} from '../../base/uuid';
16import {Actions, DeferredAction} from '../../common/actions';
17import {PrimaryTrackSortKey, SCROLLING_TRACK_GROUP} from '../../common/state';
18import {globals} from '../globals';
19import {TrackDescriptor} from '../../public';
20import {DebugSliceTrack} from './slice_track';
21import {
22  createPerfettoTable,
23  matchesSqlValue,
24} from '../../trace_processor/sql_utils';
25import {Engine} from '../../trace_processor/engine';
26import {DebugCounterTrack} from './counter_track';
27import {ARG_PREFIX} from './details_tab';
28
29// We need to add debug tracks from the core and from plugins. In order to add a
30// debug track we need to pass a context through with we can add the track. This
31// is different for plugins vs the core. This interface defines the generic
32// shape of this context, which can be supplied from a plugin or built from
33// globals.
34//
35// TODO(stevegolton): In the future, both the core and plugins should
36// have access to some Context object which implements the various things we
37// want to do in a generic way, so that we don't have to do this mangling to get
38// this to work.
39interface Context {
40  engine: Engine;
41  registerTrack(track: TrackDescriptor): unknown;
42}
43
44// Names of the columns of the underlying view to be used as
45// ts / dur / name / pivot.
46export interface SliceColumns {
47  ts: string;
48  dur: string;
49  name: string;
50  pivot?: string;
51}
52
53let debugTrackCount = 0;
54
55export interface SqlDataSource {
56  // SQL source selecting the necessary data.
57  sqlSource: string;
58
59  // Optional: Rename columns from the query result.
60  // If omitted, original column names from the query are used instead.
61  // The caller is responsible for ensuring that the number of items in this
62  // list matches the number of columns returned by sqlSource.
63  columns?: string[];
64}
65
66// Creates actions to add a debug track. The actions must be dispatched to
67// have an effect. Use this variant if you want to create many tracks at
68// once or want to tweak the actions once produced. Otherwise, use
69// addDebugSliceTrack().
70function createAddDebugTrackActions(
71  trackName: string,
72  uri: string,
73): DeferredAction<{}>[] {
74  const debugTrackId = ++debugTrackCount;
75  const trackKey = uuidv4();
76
77  const actions: DeferredAction<{}>[] = [
78    Actions.addTrack({
79      key: trackKey,
80      name: trackName.trim() || `Debug Track ${debugTrackId}`,
81      uri,
82      trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
83      trackGroup: SCROLLING_TRACK_GROUP,
84      closeable: true,
85    }),
86    Actions.toggleTrackPinned({trackKey}),
87  ];
88
89  return actions;
90}
91
92export async function addPivotDebugSliceTracks(
93  ctx: Context,
94  data: SqlDataSource,
95  trackName: string,
96  sliceColumns: SliceColumns,
97  argColumns: string[],
98) {
99  if (sliceColumns.pivot) {
100    // Get distinct values to group by
101    const pivotValues = await ctx.engine.query(`
102      with all_vals as (${data.sqlSource})
103      select DISTINCT ${sliceColumns.pivot} from all_vals;`);
104
105    const iter = pivotValues.iter({});
106
107    for (; iter.valid(); iter.next()) {
108      const pivotDataSource: SqlDataSource = {
109        sqlSource: `select * from
110        (${data.sqlSource})
111        where ${sliceColumns.pivot} ${matchesSqlValue(
112          iter.get(sliceColumns.pivot),
113        )}`,
114      };
115
116      await addDebugSliceTrack(
117        ctx,
118        pivotDataSource,
119        `${trackName.trim() || 'Pivot Track'}: ${iter.get(sliceColumns.pivot)}`,
120        sliceColumns,
121        argColumns,
122      );
123    }
124  }
125}
126
127// Adds a debug track immediately. Use createDebugSliceTrackActions() if you
128// want to create many tracks at once.
129export async function addDebugSliceTrack(
130  ctx: Context,
131  data: SqlDataSource,
132  trackName: string,
133  sliceColumns: SliceColumns,
134  argColumns: string[],
135): Promise<void> {
136  // Create a new table from the debug track definition. This will be used as
137  // the backing data source for our track and its details panel.
138  const tableName = `__debug_slice_${uuidv4Sql()}`;
139
140  // TODO(stevegolton): Right now we ignore the AsyncDisposable that this
141  // function returns, and so never clean up this table. The problem is we have
142  // no where sensible to do this cleanup.
143  // - If we did it in the track's onDestroy function, we could drop the table
144  //   while the details panel still needs access to it.
145  // - If we did it in the plugin's onTraceUnload function, we could risk
146  //   dropping it n the middle of a track update cycle as track lifecycles are
147  //   not synchronized with plugin lifecycles.
148  await createPerfettoTable(
149    ctx.engine,
150    tableName,
151    createDebugSliceTrackTableExpr(data, sliceColumns, argColumns),
152  );
153
154  const uri = `debug.slice.${uuidv4()}`;
155  ctx.registerTrack({
156    uri,
157    trackFactory: (trackCtx) => {
158      return new DebugSliceTrack(ctx.engine, trackCtx, tableName);
159    },
160  });
161
162  // Create the actions to add this track to the tracklist
163  const actions = await createAddDebugTrackActions(trackName, uri);
164  globals.dispatchMultiple(actions);
165}
166
167function createDebugSliceTrackTableExpr(
168  data: SqlDataSource,
169  sliceColumns: SliceColumns,
170  argColumns: string[],
171): string {
172  const dataColumns =
173    data.columns !== undefined ? `(${data.columns.join(', ')})` : '';
174  const dur = sliceColumns.dur === '0' ? 0 : sliceColumns.dur;
175  return `
176    with data${dataColumns} as (
177      ${data.sqlSource}
178    ),
179    prepared_data as (
180      select
181        ${sliceColumns.ts} as ts,
182        ifnull(cast(${dur} as int), -1) as dur,
183        printf('%s', ${sliceColumns.name}) as name
184        ${argColumns.length > 0 ? ',' : ''}
185        ${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',\n')}
186      from data
187    )
188    select
189      row_number() over (order by ts) as id,
190      *
191    from prepared_data
192    order by ts
193  `;
194}
195
196// Names of the columns of the underlying view to be used as ts / dur / name.
197export interface CounterColumns {
198  ts: string;
199  value: string;
200}
201
202export interface CounterDebugTrackConfig {
203  data: SqlDataSource;
204  columns: CounterColumns;
205}
206
207export interface CounterDebugTrackCreateConfig {
208  pinned?: boolean; // default true
209  closeable?: boolean; // default true
210}
211
212// Adds a debug track immediately. Use createDebugCounterTrackActions() if you
213// want to create many tracks at once.
214export async function addDebugCounterTrack(
215  ctx: Context,
216  data: SqlDataSource,
217  trackName: string,
218  columns: CounterColumns,
219): Promise<void> {
220  // Create a new table from the debug track definition. This will be used as
221  // the backing data source for our track and its details panel.
222  const tableName = `__debug_counter_${uuidv4Sql()}`;
223
224  // TODO(stevegolton): Right now we ignore the AsyncDisposable that this
225  // function returns, and so never clean up this table. The problem is we have
226  // no where sensible to do this cleanup.
227  // - If we did it in the track's onDestroy function, we could drop the table
228  //   while the details panel still needs access to it.
229  // - If we did it in the plugin's onTraceUnload function, we could risk
230  //   dropping it n the middle of a track update cycle as track lifecycles are
231  //   not synchronized with plugin lifecycles.
232  await createPerfettoTable(
233    ctx.engine,
234    tableName,
235    createDebugCounterTrackTableExpr(data, columns),
236  );
237
238  const uri = `debug.counter.${uuidv4()}`;
239  ctx.registerTrack({
240    uri,
241    trackFactory: (trackCtx) => {
242      return new DebugCounterTrack(ctx.engine, trackCtx, tableName);
243    },
244  });
245
246  // Create the actions to add this track to the tracklist
247  const actions = await createAddDebugTrackActions(trackName, uri);
248  globals.dispatchMultiple(actions);
249}
250
251function createDebugCounterTrackTableExpr(
252  data: SqlDataSource,
253  columns: CounterColumns,
254): string {
255  return `
256    with data as (
257      ${data.sqlSource}
258    )
259    select
260      ${columns.ts} as ts,
261      ${columns.value} as value
262    from data
263    order by ts
264  `;
265}
266