perfetto-ui: Go to thread state

There is a button in the details panel of the CPU slice that
will scroll you to the thread and select the running state.

Change-Id: Ia56b0b065f79962977fa6d7c09c8fea90f27ca31
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index 25f7991..85d1eff 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -95,6 +95,7 @@
   visibleTracks = new Set<string>();
   prevVisibleTracks = new Set<string>();
   searchIndex = -1;
+  scrollToTrackId: undefined|string|number = undefined;
   private scrollBarWidth: undefined|number = undefined;
 
   private _omniboxState: OmniboxState = {
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index b2e7f97..032cdb5 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -28,6 +28,7 @@
   dur?: number;
   priority?: number;
   endState?: string;
+  cpu?: number;
   id?: number;
   utid?: number;
   wakeupTs?: number;
diff --git a/ui/src/frontend/slice_panel.ts b/ui/src/frontend/slice_panel.ts
index 80f5c7b..99191fe 100644
--- a/ui/src/frontend/slice_panel.ts
+++ b/ui/src/frontend/slice_panel.ts
@@ -14,6 +14,7 @@
 
 import * as m from 'mithril';
 
+import {Actions} from '../common/actions';
 import {drawDoubleHeadedArrow} from '../common/canvas_utils';
 import {translateState} from '../common/thread_state';
 import {timeToCode} from '../common/time';
@@ -21,37 +22,46 @@
 import {globals} from './globals';
 import {Panel, PanelSize} from './panel';
 
-
 export class SliceDetailsPanel extends Panel {
   view() {
     const sliceInfo = globals.sliceDetails;
-    if (!sliceInfo.utid) return;
+    if (sliceInfo.utid === undefined) return;
     const threadInfo = globals.threads.get(sliceInfo.utid);
 
-    if (threadInfo && sliceInfo.ts && sliceInfo.dur) {
+    if (threadInfo && sliceInfo.ts !== undefined &&
+        sliceInfo.dur !== undefined) {
       return m(
           '.details-panel',
           m('.details-panel-heading', `Slice Details:`),
-          m('.details-table', [m('table', [
-              m('tr', m('th', `PID`), m('td', `${threadInfo.pid}`)),
-              m('tr',
-                m('th', `Process name`),
-                m('td', `${threadInfo.procName}`)),
-              m('tr', m('th', `TID`), m('td', `${threadInfo.tid}`)),
-              m('tr',
-                m('th', `Thread name`),
-                m('td', `${threadInfo.threadName}`)),
-              m('tr',
-                m('th', `Start time`),
-                m('td', `${timeToCode(sliceInfo.ts)}`)),
-              m('tr',
-                m('th', `Duration`),
-                m('td', `${timeToCode(sliceInfo.dur)}`)),
-              m('tr', m('th', `Prio`), m('td', `${sliceInfo.priority}`)),
-              m('tr',
-                m('th', `End State`),
-                m('td', `${translateState(sliceInfo.endState)}`))
-            ])], ));
+          m(
+              '.details-table',
+              [m('table',
+                 [
+                   m('tr',
+                     m('th', `Process`),
+                     m('td', `${threadInfo.procName} [${threadInfo.pid}]`)),
+                   m('tr',
+                     m('th', `Thread`),
+                     m('td',
+                       `${threadInfo.threadName} [${threadInfo.tid}]`,
+                       m('i.material-icons',
+                         {
+                           onclick: () => this.goToThread(),
+                           title: 'Go to thread'
+                         },
+                         'call_made'))),
+                   m('tr',
+                     m('th', `Start time`),
+                     m('td', `${timeToCode(sliceInfo.ts)}`)),
+                   m('tr',
+                     m('th', `Duration`),
+                     m('td', `${timeToCode(sliceInfo.dur)}`)),
+                   m('tr', m('th', `Prio`), m('td', `${sliceInfo.priority}`)),
+                   m('tr',
+                     m('th', `End State`),
+                     m('td', `${translateState(sliceInfo.endState)}`))
+                 ])],
+              ));
     } else {
       return m(
           '.details-panel',
@@ -62,6 +72,41 @@
     }
   }
 
+  goToThread() {
+    const sliceInfo = globals.sliceDetails;
+    if (sliceInfo.utid === undefined) return;
+    const threadInfo = globals.threads.get(sliceInfo.utid);
+
+    if (sliceInfo.id === undefined || sliceInfo.ts === undefined ||
+        sliceInfo.dur === undefined || sliceInfo.cpu === undefined ||
+        threadInfo === undefined) {
+      return;
+    }
+    globals.makeSelection(Actions.selectThreadState({
+      utid: threadInfo.utid,
+      ts: sliceInfo.ts + globals.state.traceTime.startSec,
+      dur: sliceInfo.dur,
+      state: 'Running',
+      cpu: sliceInfo.cpu,
+    }));
+    let trackId: string|number|undefined;
+    let trackGroupId;
+    for (const track of Object.values(globals.state.tracks)) {
+      if (track.kind === 'ThreadStateTrack' &&
+          (track.config as {utid: number}).utid === threadInfo.utid) {
+        trackGroupId = track.trackGroup;
+        trackId = track.id;
+      }
+    }
+    // After the track exists in the dom, it will be scrolled to.
+    globals.frontendLocalState.scrollToTrackId = trackId;
+
+    if (trackGroupId && globals.state.trackGroups[trackGroupId].collapsed) {
+      globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
+    }
+  }
+
+
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
     const details = globals.sliceDetails;
     // Show expanded details on the scheduling of the currently selected slice.
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index d60f4b4..1af0ef0 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -20,6 +20,7 @@
 import {globals} from './globals';
 import {drawGridLines} from './gridline_helper';
 import {Panel, PanelSize} from './panel';
+import {verticalScrollToTrack} from './scroll_helper';
 import {Track} from './track';
 import {TRACK_SHELL_WIDTH} from './track_constants';
 import {trackRegistry} from './track_registry';
@@ -187,6 +188,13 @@
           m(TrackContent, {track: attrs.track})
         ]);
   }
+
+  onupdate({attrs}: m.CVnode<TrackComponentAttrs>) {
+    if (globals.frontendLocalState.scrollToTrackId === attrs.trackState.id) {
+      verticalScrollToTrack(attrs.trackState.id);
+      globals.frontendLocalState.scrollToTrackId = undefined;
+    }
+  }
 }
 
 export interface TrackButtonAttrs {