blob: ee7ef32cc26adcf8f481f31bfb140e1d31d8f3e3 [file] [log] [blame]
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {uuidv4, uuidv4Sql} from '../../base/uuid';
import {globals} from '../globals';
import {TrackDescriptor} from '../../public';
import {DebugSliceTrack} from './slice_track';
import {
createPerfettoTable,
matchesSqlValue,
sqlValueToReadableString,
} from '../../trace_processor/sql_utils';
import {Engine} from '../../trace_processor/engine';
import {DebugCounterTrack} from './counter_track';
import {ARG_PREFIX} from './details_tab';
import {TrackNode} from '../../public/workspace';
import {raf} from '../../core/raf_scheduler';
// We need to add debug tracks from the core and from plugins. In order to add a
// debug track we need to pass a context through with we can add the track. This
// is different for plugins vs the core. This interface defines the generic
// shape of this context, which can be supplied from a plugin or built from
// globals.
//
// TODO(stevegolton): In the future, both the core and plugins should
// have access to some Context object which implements the various things we
// want to do in a generic way, so that we don't have to do this mangling to get
// this to work.
interface Context {
engine: Engine;
registerTrack(track: TrackDescriptor): unknown;
}
// Names of the columns of the underlying view to be used as
// ts / dur / name / pivot.
export interface SliceColumns {
ts: string;
dur: string;
name: string;
}
let debugTrackCount = 0;
export interface SqlDataSource {
// SQL source selecting the necessary data.
sqlSource: string;
// Optional: Rename columns from the query result.
// If omitted, original column names from the query are used instead.
// The caller is responsible for ensuring that the number of items in this
// list matches the number of columns returned by sqlSource.
columns?: string[];
}
// Creates actions to add a debug track. The actions must be dispatched to
// have an effect. Use this variant if you want to create many tracks at
// once or want to tweak the actions once produced. Otherwise, use
// addDebugSliceTrack().
function addDebugTrack(trackName: string, uri: string): void {
const debugTrackId = ++debugTrackCount;
const displayName = trackName.trim() || `Debug Track ${debugTrackId}`;
const track = new TrackNode(uri, displayName);
globals.workspace.prependChild(track);
track.pin();
raf.scheduleFullRedraw();
}
export async function addPivotedTracks(
ctx: Context,
data: SqlDataSource,
trackName: string,
pivotColumn: string,
createTrack: (
ctx: Context,
data: SqlDataSource,
trackName: string,
) => Promise<void>,
) {
const iter = (
await ctx.engine.query(`
with all_vals as (${data.sqlSource})
select DISTINCT ${pivotColumn} from all_vals
order by ${pivotColumn}
`)
).iter({});
for (; iter.valid(); iter.next()) {
await createTrack(
ctx,
{
sqlSource: `select * from
(${data.sqlSource})
where ${pivotColumn} ${matchesSqlValue(iter.get(pivotColumn))}`,
},
`${trackName.trim() || 'Pivot Track'}: ${sqlValueToReadableString(iter.get(pivotColumn))}`,
);
}
}
// Adds a debug track immediately. Use createDebugSliceTrackActions() if you
// want to create many tracks at once.
export async function addDebugSliceTrack(
ctx: Context,
data: SqlDataSource,
trackName: string,
sliceColumns: SliceColumns,
argColumns: string[],
): Promise<void> {
// Create a new table from the debug track definition. This will be used as
// the backing data source for our track and its details panel.
const tableName = `__debug_slice_${uuidv4Sql()}`;
// TODO(stevegolton): Right now we ignore the AsyncDisposable that this
// function returns, and so never clean up this table. The problem is we have
// no where sensible to do this cleanup.
// - If we did it in the track's onDestroy function, we could drop the table
// while the details panel still needs access to it.
// - If we did it in the plugin's onTraceUnload function, we could risk
// dropping it n the middle of a track update cycle as track lifecycles are
// not synchronized with plugin lifecycles.
await createPerfettoTable(
ctx.engine,
tableName,
createDebugSliceTrackTableExpr(data, sliceColumns, argColumns),
);
const uri = `debug.slice.${uuidv4()}`;
ctx.registerTrack({
uri,
title: trackName,
track: new DebugSliceTrack(ctx.engine, {trackUri: uri}, tableName),
});
// Create the actions to add this track to the tracklist
addDebugTrack(trackName, uri);
}
function createDebugSliceTrackTableExpr(
data: SqlDataSource,
sliceColumns: SliceColumns,
argColumns: string[],
): string {
const dataColumns =
data.columns !== undefined ? `(${data.columns.join(', ')})` : '';
const dur = sliceColumns.dur === '0' ? 0 : sliceColumns.dur;
return `
with data${dataColumns} as (
${data.sqlSource}
),
prepared_data as (
select
${sliceColumns.ts} as ts,
ifnull(cast(${dur} as int), -1) as dur,
printf('%s', ${sliceColumns.name}) as name
${argColumns.length > 0 ? ',' : ''}
${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',\n')}
from data
)
select
row_number() over (order by ts) as id,
*
from prepared_data
order by ts
`;
}
// Names of the columns of the underlying view to be used as ts / dur / name.
export interface CounterColumns {
ts: string;
value: string;
}
export interface CounterDebugTrackConfig {
data: SqlDataSource;
columns: CounterColumns;
}
export interface CounterDebugTrackCreateConfig {
pinned?: boolean; // default true
closeable?: boolean; // default true
}
// Adds a debug track immediately. Use createDebugCounterTrackActions() if you
// want to create many tracks at once.
export async function addDebugCounterTrack(
ctx: Context,
data: SqlDataSource,
trackName: string,
columns: CounterColumns,
): Promise<void> {
// Create a new table from the debug track definition. This will be used as
// the backing data source for our track and its details panel.
const tableName = `__debug_counter_${uuidv4Sql()}`;
// TODO(stevegolton): Right now we ignore the AsyncDisposable that this
// function returns, and so never clean up this table. The problem is we have
// no where sensible to do this cleanup.
// - If we did it in the track's onDestroy function, we could drop the table
// while the details panel still needs access to it.
// - If we did it in the plugin's onTraceUnload function, we could risk
// dropping it n the middle of a track update cycle as track lifecycles are
// not synchronized with plugin lifecycles.
await createPerfettoTable(
ctx.engine,
tableName,
createDebugCounterTrackTableExpr(data, columns),
);
const uri = `debug.counter.${uuidv4()}`;
ctx.registerTrack({
uri,
title: trackName,
track: new DebugCounterTrack(ctx.engine, {trackUri: uri}, tableName),
});
// Create the actions to add this track to the tracklist
addDebugTrack(trackName, uri);
}
function createDebugCounterTrackTableExpr(
data: SqlDataSource,
columns: CounterColumns,
): string {
return `
with data as (
${data.sqlSource}
)
select
${columns.ts} as ts,
${columns.value} as value
from data
order by ts
`;
}