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)),
             ),
     );