Normalize timestamp printing.

Add new function formatTime() for formatting timestamps.
Add dropdown menu to Timestamp component.
Use formatTime() or m(Timestamp, ...) for timestamps in panels.

Change-Id: Ic17d242a32be25f97bfb0094e4374827ce5a3676
diff --git a/ui/src/frontend/chrome_slice_panel.ts b/ui/src/frontend/chrome_slice_panel.ts
index d1dfcc7..93597f0 100644
--- a/ui/src/frontend/chrome_slice_panel.ts
+++ b/ui/src/frontend/chrome_slice_panel.ts
@@ -32,12 +32,14 @@
 import {runQueryInNewTab} from './query_result_tab';
 import {verticalScrollToTrack} from './scroll_helper';
 import {Icons} from './semantic_icons';
+import {asTPTimestamp} from './sql_types';
 import {Button} from './widgets/button';
 import {DetailsShell} from './widgets/details_shell';
 import {Column, GridLayout} from './widgets/grid_layout';
 import {MenuItem, PopupMenu2} from './widgets/menu';
 import {Section} from './widgets/section';
 import {SqlRef} from './widgets/sql_ref';
+import {Timestamp} from './widgets/timestamp';
 import {Tree, TreeNode} from './widgets/tree';
 import {exists} from './widgets/utils';
 
@@ -360,7 +362,7 @@
           }),
           m(TreeNode, {
             left: 'Start time',
-            right: tpTimeToCode(slice.ts - globals.state.traceTime.start),
+            right: m(Timestamp, {ts: asTPTimestamp(slice.ts)}),
           }),
           exists(slice.absTime) &&
               m(TreeNode, {left: 'Absolute Time', right: slice.absTime}),
diff --git a/ui/src/frontend/counter_panel.ts b/ui/src/frontend/counter_panel.ts
index cbeb653..e1710a8 100644
--- a/ui/src/frontend/counter_panel.ts
+++ b/ui/src/frontend/counter_panel.ts
@@ -17,9 +17,11 @@
 import {tpTimeToCode} from '../common/time';
 
 import {globals} from './globals';
+import {asTPTimestamp} from './sql_types';
 import {DetailsShell} from './widgets/details_shell';
 import {GridLayout} from './widgets/grid_layout';
 import {Section} from './widgets/section';
+import {Timestamp} from './widgets/timestamp';
 import {Tree, TreeNode} from './widgets/tree';
 
 export class CounterDetailsPanel implements m.ClassComponent {
@@ -40,10 +42,9 @@
                     m(TreeNode, {left: 'Name', right: `${counterInfo.name}`}),
                     m(TreeNode, {
                       left: 'Start time',
-                      right: `${
-                          tpTimeToCode(
-                              counterInfo.startTime -
-                              globals.state.traceTime.start)}`,
+                      right:
+                          m(Timestamp,
+                            {ts: asTPTimestamp(counterInfo.startTime)}),
                     }),
                     m(TreeNode, {
                       left: 'Value',
diff --git a/ui/src/frontend/ftrace_panel.ts b/ui/src/frontend/ftrace_panel.ts
index fdeef5c..02b3d12 100644
--- a/ui/src/frontend/ftrace_panel.ts
+++ b/ui/src/frontend/ftrace_panel.ts
@@ -18,10 +18,11 @@
 import {assertExists} from '../base/logging';
 import {Actions} from '../common/actions';
 import {colorForString} from '../common/colorizer';
-import {formatTPTime, TPTime} from '../common/time';
+import {TPTime} from '../common/time';
 
 import {globals} from './globals';
 import {Panel} from './panel';
+import {asTPTimestamp} from './sql_types';
 import {DetailsShell} from './widgets/details_shell';
 import {
   MultiSelect,
@@ -29,6 +30,7 @@
   Option as MultiSelectOption,
 } from './widgets/multiselect';
 import {PopupPosition} from './widgets/popup';
+import {Timestamp} from './widgets/timestamp';
 
 const ROW_H = 20;
 const PAGE_SIZE = 250;
@@ -187,7 +189,7 @@
       for (let i = 0; i < events.length; i++) {
         const {ts, name, cpu, process, args} = events[i];
 
-        const timestamp = formatTPTime(ts - globals.state.traceTime.start);
+        const timestamp = m(Timestamp, {ts: asTPTimestamp(ts), minimal: true});
 
         const rank = i + offset;
 
diff --git a/ui/src/frontend/logs_panel.ts b/ui/src/frontend/logs_panel.ts
index 8c50589..030763e 100644
--- a/ui/src/frontend/logs_panel.ts
+++ b/ui/src/frontend/logs_panel.ts
@@ -23,12 +23,14 @@
   LogEntries,
   LogEntriesKey,
 } from '../common/logs';
-import {formatTPTime, TPTime} from '../common/time';
+import {TPTime} from '../common/time';
 
 import {SELECTED_LOG_ROWS_COLOR} from './css_constants';
 import {globals} from './globals';
 import {LOG_PRIORITIES, LogsFilters} from './logs_filters';
 import {Panel} from './panel';
+import {asTPTimestamp} from './sql_types';
+import {Timestamp} from './widgets/timestamp';
 
 const ROW_H = 20;
 
@@ -154,7 +156,7 @@
                 'onmouseover': this.onRowOver.bind(this, ts),
                 'onmouseout': this.onRowOut.bind(this),
               },
-              m('.cell', formatTPTime(ts - globals.state.traceTime.start)),
+              m('.cell', m(Timestamp, {ts: asTPTimestamp(ts), minimal: true})),
               m('.cell', priorityLetter || '?'),
               m('.cell', tags[i]),
               hasProcessNames ? m('.cell.with-process', processNames[i]) :
diff --git a/ui/src/frontend/semantic_icons.ts b/ui/src/frontend/semantic_icons.ts
index 1ee0b61..e30eb99 100644
--- a/ui/src/frontend/semantic_icons.ts
+++ b/ui/src/frontend/semantic_icons.ts
@@ -13,8 +13,9 @@
 // limitations under the License.
 
 export class Icons {
-  static readonly ExternalLink = 'open_in_new';    // Could be undefined
-  static readonly UpdateSelection = 'call_made';   // Could be 'open_in_new'
-  static readonly ChangeViewport = 'query_stats';  // Could be 'search'
-  static readonly ContextMenu = 'arrow_drop_down';
+  static readonly ExternalLink = 'open_in_new';     // Could be undefined
+  static readonly UpdateSelection = 'call_made';    // Could be 'open_in_new'
+  static readonly ChangeViewport = 'query_stats';   // Could be 'search'
+  static readonly ContextMenu = 'arrow_drop_down';  // Could be 'more_vert'
+  static readonly Copy = 'content_copy';
 }
diff --git a/ui/src/frontend/slice_details_panel.ts b/ui/src/frontend/slice_details_panel.ts
index 1b50462..eaab6ad 100644
--- a/ui/src/frontend/slice_details_panel.ts
+++ b/ui/src/frontend/slice_details_panel.ts
@@ -16,16 +16,18 @@
 
 import {Actions} from '../common/actions';
 import {translateState} from '../common/thread_state';
-import {tpTimeToCode} from '../common/time';
+import {formatTime, tpTimeToCode} from '../common/time';
 
 import {Anchor} from './anchor';
 import {globals, SliceDetails, ThreadDesc} from './globals';
 import {scrollToTrackAndTs} from './scroll_helper';
 import {SlicePanel} from './slice_panel';
+import {asTPTimestamp} from './sql_types';
 import {DetailsShell} from './widgets/details_shell';
 import {GridLayout} from './widgets/grid_layout';
 import {Section} from './widgets/section';
 import {SqlRef} from './widgets/sql_ref';
+import {Timestamp} from './widgets/timestamp';
 import {Tree, TreeNode} from './widgets/tree';
 
 export class SliceDetailsPanel extends SlicePanel {
@@ -82,8 +84,7 @@
     if (!threadInfo) {
       return null;
     }
-    const timestamp =
-        tpTimeToCode(sliceInfo.wakeupTs! - globals.state.traceTime.start);
+    const timestamp = formatTime(sliceInfo.wakeupTs!);
     return m(
         '.slice-details-wakeup-text',
         m('', `Wakeup @ ${timestamp} on CPU ${sliceInfo.wakerCpu} by`),
@@ -161,7 +162,7 @@
         }),
         m(TreeNode, {
           left: 'Start time',
-          right: tpTimeToCode(sliceInfo.ts - globals.state.traceTime.start),
+          right: m(Timestamp, {ts: asTPTimestamp(sliceInfo.ts)}),
         }),
         m(TreeNode, {
           left: 'Duration',
diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts
index 0bc4082..1de576c 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -19,12 +19,9 @@
 import {
   TPDuration,
   TPTime,
-  tpTimeToCode,
 } from '../common/time';
 
-import {copyToClipboard} from './clipboard';
 import {globals} from './globals';
-import {menuItem} from './popup_menu';
 import {scrollToTrackAndTs} from './scroll_helper';
 import {
   asUtid,
@@ -37,12 +34,9 @@
   SQLConstraints,
 } from './sql_utils';
 import {
-  getProcessName,
   getThreadInfo,
-  getThreadName,
   ThreadInfo,
 } from './thread_and_process_info';
-import {dict, Dict, maybeValue, Value, value} from './value';
 
 // Representation of a single thread state object, corresponding to
 // a row for the |thread_slice| table.
@@ -152,56 +146,3 @@
   globals.makeSelection(Actions.selectSlice({id, trackId}));
   scrollToTrackAndTs(trackId, ts);
 }
-
-function stateToValue(
-    state: string, cpu: number|undefined, id: SchedSqlId|undefined, ts: TPTime):
-    Value|null {
-  if (!state) {
-    return null;
-  }
-  if (id === undefined || cpu === undefined) {
-    return value(state);
-  }
-  return value(`${state} on CPU ${cpu}`, {
-    rightButton: {
-      action: () => {
-        goToSchedSlice(cpu, id, ts);
-      },
-      hoverText: 'Go to CPU slice',
-    },
-  });
-}
-
-export function threadStateToDict(state: ThreadState): Dict {
-  const result: {[name: string]: Value|null} = {};
-
-  result['Start time'] =
-      value(tpTimeToCode(state.ts - globals.state.traceTime.start));
-  result['Duration'] = value(tpTimeToCode(state.dur));
-  result['State'] =
-      stateToValue(state.state, state.cpu, state.schedSqlId, state.ts);
-  result['Blocked function'] = maybeValue(state.blockedFunction);
-  const process = state?.thread?.process;
-  result['Process'] = maybeValue(process ? getProcessName(process) : undefined);
-  const thread = state?.thread;
-  result['Thread'] = maybeValue(thread ? getThreadName(thread) : undefined);
-  if (state.wakerThread) {
-    const process = state.wakerThread.process;
-    result['Waker'] = dict({
-      'Process': maybeValue(process ? getProcessName(process) : undefined),
-      'Thread': maybeValue(getThreadName(state.wakerThread)),
-    });
-  }
-  result['SQL id'] = value(`thread_state[${state.threadStateSqlId}]`, {
-    contextMenu: [
-      menuItem(
-          'Copy SQL query',
-          () => {
-            copyToClipboard(`select * from thread_state where id=${
-                state.threadStateSqlId}`);
-          }),
-    ],
-  });
-
-  return dict(result);
-}
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index 3547939..b30b540 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -19,7 +19,7 @@
 import {Anchor} from './anchor';
 import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from './bottom_tab';
 import {globals} from './globals';
-import {SchedSqlId, ThreadStateSqlId} from './sql_types';
+import {asTPTimestamp, SchedSqlId, ThreadStateSqlId} from './sql_types';
 import {
   getProcessName,
   getThreadName,
@@ -30,6 +30,7 @@
 import {GridLayout} from './widgets/grid_layout';
 import {Section} from './widgets/section';
 import {SqlRef} from './widgets/sql_ref';
+import {Timestamp} from './widgets/timestamp';
 import {Tree, TreeNode} from './widgets/tree';
 
 interface ThreadStateTabConfig {
@@ -95,7 +96,7 @@
         Tree,
         m(TreeNode, {
           left: 'Start time',
-          right: tpTimeToCode(state.ts - globals.state.traceTime.start),
+          right: m(Timestamp, {ts: asTPTimestamp(state.ts)}),
         }),
         m(TreeNode, {
           left: 'Duration',
diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts
index d8cc841..374f8e3 100644
--- a/ui/src/frontend/widgets/timestamp.ts
+++ b/ui/src/frontend/widgets/timestamp.ts
@@ -14,15 +14,37 @@
 
 import m from 'mithril';
 
-import {tpTimeToCode} from '../../common/time';
-import {toTraceTime, TPTimestamp} from '../sql_types';
+import {formatTime} from '../../common/time';
+import {Anchor} from '../anchor';
+import {copyToClipboard} from '../clipboard';
+import {Icons} from '../semantic_icons';
+import {TPTimestamp} from '../sql_types';
+
+import {MenuItem, PopupMenu2} from './menu';
 
 interface TimestampAttrs {
+  // The timestamp to print, this should be the absolute, raw timestamp as
+  // found in trace processor.
   ts: TPTimestamp;
+  minimal?: boolean;
 }
 
 export class Timestamp implements m.ClassComponent<TimestampAttrs> {
-  view(vnode: m.Vnode<TimestampAttrs>) {
-    return tpTimeToCode(toTraceTime(vnode.attrs.ts));
+  view({attrs}: m.Vnode<TimestampAttrs>) {
+    const {ts, minimal = false} = attrs;
+    return m(
+        PopupMenu2,
+        {
+          trigger:
+              m(Anchor, {icon: Icons.ContextMenu}, formatTime(ts, minimal)),
+        },
+        m(MenuItem, {
+          icon: Icons.Copy,
+          label: 'Copy raw timestamp',
+          onclick: () => {
+            copyToClipboard(ts.toString());
+          },
+        }),
+    );
   }
 }