blob: 7765be4aae046e3a1317112b0b65f06a7b4f44e2 [file]
// 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 m from 'mithril';
import {sqliteString} from '../../../../base/string_utils';
import {Duration, Time} from '../../../../base/time';
import type {Trace} from '../../../../public/trace';
import {type SqlValue, STR} from '../../../../trace_processor/query_result';
import {
asSchedSqlId,
asThreadStateSqlId,
asUpid,
asUtid,
} from '../../../sql_utils/core_types';
import {renderError} from '../../../../widgets/error';
import {DurationWidget} from '../../duration';
import {showProcessDetailsMenuItem} from '../../process';
import {SchedRef} from '../../sched';
import {showThreadDetailsMenuItem} from '../../thread';
import {ThreadStateRef} from '../../thread_state';
import {Timestamp} from '../../timestamp';
import type {
RenderedCell,
TableColumn,
RenderCellContext,
ListColumnsContext,
} from './table_column';
import {
getStandardContextMenuItems,
renderStandardCell,
} from './render_cell_utils';
import {type SqlColumn, sqlColumnId, SqlExpression} from './sql_column';
import {
type PerfettoSqlType,
PerfettoSqlTypes,
} from '../../../../trace_processor/perfetto_sql_type';
import {parseJsonWithBigints} from '../../../../base/json_utils';
import {Anchor} from '../../../../widgets/anchor';
import {MenuItem, PopupMenu} from '../../../../widgets/menu';
import {Icons} from '../../../../base/semantic_icons';
import {copyToClipboard} from '../../../../base/clipboard';
import type {Args} from '../../../sql_utils/args';
import {sqlValueToReadableString} from '../../../../trace_processor/sql_utils';
import type {
SqlTableDefinition,
SqlTableDescription,
} from './table_description';
import {TrackEventRef} from '../../track_event_ref';
// Converts a raw SqlTableDefinition (just data) into a SqlTableDescription
// with fully constructed TableColumn objects that have rendering logic.
export function resolveTableDefinition(
trace: Trace,
def: SqlTableDefinition,
): SqlTableDescription {
return {
imports: def.imports,
prefix: def.prefix,
name: def.name,
displayName: def.displayName,
columns: def.columns.map((col) =>
createTableColumn({
trace,
column: col.column,
type: col.type,
startsHidden: col.startsHidden,
}),
),
};
}
export function createTableColumn(args: {
trace: Trace;
column: SqlColumn;
type?: PerfettoSqlType;
startsHidden?: boolean;
}): TableColumn {
if (args.type?.kind === 'timestamp') {
return new TimestampColumn(args.trace, args.column, {
startsHidden: args.startsHidden,
});
}
if (args.type?.kind === 'duration') {
return new DurationColumn(args.trace, args.column, {
startsHidden: args.startsHidden,
});
}
if (args.type?.kind === 'arg_set_id') {
return new ArgSetIdColumn(args.column, {startsHidden: args.startsHidden});
}
if (args.type?.kind === 'id' || args.type?.kind === 'joinid') {
if (args.type.source.column === 'id') {
switch (args.type.source?.table.toLowerCase()) {
case 'slice':
return sliceIdColumn(args.trace, args.column, {
type: args.type.kind,
startsHidden: args.startsHidden,
});
case 'thread':
return threadIdColumn(args.trace, args.column, {
type: args.type.kind,
startsHidden: args.startsHidden,
});
case 'process':
return processIdColumn(args.trace, args.column, {
type: args.type.kind,
startsHidden: args.startsHidden,
});
case 'thread_state':
return threadStateIdColumn(args.trace, args.column, {
startsHidden: args.startsHidden,
});
case 'sched':
return schedIdColumn(args.trace, args.column, {
startsHidden: args.startsHidden,
});
case 'track':
return trackIdColumn(args.trace, args.column, {
startsHidden: args.startsHidden,
});
}
}
}
return new StandardColumn(args.column, args.type, {
startsHidden: args.startsHidden,
});
}
function wrongTypeError(type: string, name: SqlColumn, value: SqlValue) {
return renderError(
`Wrong type for ${type} column ${sqlColumnId(
name,
)}: bigint expected, ${typeof value} found`,
);
}
export type ColumnParams = {
startsHidden?: boolean;
};
export type IdColumnParams = ColumnParams & {
// Whether this column is a primary key (ID) for this table or whether it's a reference
// to another table's primary key.
type?: 'id' | 'joinid';
// Whether the column is guaranteed not to have null values.
// (this will allow us to upgrage the joins on this column to more performant INNER JOINs).
notNull?: boolean;
};
export class StandardColumn implements TableColumn {
constructor(
public readonly column: SqlColumn,
public readonly type: PerfettoSqlType | undefined,
private params?: ColumnParams,
) {}
renderCell(value: SqlValue, context?: RenderCellContext) {
return renderStandardCell(value, this.column, context);
}
initialColumns(): TableColumn[] {
return this.params?.startsHidden ? [] : [this];
}
}
export class TimestampColumn implements TableColumn {
public readonly type = PerfettoSqlTypes.TIMESTAMP;
constructor(
public readonly trace: Trace,
public readonly column: SqlColumn,
private params?: ColumnParams,
) {}
renderCell(value: SqlValue, context?: RenderCellContext) {
if (typeof value === 'number') {
value = BigInt(Math.round(value));
}
if (typeof value !== 'bigint') {
return renderStandardCell(value, this.column, context);
}
return {
content: m(Timestamp, {
trace: this.trace,
ts: Time.fromRaw(value),
}),
menu: [
context && getStandardContextMenuItems(value, this.column, context),
],
isNumerical: true,
};
}
initialColumns(): TableColumn[] {
return this.params?.startsHidden ? [] : [this];
}
}
export class DurationColumn implements TableColumn {
public readonly type = PerfettoSqlTypes.DURATION;
constructor(
public readonly trace: Trace,
public column: SqlColumn,
private params?: ColumnParams,
) {}
renderCell(value: SqlValue, context?: RenderCellContext) {
if (typeof value === 'number') {
value = BigInt(Math.round(value));
}
if (typeof value !== 'bigint') {
return renderStandardCell(value, this.column, context);
}
return {
content: m(DurationWidget, {
trace: this.trace,
dur: Duration.fromRaw(value),
}),
menu: [
context && getStandardContextMenuItems(value, this.column, context),
],
isNumerical: true,
};
}
initialColumns(): TableColumn[] {
return this.params?.startsHidden ? [] : [this];
}
}
export class IdColumn implements TableColumn {
public readonly type: PerfettoSqlType;
constructor(
public readonly trace: Trace,
public readonly column: SqlColumn,
private readonly args: {
table: {
name: string;
columns: {name: string; type: PerfettoSqlType; showWithId?: boolean}[];
};
render: (id: bigint) => {content: m.Children; menu?: m.Children};
} & IdColumnParams,
) {
this.type = {
kind: args.type === 'id' ? 'id' : 'joinid',
source: {table: args.table.name, column: 'id'},
};
}
renderCell(value: SqlValue, context?: RenderCellContext): RenderedCell {
const id = value;
if (context === undefined || id === null) {
return renderStandardCell(id, this.column, context);
}
if (typeof id !== 'bigint') {
return {content: wrongTypeError('id', this.column, id)};
}
const rendered = this.args.render(id);
return {
content: rendered.content,
menu: [
rendered.menu,
getStandardContextMenuItems(id, this.column, context),
],
isNumerical: true,
};
}
listDerivedColumns() {
if (this.args.type === 'id') return undefined;
return async () => {
const result = new Map<string, TableColumn>();
for (const col of this.args.table.columns) {
result.set(
col.name,
createTableColumn({
trace: this.trace,
column: this.getChildColumn(col.name),
type: col.type,
}),
);
}
return result;
};
}
initialColumns(): TableColumn[] {
if (this.args.startsHidden) return [];
const result: TableColumn[] = [this];
for (const col of this.args.table.columns) {
if (col.showWithId) {
result.push(
createTableColumn({
trace: this.trace,
column: this.getChildColumn(col.name),
type: col.type,
}),
);
}
}
return result;
}
private getChildColumn(name: string): SqlColumn {
return {
column: name,
source: {
table: this.args.table.name,
joinOn: {id: this.column},
innerJoin: this.args.notNull === true,
},
};
}
}
export function sliceIdColumn(
trace: Trace,
column: SqlColumn,
params?: IdColumnParams,
): IdColumn {
return new IdColumn(trace, column, {
table: {
name: 'slice',
columns: [
{name: 'ts', type: PerfettoSqlTypes.TIMESTAMP},
{name: 'dur', type: PerfettoSqlTypes.DURATION},
{name: 'name', type: PerfettoSqlTypes.STRING},
{
name: 'parent_id',
type: {kind: 'joinid', source: {table: 'slice', column: 'id'}},
},
],
},
render: (id) => ({
content: m(TrackEventRef, {
trace,
table: 'slice',
id: Number(id),
name: `${id}`,
switchToCurrentSelectionTab: false,
}),
}),
...params,
});
}
export function schedIdColumn(
trace: Trace,
column: SqlColumn,
params?: IdColumnParams,
): IdColumn {
return new IdColumn(trace, column, {
table: {
name: 'sched',
columns: [
{name: 'ts', type: PerfettoSqlTypes.TIMESTAMP},
{name: 'dur', type: PerfettoSqlTypes.DURATION},
{name: 'cpu', type: PerfettoSqlTypes.INT},
{
name: 'utid',
type: {kind: 'joinid', source: {table: 'thread', column: 'id'}},
},
{name: 'priority', type: PerfettoSqlTypes.INT},
],
},
render: (id) => ({
content: m(SchedRef, {
trace,
id: asSchedSqlId(Number(id)),
name: `${id}`,
switchToCurrentSelectionTab: false,
}),
}),
...params,
});
}
export function threadStateIdColumn(
trace: Trace,
column: SqlColumn,
params?: IdColumnParams,
): IdColumn {
return new IdColumn(trace, column, {
table: {
name: 'thread_state',
columns: [
{name: 'ts', type: PerfettoSqlTypes.TIMESTAMP},
{name: 'dur', type: PerfettoSqlTypes.DURATION},
{name: 'cpu', type: PerfettoSqlTypes.INT},
{
name: 'utid',
type: {kind: 'joinid', source: {table: 'thread', column: 'id'}},
},
{name: 'state', type: PerfettoSqlTypes.STRING},
{name: 'io_wait', type: PerfettoSqlTypes.BOOLEAN},
{name: 'blocked_function', type: PerfettoSqlTypes.STRING},
{
name: 'waker_utid',
type: {kind: 'joinid', source: {table: 'thread', column: 'id'}},
},
{
name: 'waker_id',
type: {kind: 'joinid', source: {table: 'thread_state', column: 'id'}},
},
],
},
render: (id) => ({
content: m(ThreadStateRef, {
trace,
id: asThreadStateSqlId(Number(id)),
name: `${id}`,
switchToCurrentSelectionTab: false,
}),
}),
...params,
});
}
export function threadIdColumn(
trace: Trace,
column: SqlColumn,
params?: IdColumnParams,
): IdColumn {
return new IdColumn(trace, column, {
table: {
name: 'thread',
columns: [
{name: 'tid', type: PerfettoSqlTypes.INT, showWithId: true},
{name: 'name', type: PerfettoSqlTypes.STRING, showWithId: true},
{name: 'start_ts', type: PerfettoSqlTypes.TIMESTAMP},
{name: 'end_ts', type: PerfettoSqlTypes.TIMESTAMP},
{
name: 'upid',
type: {kind: 'joinid', source: {table: 'process', column: 'id'}},
},
{name: 'is_main_thread', type: PerfettoSqlTypes.BOOLEAN},
],
},
render: (id) => ({
content: `${id}`,
menu: showThreadDetailsMenuItem(trace, asUtid(Number(id))),
}),
...params,
});
}
export function processIdColumn(
trace: Trace,
column: SqlColumn,
params?: IdColumnParams,
): IdColumn {
return new IdColumn(trace, column, {
table: {
name: 'process',
columns: [
{name: 'pid', type: PerfettoSqlTypes.INT, showWithId: true},
{name: 'name', type: PerfettoSqlTypes.STRING, showWithId: true},
{name: 'start_ts', type: PerfettoSqlTypes.TIMESTAMP},
{name: 'end_ts', type: PerfettoSqlTypes.TIMESTAMP},
{
name: 'parent_upid',
type: {kind: 'joinid', source: {table: 'process', column: 'id'}},
},
{name: 'is_main_thread', type: PerfettoSqlTypes.BOOLEAN},
],
},
render: (id) => ({
content: `${id}`,
menu: showProcessDetailsMenuItem(trace, asUpid(Number(id))),
}),
...params,
});
}
export function trackIdColumn(
trace: Trace,
column: SqlColumn,
params?: IdColumnParams,
): IdColumn {
return new IdColumn(trace, column, {
table: {
name: 'track',
columns: [
{name: 'name', type: PerfettoSqlTypes.STRING, showWithId: true},
{name: 'type', type: PerfettoSqlTypes.STRING},
{name: 'dimension_arg_set_id', type: PerfettoSqlTypes.ARG_SET_ID},
{
name: 'parent_id',
type: {kind: 'joinid', source: {table: 'track', column: 'id'}},
},
{name: 'source_arg_set_id', type: PerfettoSqlTypes.ARG_SET_ID},
{name: 'machine_id', type: PerfettoSqlTypes.INT},
{name: 'track_group_id', type: PerfettoSqlTypes.INT},
],
},
render: (id) => ({
content: `${id}`,
}),
...params,
});
}
class ArgColumn implements TableColumn {
public readonly column: SqlColumn;
public readonly display: SqlColumn;
public readonly type: PerfettoSqlType | undefined = undefined;
private id: string;
constructor(
private argSetId: SqlColumn,
private key: string,
) {
this.id = `${sqlColumnId(this.argSetId)}[${this.key}]`;
this.column = new SqlExpression(
(cols: string[]) => `COALESCE(${cols[0]}, ${cols[1]}, ${cols[2]})`,
[
this.getRawColumn('string_value'),
this.getRawColumn('int_value'),
this.getRawColumn('real_value'),
],
this.id,
);
this.display = new SqlExpression(
(cols: string[]) => `json_object(
'id', ${cols[0]},
'int_value', ${cols[1]},
'real_value', ${cols[2]},
'string_value', ${cols[3]},
'display_value', ${cols[4]}
)`,
(
[
'id',
'int_value',
'real_value',
'string_value',
'display_value',
] as const
).map((c) => this.getRawColumn(c)),
);
}
private getRawColumn(
type:
| 'string_value'
| 'int_value'
| 'real_value'
| 'id'
| 'type'
| 'display_value',
): SqlColumn {
return {
column: type,
source: {
table: 'args',
joinOn: {
arg_set_id: this.argSetId,
key: `${sqliteString(this.key)}`,
},
},
id: `${this.id}.${type.replace(/_value$/g, '')}`,
};
}
renderCell(value: SqlValue, context?: RenderCellContext): RenderedCell {
if (context === undefined) {
return renderStandardCell(value, this.column, context);
}
if (typeof value !== 'string') {
return {
content: renderError(
`Wrong type: expected string, ${typeof value} found`,
),
};
}
const argValue = parseJsonWithBigints(value);
if (argValue['id'] === null) {
return renderStandardCell(null, this.getRawColumn('id'), context);
}
if (argValue['int_value'] !== null) {
return renderStandardCell(
argValue['int_value'],
this.getRawColumn('int_value'),
context,
);
} else if (argValue['real_value'] !== null) {
return renderStandardCell(
argValue['real_value'],
this.getRawColumn('real_value'),
context,
);
} else {
return renderStandardCell(
argValue['string_value'],
this.getRawColumn('string_value'),
context,
);
}
}
}
export class ArgSetIdColumn implements TableColumn {
public readonly type = PerfettoSqlTypes.ARG_SET_ID;
constructor(
public readonly column: SqlColumn,
private params?: ColumnParams,
) {}
renderCell(value: SqlValue, context: RenderCellContext) {
return renderStandardCell(value, this.column, context);
}
listDerivedColumns(context: ListColumnsContext) {
return async () => {
const queryResult = await context.trace.engine.query(`
SELECT
DISTINCT args.key
FROM (${context.getSqlQuery({arg_set_id: this.column})}) data
JOIN args USING (arg_set_id)
`);
const it = queryResult.iter({key: STR});
const result = new Map();
for (; it.valid(); it.next()) {
result.set(it.key, argTableColumn(this.column, it.key));
}
return result;
};
}
initialColumns() {
return this.params?.startsHidden === true
? []
: [new PrintArgsColumn(this.column), this];
}
}
export function argTableColumn(argSetId: SqlColumn, key: string): TableColumn {
return new ArgColumn(argSetId, key);
}
export class PrintArgsColumn implements TableColumn {
public readonly column: SqlColumn;
public readonly type = undefined;
constructor(public readonly argSetIdColumn: SqlColumn) {
this.column = new SqlExpression(
(cols: string[]) => `__intrinsic_arg_set_to_json(${cols[0]})`,
[argSetIdColumn],
`print_args(${sqlColumnId(argSetIdColumn)})`,
);
}
renderCell(value: SqlValue, context?: RenderCellContext): RenderedCell {
if (value === null) {
return {
content: '{}',
};
}
if (typeof value !== 'string') {
return {
content: renderError(
`Unexpected type: expected string, got ${typeof value}`,
),
};
}
let data: Args;
try {
data = parseJsonWithBigints(value) as Args;
} catch (e) {
return {
content: renderError(`Failed to parse JSON: ${e}`),
};
}
const content: m.Children[] = [];
// Condense single-key nested objects into a flattened key: {a: {b: 1}} becomes {a.b: 1}.
const condense = (key: string, value: Args): {key: string; value: Args} => {
if (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value)
) {
const entries = Object.entries(value);
if (entries.length === 1) {
const [nestedKey, nestedValue] = entries[0];
return condense(`${key}.${nestedKey}`, nestedValue);
}
}
return {key, value};
};
const renderJsonValue = (value: Args, prefix?: string) => {
if (value === null) {
content.push(m('i', 'null'));
} else if (typeof value === 'object' && !Array.isArray(value)) {
content.push('{');
Object.entries(value).forEach(([rawKey, rawVal], idx) => {
if (idx > 0) {
content.push(', ');
}
const {key, value: val} = condense(rawKey, rawVal);
const isLeaf = typeof val !== 'object' || val === null;
const fullKey = prefix === undefined ? key : `${prefix}.${key}`;
const argColumn = argTableColumn(this.argSetIdColumn, fullKey);
const keyElement =
isLeaf && context
? m(
PopupMenu,
{
trigger: m(Anchor, key),
},
m(MenuItem, {
icon: Icons.Add,
label: 'Add column',
disabled: !context.hasColumn(argColumn),
onclick: () => {
context.addColumn(argColumn);
},
}),
m(MenuItem, {
icon: Icons.Copy,
label: 'Copy full key',
onclick: () => {
copyToClipboard(fullKey);
},
}),
)
: key;
content.push(keyElement, ': ');
renderJsonValue(val, fullKey);
});
content.push('}');
} else if (Array.isArray(value)) {
content.push('[');
value.forEach((item, idx) => {
if (idx > 0) {
content.push(', ');
}
renderJsonValue(item, `${prefix}[${idx}]`);
});
content.push(']');
} else if (typeof value === 'boolean') {
content.push(value ? 'true' : 'false');
} else if (typeof value === 'string') {
content.push(`"${value.replace(/"/g, '\\"')}"`);
} else {
content.push(sqlValueToReadableString(value));
}
};
renderJsonValue(data);
return {
content,
};
}
initialColumns() {
return [];
}
}