Merge "Normalize timestamp formatting throughout UI"
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
index 71dc5bf..767b078 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
@@ -1 +1 @@
-24a0c7c0bd17908474dda832f51aca06226c44f93043b6b6ff8b698d013818ea
\ No newline at end of file
+b918b41bdb245964e3b3616ef35e29b3deb04e7b256305b28546bde9c7c04d81
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
index 2e2f6fd..e0abae4 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
@@ -1 +1 @@
-01576322a83634df3f5d4fee6c7dac5c3835f926b7de752b99e827e5688fec2d
\ No newline at end of file
+a185bd58eb4cceaad5dd3e8d69d2625ee7ac50953703c9a592925b72c00717e7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256 b/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
index 58c3975..9cb399f 100644
--- a/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
@@ -1 +1 @@
-f618d3e05c466f49a4e130b02b1ab597dc8e0d51285c801fa9af3c6e9df505a5
\ No newline at end of file
+c7fb612c14f41ec904126417c4deceac68d547d1faf674014b645a8812d5f5cc
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
index 9d7b91c..48da846 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
@@ -1 +1 @@
-86d0bc258ef11ba82a78f4b28a1463fb4b732545b95f2ee8e8a4d6271370a030
\ No newline at end of file
+c249df2a8cdf3ced5894a2ddee0860222f46ed7af39c5d626244373e388fd73e
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
index 312b01f..230d996 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
@@ -1 +1 @@
-c8e073f0030da0c6d2dd514eed99a8e0eed79ec75c18e177812de73bf0440cf2
\ No newline at end of file
+17c9c7d4b0018f1843ecbcdb217fd3f822494f56242b5f1b7eb4a6baf05182f4
\ 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 02705ed..bb93f7a 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 @@
-c29b529b5bbd318e0df531f9118a78c8cce94004cc77a887cd394b3093d135c0
\ No newline at end of file
+b3f0d174fea5cb10bf1ba5237fc5128c4166206158cc1f9ddb8c47b66752a16d
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
index 8e93988..ce23972 100644
--- a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
@@ -1 +1 @@
-56ec21cedc480d6b1b2a894bc70411c89424386a7910d62a09a6a5f1a5921c15
\ No newline at end of file
+b90310189240334201a45b3e7131d7c4890ca211709d6998e947cfdd1c3e30a2
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256 b/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
index df63f69..cac11c2 100644
--- a/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
@@ -1 +1 @@
-10931936be5a35ed461fce47ede47225ffc750a85a13462a614366f26160ca11
\ No newline at end of file
+dd7344276b4bbd204f2cd3b1ab351ab924c3c2e7120fb78ff7074d2b6251beb6
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
index 6272735..85172fe 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
@@ -1 +1 @@
-c421aafc09b9ce506fd6eaa1dc358855182cd18704715491f83b7d49828c5826
\ No newline at end of file
+331b08c85045c5642e832933e7acd939bbbcd80a6af433e19959289081b8300d
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
index 8e93988..ce23972 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
@@ -1 +1 @@
-56ec21cedc480d6b1b2a894bc70411c89424386a7910d62a09a6a5f1a5921c15
\ No newline at end of file
+b90310189240334201a45b3e7131d7c4890ca211709d6998e947cfdd1c3e30a2
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
index 6272735..85172fe 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
@@ -1 +1 @@
-c421aafc09b9ce506fd6eaa1dc358855182cd18704715491f83b7d49828c5826
\ No newline at end of file
+331b08c85045c5642e832933e7acd939bbbcd80a6af433e19959289081b8300d
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
index 6272735..85172fe 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
@@ -1 +1 @@
-c421aafc09b9ce506fd6eaa1dc358855182cd18704715491f83b7d49828c5826
\ No newline at end of file
+331b08c85045c5642e832933e7acd939bbbcd80a6af433e19959289081b8300d
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
index cbb5609..9a193b7 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
@@ -1 +1 @@
-7b0fb778fef603ab28c5bc8459db420d2c9d6c6a7a3e5b4e6547b3a2fb067249
\ No newline at end of file
+3b2014254174c0180c91117e9f9c291cad30a7fafe306d76aa016e787af4a43a
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
index 8e93988..ce23972 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
@@ -1 +1 @@
-56ec21cedc480d6b1b2a894bc70411c89424386a7910d62a09a6a5f1a5921c15
\ No newline at end of file
+b90310189240334201a45b3e7131d7c4890ca211709d6998e947cfdd1c3e30a2
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
index 6272735..85172fe 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
@@ -1 +1 @@
-c421aafc09b9ce506fd6eaa1dc358855182cd18704715491f83b7d49828c5826
\ No newline at end of file
+331b08c85045c5642e832933e7acd939bbbcd80a6af433e19959289081b8300d
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
index 6272735..85172fe 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
@@ -1 +1 @@
-c421aafc09b9ce506fd6eaa1dc358855182cd18704715491f83b7d49828c5826
\ No newline at end of file
+331b08c85045c5642e832933e7acd939bbbcd80a6af433e19959289081b8300d
\ No newline at end of file
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index a83a9f6..08c2ac6 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -527,7 +527,7 @@
}
.time-axis-panel {
- height: 12px;
+ height: 22px;
}
.tickbar {
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index 9a1fd7f..70702dc 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -363,17 +363,6 @@
}
}
- button {
- background: #262f3c;
- color: white;
- border-radius: 10px;
- font-size: 10px;
- height: 22px;
- line-height: 18px;
- min-width: 7em;
- margin: auto 0 auto 1rem;
- }
-
input[type="text"] {
flex-grow: 1;
border-radius: 4px;
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 8e5afe6..640b30e 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -42,3 +42,4 @@
@import "widgets/details_shell";
@import "widgets/grid_layout";
@import "widgets/section";
+@import "widgets/timestamp";
diff --git a/ui/src/assets/widgets/button.scss b/ui/src/assets/widgets/button.scss
index 61eeb50..a73c5b2 100644
--- a/ui/src/assets/widgets/button.scss
+++ b/ui/src/assets/widgets/button.scss
@@ -98,7 +98,7 @@
// Reduce padding when we are icon-only
&.pf-icon-only {
- & > i {
+ & > .pf-left-icon {
margin: 0;
}
diff --git a/ui/src/assets/widgets/timestamp.scss b/ui/src/assets/widgets/timestamp.scss
new file mode 100644
index 0000000..f361ff4
--- /dev/null
+++ b/ui/src/assets/widgets/timestamp.scss
@@ -0,0 +1,38 @@
+// 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 "theme";
+
+// Make millis, micros, & nanos slightly smaller than hh:mm:ss for readability.
+$subsec-font-size: 0.9em;
+
+.pf-timecode {
+ // Spacing the sub sections using CSS rather than spaces makes the spaces
+ // disappear when copying.
+ .pf-timecode-millis {
+ margin-left: 1px;
+ font-size: $subsec-font-size;
+ }
+ .pf-timecode-micros {
+ margin-left: 2px;
+ font-size: $subsec-font-size;
+ }
+ .pf-timecode-nanos {
+ margin-left: 2px;
+ font-size: $subsec-font-size;
+ }
+ .pf-button {
+ margin-left: 2px;
+ }
+}
diff --git a/ui/src/base/bigint_math.ts b/ui/src/base/bigint_math.ts
index 45fab7b..1e2897c 100644
--- a/ui/src/base/bigint_math.ts
+++ b/ui/src/base/bigint_math.ts
@@ -102,4 +102,9 @@
static ratio(dividend: bigint, divisor: bigint): number {
return Number(dividend) / Number(divisor);
}
+
+ // Calculates the absolute value of a n.
+ static abs(n: bigint) {
+ return n < 0n ? -1n * n : n;
+ }
}
diff --git a/ui/src/base/bigint_math_unittest.ts b/ui/src/base/bigint_math_unittest.ts
index 33e7179..2aa6b19 100644
--- a/ui/src/base/bigint_math_unittest.ts
+++ b/ui/src/base/bigint_math_unittest.ts
@@ -221,4 +221,21 @@
.toBeCloseTo(0.125, 3);
});
});
+
+ describe('abs', () => {
+ test('should return the absolute value of a positive BigInt', () => {
+ const result = BIM.abs(12345678901234567890n);
+ expect(result).toEqual(12345678901234567890n);
+ });
+
+ test('should return the absolute value of a negative BigInt', () => {
+ const result = BIM.abs(-12345678901234567890n);
+ expect(result).toEqual(12345678901234567890n);
+ });
+
+ test('should return the absolute value of zero', () => {
+ const result = BIM.abs(0n);
+ expect(result).toEqual(0n);
+ });
+ });
});
diff --git a/ui/src/common/time.ts b/ui/src/common/time.ts
index d9157df..b823dbf 100644
--- a/ui/src/common/time.ts
+++ b/ui/src/common/time.ts
@@ -12,13 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {BigintMath} from '../base/bigint_math';
import {assertTrue} from '../base/logging';
import {asTPTimestamp, toTraceTime} from '../frontend/sql_types';
import {ColumnType} from './query_result';
-// TODO(hjd): Combine with timeToCode.
-export function tpTimeToString(time: TPTime) {
+// Print a duration using a handful of 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) {
const sec = tpTimeToSeconds(time);
const units = ['s', 'ms', 'us', 'ns'];
const sign = Math.sign(sec);
@@ -28,65 +35,73 @@
n *= 1000;
u++;
}
- return `${sign < 0 ? '-' : ''}${Math.round(n * 10) / 10} ${units[u]}`;
+ return `${sign < 0 ? '-' : ''}${Math.round(n * 10) / 10}${units[u]}`;
}
-// 1000000023ns -> "1.000 000 023"
-export function formatTPTime(time: TPTime) {
- const strTime = time.toString().padStart(10, '0');
+// 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.
+export class Timecode {
+ public readonly sign: string;
+ public readonly days: string;
+ public readonly hours: string;
+ public readonly minutes: string;
+ public readonly seconds: string;
+ public readonly millis: string;
+ public readonly micros: string;
+ public readonly nanos: string;
- const nanos = strTime.slice(-3);
- const micros = strTime.slice(-6, -3);
- const millis = strTime.slice(-9, -6);
- const seconds = strTime.slice(0, -9);
+ constructor(time: TPTime) {
+ this.sign = time < 0 ? '-' : '';
- return `${seconds}.${millis} ${micros} ${nanos}`;
+ const absTime = BigintMath.abs(time);
+
+ const days = (absTime / 86_400_000_000_000n);
+ const hours = (absTime / 3_600_000_000_000n) % 24n;
+ const minutes = (absTime / 60_000_000_000n) % 60n;
+ const seconds = (absTime / 1_000_000_000n) % 60n;
+ const millis = (absTime / 1_000_000n) % 1_000n;
+ const micros = (absTime / 1_000n) % 1_000n;
+ const nanos = absTime % 1_000n;
+
+ this.days = days.toString();
+ this.hours = hours.toString().padStart(2, '0');
+ this.minutes = minutes.toString().padStart(2, '0');
+ this.seconds = seconds.toString().padStart(2, '0');
+ this.millis = millis.toString().padStart(3, '0');
+ this.micros = micros.toString().padStart(3, '0');
+ this.nanos = nanos.toString().padStart(3, '0');
+ }
+
+ // Get the upper part of the timecode formatted as: [-]DdHH:MM:SS.
+ get dhhmmss(): string {
+ const days = this.days === '0' ? '' : `${this.days}d`;
+ return `${this.sign}${days}${this.hours}:${this.minutes}:${this.seconds}`;
+ }
+
+ // Get the subsecond part of the timecode formatted as: mmm uuu nnn.
+ // The "space" char is configurable but defaults to a normal space.
+ subsec(spaceChar: string = ' '): string {
+ return `${this.millis}${spaceChar}${this.micros}${spaceChar}${this.nanos}`;
+ }
+
+ // Formats the entire timecode to a string.
+ toString(spaceChar: string = ' '): string {
+ return `${this.dhhmmss}.${this.subsec(spaceChar)}`;
+ }
}
-// TODO(hjd): Rename to formatTimestampWithUnits
-// 1000000023ns -> "1s 23ns"
-export function tpTimeToCode(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);
+// Single entry point where timestamps can be converted to the globally
+// configured domain.
+// In the future this will be configurable.
+export function toDomainTime(time: TPTime): TPTime {
+ return toTraceTime(asTPTimestamp(time));
}
export function toNs(seconds: number) {
return Math.round(seconds * 1e9);
}
-// Given an absolute time in TP units, print the time from the start of the
-// trace as a string.
-// Going forward this shall be the universal timestamp printing function
-// superseding all others, with options to customise formatting and the domain.
-// If minimal is true, the time will be printed without any units and in a
-// minimal but still readable format, otherwise the time will be printed with
-// units on each group of digits. Use minimal in places like tables and
-// timelines where there are likely to be multiple timestamps in one place, and
-// use the normal formatting in places that have one-off timestamps.
-export function formatTime(time: TPTime, minimal: boolean = false): string {
- const relTime = toTraceTime(asTPTimestamp(time));
- if (minimal) {
- return formatTPTime(relTime);
- } else {
- return tpTimeToCode(relTime);
- }
-}
-
export function currentDateHourAndMinute(): string {
const date = new Date();
return `${date.toISOString().substr(0, 10)}-${date.getHours()}-${
diff --git a/ui/src/common/time_unittest.ts b/ui/src/common/time_unittest.ts
index 47395ec..fdf22d6 100644
--- a/ui/src/common/time_unittest.ts
+++ b/ui/src/common/time_unittest.ts
@@ -12,71 +12,53 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {globals} from '../frontend/globals';
+import {createEmptyState} from './empty_state';
import {
- formatTPTime,
+ formatDuration,
+ Timecode,
TPTime,
TPTimeSpan,
- tpTimeToCode,
- tpTimeToString,
} from './time';
-test('tpTimeToCode', () => {
- expect(tpTimeToCode(0n)).toEqual('0s');
- expect(tpTimeToCode(3_000_000_000n)).toEqual('3s');
- expect(tpTimeToCode(60_000_000_000n)).toEqual('1m');
- expect(tpTimeToCode(63_000_000_000n)).toEqual('1m 3s');
- expect(tpTimeToCode(63_200_000_000n)).toEqual('1m 3s 200ms');
- expect(tpTimeToCode(63_222_100_000n)).toEqual('1m 3s 222ms 100us');
- expect(tpTimeToCode(63_222_111_100n)).toEqual('1m 3s 222ms 111us 100ns');
- expect(tpTimeToCode(222_111_100n)).toEqual('222ms 111us 100ns');
- expect(tpTimeToCode(1_000n)).toEqual('1us');
- expect(tpTimeToCode(3_000n)).toEqual('3us');
- expect(tpTimeToCode(1_000_001_000n)).toEqual('1s 1us');
- expect(tpTimeToCode(200_000_000_030n)).toEqual('3m 20s 30ns');
- expect(tpTimeToCode(3_600_000_000_000n)).toEqual('60m');
- expect(tpTimeToCode(3_600_000_000_001n)).toEqual('60m 1ns');
- expect(tpTimeToCode(86_400_000_000_000n)).toEqual('1,440m');
- expect(tpTimeToCode(86_400_000_000_001n)).toEqual('1,440m 1ns');
- expect(tpTimeToCode(31_536_000_000_000_000n)).toEqual('525,600m');
- expect(tpTimeToCode(31_536_000_000_000_001n)).toEqual('525,600m 1ns');
+beforeAll(() => {
+ globals.state = createEmptyState();
+ globals.state.traceTime.start = 0n;
});
-test('formatTPTime', () => {
- expect(formatTPTime(0n)).toEqual('0.000 000 000');
- expect(formatTPTime(3_000_000_000n)).toEqual('3.000 000 000');
- expect(formatTPTime(60_000_000_000n)).toEqual('60.000 000 000');
- expect(formatTPTime(63_000_000_000n)).toEqual('63.000 000 000');
- expect(formatTPTime(63_200_000_000n)).toEqual('63.200 000 000');
- expect(formatTPTime(63_222_100_000n)).toEqual('63.222 100 000');
- expect(formatTPTime(63_222_111_100n)).toEqual('63.222 111 100');
- expect(formatTPTime(222_111_100n)).toEqual('0.222 111 100');
- expect(formatTPTime(1_000n)).toEqual('0.000 001 000');
- expect(formatTPTime(3_000n)).toEqual('0.000 003 000');
- expect(formatTPTime(1_000_001_000n)).toEqual('1.000 001 000');
- expect(formatTPTime(200_000_000_030n)).toEqual('200.000 000 030');
- expect(formatTPTime(3_600_000_000_000n)).toEqual('3600.000 000 000');
- expect(formatTPTime(86_400_000_000_000n)).toEqual('86400.000 000 000');
- expect(formatTPTime(86_400_000_000_001n)).toEqual('86400.000 000 001');
- expect(formatTPTime(31_536_000_000_000_000n)).toEqual('31536000.000 000 000');
- expect(formatTPTime(31_536_000_000_000_001n)).toEqual('31536000.000 000 001');
+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(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');
});
-test('tpTimeToString', () => {
- expect(tpTimeToString(0n)).toEqual('0 s');
- expect(tpTimeToString(3_000_000_000n)).toEqual('3 s');
- expect(tpTimeToString(60_000_000_000n)).toEqual('60 s');
- expect(tpTimeToString(63_000_000_000n)).toEqual('63 s');
- expect(tpTimeToString(63_200_000_000n)).toEqual('63.2 s');
- expect(tpTimeToString(63_222_100_000n)).toEqual('63.2 s');
- expect(tpTimeToString(63_222_111_100n)).toEqual('63.2 s');
- expect(tpTimeToString(222_111_100n)).toEqual('222.1 ms');
- expect(tpTimeToString(1_000n)).toEqual('1 us');
- expect(tpTimeToString(3_000n)).toEqual('3 us');
- expect(tpTimeToString(1_000_001_000n)).toEqual('1 s');
- expect(tpTimeToString(200_000_000_030n)).toEqual('200 s');
- expect(tpTimeToString(3_600_000_000_000n)).toEqual('3600 s');
- expect(tpTimeToString(86_400_000_000_000n)).toEqual('86400 s');
- expect(tpTimeToString(31_536_000_000_000_000n)).toEqual('31536000 s');
+test('timecode', () => {
+ expect(new Timecode(0n).toString(' ')).toEqual('00:00:00.000 000 000');
+ expect(new Timecode(123n).toString(' ')).toEqual('00:00:00.000 000 123');
+ expect(new Timecode(60_000_000_000n).toString(' '))
+ .toEqual('00:01:00.000 000 000');
+ expect(new Timecode(12_345_678_910n).toString(' '))
+ .toEqual('00:00:12.345 678 910');
+ expect(new Timecode(86_400_000_000_000n).toString(' '))
+ .toEqual('1d00:00:00.000 000 000');
+ expect(new Timecode(31_536_000_000_000_000n).toString(' '))
+ .toEqual('365d00:00:00.000 000 000');
+ expect(new Timecode(-123n).toString(' ')).toEqual('-00:00:00.000 000 123');
});
function mkSpan(start: TPTime, end: TPTime) {
diff --git a/ui/src/frontend/chrome_slice_panel.ts b/ui/src/frontend/chrome_slice_panel.ts
index 1102482..472ccc2 100644
--- a/ui/src/frontend/chrome_slice_panel.ts
+++ b/ui/src/frontend/chrome_slice_panel.ts
@@ -20,10 +20,10 @@
import {EngineProxy} from '../common/engine';
import {runQuery} from '../common/queries';
import {
+ formatDuration,
TPDuration,
tpDurationToSeconds,
TPTime,
- tpTimeToCode,
} from '../common/time';
import {Argument, convertArgsToTree, Key} from '../controller/args_parser';
@@ -35,6 +35,7 @@
import {asTPTimestamp} from './sql_types';
import {Button} from './widgets/button';
import {DetailsShell} from './widgets/details_shell';
+import {Duration} from './widgets/duration';
import {Column, GridLayout} from './widgets/grid_layout';
import {MenuItem, PopupMenu2} from './widgets/menu';
import {Section} from './widgets/section';
@@ -316,9 +317,13 @@
}
}
-function computeDuration(ts: TPTime, dur: TPDuration): string {
- return dur === -1n ? `${globals.state.traceTime.end - ts} (Did not end)` :
- tpTimeToCode(dur);
+function computeDuration(ts: TPTime, dur: TPDuration): m.Children {
+ if (dur === -1n) {
+ const minDuration = globals.state.traceTime.end - ts;
+ return `${formatDuration(minDuration)} (Did not end)`;
+ } else {
+ return m(Duration, {dur});
+ }
}
export class ChromeSliceDetailsPanel implements m.ClassComponent {
@@ -420,8 +425,10 @@
sliceInfo.threadDur === -1n ? '' : ` (${(ratio * 100).toFixed(2)}%)`;
return m(TreeNode, {
left: 'Thread duration',
- right: computeDuration(sliceInfo.threadTs, sliceInfo.threadDur) +
- threadDurFractionSuffix,
+ right: [
+ computeDuration(sliceInfo.threadTs, sliceInfo.threadDur),
+ threadDurFractionSuffix,
+ ],
});
} else {
return undefined;
@@ -486,7 +493,7 @@
TreeNode,
{left: 'Flow'},
m(TreeNode, {left: 'Slice', right: sliceLink}),
- m(TreeNode, {left: 'Delay', right: tpTimeToCode(dur)}),
+ m(TreeNode, {left: 'Delay', right: m(Duration, {dur})}),
m(TreeNode, {left: 'Thread', right: threadName}),
);
}
diff --git a/ui/src/frontend/counter_panel.ts b/ui/src/frontend/counter_panel.ts
index e1710a8..b9d0979 100644
--- a/ui/src/frontend/counter_panel.ts
+++ b/ui/src/frontend/counter_panel.ts
@@ -14,11 +14,10 @@
import m from 'mithril';
-import {tpTimeToCode} from '../common/time';
-
import {globals} from './globals';
import {asTPTimestamp} from './sql_types';
import {DetailsShell} from './widgets/details_shell';
+import {Duration} from './widgets/duration';
import {GridLayout} from './widgets/grid_layout';
import {Section} from './widgets/section';
import {Timestamp} from './widgets/timestamp';
@@ -56,7 +55,7 @@
}),
m(TreeNode, {
left: 'Duration',
- right: `${tpTimeToCode(counterInfo.duration)}`,
+ right: m(Duration, {dur: counterInfo.duration}),
}),
),
)),
diff --git a/ui/src/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index 0ac5f79..30a1d45 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -28,7 +28,6 @@
FlamegraphStateViewingOption,
ProfileType,
} from '../common/state';
-import {tpTimeToCode} from '../common/time';
import {profileType} from '../controller/flamegraph_controller';
import {Flamegraph, NodeRendering} from './flamegraph';
@@ -40,6 +39,7 @@
import {getCurrentTrace} from './sidebar';
import {convertTraceToPprofAndDownload} from './trace_converter';
import {Button} from './widgets/button';
+import {Duration} from './widgets/duration';
import {findRef} from './widgets/utils';
interface FlamegraphDetailsPanelAttrs {}
@@ -108,7 +108,8 @@
toSelectedCallsite(
flamegraphDetails.expandedCallsite)}`),
m('div.time',
- `Snapshot time: ${tpTimeToCode(flamegraphDetails.dur)}`),
+ `Snapshot time: `,
+ m(Duration, {dur: flamegraphDetails.dur})),
m('input[type=text][placeholder=Focus]', {
oninput: (e: Event) => {
const target = (e.target as HTMLInputElement);
diff --git a/ui/src/frontend/flow_events_panel.ts b/ui/src/frontend/flow_events_panel.ts
index ee2f146..961043c 100644
--- a/ui/src/frontend/flow_events_panel.ts
+++ b/ui/src/frontend/flow_events_panel.ts
@@ -15,11 +15,11 @@
import m from 'mithril';
import {Actions} from '../common/actions';
-import {tpTimeToCode} from '../common/time';
import {Flow, globals} from './globals';
import {BLANK_CHECKBOX, CHECKBOX} from './icons';
import {Panel, PanelSize} from './panel';
+import {Duration} from './widgets/duration';
export const ALL_CATEGORIES = '_all_';
@@ -95,7 +95,7 @@
const data = [
m('td.flow-link', args, outgoing ? 'Outgoing' : 'Incoming'),
- m('td.flow-link', args, tpTimeToCode(flow.dur)),
+ m('td.flow-link', args, m(Duration, {dur: flow.dur})),
m('td.flow-link', args, otherEnd.sliceId.toString()),
m('td.flow-link', args, otherEnd.sliceName),
m('td.flow-link', args, flow.begin.threadName),
diff --git a/ui/src/frontend/ftrace_panel.ts b/ui/src/frontend/ftrace_panel.ts
index 02b3d12..84337a9 100644
--- a/ui/src/frontend/ftrace_panel.ts
+++ b/ui/src/frontend/ftrace_panel.ts
@@ -189,7 +189,7 @@
for (let i = 0; i < events.length; i++) {
const {ts, name, cpu, process, args} = events[i];
- const timestamp = m(Timestamp, {ts: asTPTimestamp(ts), minimal: true});
+ const timestamp = m(Timestamp, {ts: asTPTimestamp(ts)});
const rank = i + offset;
diff --git a/ui/src/frontend/gridline_helper.ts b/ui/src/frontend/gridline_helper.ts
index 6581c81..00045a1 100644
--- a/ui/src/frontend/gridline_helper.ts
+++ b/ui/src/frontend/gridline_helper.ts
@@ -147,7 +147,7 @@
time: TPTime;
}
-const MIN_PX_PER_STEP = 80;
+export const MIN_PX_PER_STEP = 120;
export function getMaxMajorTicks(width: number) {
return Math.max(1, Math.floor(width / MIN_PX_PER_STEP));
}
diff --git a/ui/src/frontend/logs_panel.ts b/ui/src/frontend/logs_panel.ts
index 030763e..eb9a5c2 100644
--- a/ui/src/frontend/logs_panel.ts
+++ b/ui/src/frontend/logs_panel.ts
@@ -156,7 +156,7 @@
'onmouseover': this.onRowOver.bind(this, ts),
'onmouseout': this.onRowOut.bind(this),
},
- m('.cell', m(Timestamp, {ts: asTPTimestamp(ts), minimal: true})),
+ m('.cell', m(Timestamp, {ts: asTPTimestamp(ts)})),
m('.cell', priorityLetter || '?'),
m('.cell', tags[i]),
hasProcessNames ? m('.cell.with-process', processNames[i]) :
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 27d9ec8..d656fa6 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -17,9 +17,6 @@
import {Actions} from '../common/actions';
import {randomColor} from '../common/colorizer';
import {AreaNote, Note} from '../common/state';
-import {
- tpTimeToString,
-} from '../common/time';
import {
BottomTab,
@@ -36,7 +33,11 @@
timeScaleForVisibleWindow,
} from './gridline_helper';
import {Panel, PanelSize} from './panel';
+import {Icons} from './semantic_icons';
import {isTraceLoaded} from './sidebar';
+import {asTPTimestamp} from './sql_types';
+import {Button} from './widgets/button';
+import {Timestamp} from './widgets/timestamp';
const FLAG_WIDTH = 16;
const AREA_TRIANGLE_WIDTH = 10;
@@ -325,12 +326,13 @@
if (note === undefined) {
return m('.', `No Note with id ${this.config.id}`);
}
- const startTime = getStartTimestamp(note) - globals.state.traceTime.start;
+ const startTime = getStartTimestamp(note);
return m(
'.notes-editor-panel',
m('.notes-editor-panel-heading-bar',
m('.notes-editor-panel-heading',
- `Annotation at ${tpTimeToString(startTime)}`),
+ `Annotation at `,
+ m(Timestamp, {ts: asTPTimestamp(startTime)})),
m('input[type=text]', {
onkeydown: (e: Event) => {
e.stopImmediatePropagation();
@@ -354,15 +356,16 @@
}));
},
})),
- m('button',
- {
- onclick: () => {
- globals.dispatch(Actions.removeNote({id: this.config.id}));
- globals.dispatch(Actions.setCurrentTab({tab: undefined}));
- globals.rafScheduler.scheduleFullRedraw();
- },
+ m(Button, {
+ label: 'Remove',
+ icon: Icons.Delete,
+ minimal: true,
+ onclick: () => {
+ globals.dispatch(Actions.removeNote({id: this.config.id}));
+ globals.dispatch(Actions.setCurrentTab({tab: undefined}));
+ globals.rafScheduler.scheduleFullRedraw();
},
- 'Remove')),
+ })),
);
}
}
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index fbc275f..4c3b1db 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -17,8 +17,9 @@
import {hueForCpu} from '../common/colorizer';
import {
Span,
+ Timecode,
+ toDomainTime,
TPTime,
- tpTimeToSeconds,
} from '../common/time';
import {
@@ -32,7 +33,12 @@
import {OuterDragStrategy} from './drag/outer_drag_strategy';
import {DragGestureHandler} from './drag_gesture_handler';
import {globals} from './globals';
-import {getMaxMajorTicks, TickGenerator, TickType} from './gridline_helper';
+import {
+ getMaxMajorTicks,
+ MIN_PX_PER_STEP,
+ TickGenerator,
+ TickType,
+} from './gridline_helper';
import {Panel, PanelSize} from './panel';
import {PxSpan, TimeScale} from './time_scale';
@@ -103,8 +109,9 @@
if (xPos > this.width) break;
if (type === TickType.MAJOR) {
ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
- const sec = tpTimeToSeconds(time - globals.state.traceTime.start);
- ctx.fillText(sec.toFixed(tickGen.digits) + ' s', xPos + 5, 18);
+ const relTime = toDomainTime(time);
+ const timecode = new Timecode(relTime);
+ ctx.fillText(timecode.dhhmmss, xPos + 5, 18, MIN_PX_PER_STEP);
} else if (type == TickType.MEDIUM) {
ctx.fillRect(xPos - 1, 0, 1, 8);
} else if (type == TickType.MINOR) {
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index bf56ad2..e8a8156 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -27,7 +27,6 @@
PivotTableResult,
SortDirection,
} from '../common/state';
-import {tpTimeToCode} from '../common/time';
import {globals} from './globals';
import {Panel} from './panel';
@@ -49,6 +48,7 @@
import {runQueryInNewTab} from './query_result_tab';
import {ReorderableCell, ReorderableCellGroup} from './reorderable_cells';
import {AttributeModalHolder} from './tables/attribute_modal_holder';
+import {Duration} from './widgets/duration';
interface PathItem {
@@ -195,11 +195,11 @@
return m('tr', renderedCells);
}
- renderCell(column: TableColumn, value: ColumnType): string {
+ renderCell(column: TableColumn, value: ColumnType): m.Children {
if (column.kind === 'regular' &&
(column.column === 'dur' || column.column === 'thread_dur')) {
if (typeof value === 'bigint') {
- return tpTimeToCode(value);
+ return m(Duration, {dur: value});
}
}
return `${value}`;
diff --git a/ui/src/frontend/semantic_icons.ts b/ui/src/frontend/semantic_icons.ts
index e30eb99..a80acca 100644
--- a/ui/src/frontend/semantic_icons.ts
+++ b/ui/src/frontend/semantic_icons.ts
@@ -18,4 +18,5 @@
static readonly ChangeViewport = 'query_stats'; // Could be 'search'
static readonly ContextMenu = 'arrow_drop_down'; // Could be 'more_vert'
static readonly Copy = 'content_copy';
+ static readonly Delete = 'delete';
}
diff --git a/ui/src/frontend/slice_details_panel.ts b/ui/src/frontend/slice_details_panel.ts
index eaab6ad..e1adb23 100644
--- a/ui/src/frontend/slice_details_panel.ts
+++ b/ui/src/frontend/slice_details_panel.ts
@@ -16,7 +16,6 @@
import {Actions} from '../common/actions';
import {translateState} from '../common/thread_state';
-import {formatTime, tpTimeToCode} from '../common/time';
import {Anchor} from './anchor';
import {globals, SliceDetails, ThreadDesc} from './globals';
@@ -24,6 +23,7 @@
import {SlicePanel} from './slice_panel';
import {asTPTimestamp} from './sql_types';
import {DetailsShell} from './widgets/details_shell';
+import {Duration} from './widgets/duration';
import {GridLayout} from './widgets/grid_layout';
import {Section} from './widgets/section';
import {SqlRef} from './widgets/sql_ref';
@@ -84,10 +84,13 @@
if (!threadInfo) {
return null;
}
- const timestamp = formatTime(sliceInfo.wakeupTs!);
+ const ts = asTPTimestamp(sliceInfo.wakeupTs!);
return m(
'.slice-details-wakeup-text',
- m('', `Wakeup @ ${timestamp} on CPU ${sliceInfo.wakerCpu} by`),
+ m('',
+ `Wakeup @ `,
+ m(Timestamp, {ts}),
+ ` on CPU ${sliceInfo.wakerCpu} by`),
m('', `P: ${threadInfo.procName} [${threadInfo.pid}]`),
m('', `T: ${threadInfo.threadName} [${threadInfo.tid}]`),
);
@@ -98,10 +101,10 @@
return null;
}
- const latency = tpTimeToCode(sliceInfo.ts - sliceInfo.wakeupTs);
+ const latency = sliceInfo.ts - sliceInfo.wakeupTs;
return m(
'.slice-details-latency-text',
- m('', `Scheduling latency: ${latency}`),
+ m('', `Scheduling latency: `, m(Duration, {dur: latency})),
m('.text-detail',
`This is the interval from when the task became eligible to run
(e.g. because of notifying a wait queue it was suspended on) to
diff --git a/ui/src/frontend/slice_panel.ts b/ui/src/frontend/slice_panel.ts
index 9d6f542..b4adda0 100644
--- a/ui/src/frontend/slice_panel.ts
+++ b/ui/src/frontend/slice_panel.ts
@@ -12,10 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {TPDuration, TPTime, tpTimeToCode} from '../common/time';
+import m from 'mithril';
+
+import {formatDuration, TPDuration, TPTime} from '../common/time';
import {globals, SliceDetails} from './globals';
import {Panel} from './panel';
+import {Duration} from './widgets/duration';
// To display process or thread, we want to concatenate their name with ID, but
// either can be undefined and all the cases need to be considered carefully to
@@ -34,9 +37,13 @@
}
export abstract class SlicePanel extends Panel {
- protected computeDuration(ts: TPTime, dur: TPDuration): string {
- return dur === -1n ? `${globals.state.traceTime.end - ts} (Did not end)` :
- tpTimeToCode(dur);
+ protected computeDuration(ts: TPTime, dur: TPDuration): m.Children {
+ if (dur === -1n) {
+ const minDuration = globals.state.traceTime.end - ts;
+ return `${formatDuration(minDuration)} (Did not end)`;
+ } else {
+ return m(Duration, {dur});
+ }
}
protected getProcessThreadDetails(sliceInfo: SliceDetails) {
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index b086104..c4a7295 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -14,7 +14,7 @@
import m from 'mithril';
-import {TPTime, tpTimeToCode} from '../common/time';
+import {TPTime} from '../common/time';
import {Anchor} from './anchor';
import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from './bottom_tab';
@@ -27,6 +27,7 @@
} from './thread_and_process_info';
import {getThreadState, goToSchedSlice, ThreadState} from './thread_state';
import {DetailsShell} from './widgets/details_shell';
+import {Duration} from './widgets/duration';
import {GridLayout} from './widgets/grid_layout';
import {Section} from './widgets/section';
import {SqlRef} from './widgets/sql_ref';
@@ -100,7 +101,7 @@
}),
m(TreeNode, {
left: 'Duration',
- right: tpTimeToCode(state.dur),
+ right: m(Duration, {dur: state.dur}),
}),
m(TreeNode, {
left: 'State',
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index 4819d54..5d7cf83 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -14,15 +14,13 @@
import m from 'mithril';
-import {
- tpTimeToSeconds,
- tpTimeToString,
-} from '../common/time';
+import {Timecode, toDomainTime, TPTime} from '../common/time';
import {TRACK_SHELL_WIDTH} from './css_constants';
import {globals} from './globals';
import {
getMaxMajorTicks,
+ MIN_PX_PER_STEP,
TickGenerator,
TickType,
timeScaleForVisibleWindow,
@@ -36,11 +34,12 @@
renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
ctx.fillStyle = '#999';
- ctx.font = '10px Roboto Condensed';
ctx.textAlign = 'left';
+ ctx.font = '11px Roboto Condensed';
- const startTime = tpTimeToString(globals.state.traceTime.start);
- ctx.fillText(startTime + ' +', 6, 11);
+ const traceStartTime = globals.state.traceTime.start;
+ const width = renderTimecode(ctx, traceStartTime, 6, 10);
+ ctx.fillText('+', 6 + width, 15, 6);
ctx.save();
ctx.beginPath();
@@ -52,20 +51,39 @@
if (size.width > TRACK_SHELL_WIDTH && span.duration > 0n) {
const maxMajorTicks = getMaxMajorTicks(size.width - TRACK_SHELL_WIDTH);
const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
- const tickGen =
- new TickGenerator(span, maxMajorTicks, globals.state.traceTime.start);
+ const tickGen = new TickGenerator(span, maxMajorTicks, traceStartTime);
for (const {type, time} of tickGen) {
- const position = Math.floor(map.tpTimeToPx(time));
- const sec = tpTimeToSeconds(time - globals.state.traceTime.start);
if (type === TickType.MAJOR) {
+ const position = Math.floor(map.tpTimeToPx(time));
ctx.fillRect(position, 0, 1, size.height);
- ctx.fillText(sec.toFixed(tickGen.digits) + ' s', position + 5, 10);
+ const relTime = toDomainTime(time);
+ renderTimecode(ctx, relTime, position + 5, 10);
}
}
}
-
ctx.restore();
-
ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
}
}
+
+// Print a timecode over 2 lines with this formatting:
+// DdHH:MM:SS
+// mmm uuu nnn
+// Returns the resultant width of the timecode.
+function renderTimecode(
+ ctx: CanvasRenderingContext2D, time: TPTime, x: number, y: number): number {
+ const timecode = new Timecode(time);
+ ctx.font = '11px Roboto Condensed';
+
+ const {dhhmmss} = timecode;
+ const thinSpace = '\u2009';
+ const subsec = timecode.subsec(thinSpace);
+ ctx.fillText(dhhmmss, x, y, MIN_PX_PER_STEP);
+ const {width: firstRowWidth} = ctx.measureText(subsec);
+
+ ctx.font = '10.5px Roboto Condensed';
+ ctx.fillText(subsec, x, y + 10, MIN_PX_PER_STEP);
+ const {width: secondRowWidth} = ctx.measureText(subsec);
+
+ return Math.max(firstRowWidth, secondRowWidth);
+}
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index 5711e6a..0c19fed 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -13,9 +13,9 @@
// limitations under the License.
import m from 'mithril';
-import {BigintMath} from '../base/bigint_math';
-import {Span, tpTimeToString} from '../common/time';
+import {BigintMath} from '../base/bigint_math';
+import {formatDuration, Span, Timecode, toDomainTime} from '../common/time';
import {
TPTime,
TPTimeSpan,
@@ -188,9 +188,8 @@
const {visibleTimeScale} = globals.frontendLocalState;
const xPos =
TRACK_SHELL_WIDTH + Math.floor(visibleTimeScale.tpTimeToPx(ts));
- const offsetTime = tpTimeToString(ts - globals.state.traceTime.start);
- const timeFromStart = tpTimeToString(ts);
- const label = `${offsetTime} (${timeFromStart})`;
+ const domainTime = toDomainTime(ts);
+ const label = new Timecode(domainTime).dhhmmss;
drawIBar(ctx, xPos, this.bounds(size), label);
}
@@ -199,7 +198,7 @@
const {visibleTimeScale} = globals.frontendLocalState;
const xLeft = visibleTimeScale.tpTimeToPx(span.start);
const xRight = visibleTimeScale.tpTimeToPx(span.end);
- const label = tpTimeToString(span.duration);
+ const label = formatDuration(span.duration);
drawHBar(
ctx,
{
diff --git a/ui/src/frontend/widgets/duration.ts b/ui/src/frontend/widgets/duration.ts
index cd18e02..10f61bd 100644
--- a/ui/src/frontend/widgets/duration.ts
+++ b/ui/src/frontend/widgets/duration.ts
@@ -14,7 +14,7 @@
import m from 'mithril';
-import {TPDuration, tpTimeToCode} from '../../common/time';
+import {formatDuration, TPDuration} from '../../common/time';
interface DurationAttrs {
dur: TPDuration;
@@ -22,6 +22,6 @@
export class Duration implements m.ClassComponent<DurationAttrs> {
view(vnode: m.Vnode<DurationAttrs>) {
- return tpTimeToCode(vnode.attrs.dur);
+ return formatDuration(vnode.attrs.dur);
}
}
diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts
index 374f8e3..b945489 100644
--- a/ui/src/frontend/widgets/timestamp.ts
+++ b/ui/src/frontend/widgets/timestamp.ts
@@ -14,37 +14,57 @@
import m from 'mithril';
-import {formatTime} from '../../common/time';
-import {Anchor} from '../anchor';
+import {Timecode, toDomainTime} from '../../common/time';
import {copyToClipboard} from '../clipboard';
import {Icons} from '../semantic_icons';
import {TPTimestamp} from '../sql_types';
+import {Button} from './button';
import {MenuItem, PopupMenu2} from './menu';
+// 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({attrs}: m.Vnode<TimestampAttrs>) {
- const {ts, minimal = false} = attrs;
+ const {ts} = 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());
- },
- }),
+ 'span.pf-timecode',
+ renderTimecode(ts),
+ m(
+ PopupMenu2,
+ {
+ trigger: m(Button, {
+ icon: Icons.ContextMenu,
+ compact: true,
+ minimal: true,
+ }),
+ },
+ m(MenuItem, {
+ icon: Icons.Copy,
+ label: `Copy raw value`,
+ onclick: () => {
+ copyToClipboard(ts.toString());
+ },
+ }),
+ ),
);
}
}
+
+function renderTimecode(ts: TPTimestamp): m.Children {
+ const relTime = toDomainTime(ts);
+ const {dhhmmss, millis, micros, nanos} = new Timecode(relTime);
+ return [
+ m('span.pf-timecode-hms', dhhmmss),
+ '.',
+ m('span.pf-timecode-millis', millis),
+ m('span.pf-timecode-micros', micros),
+ m('span.pf-timecode-nanos', nanos),
+ ];
+}
diff --git a/ui/src/frontend/widgets/utils.ts b/ui/src/frontend/widgets/utils.ts
index 6521162..859a5cb 100644
--- a/ui/src/frontend/widgets/utils.ts
+++ b/ui/src/frontend/widgets/utils.ts
@@ -42,7 +42,7 @@
// Allows doing the following
// exists(val) && m('div', val)
// Even if val is a non-nullish falsey value like 0 or ''
-export function exists<T>(value: T): value is Exclude<T, null|undefined> {
+export function exists<T>(value: T): value is NonNullable<T> {
return value !== undefined && value !== null;
}
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 2b2d03d..c3c9a25 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -24,11 +24,7 @@
import {colorForThread} from '../../common/colorizer';
import {PluginContext} from '../../common/plugin_api';
import {LONG, NUM} from '../../common/query_result';
-import {
- TPDuration,
- TPTime,
- tpTimeToString,
-} from '../../common/time';
+import {formatDuration, TPDuration, TPTime} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {
TrackController,
@@ -366,7 +362,7 @@
latencyWidth >= 20);
// Latency time with a white semi-transparent background.
const latency = tStart - details.wakeupTs;
- const displayText = tpTimeToString(latency);
+ const displayText = formatDuration(latency);
const measured = ctx.measureText(displayText);
if (latencyWidth >= measured.width + 2) {
ctx.fillStyle = 'rgba(255,255,255,0.7)';