blob: c695ce067ef07966e7674f3024b31fff28117bba [file] [log] [blame] [edit]
// 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 {Trace} from '../../public/trace';
import {TrackNode} from '../../public/workspace';
import {SourceDataset} from '../../trace_processor/dataset';
import {Engine} from '../../trace_processor/engine';
import {LONG, NUM, STR, UNKNOWN} from '../../trace_processor/query_result';
import {
createPerfettoTable,
sqlValueToReadableString,
sqlValueToSqliteString,
} from '../../trace_processor/sql_utils';
import {SliceTrack} from './slice_track';
import {
RAW_PREFIX,
DebugSliceTrackDetailsPanel,
} from './debug_slice_track_details_panel';
import {
CounterColumnMapping,
SqlTableCounterTrack,
} from './query_counter_track';
import {getColorForSlice} from '../colorizer';
export interface SqlDataSource {
// SQL source selecting the necessary data.
readonly 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.
readonly columns?: ReadonlyArray<string>;
}
export interface SliceColumnMapping {
readonly ts: string;
readonly dur: string;
readonly name: string;
}
let trackCounter = 0; // For reproducible ids.
function getUniqueTrackCounter() {
return trackCounter++;
}
export interface DebugSliceTrackArgs {
readonly trace: Trace;
readonly data: SqlDataSource;
readonly title?: string;
readonly columns?: Partial<SliceColumnMapping>;
readonly rawColumns?: ReadonlyArray<string>;
readonly pivotOn?: string;
readonly argSetIdColumn?: string;
readonly colorColumn?: string;
}
/**
* Adds a new debug slice track to the workspace.
*
* A debug slice track is a track based on a query which is:
* - Based on a query.
* - Uses automatic slice layout.
* - Automatically added to the top of the current workspace.
* - Pinned.
* - Has a close button to remove it.
*
* @param args - Args to pass to the trace.
* @param args.trace - The trace to use.
* @param args.data.sqlSource - The query to run.
* @param args.data.columns - Optional: Override columns.
* @param args.title - Optional: Title for the track. If pivotOn is supplied,
* this will be used as the root title for each track, but each title will have
* the value appended.
* @param args.columns - Optional: The columns names to use for the various
* essential column names.
* @param args.rawColumns - Optional: A list of columns to be displayed in the
* 'Raw columns' section of the details panel.
* @param args.pivotOn - Optional: The name of a column on which to pivot. If
* provided, we will create N tracks, one for each distinct value of the pivotOn
* column. Each track will only show the slices which have the corresponding
* value in their pivotOn column.
* @param args.colorColumn - Optional: The name of a column to use for coloring
* slices. If provided, slices will be colored based on the value in this column.
* If omitted, slices are colored based on their name.
*/
export async function addDebugSliceTrack(args: DebugSliceTrackArgs) {
const tableId = getUniqueTrackCounter();
const tableName = `__debug_track_${tableId}`;
const titleBase = args.title?.trim() || `Debug Slice Track ${tableId}`;
const uriBase = `debug.track${tableId}`;
// Create a table for this query before doing anything
await createTableForSliceTrack(
args.trace.engine,
tableName,
args.data,
args.columns,
args.rawColumns,
args.pivotOn,
args.argSetIdColumn,
args.colorColumn,
);
if (args.pivotOn) {
await addPivotedSliceTracks(
args.trace,
tableName,
titleBase,
uriBase,
args.pivotOn,
args.colorColumn,
);
} else {
addSingleSliceTrack(
args.trace,
tableName,
titleBase,
uriBase,
args.argSetIdColumn,
args.colorColumn,
);
}
}
async function createTableForSliceTrack(
engine: Engine,
tableName: string,
data: SqlDataSource,
columns: Partial<SliceColumnMapping> = {},
rawColumns?: ReadonlyArray<string>,
pivotCol?: string,
argSetIdColumn?: string,
colorCol?: string,
) {
if (rawColumns === undefined) {
// Find the raw columns list from the query if not provided.
// TODO(stevegolton): Potential performance improvement to be obtained from
// using the prepare statement API rather than a LIMIT 0 query.
const query = `
WITH data AS (
${data.sqlSource}
)
SELECT *
FROM data
LIMIT 0
`;
const result = await engine.query(query);
rawColumns = result.columns();
}
const {ts = 'ts', dur = 'dur', name = 'name'} = columns;
// If the view has clashing names (e.g. "name" coming from joining two
// different tables, we will see names like "name_1", "name_2", but they
// won't be addressable from the SQL. So we explicitly name them through a
// list of columns passed to CTE.
const dataColumns =
data.columns !== undefined ? `(${data.columns.join(', ')})` : '';
const cols = [
`${ts} as ts`,
`ifnull(cast(${dur} as int), -1) as dur`,
`printf('%s', ${name}) as name`,
rawColumns.map((c) => `${c} as ${RAW_PREFIX}${c}`),
pivotCol && `${pivotCol} as pivot`,
argSetIdColumn && `${argSetIdColumn} as arg_set_id`,
colorCol && `${colorCol} as color`,
]
.flat() // Convert to flattened list
.filter(Boolean) // Remove falsy values
.join(',');
const query = `
with data${dataColumns} as (
${data.sqlSource}
),
prepared_data as (
select ${cols}
from data
)
select
row_number() over (order by ts) as id,
*
from prepared_data
order by ts
`;
return await createPerfettoTable({engine, name: tableName, as: query});
}
async function addPivotedSliceTracks(
trace: Trace,
tableName: string,
titleBase: string,
uriBase: string,
pivotColName: string,
colorCol?: string,
) {
const result = await trace.engine.query(`
SELECT DISTINCT pivot
FROM ${tableName}
ORDER BY pivot
`);
let trackCount = 0;
for (const iter = result.iter({}); iter.valid(); iter.next()) {
const uri = `${uriBase}_${trackCount++}`;
const pivotValue = iter.get('pivot');
const name = `${titleBase}: ${pivotColName} = ${sqlValueToReadableString(pivotValue)}`;
const schema = {
id: NUM,
ts: LONG,
dur: LONG,
name: STR,
...(colorCol && {color: UNKNOWN}),
};
trace.tracks.registerTrack({
uri,
renderer: SliceTrack.create({
trace,
uri,
dataset: new SourceDataset({
schema,
src: tableName,
filter: {
col: 'pivot',
eq: pivotValue,
},
}),
colorizer: (row) =>
getColorForSlice(sqlValueToReadableString(row.color) ?? row.name),
detailsPanel: (row) => {
return new DebugSliceTrackDetailsPanel(trace, tableName, row.id);
},
}),
});
const trackNode = new TrackNode({uri, name, removable: true});
trace.currentWorkspace.pinnedTracksNode.addChildLast(trackNode);
}
}
function addSingleSliceTrack(
trace: Trace,
tableName: string,
name: string,
uri: string,
argSetIdCol?: string,
colorCol?: string,
) {
const schema = {
id: NUM,
ts: LONG,
dur: LONG,
name: STR,
...(colorCol && {color: UNKNOWN}),
};
trace.tracks.registerTrack({
uri,
renderer: SliceTrack.create({
trace,
uri,
dataset: new SourceDataset({
schema,
src: tableName,
}),
colorizer: (row) =>
getColorForSlice(sqlValueToReadableString(row.color) ?? row.name),
detailsPanel: (row) => {
return new DebugSliceTrackDetailsPanel(
trace,
tableName,
row.id,
argSetIdCol,
);
},
}),
});
const trackNode = new TrackNode({uri, name, removable: true});
trace.currentWorkspace.pinnedTracksNode.addChildLast(trackNode);
}
export interface DebugCounterTrackArgs {
readonly trace: Trace;
readonly data: SqlDataSource;
readonly title?: string;
readonly columns?: Partial<CounterColumnMapping>;
readonly pivotOn?: string;
}
/**
* Adds a new debug counter track to the workspace.
*
* A debug slice track is a track based on a query which is:
* - Based on a query.
* - Automatically added to the top of the current workspace.
* - Pinned.
* - Has a close button to remove it.
*
* @param args - Args to pass to the trace.
* @param args.trace - The trace to use.
* @param args.data.sqlSource - The query to run.
* @param args.data.columns - Optional: Override columns.
* @param args.title - Optional: Title for the track. If pivotOn is supplied,
* this will be used as the root title for each track, but each title will have
* the value appended.
* @param args.columns - Optional: The columns names to use for the various
* essential column names.
* @param args.pivotOn - Optional: The name of a column on which to pivot. If
* provided, we will create N tracks, one for each distinct value of the pivotOn
* column. Each track will only show the slices which have the corresponding
* value in their pivotOn column.
*/
export async function addDebugCounterTrack(args: DebugCounterTrackArgs) {
const tableId = getUniqueTrackCounter();
const tableName = `__debug_track_${tableId}`;
const titleBase = args.title?.trim() || `Debug Slice Track ${tableId}`;
const uriBase = `debug.track${tableId}`;
// Create a table for this query before doing anything
await createTableForCounterTrack(
args.trace.engine,
tableName,
args.data,
args.columns,
args.pivotOn,
);
if (args.pivotOn) {
await addPivotedCounterTracks(
args.trace,
tableName,
titleBase,
uriBase,
args.pivotOn,
);
} else {
addSingleCounterTrack(args.trace, tableName, titleBase, uriBase);
}
}
async function createTableForCounterTrack(
engine: Engine,
tableName: string,
data: SqlDataSource,
columnMapping: Partial<CounterColumnMapping> = {},
pivotCol?: string,
) {
const {ts = 'ts', value = 'value'} = columnMapping;
const cols = [
`${ts} as ts`,
`${value} as value`,
pivotCol && `${pivotCol} as pivot`,
]
.flat() // Convert to flattened list
.filter(Boolean) // Remove falsy values
.join(',');
const query = `
with data as (
${data.sqlSource}
)
select ${cols}
from data
order by ts
`;
return await createPerfettoTable({engine, name: tableName, as: query});
}
async function addPivotedCounterTracks(
trace: Trace,
tableName: string,
titleBase: string,
uriBase: string,
pivotColName: string,
) {
const result = await trace.engine.query(`
SELECT DISTINCT pivot
FROM ${tableName}
ORDER BY pivot
`);
let trackCount = 0;
for (const iter = result.iter({}); iter.valid(); iter.next()) {
const uri = `${uriBase}_${trackCount++}`;
const pivotValue = iter.get('pivot');
const name = `${titleBase}: ${pivotColName} = ${sqlValueToReadableString(pivotValue)}`;
trace.tracks.registerTrack({
uri,
renderer: new SqlTableCounterTrack(
trace,
uri,
`
SELECT *
FROM ${tableName}
WHERE pivot = ${sqlValueToSqliteString(pivotValue)}
`,
),
});
const trackNode = new TrackNode({uri, name, removable: true});
trace.currentWorkspace.pinnedTracksNode.addChildLast(trackNode);
}
}
function addSingleCounterTrack(
trace: Trace,
tableName: string,
name: string,
uri: string,
) {
trace.tracks.registerTrack({
uri,
renderer: new SqlTableCounterTrack(trace, uri, tableName),
});
const trackNode = new TrackNode({uri, name, removable: true});
trace.currentWorkspace.pinnedTracksNode.addChildLast(trackNode);
}