DebugTrackV2: links to slices.
For the cases when the debug track is generated from the slice or
thread_state table, generate a link go to that slice / thread_state
in the trace.
The implementation looks at the presence of a few key columns (id,
ts, dur, track_id for slice; id, ts, dur, utid for thread_state),
queries these tables, validates the consistency of the data and
shows the link when the data is indeed consistent.
Bug: b/273215738
Change-Id: I50c273dfe68bb99ea3426179e0b979b3e0954beb
diff --git a/include/perfetto/tracing/internal/data_source_type.h b/include/perfetto/tracing/internal/data_source_type.h
index 73c84be..f29798e 100644
--- a/include/perfetto/tracing/internal/data_source_type.h
+++ b/include/perfetto/tracing/internal/data_source_type.h
@@ -182,9 +182,10 @@
// `TracePointTraits` and `trace_point_data` are customization point for
// getting the active instances bitmap.
template <typename TracePointTraits>
- void NextIteration(InstancesIterator* iterator,
- DataSourceThreadLocalState* tls_state,
- typename TracePointTraits::TracePointData trace_point_data) {
+ void NextIteration(
+ InstancesIterator* iterator,
+ DataSourceThreadLocalState* tls_state,
+ typename TracePointTraits::TracePointData trace_point_data) {
iterator->i++;
FirstActiveInstance<TracePointTraits>(iterator, tls_state,
trace_point_data);
diff --git a/ui/src/frontend/sql/slice.ts b/ui/src/frontend/sql/slice.ts
new file mode 100644
index 0000000..4fdc41b
--- /dev/null
+++ b/ui/src/frontend/sql/slice.ts
@@ -0,0 +1,187 @@
+// Copyright (C) 2023 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 {Actions} from '../../common/actions';
+import {EngineProxy} from '../../common/engine';
+import {LONG, NUM, STR} from '../../common/query_result';
+import {TPDuration} from '../../common/time';
+import {Anchor} from '../anchor';
+import {globals} from '../globals';
+import {focusHorizontalRange, verticalScrollToTrack} from '../scroll_helper';
+import {
+ asSliceSqlId,
+ asUpid,
+ asUtid,
+ SliceSqlId,
+ TPTimestamp,
+ Upid,
+ Utid,
+} from '../sql_types';
+import {asTPTimestamp} from '../sql_types';
+import {constraintsToQueryFragment, SQLConstraints} from '../sql_utils';
+import {
+ getProcessInfo,
+ getThreadInfo,
+ ProcessInfo,
+ ThreadInfo,
+} from '../thread_and_process_info';
+
+export interface SliceDetails {
+ id: SliceSqlId;
+ name: string;
+ ts: TPTimestamp;
+ dur: TPDuration;
+ sqlTrackId: number;
+ thread?: ThreadInfo;
+ process?: ProcessInfo;
+}
+
+async function getUtidAndUpid(engine: EngineProxy, sqlTrackId: number):
+ Promise<{utid?: Utid, upid?: Upid}> {
+ const columnInfo = (await engine.query(`
+ WITH
+ leafTrackTable AS (SELECT type FROM track WHERE id = ${sqlTrackId}),
+ cols AS (
+ SELECT name
+ FROM pragma_table_info((SELECT type FROM leafTrackTable))
+ )
+ SELECT
+ type as leafTrackTable,
+ 'upid' in cols AS hasUpid,
+ 'utid' in cols AS hasUtid
+ FROM leafTrackTable
+ `)).firstRow({hasUpid: NUM, hasUtid: NUM, leafTrackTable: STR});
+ const hasUpid = columnInfo.hasUpid !== 0;
+ const hasUtid = columnInfo.hasUtid !== 0;
+
+ const result: {utid?: Utid, upid?: Upid} = {};
+
+ if (hasUtid) {
+ const utid = (await engine.query(`
+ SELECT utid
+ FROM ${columnInfo.leafTrackTable}
+ WHERE id = ${sqlTrackId};
+ `)).firstRow({
+ utid: NUM,
+ }).utid;
+ result.utid = asUtid(utid);
+ } else if (hasUpid) {
+ const upid = (await engine.query(`
+ SELECT upid
+ FROM ${columnInfo.leafTrackTable}
+ WHERE id = ${sqlTrackId};
+ `)).firstRow({
+ upid: NUM,
+ }).upid;
+ result.upid = asUpid(upid);
+ }
+ return result;
+}
+
+async function getSliceFromConstraints(
+ engine: EngineProxy, constraints: SQLConstraints): Promise<SliceDetails[]> {
+ const query = await engine.query(`
+ SELECT
+ id,
+ name,
+ ts,
+ dur,
+ track_id as trackId
+ FROM slice
+ ${constraintsToQueryFragment(constraints)}`);
+ const it = query.iter({
+ id: NUM,
+ name: STR,
+ ts: LONG,
+ dur: LONG,
+ trackId: NUM,
+ });
+
+ const result: SliceDetails[] = [];
+ for (; it.valid(); it.next()) {
+ const {utid, upid} = await getUtidAndUpid(engine, it.trackId);
+
+ const thread: ThreadInfo|undefined =
+ utid === undefined ? undefined : await getThreadInfo(engine, utid);
+ const process: ProcessInfo|undefined = thread !== undefined ?
+ thread.process :
+ (upid === undefined ? undefined : await getProcessInfo(engine, upid));
+
+ result.push({
+ id: asSliceSqlId(it.id),
+ name: it.name,
+ ts: asTPTimestamp(it.ts),
+ dur: it.dur,
+ sqlTrackId: it.trackId,
+ thread,
+ process,
+ });
+ }
+ return result;
+}
+
+export async function getSlice(
+ engine: EngineProxy, id: SliceSqlId): Promise<SliceDetails|undefined> {
+ const result = await getSliceFromConstraints(engine, {
+ filters: [`id=${id}`],
+ });
+ if (result.length > 1) {
+ throw new Error(`slice table has more than one row with id ${id}`);
+ }
+ if (result.length === 0) {
+ return undefined;
+ }
+ return result[0];
+}
+
+interface SliceRefAttrs {
+ readonly id: SliceSqlId;
+ readonly name: string;
+ readonly ts: TPTimestamp;
+ readonly dur: TPDuration;
+ readonly sqlTrackId: number;
+}
+
+export class SliceRef implements m.ClassComponent<SliceRefAttrs> {
+ view(vnode: m.Vnode<SliceRefAttrs>) {
+ return m(
+ Anchor,
+ {
+ icon: 'open_in_new',
+ onclick: () => {
+ const uiTrackId =
+ globals.state.uiTrackIdByTraceTrackId[vnode.attrs.sqlTrackId];
+ if (uiTrackId === undefined) return;
+ verticalScrollToTrack(uiTrackId, true);
+ focusHorizontalRange(
+ vnode.attrs.ts, vnode.attrs.ts + vnode.attrs.dur);
+ globals.makeSelection(Actions.selectChromeSlice(
+ {id: vnode.attrs.id, trackId: uiTrackId, table: 'slice'}));
+ },
+ },
+ vnode.attrs.name);
+ }
+}
+
+export function sliceRef(slice: SliceDetails, name?: string): m.Child {
+ return m(SliceRef, {
+ id: slice.id,
+ name: name ?? slice.name,
+ ts: slice.ts,
+ dur: slice.dur,
+ sqlTrackId: slice.sqlTrackId,
+ });
+}
diff --git a/ui/src/frontend/sql_types.ts b/ui/src/frontend/sql_types.ts
index b7e2df2..fd810bc 100644
--- a/ui/src/frontend/sql_types.ts
+++ b/ui/src/frontend/sql_types.ts
@@ -63,12 +63,35 @@
return v as (Utid | undefined);
}
+// Id into |slice| SQL table.
+export type SliceSqlId = number&{
+ __type: 'SliceSqlId'
+}
+
+export function asSliceSqlId(v: number): SliceSqlId;
+export function asSliceSqlId(v?: number): SliceSqlId|undefined;
+export function asSliceSqlId(v?: number): SliceSqlId|undefined {
+ return v as (SliceSqlId | undefined);
+}
+
// Id into |sched| SQL table.
export type SchedSqlId = number&{
__type: 'SchedSqlId'
}
+export function asSchedSqlId(v: number): SchedSqlId;
+export function asSchedSqlId(v?: number): SchedSqlId|undefined;
+export function asSchedSqlId(v?: number): SchedSqlId|undefined {
+ return v as (SchedSqlId | undefined);
+}
+
// Id into |thread_state| SQL table.
export type ThreadStateSqlId = number&{
__type: 'ThreadStateSqlId'
}
+
+export function asThreadStateSqlId(v: number): ThreadStateSqlId;
+export function asThreadStateSqlId(v?: number): ThreadStateSqlId|undefined;
+export function asThreadStateSqlId(v?: number): ThreadStateSqlId|undefined {
+ return v as (ThreadStateSqlId | undefined);
+}
diff --git a/ui/src/frontend/thread_and_process_info.ts b/ui/src/frontend/thread_and_process_info.ts
index d524e02..46ab39e 100644
--- a/ui/src/frontend/thread_and_process_info.ts
+++ b/ui/src/frontend/thread_and_process_info.ts
@@ -34,7 +34,7 @@
versionCode?: number;
}
-async function getProcessInfo(
+export async function getProcessInfo(
engine: EngineProxy, upid: Upid): Promise<ProcessInfo> {
const it = (await engine.query(`
SELECT pid, name, uid FROM process WHERE upid = ${upid};
diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts
index 77f58c8..a4a71a1 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import m from 'mithril';
+
import {Actions} from '../common/actions';
import {EngineProxy} from '../common/engine';
import {LONG, NUM, NUM_NULL, STR_NULL} from '../common/query_result';
@@ -20,6 +22,7 @@
TPDuration,
TPTime,
} from '../common/time';
+import {Anchor} from './anchor';
import {globals} from './globals';
import {scrollToTrackAndTs} from './scroll_helper';
@@ -27,6 +30,7 @@
asUtid,
SchedSqlId,
ThreadStateSqlId,
+ Utid,
} from './sql_types';
import {
constraintsToQueryFragment,
@@ -146,3 +150,53 @@
globals.makeSelection(Actions.selectSlice({id, trackId}));
scrollToTrackAndTs(trackId, ts);
}
+
+interface ThreadStateRefAttrs {
+ id: ThreadStateSqlId;
+ ts: TPTime;
+ dur: TPDuration;
+ utid: Utid;
+ // If not present, a placeholder name will be used.
+ name?: string;
+}
+
+export class ThreadStateRef implements m.ClassComponent<ThreadStateRefAttrs> {
+ view(vnode: m.Vnode<ThreadStateRefAttrs>) {
+ return m(
+ Anchor,
+ {
+ icon: 'open_in_new',
+ onclick: () => {
+ let trackId: string|number|undefined;
+ for (const track of Object.values(globals.state.tracks)) {
+ if (track.kind === 'ThreadStateTrack' &&
+ (track.config as {utid: number}).utid === vnode.attrs.utid) {
+ trackId = track.id;
+ }
+ }
+
+ if (trackId) {
+ globals.makeSelection(Actions.selectThreadState({
+ id: vnode.attrs.id,
+ trackId: trackId.toString(),
+ }));
+
+ scrollToTrackAndTs(trackId, vnode.attrs.ts, true);
+ }
+ },
+ },
+ vnode.attrs.name ?? `Thread State ${vnode.attrs.id}`,
+ );
+ }
+}
+
+export function threadStateRef(state: ThreadState): m.Child {
+ if (state.thread === undefined) return null;
+
+ return m(ThreadStateRef, {
+ id: state.threadStateSqlId,
+ ts: state.ts,
+ dur: state.dur,
+ utid: state.thread?.utid,
+ });
+}
diff --git a/ui/src/frontend/widgets/tree.ts b/ui/src/frontend/widgets/tree.ts
index 0693829..141fda6 100644
--- a/ui/src/frontend/widgets/tree.ts
+++ b/ui/src/frontend/widgets/tree.ts
@@ -121,8 +121,7 @@
}
}
-// Create a flat tree from a POJO
-export function dictToTree(dict: {[key: string]: m.Child}): m.Children {
+export function dictToTreeNodes(dict: {[key: string]: m.Child}): m.Child[] {
const children: m.Child[] = [];
for (const key of Object.keys(dict)) {
children.push(m(TreeNode, {
@@ -130,7 +129,12 @@
right: dict[key],
}));
}
- return m(Tree, children);
+ return children;
+}
+
+// Create a flat tree from a POJO
+export function dictToTree(dict: {[key: string]: m.Child}): m.Children {
+ return m(Tree, dictToTreeNodes(dict));
}
interface LazyTreeNodeAttrs {
diff --git a/ui/src/tracks/debug/add_debug_track_menu.ts b/ui/src/tracks/debug/add_debug_track_menu.ts
index 32bc871..896c877 100644
--- a/ui/src/tracks/debug/add_debug_track_menu.ts
+++ b/ui/src/tracks/debug/add_debug_track_menu.ts
@@ -36,10 +36,18 @@
export class AddDebugTrackMenu implements
m.ClassComponent<AddDebugTrackMenuAttrs> {
+ readonly columns: string[];
+
name: string = '';
sliceColumns: SliceColumns;
+ arrangeBy?: {
+ type: 'thread'|'process',
+ column: string,
+ };
constructor(vnode: m.Vnode<AddDebugTrackMenuAttrs>) {
+ this.columns = [...vnode.attrs.columns];
+
const chooseDefaultOption = (name: string) => {
for (const column of vnode.attrs.columns) {
if (column === name) return column;
diff --git a/ui/src/tracks/debug/details_tab.ts b/ui/src/tracks/debug/details_tab.ts
index 9012875..9b7868a 100644
--- a/ui/src/tracks/debug/details_tab.ts
+++ b/ui/src/tracks/debug/details_tab.ts
@@ -16,20 +16,43 @@
import {GridLayout} from '../..//frontend/widgets/grid_layout';
import {Section} from '../..//frontend/widgets/section';
-import {ColumnType} from '../../common/query_result';
-import {tpDurationFromSql, tpTimeFromSql} from '../../common/time';
+import {ColumnType, LONG, STR} from '../../common/query_result';
+import {TPDuration, tpDurationFromSql, tpTimeFromSql} from '../../common/time';
import {
BottomTab,
bottomTabRegistry,
NewBottomTabArgs,
} from '../../frontend/bottom_tab';
import {globals} from '../../frontend/globals';
-import {asTPTimestamp} from '../../frontend/sql_types';
+import {
+ getSlice,
+ SliceDetails,
+ sliceRef,
+} from '../../frontend/sql/slice';
+import {
+ asSliceSqlId,
+ asTPTimestamp,
+ TPTimestamp,
+ Utid,
+} from '../../frontend/sql_types';
+import {
+ getProcessName,
+ getThreadName,
+} from '../../frontend/thread_and_process_info';
+import {
+ getThreadState,
+ ThreadState,
+ threadStateRef,
+} from '../../frontend/thread_state';
import {DetailsShell} from '../../frontend/widgets/details_shell';
import {Duration} from '../../frontend/widgets/duration';
import {Timestamp} from '../../frontend/widgets/timestamp';
-import {dictToTree} from '../../frontend/widgets/tree';
-
+import {
+ dictToTree,
+ dictToTreeNodes,
+ Tree,
+ TreeNode,
+} from '../../frontend/widgets/tree';
import {ARG_PREFIX} from './add_debug_track_menu';
interface DebugSliceDetailsTabConfig {
@@ -37,7 +60,7 @@
id: number;
}
-function SqlValueToString(val: ColumnType) {
+function sqlValueToString(val: ColumnType): string {
if (val instanceof Uint8Array) {
return `<blob length=${val.length}>`;
}
@@ -47,45 +70,176 @@
return val.toString();
}
+function sqlValueToNumber(value?: ColumnType): number|undefined {
+ if (typeof value === 'bigint') return Number(value);
+ if (typeof value !== 'number') return undefined;
+ return value;
+}
+
+function sqlValueToUtid(value?: ColumnType): Utid|undefined {
+ if (typeof value === 'bigint') return Number(value) as Utid;
+ if (typeof value !== 'number') return undefined;
+ return value as Utid;
+}
+
+function renderTreeContents(dict: {[key: string]: m.Child}): m.Child[] {
+ const children: m.Child[] = [];
+ for (const key of Object.keys(dict)) {
+ if (dict[key] === null || dict[key] === undefined) continue;
+ children.push(m(TreeNode, {
+ left: key,
+ right: dict[key],
+ }));
+ }
+ return children;
+}
+
export class DebugSliceDetailsTab extends
BottomTab<DebugSliceDetailsTabConfig> {
static readonly kind = 'org.perfetto.DebugSliceDetailsTab';
- data: {[key: string]: ColumnType}|undefined;
+ data?: {
+ name: string,
+ ts: TPTimestamp,
+ dur: TPDuration,
+ args: {[key: string]: ColumnType};
+ };
+ // We will try to interpret the arguments as references into well-known
+ // tables. These values will be set if the relevant columns exist and
+ // are consistent (e.g. 'ts' and 'dur' for this slice correspond to values
+ // in these well-known tables).
+ threadState?: ThreadState;
+ slice?: SliceDetails;
static create(args: NewBottomTabArgs): DebugSliceDetailsTab {
return new DebugSliceDetailsTab(args);
}
+ private async maybeLoadThreadState(
+ id: number|undefined, ts: TPTimestamp, dur: TPDuration,
+ utid?: Utid): Promise<ThreadState|undefined> {
+ if (id === undefined) return undefined;
+ if (utid === undefined) return undefined;
+
+ const threadState = await getThreadState(this.engine, id);
+ if (threadState === undefined) return undefined;
+ if (threadState.ts === ts && threadState.dur === dur &&
+ threadState.thread?.utid === utid) {
+ return threadState;
+ } else {
+ return undefined;
+ }
+ }
+
+ private renderThreadStateInfo(): m.Child {
+ if (this.threadState === undefined) return null;
+ return m(
+ TreeNode,
+ {
+ left: threadStateRef(this.threadState),
+ right: '',
+ },
+ renderTreeContents({
+ 'Thread': getThreadName(this.threadState.thread),
+ 'Process': getProcessName(this.threadState.thread?.process),
+ 'State': this.threadState.state,
+ }));
+ }
+
+ private async maybeLoadSlice(
+ id: number|undefined, ts: TPTimestamp, dur: TPDuration,
+ sqlTrackId?: number): Promise<SliceDetails|undefined> {
+ if (id === undefined) return undefined;
+ if (sqlTrackId === undefined) return undefined;
+
+ const slice = await getSlice(this.engine, asSliceSqlId(id));
+ if (slice === undefined) return undefined;
+ if (slice.ts === ts && slice.dur === dur &&
+ slice.sqlTrackId === sqlTrackId) {
+ return slice;
+ } else {
+ return undefined;
+ }
+ }
+
+ private renderSliceInfo(): m.Child {
+ if (this.slice === undefined) return null;
+ return m(
+ TreeNode,
+ {
+ left: sliceRef(this.slice, 'Slice'),
+ right: '',
+ },
+ renderTreeContents({
+ 'Name': this.slice.name,
+ 'Thread': getThreadName(this.slice.thread),
+ 'Process': getProcessName(this.slice.process),
+ }));
+ }
+
+
+ private async loadData() {
+ const queryResult = await this.engine.query(`select * from ${
+ this.config.sqlTableName} where id = ${this.config.id}`);
+ const row = queryResult.firstRow({
+ ts: LONG,
+ dur: LONG,
+ name: STR,
+ });
+ this.data = {
+ name: row.name,
+ ts: row.ts as TPTimestamp,
+ dur: row.dur,
+ args: {},
+ };
+
+ for (const key of Object.keys(row)) {
+ if (key.startsWith(ARG_PREFIX)) {
+ this.data.args[key.substr(ARG_PREFIX.length)] =
+ (row as {[key: string]: ColumnType})[key];
+ }
+ }
+
+ this.threadState = await this.maybeLoadThreadState(
+ sqlValueToNumber(this.data.args['id']),
+ this.data.ts,
+ this.data.dur,
+ sqlValueToUtid(this.data.args['utid']));
+
+ this.slice = await this.maybeLoadSlice(
+ sqlValueToNumber(this.data.args['id']) ??
+ sqlValueToNumber(this.data.args['slice_id']),
+ this.data.ts,
+ this.data.dur,
+ sqlValueToNumber(this.data.args['track_id']));
+
+ globals.rafScheduler.scheduleRedraw();
+ }
+
constructor(args: NewBottomTabArgs) {
super(args);
-
- this.engine
- .query(`select * from ${this.config.sqlTableName} where id = ${
- this.config.id}`)
- .then((queryResult) => {
- this.data = queryResult.firstRow({});
- globals.rafScheduler.scheduleFullRedraw();
- });
+ this.loadData();
}
viewTab() {
if (this.data === undefined) {
return m('h2', 'Loading');
}
- const left = dictToTree({
+ const details = dictToTreeNodes({
'Name': this.data['name'] as string,
'Start time':
m(Timestamp, {ts: asTPTimestamp(tpTimeFromSql(this.data['ts']))}),
'Duration': m(Duration, {dur: tpDurationFromSql(this.data['dur'])}),
'Debug slice id': `${this.config.sqlTableName}[${this.config.id}]`,
});
+ details.push(this.renderThreadStateInfo());
+ details.push(this.renderSliceInfo());
+
const args: {[key: string]: m.Child} = {};
- for (const key of Object.keys(this.data)) {
- if (key.startsWith(ARG_PREFIX)) {
- args[key.substr(ARG_PREFIX.length)] = SqlValueToString(this.data[key]);
- }
+ for (const key of Object.keys(this.data.args)) {
+ args[key] = sqlValueToString(this.data.args[key]);
}
+
return m(
DetailsShell,
{
@@ -93,7 +247,11 @@
},
m(
GridLayout,
- m(Section, {title: 'Details'}, left),
+ m(
+ Section,
+ {title: 'Details'},
+ m(Tree, details),
+ ),
m(Section, {title: 'Arguments'}, dictToTree(args)),
),
);