Print DOM durations with absolute precision

Change-Id: I090f2d58e108567dee4b506f067ac0ad4e7ae633
diff --git a/test/data/example_android_trace_30s_thread_track.csv.sha256 b/test/data/example_android_trace_30s_thread_track.csv.sha256
new file mode 100644
index 0000000..7fa93c2
--- /dev/null
+++ b/test/data/example_android_trace_30s_thread_track.csv.sha256
@@ -0,0 +1 @@
+98e888cc31e95cf237b413cf3684833be68285425815e8be51a4b27b40efa36e
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
index eb702e3..39a60c7 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
@@ -1 +1 @@
-8ae7268927f1c102abf7749c41a8512af859bb095ca59cc925d27958929269b9
\ No newline at end of file
+bf757ab18ce503081bec7df2ef8cb5b6ca0e84ea1c7341db0fa8afd6580623ed
\ No newline at end of file
diff --git a/ui/src/common/time.ts b/ui/src/common/time.ts
index b823dbf..939c74f 100644
--- a/ui/src/common/time.ts
+++ b/ui/src/common/time.ts
@@ -18,14 +18,14 @@
 
 import {ColumnType} from './query_result';
 
-// Print a duration using a handful of significant figures.
+// Print time to a few significant figures.
 // Use this when readability is more desireable than precision.
 // Examples: 1234 -> 1.23ns
 //           123456789 -> 123ms
 //           123,123,123,123,123 -> 34h 12m
 //           1,000,000,023 -> 1 s
 //           1,230,000,023 -> 1.2 s
-export function formatDuration(time: TPTime) {
+export function formatDurationShort(time: TPTime) {
   const sec = tpTimeToSeconds(time);
   const units = ['s', 'ms', 'us', 'ns'];
   const sign = Math.sign(sec);
@@ -38,6 +38,28 @@
   return `${sign < 0 ? '-' : ''}${Math.round(n * 10) / 10}${units[u]}`;
 }
 
+// Print time with absolute precision.
+// TODO(stevegolton): Merge this with formatDurationShort
+export function formatDuration(time: TPTime): string {
+  let result = '';
+  if (time < 1) return '0s';
+  const unitAndValue: [string, bigint][] = [
+    ['m', 60000000000n],
+    ['s', 1000000000n],
+    ['ms', 1000000n],
+    ['us', 1000n],
+    ['ns', 1n],
+  ];
+  unitAndValue.forEach(([unit, unitSize]) => {
+    if (time >= unitSize) {
+      const unitCount = time / unitSize;
+      result += unitCount.toLocaleString() + unit + ' ';
+      time %= unitSize;
+    }
+  });
+  return result.slice(0, -1);
+}
+
 // This class takes a time and converts it to a set of strings representing a
 // time code where each string represents a group of time units formatted with
 // an appropriate number of leading zeros.
diff --git a/ui/src/common/time_unittest.ts b/ui/src/common/time_unittest.ts
index fdf22d6..00008d9 100644
--- a/ui/src/common/time_unittest.ts
+++ b/ui/src/common/time_unittest.ts
@@ -16,6 +16,7 @@
 import {createEmptyState} from './empty_state';
 import {
   formatDuration,
+  formatDurationShort,
   Timecode,
   TPTime,
   TPTimeSpan,
@@ -28,23 +29,44 @@
 
 test('formatDuration', () => {
   expect(formatDuration(0n)).toEqual('0s');
-  expect(formatDuration(123n)).toEqual('123ns');
-  expect(formatDuration(1_234n)).toEqual('1.2us');
-  expect(formatDuration(12_345n)).toEqual('12.3us');
   expect(formatDuration(3_000_000_000n)).toEqual('3s');
-  expect(formatDuration(60_000_000_000n)).toEqual('60s');
-  expect(formatDuration(63_000_000_000n)).toEqual('63s');
-  expect(formatDuration(63_200_000_000n)).toEqual('63.2s');
-  expect(formatDuration(63_222_100_000n)).toEqual('63.2s');
-  expect(formatDuration(63_222_111_100n)).toEqual('63.2s');
-  expect(formatDuration(222_111_100n)).toEqual('222.1ms');
+  expect(formatDuration(60_000_000_000n)).toEqual('1m');
+  expect(formatDuration(63_000_000_000n)).toEqual('1m 3s');
+  expect(formatDuration(63_200_000_000n)).toEqual('1m 3s 200ms');
+  expect(formatDuration(63_222_100_000n)).toEqual('1m 3s 222ms 100us');
+  expect(formatDuration(63_222_111_100n)).toEqual('1m 3s 222ms 111us 100ns');
+  expect(formatDuration(222_111_100n)).toEqual('222ms 111us 100ns');
   expect(formatDuration(1_000n)).toEqual('1us');
   expect(formatDuration(3_000n)).toEqual('3us');
-  expect(formatDuration(1_000_001_000n)).toEqual('1s');
-  expect(formatDuration(200_000_000_030n)).toEqual('200s');
-  expect(formatDuration(3_600_000_000_000n)).toEqual('3600s');
-  expect(formatDuration(86_400_000_000_000n)).toEqual('86400s');
-  expect(formatDuration(31_536_000_000_000_000n)).toEqual('31536000s');
+  expect(formatDuration(1_000_001_000n)).toEqual('1s 1us');
+  expect(formatDuration(200_000_000_030n)).toEqual('3m 20s 30ns');
+  expect(formatDuration(3_600_000_000_000n)).toEqual('60m');
+  expect(formatDuration(3_600_000_000_001n)).toEqual('60m 1ns');
+  expect(formatDuration(86_400_000_000_000n)).toEqual('1,440m');
+  expect(formatDuration(86_400_000_000_001n)).toEqual('1,440m 1ns');
+  expect(formatDuration(31_536_000_000_000_000n)).toEqual('525,600m');
+  expect(formatDuration(31_536_000_000_000_001n)).toEqual('525,600m 1ns');
+});
+
+test('formatDurationShort', () => {
+  expect(formatDurationShort(0n)).toEqual('0s');
+  expect(formatDurationShort(123n)).toEqual('123ns');
+  expect(formatDurationShort(1_234n)).toEqual('1.2us');
+  expect(formatDurationShort(12_345n)).toEqual('12.3us');
+  expect(formatDurationShort(3_000_000_000n)).toEqual('3s');
+  expect(formatDurationShort(60_000_000_000n)).toEqual('60s');
+  expect(formatDurationShort(63_000_000_000n)).toEqual('63s');
+  expect(formatDurationShort(63_200_000_000n)).toEqual('63.2s');
+  expect(formatDurationShort(63_222_100_000n)).toEqual('63.2s');
+  expect(formatDurationShort(63_222_111_100n)).toEqual('63.2s');
+  expect(formatDurationShort(222_111_100n)).toEqual('222.1ms');
+  expect(formatDurationShort(1_000n)).toEqual('1us');
+  expect(formatDurationShort(3_000n)).toEqual('3us');
+  expect(formatDurationShort(1_000_001_000n)).toEqual('1s');
+  expect(formatDurationShort(200_000_000_030n)).toEqual('200s');
+  expect(formatDurationShort(3_600_000_000_000n)).toEqual('3600s');
+  expect(formatDurationShort(86_400_000_000_000n)).toEqual('86400s');
+  expect(formatDurationShort(31_536_000_000_000_000n)).toEqual('31536000s');
 });
 
 test('timecode', () => {
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 47ff904..4dd2fdf 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -21,7 +21,6 @@
 import {runQuery} from '../common/queries';
 import {LONG, LONG_NULL, NUM, STR_NULL} from '../common/query_result';
 import {
-  formatDuration,
   TPDuration,
   TPTime,
 } from '../common/time';
@@ -267,7 +266,7 @@
 function computeDuration(ts: TPTime, dur: TPDuration): m.Children {
   if (dur === -1n) {
     const minDuration = globals.state.traceTime.end - ts;
-    return `${formatDuration(minDuration)} (Did not end)`;
+    return [m(Duration, {dur: minDuration}), ' (Did not end)'];
   } else {
     return m(Duration, {dur});
   }
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
index aef1352..e950710 100644
--- a/ui/src/frontend/details_panel.ts
+++ b/ui/src/frontend/details_panel.ts
@@ -33,7 +33,6 @@
   FlowEventsPanel,
 } from './flow_events_panel';
 import {FtracePanel} from './ftrace_panel';
-import {GenericSliceDetailsTabConfig} from './generic_slice_details_tab';
 import {globals} from './globals';
 import {LogPanel} from './logs_panel';
 import {NotesEditorTab} from './notes_panel';
diff --git a/ui/src/frontend/slice_panel.ts b/ui/src/frontend/slice_panel.ts
index b4adda0..de867ee 100644
--- a/ui/src/frontend/slice_panel.ts
+++ b/ui/src/frontend/slice_panel.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 
-import {formatDuration, TPDuration, TPTime} from '../common/time';
+import {TPDuration, TPTime} from '../common/time';
 
 import {globals, SliceDetails} from './globals';
 import {Panel} from './panel';
@@ -40,7 +40,7 @@
   protected computeDuration(ts: TPTime, dur: TPDuration): m.Children {
     if (dur === -1n) {
       const minDuration = globals.state.traceTime.end - ts;
-      return `${formatDuration(minDuration)} (Did not end)`;
+      return [m(Duration, {dur: minDuration}), ' (Did not end)'];
     } else {
       return m(Duration, {dur});
     }
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index 0c19fed..2c8b2f2 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -15,7 +15,12 @@
 import m from 'mithril';
 
 import {BigintMath} from '../base/bigint_math';
-import {formatDuration, Span, Timecode, toDomainTime} from '../common/time';
+import {
+  formatDurationShort,
+  Span,
+  Timecode,
+  toDomainTime,
+} from '../common/time';
 import {
   TPTime,
   TPTimeSpan,
@@ -189,7 +194,8 @@
     const xPos =
         TRACK_SHELL_WIDTH + Math.floor(visibleTimeScale.tpTimeToPx(ts));
     const domainTime = toDomainTime(ts);
-    const label = new Timecode(domainTime).dhhmmss;
+    const thinSpace = '\u2009';
+    const label = new Timecode(domainTime).toString(thinSpace);
     drawIBar(ctx, xPos, this.bounds(size), label);
   }
 
@@ -198,7 +204,7 @@
     const {visibleTimeScale} = globals.frontendLocalState;
     const xLeft = visibleTimeScale.tpTimeToPx(span.start);
     const xRight = visibleTimeScale.tpTimeToPx(span.end);
-    const label = formatDuration(span.duration);
+    const label = formatDurationShort(span.duration);
     drawHBar(
         ctx,
         {
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index c3c9a25..5c1e4ec 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -24,7 +24,7 @@
 import {colorForThread} from '../../common/colorizer';
 import {PluginContext} from '../../common/plugin_api';
 import {LONG, NUM} from '../../common/query_result';
-import {formatDuration, TPDuration, TPTime} from '../../common/time';
+import {formatDurationShort, TPDuration, TPTime} from '../../common/time';
 import {TrackData} from '../../common/track_data';
 import {
   TrackController,
@@ -362,7 +362,7 @@
               latencyWidth >= 20);
           // Latency time with a white semi-transparent background.
           const latency = tStart - details.wakeupTs;
-          const displayText = formatDuration(latency);
+          const displayText = formatDurationShort(latency);
           const measured = ctx.measureText(displayText);
           if (latencyWidth >= measured.width + 2) {
             ctx.fillStyle = 'rgba(255,255,255,0.7)';