Merge "tp: add documentation for most slice tables"
diff --git a/Android.bp b/Android.bp
index 99e833d..344d596 100644
--- a/Android.bp
+++ b/Android.bp
@@ -10360,8 +10360,10 @@
name: "perfetto_src_trace_processor_stdlib_gen_amalgamated_stdlib",
srcs: [
"src/trace_processor/stdlib/android/battery.sql",
+ "src/trace_processor/stdlib/android/battery_stats.sql",
"src/trace_processor/stdlib/android/binder.sql",
"src/trace_processor/stdlib/android/monitor_contention.sql",
+ "src/trace_processor/stdlib/android/network_packets.sql",
"src/trace_processor/stdlib/android/process_metadata.sql",
"src/trace_processor/stdlib/android/slices.sql",
"src/trace_processor/stdlib/android/startup/internal_startups_maxsdk28.sql",
diff --git a/BUILD b/BUILD
index c22f37c..8015a7c 100644
--- a/BUILD
+++ b/BUILD
@@ -2167,8 +2167,10 @@
name = "src_trace_processor_stdlib_android_android",
srcs = [
"src/trace_processor/stdlib/android/battery.sql",
+ "src/trace_processor/stdlib/android/battery_stats.sql",
"src/trace_processor/stdlib/android/binder.sql",
"src/trace_processor/stdlib/android/monitor_contention.sql",
+ "src/trace_processor/stdlib/android/network_packets.sql",
"src/trace_processor/stdlib/android/process_metadata.sql",
"src/trace_processor/stdlib/android/slices.sql",
],
diff --git a/docs/contributing/perfetto-in-the-press.md b/docs/contributing/perfetto-in-the-press.md
index 108849a..4546bd6 100644
--- a/docs/contributing/perfetto-in-the-press.md
+++ b/docs/contributing/perfetto-in-the-press.md
@@ -2,6 +2,8 @@
This a partial collection of the talks, blogposts, presentations, and articles that mention Perfetto.
+- [Google IO 2023 - What's new in Dart and Flutter](https://youtu.be/yRlwOdCK7Ho?t=798)
+- [Google IO 2023 - Debugging Jetpack Compose](https://youtu.be/Kp-aiSU8qCU?t=1092)
- [Performance: Perfetto Traceviewer - MAD Skills](https://www.youtube.com/watch?v=phhLFicMacY)
"On this episode of the MAD Skills series on Performance, Android Performance Engineer Carmen Jackson discusses the Perfetto traceviewer, an alternative to Android Studio for viewing system traces."
- [Performance and optimisation on the Meta Quest Platform](https://m.facebook.com/RealityLabs/videos/performance-and-optimization-on-meta-quest-platform/488126049869673/)
diff --git a/src/profiling/perf/perf_producer.cc b/src/profiling/perf/perf_producer.cc
index 0fb7be3..fec5b6d 100644
--- a/src/profiling/perf/perf_producer.cc
+++ b/src/profiling/perf/perf_producer.cc
@@ -16,8 +16,10 @@
#include "src/profiling/perf/perf_producer.h"
+#include <optional>
#include <random>
#include <utility>
+#include <vector>
#include <unistd.h>
@@ -26,7 +28,9 @@
#include "perfetto/base/logging.h"
#include "perfetto/base/task_runner.h"
+#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/metatrace.h"
+#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/utils.h"
#include "perfetto/ext/base/weak_ptr.h"
#include "perfetto/ext/tracing/core/basic_types.h"
@@ -80,6 +84,35 @@
return static_cast<size_t>(sysconf(_SC_NPROCESSORS_CONF));
}
+std::vector<uint32_t> GetOnlineCpus() {
+ size_t cpu_count = NumberOfCpus();
+ if (cpu_count == 0) {
+ return {};
+ }
+
+ static constexpr char kOnlineValue[] = "1\n";
+ std::vector<uint32_t> online_cpus;
+ online_cpus.reserve(cpu_count);
+ for (uint32_t cpu = 0; cpu < cpu_count; ++cpu) {
+ std::string res;
+ base::StackString<1024> path("/sys/devices/system/cpu/cpu%u/online", cpu);
+ if (!base::ReadFile(path.c_str(), &res)) {
+ // Always consider CPU 0 to be online if the "online" file does not exist
+ // for it. There seem to be several assumptions in the kernel which make
+ // CPU 0 special so this is a pretty safe bet.
+ if (cpu != 0) {
+ return {};
+ }
+ res = kOnlineValue;
+ }
+ if (res != kOnlineValue) {
+ continue;
+ }
+ online_cpus.push_back(cpu);
+ }
+ return online_cpus;
+}
+
int32_t ToBuiltinClock(int32_t clockid) {
switch (clockid) {
case CLOCK_REALTIME:
@@ -394,9 +427,14 @@
return;
}
- size_t num_cpus = NumberOfCpus();
+ std::vector<uint32_t> online_cpus = GetOnlineCpus();
+ if (online_cpus.empty()) {
+ PERFETTO_ELOG("No online CPUs found.");
+ return;
+ }
+
std::vector<EventReader> per_cpu_readers;
- for (uint32_t cpu = 0; cpu < num_cpus; cpu++) {
+ for (uint32_t cpu : online_cpus) {
std::optional<EventReader> event_reader =
EventReader::ConfigureEvents(cpu, event_config.value());
if (!event_reader.has_value()) {
diff --git a/src/trace_processor/importers/proto/proto_trace_parser.cc b/src/trace_processor/importers/proto/proto_trace_parser.cc
index fa1ffd6..b18bdea 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser.cc
@@ -357,7 +357,7 @@
// args in arrays.
std::stable_sort(interned.begin(), interned.end(),
[](const Arg& a, const Arg& b) {
- return a.first.raw_id() < b.second.raw_id();
+ return a.first.raw_id() < b.first.raw_id();
});
// Compute the correct key for each arg, possibly adding an index to
@@ -373,20 +373,16 @@
inserter->AddArg(key, Variadic::String(it->second));
} else {
constexpr size_t kMaxIndexSize = 20;
- base::StringView key_str = context_->storage->GetString(key);
+ NullTermStringView key_str = context_->storage->GetString(key);
if (key_str.size() >= sizeof(buffer) - kMaxIndexSize) {
PERFETTO_DLOG("Ignoring arg with unreasonbly large size");
continue;
}
- base::StringWriter writer(buffer, sizeof(buffer));
- writer.AppendString(key_str);
- writer.AppendChar('[');
- writer.AppendUnsignedInt(current_idx);
- writer.AppendChar(']');
-
+ base::StackString<2048> array_key("%s[%u]", key_str.c_str(),
+ current_idx);
StringId new_key =
- context_->storage->InternString(writer.GetStringView());
+ context_->storage->InternString(array_key.string_view());
inserter->AddArg(key, new_key, Variadic::String(it->second));
current_idx = key == next_key ? current_idx + 1 : 0;
diff --git a/src/trace_processor/stdlib/android/BUILD.gn b/src/trace_processor/stdlib/android/BUILD.gn
index 86bdf6d..693cabc 100644
--- a/src/trace_processor/stdlib/android/BUILD.gn
+++ b/src/trace_processor/stdlib/android/BUILD.gn
@@ -18,8 +18,10 @@
deps = [ "startup" ]
sources = [
"battery.sql",
+ "battery_stats.sql",
"binder.sql",
"monitor_contention.sql",
+ "network_packets.sql",
"process_metadata.sql",
"slices.sql",
]
diff --git a/src/trace_processor/stdlib/android/battery_stats.sql b/src/trace_processor/stdlib/android/battery_stats.sql
new file mode 100644
index 0000000..753fd61
--- /dev/null
+++ b/src/trace_processor/stdlib/android/battery_stats.sql
@@ -0,0 +1,210 @@
+--
+-- Copyright 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
+--
+-- https://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.
+
+SELECT IMPORT('common.timestamps');
+
+-- Converts a battery_stats counter value to human readable string.
+--
+-- @arg track STRING The counter track name (e.g. 'battery_stats.audio').
+-- @arg value LONG The counter value.
+-- @ret STRING The human-readable name for the counter value.
+SELECT CREATE_FUNCTION(
+ 'BATTERY_STATS_COUNTER_TO_STRING(track STRING, value LONG)',
+ 'STRING',
+ '
+ SELECT
+ CASE
+ WHEN ($track = "battery_stats.wifi_scan" OR
+ $track = "battery_stats.wifi_radio" OR
+ $track = "battery_stats.mobile_radio" OR
+ $track = "battery_stats.audio" OR
+ $track = "battery_stats.video" OR
+ $track = "battery_stats.camera" OR
+ $track = "battery_stats.power_save" OR
+ $track = "battery_stats.phone_in_call")
+ THEN
+ CASE $value
+ WHEN 0 THEN "inactive"
+ WHEN 1 THEN "active"
+ ELSE "unknown"
+ END
+ WHEN $track = "battery_stats.wifi"
+ THEN
+ CASE $value
+ WHEN 0 THEN "off"
+ WHEN 1 THEN "on"
+ ELSE "unknown"
+ END
+ WHEN $track = "battery_stats.phone_state"
+ THEN
+ CASE $value
+ WHEN 0 THEN "in"
+ WHEN 1 THEN "out"
+ WHEN 2 THEN "emergency"
+ WHEN 3 THEN "off"
+ ELSE "unknown"
+ END
+ WHEN ($track = "battery_stats.phone_signal_strength" OR
+ $track = "battery_stats.wifi_signal_strength")
+ THEN
+ CASE $value
+ WHEN 0 THEN "none"
+ WHEN 1 THEN "poor"
+ WHEN 2 THEN "moderate"
+ WHEN 3 THEN "good"
+ WHEN 4 THEN "great"
+ ELSE "unknown"
+ END
+ WHEN $track = "battery_stats.wifi_suppl"
+ THEN
+ CASE $value
+ WHEN 0 THEN "invalid"
+ WHEN 1 THEN "disconn"
+ WHEN 2 THEN "disabled"
+ WHEN 3 THEN "inactive"
+ WHEN 4 THEN "scanning"
+ WHEN 5 THEN "authenticating"
+ WHEN 6 THEN "associating"
+ WHEN 7 THEN "associated"
+ WHEN 8 THEN "4-way-handshake"
+ WHEN 9 THEN "group-handshake"
+ WHEN 10 THEN "completed"
+ WHEN 11 THEN "dormant"
+ WHEN 12 THEN "uninit"
+ ELSE "unknown"
+ END
+ WHEN $track = "battery_stats.data_conn"
+ THEN
+ CASE $value
+ WHEN 0 THEN "oos"
+ WHEN 1 THEN "gprs"
+ WHEN 2 THEN "edge"
+ WHEN 3 THEN "umts"
+ WHEN 4 THEN "cdma"
+ WHEN 5 THEN "evdo_0"
+ WHEN 6 THEN "evdo_A"
+ WHEN 7 THEN "1xrtt"
+ WHEN 8 THEN "hsdpa"
+ WHEN 9 THEN "hsupa"
+ WHEN 10 THEN "hspa"
+ WHEN 11 THEN "iden"
+ WHEN 12 THEN "evdo_b"
+ WHEN 13 THEN "lte"
+ WHEN 14 THEN "ehrpd"
+ WHEN 15 THEN "hspap"
+ WHEN 16 THEN "gsm"
+ WHEN 17 THEN "td_scdma"
+ WHEN 18 THEN "iwlan"
+ WHEN 19 THEN "lte_ca"
+ WHEN 20 THEN "nr"
+ WHEN 21 THEN "emngcy"
+ WHEN 22 THEN "other"
+ ELSE "unknown"
+ END
+ ELSE CAST($value AS text)
+ END
+ '
+);
+
+
+-- View of human readable battery stats counter-based states. These are recorded
+-- by BatteryStats as a bitmap where each 'category' has a unique value at any
+-- given time.
+--
+-- @column ts Timestamp in nanoseconds.
+-- @column dur The duration the state was active.
+-- @column name The name of the counter track.
+-- @column value The counter value as a number.
+-- @column value_name The counter value as a human-readable string.
+CREATE VIEW android_battery_stats_state AS
+SELECT
+ ts,
+ name,
+ value,
+ BATTERY_STATS_VALUE_TO_STRING(name, value) AS value_name,
+ LEAD(ts, 1, TRACE_END()) OVER (PARTITION BY track_id ORDER BY ts) - ts AS dur
+FROM counter
+JOIN counter_track
+ ON counter.track_id = counter_track.id
+WHERE counter_track.name GLOB 'battery_stats.*';
+
+
+-- View of slices derived from battery_stats events. Battery stats records all
+-- events as instants, however some may indicate whether something started or
+-- stopped with a '+' or '-' prefix. Events such as jobs, top apps, foreground
+-- apps or long wakes include these details and allow drawing slices between
+-- instant events found in a trace.
+--
+-- For example, we may see an event like the following on 'battery_stats.top':
+--
+-- -top=10215:"com.google.android.apps.nexuslauncher"
+--
+-- This view will find the associated start ('+top') with the matching suffix
+-- (everything after the '=') to construct a slice. It computes the timestamp
+-- and duration from the events and extract the details as follows:
+--
+-- track_name='battery_stats.top'
+-- str_value='com.google.android.apps.nexuslauncher'
+-- int_value=10215
+--
+-- @column track_name The battery stats track name.
+-- @column ts Timestamp in nanoseconds.
+-- @column dur The duration of the event.
+-- @column str_value The string part of the event identifier.
+-- @column int_value The integer part of the event identifier.
+CREATE VIEW android_battery_stats_event_slices AS
+WITH
+ event_markers AS (
+ SELECT
+ ts,
+ track.name AS track_name,
+ str_split(slice.name, '=', 1) AS key,
+ substr(slice.name, 1, 1) = '+' AS start
+ FROM slice
+ JOIN track
+ ON slice.track_id = track.id
+ WHERE
+ track_name GLOB 'battery_stats.*'
+ AND substr(slice.name, 1, 1) IN ('+', '-')
+ ),
+ with_neighbors AS (
+ SELECT
+ *,
+ LAG(ts) OVER (PARTITION BY track_name, key ORDER BY ts) AS last_ts,
+ LEAD(ts) OVER (PARTITION BY track_name, key ORDER BY ts) AS next_ts
+ FROM event_markers
+ ),
+ -- Note: query performance depends on the ability to push down filters on
+ -- the track_name. It would be more clear below to have two queries and union
+ -- them, but doing so prevents push down through the above window functions.
+ event_spans AS (
+ SELECT
+ track_name, key,
+ IIF(start, ts, TRACE_START()) AS ts,
+ IIF(start, next_ts, ts) AS end_ts
+ FROM with_neighbors
+ -- For the majority of events, we take the `start` event and compute the dur
+ -- based on next_ts. In the off chance we get an end event with no prior
+ -- start (matched by the second half of this where), we can create an event
+ -- starting from the beginning of the trace ending at the current event.
+ WHERE (start OR last_ts IS NULL)
+ )
+SELECT
+ ts,
+ IFNULL(end_ts-ts, -1) AS dur,
+ track_name,
+ str_split(key, '"', 1) AS str_value,
+ CAST(str_split(key, ':', 0) AS INT64) AS int_value
+FROM event_spans;
diff --git a/src/trace_processor/stdlib/android/network_packets.sql b/src/trace_processor/stdlib/android/network_packets.sql
new file mode 100644
index 0000000..a88615a
--- /dev/null
+++ b/src/trace_processor/stdlib/android/network_packets.sql
@@ -0,0 +1,52 @@
+--
+-- Copyright 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
+--
+-- https://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.
+
+-- Android network packet events (from android.network_packets data source).
+--
+-- @column ts Timestamp in nanoseconds.
+-- @column dur Duration (non-zero only in aggregate events)
+-- @column track_name The track name (interface and direction)
+-- @column package_name Traffic package source (or uid=$X if not found)
+-- @column iface Traffic interface name (linux interface name)
+-- @column direction Traffic direction ('Transmitted' or 'Received')
+-- @column packet_count Number of packets in this event
+-- @column packet_length Number of bytes in this event (wire size)
+-- @column packet_transport Transport used for traffic in this event
+-- @column packet_tcp_flags TCP flags used by tcp frames in this event
+-- @column socket_tag The Android traffic tag of the network socket
+-- @column socket_uid The Linux user id of the network socket
+-- @column local_port The local port number (for udp or tcp only)
+-- @column remote_port The remote port number (for udp or tcp only)
+CREATE VIEW android_network_packets AS
+SELECT
+ ts,
+ dur,
+ track.name AS track_name,
+ slice.name AS package_name,
+ str_split(track.name, ' ', 0) AS iface,
+ str_split(track.name, ' ', 1) AS direction,
+ ifnull(extract_arg(arg_set_id, 'packet_count'), 1) AS packet_count,
+ extract_arg(arg_set_id, 'packet_length') AS packet_length,
+ extract_arg(arg_set_id, 'packet_transport') AS packet_transport,
+ extract_arg(arg_set_id, 'packet_tcp_flags') AS packet_tcp_flags,
+ extract_arg(arg_set_id, 'socket_tag') AS socket_tag,
+ extract_arg(arg_set_id, 'socket_uid') AS socket_uid,
+ extract_arg(arg_set_id, 'local_port') AS local_port,
+ extract_arg(arg_set_id, 'remote_port') AS remote_port
+FROM slice
+JOIN track
+ ON slice.track_id = track.id
+WHERE (track.name GLOB '* Transmitted' OR
+ track.name GLOB '* Received');
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 b78630c..adfa483 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 @@
-59f5feeab8b216aab64795b4fb620d9386d45769e46cd3b7df12e0b8c85dca9a
\ No newline at end of file
+37ce86e92a72fe05c24acfcadf1393f3407a99e3dcdda6b3c883f380ffd00b41
\ 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 39ac1e4..ba42f34 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 @@
-f146aec8fe3dff85764c2760a8726e38f085006c1a217aca89b0a847390684cf
\ No newline at end of file
+0c7f3705f1d29c0b16756b66cb748635352fef3928a173b34d995356e6d3d58e
\ 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 cf2b69d..4c65000 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 @@
-a238887636bc86b86e179fc5bc11024cd2b5151797365146b4f94c6de6994280
\ No newline at end of file
+c8277a777519e7c1bf762d4e0e299a79a66da88ce54ed26e6d27939dca6128c9
\ 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 29aeeb0..f504faf 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 @@
-40026a723cf944adb25668cc6618513271aff14cd7271804b4e208bf6a9817d8
\ No newline at end of file
+dc246b0bf68834a63464c0c98e35a060cb3698947084cdb9d976207a37bf8156
\ 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 4af29fe..0ee6b17 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 @@
-81154f3a88aee01576eba17432269818f1431166cb6071256ee1c35e14851271
\ No newline at end of file
+e79c53f02bed0a629c76fd3d044492dc8e619fe9f58c597db5f4417cbe91f805
\ 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 2a17dfb..0e50d9b 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 @@
-3ae58540e40e9597d92aa15735e253d14ed9fc87753e543695226fce51481004
\ No newline at end of file
+cc543d6db95f56863f5d6c90dbbb8b07e8dff539534c53d595b1faee807903da
\ 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 d26043d..b3b1619 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 @@
-ef5dfa6588634af3b6ac4950d751f7f8b73eeaae7f74c9e45d65ede4134451fe
\ No newline at end of file
+ae78e1f372a6a052a650974464f2e706ab8cb665c639ca4b0b15b48dce95d185
\ 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 ad17707..9287876 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 @@
-a16bc9d707e713ef1ce9110a74790bbbeaf27e1fc7f798152c18e9c81bcedbbf
\ No newline at end of file
+30faf223bfc57acbbcca2308c716064d3447dee2673db6329ad9188bf08a59e6
\ 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 ad17707..9287876 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 @@
-a16bc9d707e713ef1ce9110a74790bbbeaf27e1fc7f798152c18e9c81bcedbbf
\ No newline at end of file
+30faf223bfc57acbbcca2308c716064d3447dee2673db6329ad9188bf08a59e6
\ 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 796c14f..9287876 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 @@
-7c2317088fd3ada6af276ab517e67163b091f7e9bd98e443794397fe58b61ae3
\ No newline at end of file
+30faf223bfc57acbbcca2308c716064d3447dee2673db6329ad9188bf08a59e6
\ 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 060bb57..535b9b1 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 @@
-da73a7a9d403491d5de58ae013e8b66337c9bf48422ae2ce212ab91bc33a6d5c
\ No newline at end of file
+b0d5928fbcf0b7adfc06386ea1961d301ee19f66c61b190f9d00fd5acef824c0
\ 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 ad17707..9287876 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 @@
-a16bc9d707e713ef1ce9110a74790bbbeaf27e1fc7f798152c18e9c81bcedbbf
\ No newline at end of file
+30faf223bfc57acbbcca2308c716064d3447dee2673db6329ad9188bf08a59e6
\ 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 ad17707..9287876 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 @@
-a16bc9d707e713ef1ce9110a74790bbbeaf27e1fc7f798152c18e9c81bcedbbf
\ No newline at end of file
+30faf223bfc57acbbcca2308c716064d3447dee2673db6329ad9188bf08a59e6
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/android/android_battery_stats_event_slices.out b/test/trace_processor/diff_tests/android/android_battery_stats_event_slices.out
new file mode 100644
index 0000000..82cd36c
--- /dev/null
+++ b/test/trace_processor/diff_tests/android/android_battery_stats_event_slices.out
@@ -0,0 +1,4 @@
+"ts","dur","track_name","str_value","int_value"
+1000,8000,"battery_stats.top","mail",123
+3000,-1,"battery_stats.job","mail_job",456
+1000,3000,"battery_stats.job","video_job",789
diff --git a/test/trace_processor/diff_tests/android/tests.py b/test/trace_processor/diff_tests/android/tests.py
index 25190b0..cec98ae 100644
--- a/test/trace_processor/diff_tests/android/tests.py
+++ b/test/trace_processor/diff_tests/android/tests.py
@@ -111,6 +111,54 @@
""",
out=Path('android_system_property_slice.out'))
+ def test_android_battery_stats_event_slices(self):
+ # The following has three events
+ # * top (123, mail) from 1000 to 9000 explicit
+ # * job (456, mail_job) starting at 3000 (end is inferred as trace end)
+ # * job (789, video_job) ending at 4000 (start is inferred as trace start)
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ packet {
+ ftrace_events {
+ cpu: 1
+ event {
+ timestamp: 1000
+ pid: 1
+ print {
+ buf: "N|1000|battery_stats.top|+top=123:\"mail\"\n"
+ }
+ }
+ event {
+ timestamp: 3000
+ pid: 1
+ print {
+ buf: "N|1000|battery_stats.job|+job=456:\"mail_job\"\n"
+ }
+ }
+ event {
+ timestamp: 4000
+ pid: 1
+ print {
+ buf: "N|1000|battery_stats.job|-job=789:\"video_job\"\n"
+ }
+ }
+ event {
+ timestamp: 9000
+ pid: 1
+ print {
+ buf: "N|1000|battery_stats.top|-top=123:\"mail\"\n"
+ }
+ }
+ }
+ }
+ """),
+ query="""
+ SELECT IMPORT('android.battery_stats');
+ SELECT * FROM android_battery_stats_event_slices
+ ORDER BY str_value;
+ """,
+ out=Path('android_battery_stats_event_slices.out'))
+
def test_binder_sync_binder_metrics(self):
return DiffTestBlueprint(
trace=DataPath('android_binder_metric_trace.atr'),
diff --git a/ui/src/base/bigint_math.ts b/ui/src/base/bigint_math.ts
new file mode 100644
index 0000000..1860afe
--- /dev/null
+++ b/ui/src/base/bigint_math.ts
@@ -0,0 +1,77 @@
+// 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.
+
+export class BigintMath {
+ static INT64_MAX: bigint = (2n ** 63n) - 1n;
+
+ // Returns the smallest integral power of 2 that is not smaller than n.
+ // If n is less than or equal to 0, returns 1.
+ static bitCeil(n: bigint): bigint {
+ let result = 1n;
+ while (result < n) {
+ result <<= 1n;
+ }
+ return result;
+ };
+
+ // Returns the largest integral power of 2 which is not greater than n.
+ // If n is less than or equal to 0, returns 1.
+ static bitFloor(n: bigint): bigint {
+ let result = 1n;
+ while ((result << 1n) <= n) {
+ result <<= 1n;
+ }
+ return result;
+ };
+
+ // Returns the largest integral multiple of step which is not larger than n.
+ // If step is less than or equal to 0, returns n.
+ static quantizeFloor(n: bigint, step: bigint): bigint {
+ step = BigintMath.max(1n, step);
+ return step * (n / step);
+ }
+
+ // Return the integral multiple of step which is closest to n.
+ // If step is less than or equal to 0, returns n.
+ static quantize(n: bigint, step: bigint): bigint {
+ step = BigintMath.max(1n, step);
+ const halfStep = step / 2n;
+ return step * ((n + halfStep) / step);
+ }
+
+ // Return the greater of a and b
+ static max(a: bigint, b: bigint): bigint {
+ return a > b ? a : b;
+ }
+
+ // Return the smaller of a and b
+ static min(a: bigint, b: bigint): bigint {
+ return a < b ? a : b;
+ }
+
+ // Returns the number of 1 bits in n
+ static popcount(n: bigint): number {
+ if (n < 0n) {
+ throw Error(`Can\'t get popcount of negative number ${n}`);
+ }
+ let count = 0;
+ while (n) {
+ if (n & 1n) {
+ ++count;
+ }
+ n >>= 1n;
+ }
+ return count;
+ }
+}
diff --git a/ui/src/base/bigint_math_unittest.ts b/ui/src/base/bigint_math_unittest.ts
new file mode 100644
index 0000000..291eb32
--- /dev/null
+++ b/ui/src/base/bigint_math_unittest.ts
@@ -0,0 +1,141 @@
+// 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 {
+ BigintMath as BIM,
+} from './bigint_math';
+
+describe('BigIntMath.bitCeil', () => {
+ it('rounds powers of 2 to themselves', () => {
+ expect(BIM.bitCeil(1n)).toBe(1n);
+ expect(BIM.bitCeil(2n)).toBe(2n);
+ expect(BIM.bitCeil(4n)).toBe(4n);
+ expect(BIM.bitCeil(4294967296n)).toBe(4294967296n);
+ expect(BIM.bitCeil(2305843009213693952n)).toBe(2305843009213693952n);
+ });
+
+ it('rounds non powers of 2 up to nearest power of 2', () => {
+ expect(BIM.bitCeil(3n)).toBe(4n);
+ expect(BIM.bitCeil(11n)).toBe(16n);
+ expect(BIM.bitCeil(33n)).toBe(64n);
+ expect(BIM.bitCeil(63n)).toBe(64n);
+ expect(BIM.bitCeil(1234567890123456789n)).toBe(2305843009213693952n);
+ });
+
+ it('rounds 0 or negative values up to 1', () => {
+ expect(BIM.bitCeil(0n)).toBe(1n);
+ expect(BIM.bitCeil(-123n)).toBe(1n);
+ });
+});
+
+describe('BigIntMath.bigFloor', () => {
+ it('rounds powers of 2 to themselves', () => {
+ expect(BIM.bitFloor(1n)).toBe(1n);
+ expect(BIM.bitFloor(2n)).toBe(2n);
+ expect(BIM.bitFloor(4n)).toBe(4n);
+ expect(BIM.bitFloor(4294967296n)).toBe(4294967296n);
+ expect(BIM.bitFloor(2305843009213693952n)).toBe(2305843009213693952n);
+ });
+
+ it('rounds non powers of 2 down to nearest power of 2', () => {
+ expect(BIM.bitFloor(3n)).toBe(2n);
+ expect(BIM.bitFloor(11n)).toBe(8n);
+ expect(BIM.bitFloor(33n)).toBe(32n);
+ expect(BIM.bitFloor(63n)).toBe(32n);
+ expect(BIM.bitFloor(1234567890123456789n)).toBe(1152921504606846976n);
+ });
+
+ it('rounds 0 or negative values up to 1', () => {
+ expect(BIM.bitFloor(0n)).toBe(1n);
+ expect(BIM.bitFloor(-123n)).toBe(1n);
+ });
+});
+
+describe('quantize', () => {
+ it('should quantize a number to the nearest multiple of a stepsize', () => {
+ expect(BIM.quantizeFloor(10n, 2n)).toEqual(10n);
+ expect(BIM.quantizeFloor(11n, 2n)).toEqual(10n);
+ expect(BIM.quantizeFloor(12n, 2n)).toEqual(12n);
+ expect(BIM.quantizeFloor(13n, 2n)).toEqual(12n);
+
+ expect(BIM.quantizeFloor(9n, 4n)).toEqual(8n);
+ expect(BIM.quantizeFloor(10n, 4n)).toEqual(8n);
+ expect(BIM.quantizeFloor(11n, 4n)).toEqual(8n);
+ expect(BIM.quantizeFloor(12n, 4n)).toEqual(12n);
+ expect(BIM.quantizeFloor(13n, 4n)).toEqual(12n);
+ });
+
+ it('should return value if stepsize is smaller than 1', () => {
+ expect(BIM.quantizeFloor(123n, 0n)).toEqual(123n);
+ expect(BIM.quantizeFloor(123n, -10n)).toEqual(123n);
+ });
+});
+
+describe('quantizeRound', () => {
+ it('should quantize a number to the nearest multiple of a stepsize', () => {
+ expect(BIM.quantize(0n, 2n)).toEqual(0n);
+ expect(BIM.quantize(1n, 2n)).toEqual(2n);
+ expect(BIM.quantize(2n, 2n)).toEqual(2n);
+ expect(BIM.quantize(3n, 2n)).toEqual(4n);
+ expect(BIM.quantize(4n, 2n)).toEqual(4n);
+
+ expect(BIM.quantize(0n, 3n)).toEqual(0n);
+ expect(BIM.quantize(1n, 3n)).toEqual(0n);
+ expect(BIM.quantize(2n, 3n)).toEqual(3n);
+ expect(BIM.quantize(3n, 3n)).toEqual(3n);
+ expect(BIM.quantize(4n, 3n)).toEqual(3n);
+ expect(BIM.quantize(5n, 3n)).toEqual(6n);
+ expect(BIM.quantize(6n, 3n)).toEqual(6n);
+ });
+
+ it('should return value if stepsize is smaller than 1', () => {
+ expect(BIM.quantize(123n, 0n)).toEqual(123n);
+ expect(BIM.quantize(123n, -10n)).toEqual(123n);
+ });
+});
+
+describe('max', () => {
+ it('should return the greater of two numbers', () => {
+ expect(BIM.max(5n, 8n)).toEqual(8n);
+ expect(BIM.max(3n, 7n)).toEqual(7n);
+ expect(BIM.max(6n, 6n)).toEqual(6n);
+ expect(BIM.max(-7n, -12n)).toEqual(-7n);
+ });
+});
+
+describe('min', () => {
+ it('should return the smaller of two numbers', () => {
+ expect(BIM.min(5n, 8n)).toEqual(5n);
+ expect(BIM.min(3n, 7n)).toEqual(3n);
+ expect(BIM.min(6n, 6n)).toEqual(6n);
+ expect(BIM.min(-7n, -12n)).toEqual(-12n);
+ });
+});
+
+describe('popcount', () => {
+ it('should return the number of set bits in an integer', () => {
+ expect(BIM.popcount(0n)).toBe(0);
+ expect(BIM.popcount(1n)).toBe(1);
+ expect(BIM.popcount(2n)).toBe(1);
+ expect(BIM.popcount(3n)).toBe(2);
+ expect(BIM.popcount(4n)).toBe(1);
+ expect(BIM.popcount(5n)).toBe(2);
+ expect(BIM.popcount(3462151285050974216n)).toBe(10);
+ });
+
+ it('should throw when presented with a negative integer', () => {
+ expect(() => BIM.popcount(-1n))
+ .toThrowError('Can\'t get popcount of negative number -1');
+ });
+});
diff --git a/ui/src/base/math_utils.ts b/ui/src/base/math_utils.ts
deleted file mode 100644
index f1c1816..0000000
--- a/ui/src/base/math_utils.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// 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.
-
-// Round a number up to the nearest stepsize.
-export function roundUpNearest(val: number, stepsize: number): number {
- return stepsize * Math.ceil(val / stepsize);
-}
-
-// Round a number down to the nearest stepsize.
-export function roundDownNearest(val: number, stepsize: number): number {
- return stepsize * Math.floor(val / stepsize);
-}
diff --git a/ui/src/base/math_utils_unittest.ts b/ui/src/base/math_utils_unittest.ts
deleted file mode 100644
index 169b793..0000000
--- a/ui/src/base/math_utils_unittest.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-// 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 {roundDownNearest, roundUpNearest} from './math_utils';
-
-describe('roundUpNearest()', () => {
- it('rounds decimal values up to the right step size', () => {
- expect(roundUpNearest(0.1, 0.5)).toBeCloseTo(0.5);
- expect(roundUpNearest(17.2, 0.5)).toBeCloseTo(17.5);
- });
-});
-
-describe('roundDownNearest()', () => {
- it('rounds decimal values down to the right step size', () => {
- expect(roundDownNearest(0.4, 0.5)).toBeCloseTo(0.0);
- expect(roundDownNearest(17.4, 0.5)).toBeCloseTo(17.0);
- });
-});
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 554722c..286f82f 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -61,7 +61,7 @@
UtidToTrackSortKey,
VisibleState,
} from './state';
-import {toNs} from './time';
+import {TPDuration, TPTime} from './time';
export const DEBUG_SLICE_TRACK_KIND = 'DebugSliceTrack';
@@ -89,8 +89,8 @@
}
export interface PostedScrollToRange {
- timeStart: number;
- timeEnd: number;
+ timeStart: TPTime;
+ timeEnd: TPTime;
viewPercentage?: number;
}
@@ -552,7 +552,7 @@
addAutomaticNote(
state: StateDraft,
- args: {timestamp: number, color: string, text: string}): void {
+ args: {timestamp: TPTime, color: string, text: string}): void {
const id = generateNextId(state);
state.notes[id] = {
noteType: 'DEFAULT',
@@ -563,7 +563,7 @@
};
},
- addNote(state: StateDraft, args: {timestamp: number, color: string}): void {
+ addNote(state: StateDraft, args: {timestamp: TPTime, color: string}): void {
const id = generateNextId(state);
state.notes[id] = {
noteType: 'DEFAULT',
@@ -606,14 +606,10 @@
},
markArea(state: StateDraft, args: {area: Area, persistent: boolean}): void {
+ const {start, end, tracks} = args.area;
+ assertTrue(start <= end);
const areaId = generateNextId(state);
- assertTrue(args.area.endSec >= args.area.startSec);
- state.areas[areaId] = {
- id: areaId,
- startSec: args.area.startSec,
- endSec: args.area.endSec,
- tracks: args.area.tracks,
- };
+ state.areas[areaId] = {id: areaId, start, end, tracks};
const noteId = args.persistent ? generateNextId(state) : '0';
const color = args.persistent ? randomColor() : '#344596';
state.notes[noteId] = {
@@ -667,7 +663,7 @@
selectCounter(
state: StateDraft,
- args: {leftTs: number, rightTs: number, id: number, trackId: string}):
+ args: {leftTs: TPTime, rightTs: TPTime, id: number, trackId: string}):
void {
state.currentSelection = {
kind: 'COUNTER',
@@ -680,7 +676,7 @@
selectHeapProfile(
state: StateDraft,
- args: {id: number, upid: number, ts: number, type: ProfileType}): void {
+ args: {id: number, upid: number, ts: TPTime, type: ProfileType}): void {
state.currentSelection = {
kind: 'HEAP_PROFILE',
id: args.id,
@@ -690,8 +686,8 @@
};
this.openFlamegraph(state, {
type: args.type,
- startNs: toNs(state.traceTime.startSec),
- endNs: args.ts,
+ start: state.traceTime.start,
+ end: args.ts,
upids: [args.upid],
viewingOption: DEFAULT_VIEWING_OPTION,
});
@@ -700,8 +696,8 @@
selectPerfSamples(state: StateDraft, args: {
id: number,
upid: number,
- leftTs: number,
- rightTs: number,
+ leftTs: TPTime,
+ rightTs: TPTime,
type: ProfileType
}): void {
state.currentSelection = {
@@ -714,8 +710,8 @@
};
this.openFlamegraph(state, {
type: args.type,
- startNs: args.leftTs,
- endNs: args.rightTs,
+ start: args.leftTs,
+ end: args.rightTs,
upids: [args.upid],
viewingOption: PERF_SAMPLES_KEY,
});
@@ -723,16 +719,16 @@
openFlamegraph(state: StateDraft, args: {
upids: number[],
- startNs: number,
- endNs: number,
+ start: TPTime,
+ end: TPTime,
type: ProfileType,
viewingOption: FlamegraphStateViewingOption
}): void {
state.currentFlamegraphState = {
kind: 'FLAMEGRAPH_STATE',
upids: args.upids,
- startNs: args.startNs,
- endNs: args.endNs,
+ start: args.start,
+ end: args.end,
type: args.type,
viewingOption: args.viewingOption,
focusRegex: '',
@@ -784,16 +780,16 @@
selectDebugSlice(state: StateDraft, args: {
id: number,
sqlTableName: string,
- startS: number,
- durationS: number,
+ start: TPTime,
+ duration: TPDuration,
trackId: string,
}): void {
state.currentSelection = {
kind: 'DEBUG_SLICE',
id: args.id,
sqlTableName: args.sqlTableName,
- startS: args.startS,
- durationS: args.durationS,
+ start: args.start,
+ duration: args.duration,
trackId: args.trackId,
};
},
@@ -801,16 +797,16 @@
selectTopLevelScrollSlice(state: StateDraft, args: {
id: number,
sqlTableName: string,
- startS: number,
- durationS: number,
+ start: TPTime,
+ duration: TPTime,
trackId: string,
}): void {
state.currentSelection = {
kind: 'TOP_LEVEL_SCROLL',
id: args.id,
sqlTableName: args.sqlTableName,
- startS: args.startS,
- durationS: args.durationS,
+ start: args.start,
+ duration: args.duration,
trackId: args.trackId,
};
},
@@ -910,25 +906,17 @@
},
selectArea(state: StateDraft, args: {area: Area}): void {
+ const {start, end, tracks} = args.area;
+ assertTrue(start <= end);
const areaId = generateNextId(state);
- assertTrue(args.area.endSec >= args.area.startSec);
- state.areas[areaId] = {
- id: areaId,
- startSec: args.area.startSec,
- endSec: args.area.endSec,
- tracks: args.area.tracks,
- };
+ state.areas[areaId] = {id: areaId, start, end, tracks};
state.currentSelection = {kind: 'AREA', areaId};
},
editArea(state: StateDraft, args: {area: Area, areaId: string}): void {
- assertTrue(args.area.endSec >= args.area.startSec);
- state.areas[args.areaId] = {
- id: args.areaId,
- startSec: args.area.startSec,
- endSec: args.area.endSec,
- tracks: args.area.tracks,
- };
+ const {start, end, tracks} = args.area;
+ assertTrue(start <= end);
+ state.areas[args.areaId] = {id: args.areaId, start, end, tracks};
},
reSelectArea(state: StateDraft, args: {areaId: string, noteId: string}):
@@ -1048,11 +1036,11 @@
state.searchIndex = args.index;
},
- setHoverCursorTimestamp(state: StateDraft, args: {ts: number}) {
+ setHoverCursorTimestamp(state: StateDraft, args: {ts: TPTime}) {
state.hoverCursorTimestamp = args.ts;
},
- setHoveredNoteTimestamp(state: StateDraft, args: {ts: number}) {
+ setHoveredNoteTimestamp(state: StateDraft, args: {ts: TPTime}) {
state.hoveredNoteTimestamp = args.ts;
},
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index 6702e72..b613777 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -446,9 +446,13 @@
const state = createEmptyState();
const afterSelectingPerf = produce(state, (draft) => {
- StateActions.selectPerfSamples(
- draft,
- {id: 0, upid: 0, leftTs: 0, rightTs: 0, type: ProfileType.PERF_SAMPLE});
+ StateActions.selectPerfSamples(draft, {
+ id: 0,
+ upid: 0,
+ leftTs: 0n,
+ rightTs: 0n,
+ type: ProfileType.PERF_SAMPLE,
+ });
});
expect(assertExists(afterSelectingPerf.currentFlamegraphState).type)
@@ -460,7 +464,7 @@
const afterSelectingPerf = produce(state, (draft) => {
StateActions.selectHeapProfile(
- draft, {id: 0, upid: 0, ts: 0, type: ProfileType.JAVA_HEAP_GRAPH});
+ draft, {id: 0, upid: 0, ts: 0n, type: ProfileType.JAVA_HEAP_GRAPH});
});
expect(assertExists(afterSelectingPerf.currentFlamegraphState).type)
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index bd1e6b6..6d806a2 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -110,7 +110,7 @@
visibleState: {
...defaultTraceTime,
lastUpdate: 0,
- resolution: 0,
+ resolution: 0n,
},
},
@@ -142,8 +142,8 @@
sidebarVisible: true,
hoveredUtid: -1,
hoveredPid: -1,
- hoverCursorTimestamp: -1,
- hoveredNoteTimestamp: -1,
+ hoverCursorTimestamp: -1n,
+ hoveredNoteTimestamp: -1n,
highlightedSliceId: -1,
focusedFlowIdLeft: -1,
focusedFlowIdRight: -1,
diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts
index 8b6cb46..861601f 100644
--- a/ui/src/common/engine.ts
+++ b/ui/src/common/engine.ts
@@ -24,18 +24,20 @@
QueryArgs,
ResetTraceProcessorArgs,
} from './protos';
-import {NUM, NUM_NULL, STR} from './query_result';
+import {LONG, LONG_NULL, NUM, STR} from './query_result';
import {
createQueryResult,
QueryError,
QueryResult,
WritableQueryResult,
} from './query_result';
-import {TimeSpan} from './time';
+import {TPTime, TPTimeSpan} from './time';
import TraceProcessorRpc = perfetto.protos.TraceProcessorRpc;
import TraceProcessorRpcStream = perfetto.protos.TraceProcessorRpcStream;
import TPM = perfetto.protos.TraceProcessorRpc.TraceProcessorMethod;
+import {Span} from '../common/time';
+import {BigintMath} from '../base/bigint_math';
export interface LoadingTracker {
beginLoading(): void;
@@ -410,38 +412,38 @@
return result.firstRow({cnt: NUM}).cnt;
}
- async getTraceTimeBounds(): Promise<TimeSpan> {
+ async getTraceTimeBounds(): Promise<Span<TPTime>> {
const result = await this.query(
`select start_ts as startTs, end_ts as endTs from trace_bounds`);
const bounds = result.firstRow({
- startTs: NUM,
- endTs: NUM,
+ startTs: LONG,
+ endTs: LONG,
});
- return new TimeSpan(bounds.startTs / 1e9, bounds.endTs / 1e9);
+ return new TPTimeSpan(bounds.startTs, bounds.endTs);
}
- async getTracingMetadataTimeBounds(): Promise<TimeSpan> {
+ async getTracingMetadataTimeBounds(): Promise<Span<TPTime>> {
const queryRes = await this.query(`select
name,
int_value as intValue
from metadata
where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
or name = 'all_data_source_started_ns'`);
- let startBound = -Infinity;
- let endBound = Infinity;
- const it = queryRes.iter({'name': STR, 'intValue': NUM_NULL});
+ let startBound = 0n;
+ let endBound = BigintMath.INT64_MAX;
+ const it = queryRes.iter({'name': STR, 'intValue': LONG_NULL});
for (; it.valid(); it.next()) {
const columnName = it.name;
const timestamp = it.intValue;
if (timestamp === null) continue;
if (columnName === 'tracing_disabled_ns') {
- endBound = Math.min(endBound, timestamp / 1e9);
+ endBound = BigintMath.min(endBound, timestamp);
} else {
- startBound = Math.max(startBound, timestamp / 1e9);
+ startBound = BigintMath.max(startBound, timestamp);
}
}
- return new TimeSpan(startBound, endBound);
+ return new TPTimeSpan(startBound, endBound);
}
getProxy(tag: string): EngineProxy {
diff --git a/ui/src/common/high_precision_time.ts b/ui/src/common/high_precision_time.ts
new file mode 100644
index 0000000..927b7df
--- /dev/null
+++ b/ui/src/common/high_precision_time.ts
@@ -0,0 +1,258 @@
+// 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 {assertTrue} from '../base/logging';
+import {Span, TPTime} from './time';
+
+export type RoundMode = 'round'|'floor'|'ceil';
+
+// Stores a time as a bigint and an offset which is capable of:
+// - Storing and reproducing "TPTime"s without losing precision.
+// - Storing time with sub-nanosecond precision.
+// This class is immutable - each operation returns a new object.
+export class HighPrecisionTime {
+ // Time in nanoseconds == base + offset
+ // offset is kept in the range 0 <= x < 1 to avoid losing precision
+ readonly base: bigint;
+ readonly offset: number;
+
+ static get ZERO(): HighPrecisionTime {
+ return new HighPrecisionTime(0n);
+ }
+
+ constructor(base: bigint = 0n, offset: number = 0) {
+ // Normalize offset to sit in the range 0.0 <= x < 1.0
+ const offsetFloor = Math.floor(offset);
+ this.base = base + BigInt(offsetFloor);
+ this.offset = offset - offsetFloor;
+ }
+
+ static fromTPTime(timestamp: TPTime): HighPrecisionTime {
+ return new HighPrecisionTime(timestamp, 0);
+ }
+
+ static fromNanos(nanos: number|bigint) {
+ if (typeof nanos === 'number') {
+ return new HighPrecisionTime(0n, nanos);
+ } else if (typeof nanos === 'bigint') {
+ return new HighPrecisionTime(nanos);
+ } else {
+ const value: never = nanos;
+ throw new Error(`Value ${value} is neither a number nor a bigint`);
+ }
+ }
+
+ static fromSeconds(seconds: number) {
+ const nanos = seconds * 1e9;
+ const offset = nanos - Math.floor(nanos);
+ return new HighPrecisionTime(BigInt(Math.floor(nanos)), offset);
+ }
+
+ static max(a: HighPrecisionTime, b: HighPrecisionTime): HighPrecisionTime {
+ return a.isGreaterThan(b) ? a : b;
+ }
+
+ static min(a: HighPrecisionTime, b: HighPrecisionTime): HighPrecisionTime {
+ return a.isLessThan(b) ? a : b;
+ }
+
+ toTPTime(roundMode: RoundMode = 'floor'): TPTime {
+ switch (roundMode) {
+ case 'round':
+ return this.base + BigInt(Math.round(this.offset));
+ case 'floor':
+ return this.base;
+ case 'ceil':
+ return this.base + BigInt(Math.ceil(this.offset));
+ default:
+ const exhaustiveCheck: never = roundMode;
+ throw new Error(`Unhandled roundMode case: ${exhaustiveCheck}`);
+ }
+ }
+
+ get nanos(): number {
+ // WARNING: Number(bigint) can be surprisingly slow.
+ // WARNING: Precision may be lost here.
+ return Number(this.base) + this.offset;
+ }
+
+ get seconds(): number {
+ // WARNING: Number(bigint) can be surprisingly slow.
+ // WARNING: Precision may be lost here.
+ return (Number(this.base) + this.offset) / 1e9;
+ }
+
+ add(other: HighPrecisionTime): HighPrecisionTime {
+ return new HighPrecisionTime(
+ this.base + other.base, this.offset + other.offset);
+ }
+
+ addNanos(nanos: number|bigint): HighPrecisionTime {
+ return this.add(HighPrecisionTime.fromNanos(nanos));
+ }
+
+ addSeconds(seconds: number): HighPrecisionTime {
+ return new HighPrecisionTime(this.base, this.offset + seconds * 1e9);
+ }
+
+ addTPTime(ts: TPTime): HighPrecisionTime {
+ return new HighPrecisionTime(this.base + ts, this.offset);
+ }
+
+ subtract(other: HighPrecisionTime): HighPrecisionTime {
+ return new HighPrecisionTime(
+ this.base - other.base, this.offset - other.offset);
+ }
+
+ subtractTPTime(ts: TPTime): HighPrecisionTime {
+ return this.addTPTime(-ts);
+ }
+
+ subtractNanos(nanos: number|bigint): HighPrecisionTime {
+ return this.add(HighPrecisionTime.fromNanos(-nanos));
+ }
+
+ divide(divisor: number): HighPrecisionTime {
+ return this.multiply(1 / divisor);
+ }
+
+ multiply(factor: number): HighPrecisionTime {
+ const factorFloor = Math.floor(factor);
+ const newBase = this.base * BigInt(factorFloor);
+ const additionalBit = Number(this.base) * (factor - factorFloor);
+ const newOffset = factor * this.offset + additionalBit;
+ return new HighPrecisionTime(newBase, newOffset);
+ }
+
+ // Return true if other time is within some epsilon, default 1 femtosecond
+ equals(other: HighPrecisionTime, epsilon: number = 1e-6): boolean {
+ return Math.abs(this.subtract(other).nanos) < epsilon;
+ }
+
+ isLessThan(other: HighPrecisionTime): boolean {
+ if (this.base < other.base) {
+ return true;
+ } else if (this.base === other.base) {
+ return this.offset < other.offset;
+ } else {
+ return false;
+ }
+ }
+
+ isLessThanOrEqual(other: HighPrecisionTime): boolean {
+ if (this.equals(other)) {
+ return true;
+ } else {
+ return this.isLessThan(other);
+ }
+ }
+
+ isGreaterThan(other: HighPrecisionTime): boolean {
+ return !this.isLessThanOrEqual(other);
+ }
+
+ isGreaterThanOrEqual(other: HighPrecisionTime): boolean {
+ return !this.isLessThan(other);
+ }
+
+ clamp(lower: HighPrecisionTime, upper: HighPrecisionTime): HighPrecisionTime {
+ if (this.isLessThan(lower)) {
+ return lower;
+ } else if (this.isGreaterThan(upper)) {
+ return upper;
+ } else {
+ return this;
+ }
+ }
+
+ toString(): string {
+ const offsetAsString = this.offset.toString();
+ if (offsetAsString === '0') {
+ return this.base.toString();
+ } else {
+ return `${this.base}${offsetAsString.substring(1)}`;
+ }
+ }
+}
+
+export class HighPrecisionTimeSpan implements Span<HighPrecisionTime> {
+ readonly start: HighPrecisionTime;
+ readonly end: HighPrecisionTime;
+
+ constructor(start: TPTime|HighPrecisionTime, end: TPTime|HighPrecisionTime) {
+ this.start = (start instanceof HighPrecisionTime) ?
+ start :
+ HighPrecisionTime.fromTPTime(start);
+ this.end = (end instanceof HighPrecisionTime) ?
+ end :
+ HighPrecisionTime.fromTPTime(end);
+ assertTrue(
+ this.start.isLessThanOrEqual(this.end),
+ `TimeSpan start [${this.start}] cannot be greater than end [${
+ this.end}]`);
+ }
+
+ static fromTpTime(start: TPTime, end: TPTime): HighPrecisionTimeSpan {
+ return new HighPrecisionTimeSpan(
+ HighPrecisionTime.fromTPTime(start),
+ HighPrecisionTime.fromTPTime(end),
+ );
+ }
+
+ static get ZERO(): HighPrecisionTimeSpan {
+ return new HighPrecisionTimeSpan(
+ HighPrecisionTime.ZERO,
+ HighPrecisionTime.ZERO,
+ );
+ }
+
+ get duration(): HighPrecisionTime {
+ return this.end.subtract(this.start);
+ }
+
+ get midpoint(): HighPrecisionTime {
+ return this.start.add(this.end).divide(2);
+ }
+
+ equals(other: Span<HighPrecisionTime>): boolean {
+ return this.start.equals(other.start) && this.end.equals(other.end);
+ }
+
+ contains(x: HighPrecisionTime|Span<HighPrecisionTime>): boolean {
+ if (x instanceof HighPrecisionTime) {
+ return this.start.isLessThanOrEqual(x) && x.isLessThan(this.end);
+ } else {
+ return this.start.isLessThanOrEqual(x.start) &&
+ x.end.isLessThanOrEqual(this.end);
+ }
+ }
+
+ intersects(x: Span<HighPrecisionTime>): boolean {
+ return !(
+ x.end.isLessThanOrEqual(this.start) ||
+ x.start.isGreaterThanOrEqual(this.end));
+ }
+
+ add(time: HighPrecisionTime): Span<HighPrecisionTime> {
+ return new HighPrecisionTimeSpan(this.start.add(time), this.end.add(time));
+ }
+
+ // Move the start and end away from each other a certain amount
+ pad(time: HighPrecisionTime): Span<HighPrecisionTime> {
+ return new HighPrecisionTimeSpan(
+ this.start.subtract(time),
+ this.end.add(time),
+ );
+ }
+}
diff --git a/ui/src/common/high_precision_time_unittest.ts b/ui/src/common/high_precision_time_unittest.ts
new file mode 100644
index 0000000..c3a2033
--- /dev/null
+++ b/ui/src/common/high_precision_time_unittest.ts
@@ -0,0 +1,308 @@
+// 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 {
+ HighPrecisionTime as HPTime,
+ HighPrecisionTimeSpan as HPTimeSpan,
+} from './high_precision_time';
+import {TPTime} from './time';
+
+// Quick 'n' dirty function to convert a string to a HPtime
+// Used to make tests more readable
+// E.g. '1.3' -> {base: 1, offset: 0.3}
+// E.g. '-0.3' -> {base: -1, offset: 0.7}
+function mkTime(time: string): HPTime {
+ const array = time.split('.');
+ if (array.length > 2) throw new Error(`Bad time format ${time}`);
+ const [base, fractions] = array;
+ const negative = time.startsWith('-');
+ const numBase = BigInt(base);
+
+ if (fractions) {
+ const numFractions = Number(`0.${fractions}`);
+ if (negative) {
+ return new HPTime(numBase - 1n, 1.0 - numFractions);
+ } else {
+ return new HPTime(numBase, numFractions);
+ }
+ } else {
+ return new HPTime(numBase);
+ }
+}
+
+function mkSpan(t1: string, t2: string): HPTimeSpan {
+ return new HPTimeSpan(mkTime(t1), mkTime(t2));
+}
+
+describe('Time', () => {
+ it('should create a new Time object with the given base and offset', () => {
+ const time = new HPTime(136n, 0.3);
+ expect(time.base).toBe(136n);
+ expect(time.offset).toBeCloseTo(0.3);
+ });
+
+ it('should normalize when offset is >= 1', () => {
+ let time = new HPTime(1n, 2.3);
+ expect(time.base).toBe(3n);
+ expect(time.offset).toBeCloseTo(0.3);
+
+ time = new HPTime(1n, 1);
+ expect(time.base).toBe(2n);
+ expect(time.offset).toBeCloseTo(0);
+ });
+
+ it('should normalize when offset is < 0', () => {
+ const time = new HPTime(1n, -0.4);
+ expect(time.base).toBe(0n);
+ expect(time.offset).toBeCloseTo(0.6);
+ });
+
+ it('should store timestamps without losing precision', () => {
+ let time = HPTime.fromTPTime(123n as TPTime);
+ expect(time.toTPTime()).toBe(123n as TPTime);
+
+ time = HPTime.fromTPTime(1152921504606846976n as TPTime);
+ expect(time.toTPTime()).toBe(1152921504606846976n as TPTime);
+ });
+
+ it('should store and manipulate timestamps without losing precision', () => {
+ let time = HPTime.fromTPTime(123n as TPTime);
+ time = time.addTPTime(456n);
+ expect(time.toTPTime()).toBe(579n);
+
+ time = HPTime.fromTPTime(2315700508990407843n as TPTime);
+ time = time.addTPTime(2315718101717517451n as TPTime);
+ expect(time.toTPTime()).toBe(4631418610707925294n);
+ });
+
+ it('should add time', () => {
+ const time1 = mkTime('1.3');
+ const time2 = mkTime('3.1');
+ const result = time1.add(time2);
+ expect(result.base).toEqual(4n);
+ expect(result.offset).toBeCloseTo(0.4);
+ });
+
+ it('should subtract time', () => {
+ const time1 = mkTime('3.1');
+ const time2 = mkTime('1.3');
+ const result = time1.subtract(time2);
+ expect(result.base).toEqual(1n);
+ expect(result.offset).toBeCloseTo(0.8);
+ });
+
+ it('should add nanoseconds', () => {
+ const time = mkTime('1.3');
+ const result = time.addNanos(0.8);
+ expect(result.base).toEqual(2n);
+ expect(result.offset).toBeCloseTo(0.1);
+ });
+
+ it('should add seconds', () => {
+ const time = mkTime('1.3');
+ const result = time.addSeconds(0.008);
+ expect(result.base).toEqual(8000001n);
+ expect(result.offset).toBeCloseTo(0.3);
+ });
+
+ it('should perform gt comparisions', () => {
+ const time = mkTime('1.2');
+ expect(time.isGreaterThanOrEqual(mkTime('0.5'))).toBeTruthy();
+ expect(time.isGreaterThanOrEqual(mkTime('1.1'))).toBeTruthy();
+ expect(time.isGreaterThanOrEqual(mkTime('1.2'))).toBeTruthy();
+ expect(time.isGreaterThanOrEqual(mkTime('1.5'))).toBeFalsy();
+ expect(time.isGreaterThanOrEqual(mkTime('5.5'))).toBeFalsy();
+ });
+
+ it('should perform gte comparisions', () => {
+ const time = mkTime('1.2');
+ expect(time.isGreaterThan(mkTime('0.5'))).toBeTruthy();
+ expect(time.isGreaterThan(mkTime('1.1'))).toBeTruthy();
+ expect(time.isGreaterThan(mkTime('1.2'))).toBeFalsy();
+ expect(time.isGreaterThan(mkTime('1.5'))).toBeFalsy();
+ expect(time.isGreaterThan(mkTime('5.5'))).toBeFalsy();
+ });
+
+ it('should perform lt comparisions', () => {
+ const time = mkTime('1.2');
+ expect(time.isLessThan(mkTime('0.5'))).toBeFalsy();
+ expect(time.isLessThan(mkTime('1.1'))).toBeFalsy();
+ expect(time.isLessThan(mkTime('1.2'))).toBeFalsy();
+ expect(time.isLessThan(mkTime('1.5'))).toBeTruthy();
+ expect(time.isLessThan(mkTime('5.5'))).toBeTruthy();
+ });
+
+ it('should perform lte comparisions', () => {
+ const time = mkTime('1.2');
+ expect(time.isLessThanOrEqual(mkTime('0.5'))).toBeFalsy();
+ expect(time.isLessThanOrEqual(mkTime('1.1'))).toBeFalsy();
+ expect(time.isLessThanOrEqual(mkTime('1.2'))).toBeTruthy();
+ expect(time.isLessThanOrEqual(mkTime('1.5'))).toBeTruthy();
+ expect(time.isLessThanOrEqual(mkTime('5.5'))).toBeTruthy();
+ });
+
+ it('should detect equality', () => {
+ const time = new HPTime(1n, 0.2);
+ expect(time.equals(new HPTime(1n, 0.2))).toBeTruthy();
+ expect(time.equals(new HPTime(0n, 1.2))).toBeTruthy();
+ expect(time.equals(new HPTime(-100n, 101.2))).toBeTruthy();
+ expect(time.equals(new HPTime(1n, 0.3))).toBeFalsy();
+ expect(time.equals(new HPTime(2n, 0.2))).toBeFalsy();
+ });
+
+ it('should clamp a time to a range', () => {
+ const time1 = mkTime('1.2');
+ const time2 = mkTime('5.4');
+ const time3 = mkTime('2.8');
+ const lower = mkTime('2.3');
+ const upper = mkTime('4.5');
+ expect(time1.clamp(lower, upper)).toEqual(lower);
+ expect(time2.clamp(lower, upper)).toEqual(upper);
+ expect(time3.clamp(lower, upper)).toEqual(time3);
+ });
+
+ it('should convert to seconds', () => {
+ expect(new HPTime(1n, .2).seconds).toBeCloseTo(0.0000000012);
+ expect(new HPTime(1000000000n, .0).seconds).toBeCloseTo(1);
+ });
+
+ it('should convert to nanos', () => {
+ expect(new HPTime(1n, .2).nanos).toBeCloseTo(1.2);
+ expect(new HPTime(1000000000n, .0).nanos).toBeCloseTo(1e9);
+ });
+
+ it('should convert to timestamps', () => {
+ expect(new HPTime(1n, .2).toTPTime('round')).toBe(1n);
+ expect(new HPTime(1n, .5).toTPTime('round')).toBe(2n);
+ expect(new HPTime(1n, .2).toTPTime('floor')).toBe(1n);
+ expect(new HPTime(1n, .5).toTPTime('floor')).toBe(1n);
+ expect(new HPTime(1n, .2).toTPTime('ceil')).toBe(2n);
+ expect(new HPTime(1n, .5).toTPTime('ceil')).toBe(2n);
+ });
+
+ it('should divide', () => {
+ let result = mkTime('1').divide(2);
+ expect(result.base).toBe(0n);
+ expect(result.offset).toBeCloseTo(0.5);
+
+ result = mkTime('1.6').divide(2);
+ expect(result.base).toBe(0n);
+ expect(result.offset).toBeCloseTo(0.8);
+
+ result = mkTime('-0.5').divide(2);
+ expect(result.base).toBe(-1n);
+ expect(result.offset).toBeCloseTo(0.75);
+
+ result = mkTime('123.1').divide(123);
+ expect(result.base).toBe(1n);
+ expect(result.offset).toBeCloseTo(0.000813, 6);
+ });
+
+ it('should multiply', () => {
+ let result = mkTime('1').multiply(2);
+ expect(result.base).toBe(2n);
+ expect(result.offset).toBeCloseTo(0);
+
+ result = mkTime('1').multiply(2.5);
+ expect(result.base).toBe(2n);
+ expect(result.offset).toBeCloseTo(0.5);
+
+ result = mkTime('-0.5').multiply(2);
+ expect(result.base).toBe(-1n);
+ expect(result.offset).toBeCloseTo(0.0);
+
+ result = mkTime('123.1').multiply(25.5);
+ expect(result.base).toBe(3139n);
+ expect(result.offset).toBeCloseTo(0.05);
+ });
+
+ it('should convert to string', () => {
+ expect(mkTime('1.3').toString()).toBe('1.3');
+ expect(mkTime('12983423847.332533').toString()).toBe('12983423847.332533');
+ expect(new HPTime(234n).toString()).toBe('234');
+ });
+});
+
+describe('HighPrecisionTimeSpan', () => {
+ it('can be constructed from HP time', () => {
+ const span = new HPTimeSpan(mkTime('10'), mkTime('20'));
+ expect(span.start).toEqual(mkTime('10'));
+ expect(span.end).toEqual(mkTime('20'));
+ });
+
+ it('can be constructed from integer time', () => {
+ const span = new HPTimeSpan(10n, 20n);
+ expect(span.start).toEqual(mkTime('10'));
+ expect(span.end).toEqual(mkTime('20'));
+ });
+
+ it('throws when start is later than end', () => {
+ expect(() => new HPTimeSpan(mkTime('0.1'), mkTime('0'))).toThrow();
+ expect(() => new HPTimeSpan(mkTime('1124.0001'), mkTime('1124'))).toThrow();
+ });
+
+ it('can calc duration', () => {
+ let dur = mkSpan('10', '20').duration;
+ expect(dur.base).toBe(10n);
+ expect(dur.offset).toBeCloseTo(0);
+
+ dur = mkSpan('10.123', '20.456').duration;
+ expect(dur.base).toBe(10n);
+ expect(dur.offset).toBeCloseTo(0.333);
+ });
+
+ it('can calc midpoint', () => {
+ let mid = mkSpan('10', '20').midpoint;
+ expect(mid.base).toBe(15n);
+ expect(mid.offset).toBeCloseTo(0);
+
+ mid = mkSpan('10.25', '16.75').midpoint;
+ expect(mid.base).toBe(13n);
+ expect(mid.offset).toBeCloseTo(0.5);
+ });
+
+ it('can be compared', () => {
+ expect(mkSpan('0.1', '34.2').equals(mkSpan('0.1', '34.2'))).toBeTruthy();
+ expect(mkSpan('0.1', '34.5').equals(mkSpan('0.1', '34.2'))).toBeFalsy();
+ expect(mkSpan('0.9', '34.2').equals(mkSpan('0.1', '34.2'))).toBeFalsy();
+ });
+
+ it('checks if span contains another span', () => {
+ const x = mkSpan('10', '20');
+
+ expect(x.contains(mkTime('9'))).toBeFalsy();
+ expect(x.contains(mkTime('10'))).toBeTruthy();
+ expect(x.contains(mkTime('15'))).toBeTruthy();
+ expect(x.contains(mkTime('20'))).toBeFalsy();
+ expect(x.contains(mkTime('21'))).toBeFalsy();
+
+ expect(x.contains(mkSpan('12', '18'))).toBeTruthy();
+ expect(x.contains(mkSpan('5', '25'))).toBeFalsy();
+ expect(x.contains(mkSpan('5', '15'))).toBeFalsy();
+ expect(x.contains(mkSpan('15', '25'))).toBeFalsy();
+ expect(x.contains(mkSpan('0', '10'))).toBeFalsy();
+ expect(x.contains(mkSpan('20', '30'))).toBeFalsy();
+ });
+
+ it('checks if span intersects another span', () => {
+ const x = mkSpan('10', '20');
+
+ expect(x.intersects(mkSpan('0', '10'))).toBeFalsy();
+ expect(x.intersects(mkSpan('5', '15'))).toBeTruthy();
+ expect(x.intersects(mkSpan('12', '18'))).toBeTruthy();
+ expect(x.intersects(mkSpan('15', '25'))).toBeTruthy();
+ expect(x.intersects(mkSpan('20', '30'))).toBeFalsy();
+ expect(x.intersects(mkSpan('5', '25'))).toBeTruthy();
+ });
+});
diff --git a/ui/src/common/logs.ts b/ui/src/common/logs.ts
index 0fc2fae..04cf065 100644
--- a/ui/src/common/logs.ts
+++ b/ui/src/common/logs.ts
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {TPTime} from './time';
+
export const LogExistsKey = 'log-exists';
export const LogBoundsKey = 'log-bounds';
export const LogEntriesKey = 'log-entries';
@@ -19,16 +21,16 @@
export interface LogExists { exists: boolean; }
export interface LogBounds {
- startTs: number;
- endTs: number;
- firstRowTs: number;
- lastRowTs: number;
- total: number;
+ firstLogTs: TPTime;
+ lastLogTs: TPTime;
+ firstVisibleLogTs: TPTime;
+ lastVisibleLogTs: TPTime;
+ totalVisibleLogs: number;
}
export interface LogEntries {
offset: number;
- timestamps: number[];
+ timestamps: TPTime[];
priorities: number[];
tags: string[];
messages: string[];
diff --git a/ui/src/common/plugin_api.ts b/ui/src/common/plugin_api.ts
index 89c1af7..0dc66d6 100644
--- a/ui/src/common/plugin_api.ts
+++ b/ui/src/common/plugin_api.ts
@@ -19,6 +19,8 @@
export {EngineProxy} from '../common/engine';
export {
+ LONG,
+ LONG_NULL,
NUM,
NUM_NULL,
STR,
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 0a0c772..91ca358 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -21,6 +21,7 @@
import {TopLevelScrollSelection} from '../tracks/scroll_jank/scroll_track';
import {Direction} from './event_set';
+import {TPDuration, TPTime} from './time';
/**
* A plain js object, holding objects of type |Class| keyed by string id.
@@ -41,9 +42,9 @@
}
export interface VisibleState extends Timestamped {
- startSec: number;
- endSec: number;
- resolution: number;
+ start: TPTime;
+ end: TPTime;
+ resolution: TPDuration;
}
export interface AreaSelection {
@@ -61,8 +62,8 @@
export type AreaById = Area&{id: string};
export interface Area {
- startSec: number;
- endSec: number;
+ start: TPTime;
+ end: TPTime;
tracks: string[];
}
@@ -102,7 +103,8 @@
// 28. Add a boolean indicating if non matching log entries are hidden.
// 29. Add ftrace state. <-- Borked, state contains a non-serializable object.
// 30. Convert ftraceFilter.excludedNames from Set<string> to string[].
-export const STATE_VERSION = 30;
+// 31. Convert all timestamps to bigints.
+export const STATE_VERSION = 31;
export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
@@ -270,8 +272,8 @@
}
export interface TraceTime {
- startSec: number;
- endSec: number;
+ start: TPTime;
+ end: TPTime;
}
export interface FrontendLocalState {
@@ -286,7 +288,7 @@
export interface Note {
noteType: 'DEFAULT';
id: string;
- timestamp: number;
+ timestamp: TPTime;
color: string;
text: string;
}
@@ -313,14 +315,14 @@
kind: 'DEBUG_SLICE';
id: number;
sqlTableName: string;
- startS: number;
- durationS: number;
+ start: TPTime;
+ duration: TPDuration;
}
export interface CounterSelection {
kind: 'COUNTER';
- leftTs: number;
- rightTs: number;
+ leftTs: TPTime;
+ rightTs: TPTime;
id: number;
}
@@ -328,7 +330,7 @@
kind: 'HEAP_PROFILE';
id: number;
upid: number;
- ts: number;
+ ts: TPTime;
type: ProfileType;
}
@@ -336,16 +338,16 @@
kind: 'PERF_SAMPLES';
id: number;
upid: number;
- leftTs: number;
- rightTs: number;
+ leftTs: TPTime;
+ rightTs: TPTime;
type: ProfileType;
}
export interface FlamegraphState {
kind: 'FLAMEGRAPH_STATE';
upids: number[];
- startNs: number;
- endNs: number;
+ start: TPTime;
+ end: TPTime;
type: ProfileType;
viewingOption: FlamegraphStateViewingOption;
focusRegex: string;
@@ -572,8 +574,8 @@
// Hovered and focused events
hoveredUtid: number;
hoveredPid: number;
- hoverCursorTimestamp: number;
- hoveredNoteTimestamp: number;
+ hoverCursorTimestamp: TPTime;
+ hoveredNoteTimestamp: TPTime;
highlightedSliceId: number;
focusedFlowIdLeft: number;
focusedFlowIdRight: number;
@@ -610,8 +612,8 @@
}
export const defaultTraceTime = {
- startSec: 0,
- endSec: 10,
+ start: 0n,
+ end: BigInt(10e9),
};
export declare type RecordMode =
diff --git a/ui/src/common/time.ts b/ui/src/common/time.ts
index ea5e9d8..5608d3f 100644
--- a/ui/src/common/time.ts
+++ b/ui/src/common/time.ts
@@ -13,8 +13,7 @@
// limitations under the License.
import {assertTrue} from '../base/logging';
-
-const EPSILON = 0.0000000001;
+import {ColumnType} from './query_result';
// TODO(hjd): Combine with timeToCode.
export function timeToString(sec: number) {
@@ -29,6 +28,10 @@
return `${sign < 0 ? '-' : ''}${Math.round(n * 10) / 10} ${units[u]}`;
}
+export function tpTimeToString(time: TPTime) {
+ return timeToString(tpTimeToSeconds(time));
+}
+
export function fromNs(ns: number) {
return ns / 1e9;
}
@@ -52,6 +55,10 @@
return parts.join('.');
}
+export function formatTPTime(time: TPTime) {
+ return formatTimestamp(tpTimeToSeconds(time));
+}
+
// TODO(hjd): Rename to formatTimestampWithUnits
// 1000000023ns -> "1s 23ns"
export function timeToCode(sec: number): string {
@@ -77,44 +84,128 @@
return result.slice(0, -1);
}
+export function tpTimeToCode(time: TPTime) {
+ return timeToCode(tpTimeToSeconds(time));
+}
+
export function currentDateHourAndMinute(): string {
const date = new Date();
return `${date.toISOString().substr(0, 10)}-${date.getHours()}-${
date.getMinutes()}`;
}
-export class TimeSpan {
- readonly start: number;
- readonly end: number;
+// Aliased "Trace Processor" time and duration types.
+// Note(stevegolton): While it might be nice to type brand these in the future,
+// for now we're going to keep things simple. We do a lot of maths with these
+// timestamps and type branding requires a lot of jumping through hoops to
+// coerse the type back to the correct format.
+export type TPTime = bigint;
+export type TPDuration = bigint;
- constructor(start: number, end: number) {
- assertTrue(start <= end);
+export function tpTimeFromNanos(nanos: number): TPTime {
+ return BigInt(Math.floor(nanos));
+}
+
+export function tpTimeFromSeconds(seconds: number): TPTime {
+ return BigInt(Math.floor(seconds * 1e9));
+}
+
+export function tpTimeToNanos(time: TPTime): number {
+ return Number(time);
+}
+
+export function tpTimeToMillis(time: TPTime): number {
+ return Number(time) / 1e6;
+}
+
+export function tpTimeToSeconds(time: TPTime): number {
+ return Number(time) / 1e9;
+}
+
+// Create a TPTime from an arbitrary SQL value.
+// Throws if the value cannot be reasonably converted to a bigint.
+// Assumes value is in nanoseconds.
+export function tpTimeFromSql(value: ColumnType): TPTime {
+ if (typeof value === 'bigint') {
+ return value;
+ } else if (typeof value === 'number') {
+ return tpTimeFromNanos(value);
+ } else if (value === null) {
+ return 0n;
+ } else {
+ throw Error(`Refusing to create Timestamp from unrelated type ${value}`);
+ }
+}
+
+export function tpDurationToSeconds(dur: TPDuration): number {
+ return tpTimeToSeconds(dur);
+}
+
+export function tpDurationToNanos(dur: TPDuration): number {
+ return tpTimeToSeconds(dur);
+}
+
+export function tpDurationFromNanos(nanos: number): TPDuration {
+ return tpTimeFromNanos(nanos);
+}
+
+export function tpDurationFromSql(nanos: ColumnType): TPDuration {
+ return tpTimeFromSql(nanos);
+}
+
+export interface Span<Unit, Duration = Unit> {
+ get start(): Unit;
+ get end(): Unit;
+ get duration(): Duration;
+ get midpoint(): Unit;
+ contains(span: Unit|Span<Unit, Duration>): boolean;
+ intersects(x: Span<Unit>): boolean;
+ equals(span: Span<Unit, Duration>): boolean;
+ add(offset: Duration): Span<Unit, Duration>;
+ pad(padding: Duration): Span<Unit, Duration>;
+}
+
+export class TPTimeSpan implements Span<TPTime, TPDuration> {
+ readonly start: TPTime;
+ readonly end: TPTime;
+
+ constructor(start: TPTime, end: TPTime) {
+ assertTrue(
+ start <= end,
+ `Span start [${start}] cannot be greater than end [${end}]`);
this.start = start;
this.end = end;
}
- clone() {
- return new TimeSpan(this.start, this.end);
- }
-
- equals(other: TimeSpan): boolean {
- return Math.abs(this.start - other.start) < EPSILON &&
- Math.abs(this.end - other.end) < EPSILON;
- }
-
- get duration() {
+ get duration(): TPDuration {
return this.end - this.start;
}
- isInBounds(sec: number) {
- return this.start <= sec && sec <= this.end;
+ get midpoint(): TPTime {
+ return (this.start + this.end) / 2n;
}
- add(sec: number): TimeSpan {
- return new TimeSpan(this.start + sec, this.end + sec);
+ contains(x: TPTime|Span<TPTime, TPDuration>): boolean {
+ if (typeof x === 'bigint') {
+ return this.start <= x && x < this.end;
+ } else {
+ return this.start <= x.start && x.end <= this.end;
+ }
}
- contains(other: TimeSpan) {
- return this.start <= other.start && other.end <= this.end;
+ intersects(x: Span<TPTime, TPDuration>): boolean {
+ return !(x.end <= this.start || x.start >= this.end);
+ }
+
+ equals(span: Span<TPTime, TPDuration>): boolean {
+ return this.start === span.start && this.end === span.end;
+ }
+
+ add(x: TPTime): Span<TPTime, TPDuration> {
+ return new TPTimeSpan(this.start + x, this.end + x);
+ }
+
+ pad(padding: TPDuration): Span<TPTime, TPDuration> {
+ return new TPTimeSpan(this.start - padding, this.end + padding);
}
}
diff --git a/ui/src/common/time_unittest.ts b/ui/src/common/time_unittest.ts
index 7bfead1..b9e6bd9 100644
--- a/ui/src/common/time_unittest.ts
+++ b/ui/src/common/time_unittest.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {TimeSpan, timeToCode} from './time';
+import {timeToCode, TPTime, TPTimeSpan} from './time';
test('seconds to code', () => {
expect(timeToCode(3)).toEqual('3s');
@@ -29,9 +29,67 @@
expect(timeToCode(0)).toEqual('0s');
});
-test('Time span equality', () => {
- expect((new TimeSpan(0, 1)).equals(new TimeSpan(0, 1))).toBe(true);
- expect((new TimeSpan(0, 1)).equals(new TimeSpan(0, 2))).toBe(false);
- expect((new TimeSpan(0, 1)).equals(new TimeSpan(0, 1 + Number.EPSILON)))
- .toBe(true);
+function mkSpan(start: TPTime, end: TPTime) {
+ return new TPTimeSpan(start, end);
+}
+
+describe('TPTimeSpan', () => {
+ it('throws when start is later than end', () => {
+ expect(() => mkSpan(1n, 0n)).toThrow();
+ });
+
+ it('can calc duration', () => {
+ expect(mkSpan(10n, 20n).duration).toBe(10n);
+ });
+
+ it('can calc midpoint', () => {
+ expect(mkSpan(10n, 20n).midpoint).toBe(15n);
+ expect(mkSpan(10n, 19n).midpoint).toBe(14n);
+ expect(mkSpan(10n, 10n).midpoint).toBe(10n);
+ });
+
+ it('can be compared', () => {
+ const x = mkSpan(10n, 20n);
+ expect(x.equals(mkSpan(10n, 20n))).toBeTruthy();
+ expect(x.equals(mkSpan(11n, 20n))).toBeFalsy();
+ expect(x.equals(mkSpan(10n, 19n))).toBeFalsy();
+ });
+
+ it('checks containment', () => {
+ const x = mkSpan(10n, 20n);
+
+ expect(x.contains(9n)).toBeFalsy();
+ expect(x.contains(10n)).toBeTruthy();
+ expect(x.contains(15n)).toBeTruthy();
+ expect(x.contains(20n)).toBeFalsy();
+ expect(x.contains(21n)).toBeFalsy();
+
+ expect(x.contains(mkSpan(12n, 18n))).toBeTruthy();
+ expect(x.contains(mkSpan(5n, 25n))).toBeFalsy();
+ expect(x.contains(mkSpan(5n, 15n))).toBeFalsy();
+ expect(x.contains(mkSpan(15n, 25n))).toBeFalsy();
+ expect(x.contains(mkSpan(0n, 10n))).toBeFalsy();
+ expect(x.contains(mkSpan(20n, 30n))).toBeFalsy();
+ });
+
+ it('checks intersection', () => {
+ const x = mkSpan(10n, 20n);
+
+ expect(x.intersects(mkSpan(0n, 10n))).toBeFalsy();
+ expect(x.intersects(mkSpan(5n, 15n))).toBeTruthy();
+ expect(x.intersects(mkSpan(12n, 18n))).toBeTruthy();
+ expect(x.intersects(mkSpan(15n, 25n))).toBeTruthy();
+ expect(x.intersects(mkSpan(20n, 30n))).toBeFalsy();
+ expect(x.intersects(mkSpan(5n, 25n))).toBeTruthy();
+ });
+
+ it('can add', () => {
+ const x = mkSpan(10n, 20n);
+ expect(x.add(5n)).toEqual(mkSpan(15n, 25n));
+ });
+
+ it('can pad', () => {
+ const x = mkSpan(10n, 20n);
+ expect(x.pad(5n)).toEqual(mkSpan(5n, 25n));
+ });
});
diff --git a/ui/src/common/track_data.ts b/ui/src/common/track_data.ts
index 9af7fff..a49a56d 100644
--- a/ui/src/common/track_data.ts
+++ b/ui/src/common/track_data.ts
@@ -12,12 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {TPDuration, TPTime} from './time';
+
// TODO(hjd): Refactor into method on TrackController
export const LIMIT = 10000;
export interface TrackData {
- start: number;
- end: number;
- resolution: number;
+ start: TPTime;
+ end: TPTime;
+ resolution: TPDuration;
length: number;
}
diff --git a/ui/src/controller/aggregation/counter_aggregation_controller.ts b/ui/src/controller/aggregation/counter_aggregation_controller.ts
index dd7f14a..e632f8e 100644
--- a/ui/src/controller/aggregation/counter_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/counter_aggregation_controller.ts
@@ -15,7 +15,7 @@
import {ColumnDef} from '../../common/aggregation_data';
import {Engine} from '../../common/engine';
import {Area, Sorting} from '../../common/state';
-import {toNs} from '../../common/time';
+import {tpDurationToSeconds} from '../../common/time';
import {globals} from '../../frontend/globals';
import {Config, COUNTER_TRACK_KIND} from '../../tracks/counter';
@@ -38,21 +38,22 @@
}
}
if (ids.length === 0) return false;
+ const duration = area.end - area.start;
+ const durationSec = tpDurationToSeconds(duration);
const query = `create view ${this.kind} as select
name,
count(1) as count,
- round(sum(weighted_value)/${
- toNs(area.endSec) - toNs(area.startSec)}, 2) as avg_value,
+ round(sum(weighted_value)/${duration}, 2) as avg_value,
last as last_value,
first as first_value,
max(last) - min(first) as delta_value,
- round((max(last) - min(first))/${area.endSec - area.startSec}, 2) as rate,
+ round((max(last) - min(first))/${durationSec}, 2) as rate,
min(value) as min_value,
max(value) as max_value
from
(select *,
- (min(ts + dur, ${toNs(area.endSec)}) - max(ts,${toNs(area.startSec)}))
+ (min(ts + dur, ${area.end}) - max(ts,${area.start}))
* value as weighted_value,
first_value(value) over
(partition by track_id order by ts) as first,
@@ -61,8 +62,8 @@
range between unbounded preceding and unbounded following) as last
from experimental_counter_dur
where track_id in (${ids})
- and ts + dur >= ${toNs(area.startSec)} and
- ts <= ${toNs(area.endSec)})
+ and ts + dur >= ${area.start} and
+ ts <= ${area.end})
join counter_track
on track_id = counter_track.id
group by track_id`;
diff --git a/ui/src/controller/aggregation/cpu_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
index 94d3950..452ca75 100644
--- a/ui/src/controller/aggregation/cpu_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
@@ -15,7 +15,6 @@
import {ColumnDef} from '../../common/aggregation_data';
import {Engine} from '../../common/engine';
import {Area, Sorting} from '../../common/state';
-import {toNs} from '../../common/time';
import {globals} from '../../frontend/globals';
import {Config, CPU_SLICE_TRACK_KIND} from '../../tracks/cpu_slices';
@@ -46,8 +45,8 @@
JOIN thread_state USING(utid)
WHERE cpu IN (${selectedCpus}) AND
state = "Running" AND
- thread_state.ts + thread_state.dur > ${toNs(area.startSec)} AND
- thread_state.ts < ${toNs(area.endSec)} group by utid`;
+ thread_state.ts + thread_state.dur > ${area.start} AND
+ thread_state.ts < ${area.end} group by utid`;
await engine.query(query);
return true;
diff --git a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
index b28e496..fc24d1e 100644
--- a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
@@ -15,7 +15,6 @@
import {ColumnDef} from '../../common/aggregation_data';
import {Engine} from '../../common/engine';
import {Area, Sorting} from '../../common/state';
-import {toNs} from '../../common/time';
import {globals} from '../../frontend/globals';
import {Config, CPU_SLICE_TRACK_KIND} from '../../tracks/cpu_slices';
@@ -45,8 +44,8 @@
JOIN thread_state USING(utid)
WHERE cpu IN (${selectedCpus}) AND
state = "Running" AND
- thread_state.ts + thread_state.dur > ${toNs(area.startSec)} AND
- thread_state.ts < ${toNs(area.endSec)} group by upid`;
+ thread_state.ts + thread_state.dur > ${area.start} AND
+ thread_state.ts < ${area.end} group by upid`;
await engine.query(query);
return true;
diff --git a/ui/src/controller/aggregation/frame_aggregation_controller.ts b/ui/src/controller/aggregation/frame_aggregation_controller.ts
index 97e1f86..a22a83d 100644
--- a/ui/src/controller/aggregation/frame_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/frame_aggregation_controller.ts
@@ -15,7 +15,6 @@
import {ColumnDef} from '../../common/aggregation_data';
import {Engine} from '../../common/engine';
import {Area, Sorting} from '../../common/state';
-import {toNs} from '../../common/time';
import {globals} from '../../frontend/globals';
import {
ACTUAL_FRAMES_SLICE_TRACK_KIND,
@@ -48,8 +47,8 @@
MAX(dur) as maxDur
FROM actual_frame_timeline_slice
WHERE track_id IN (${selectedSqlTrackIds}) AND
- ts + dur > ${toNs(area.startSec)} AND
- ts < ${toNs(area.endSec)} group by jank_type`;
+ ts + dur > ${area.start} AND
+ ts < ${area.end} group by jank_type`;
await engine.query(query);
return true;
diff --git a/ui/src/controller/aggregation/slice_aggregation_controller.ts b/ui/src/controller/aggregation/slice_aggregation_controller.ts
index ebb8000..8dcaccc 100644
--- a/ui/src/controller/aggregation/slice_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/slice_aggregation_controller.ts
@@ -15,7 +15,6 @@
import {ColumnDef} from '../../common/aggregation_data';
import {Engine} from '../../common/engine';
import {Area, Sorting} from '../../common/state';
-import {toNs} from '../../common/time';
import {globals} from '../../frontend/globals';
import {
ASYNC_SLICE_TRACK_KIND,
@@ -64,8 +63,8 @@
count(1) as occurrences
FROM slices
WHERE track_id IN (${selectedTrackIds}) AND
- ts + dur > ${toNs(area.startSec)} AND
- ts < ${toNs(area.endSec)} group by name`;
+ ts + dur > ${area.start} AND
+ ts < ${area.end} group by name`;
await engine.query(query);
return true;
diff --git a/ui/src/controller/aggregation/thread_aggregation_controller.ts b/ui/src/controller/aggregation/thread_aggregation_controller.ts
index ddfadae..6e288d5 100644
--- a/ui/src/controller/aggregation/thread_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/thread_aggregation_controller.ts
@@ -17,7 +17,6 @@
import {NUM, NUM_NULL, STR_NULL} from '../../common/query_result';
import {Area, Sorting} from '../../common/state';
import {translateState} from '../../common/thread_state';
-import {toNs} from '../../common/time';
import {globals} from '../../frontend/globals';
import {
Config,
@@ -60,8 +59,8 @@
JOIN thread USING(upid)
JOIN thread_state USING(utid)
WHERE utid IN (${this.utids}) AND
- thread_state.ts + thread_state.dur > ${toNs(area.startSec)} AND
- thread_state.ts < ${toNs(area.endSec)}
+ thread_state.ts + thread_state.dur > ${area.start} AND
+ thread_state.ts < ${area.end}
GROUP BY utid, concat_state
`;
@@ -78,8 +77,8 @@
JOIN thread USING(upid)
JOIN thread_state USING(utid)
WHERE utid IN (${this.utids}) AND thread_state.ts + thread_state.dur > ${
- toNs(area.startSec)} AND
- thread_state.ts < ${toNs(area.endSec)}
+ area.start} AND
+ thread_state.ts < ${area.end}
GROUP BY state, io_wait`;
const result = await engine.query(query);
diff --git a/ui/src/controller/area_selection_handler.ts b/ui/src/controller/area_selection_handler.ts
index b1d1c7e..32dcb11 100644
--- a/ui/src/controller/area_selection_handler.ts
+++ b/ui/src/controller/area_selection_handler.ts
@@ -36,10 +36,10 @@
// where `a ||= b` is formatted to `a || = b`, by inserting a space which
// breaks the operator.
// Therefore, we are using the pattern `a = a || b` instead.
- hasAreaChanged = hasAreaChanged ||
- selectedArea.startSec !== this.previousArea.startSec;
hasAreaChanged =
- hasAreaChanged || selectedArea.endSec !== this.previousArea.endSec;
+ hasAreaChanged || selectedArea.start !== this.previousArea.start;
+ hasAreaChanged =
+ hasAreaChanged || selectedArea.end !== this.previousArea.end;
hasAreaChanged = hasAreaChanged ||
selectedArea.tracks.length !== this.previousArea.tracks.length;
for (let i = 0; i < selectedArea.tracks.length; ++i) {
diff --git a/ui/src/controller/area_selection_handler_unittest.ts b/ui/src/controller/area_selection_handler_unittest.ts
index c5a27c0..caac678 100644
--- a/ui/src/controller/area_selection_handler_unittest.ts
+++ b/ui/src/controller/area_selection_handler_unittest.ts
@@ -20,7 +20,7 @@
test('validAreaAfterUndefinedArea', () => {
const areaId = '0';
- const latestArea: AreaById = {startSec: 0, endSec: 1, tracks: [], id: areaId};
+ const latestArea: AreaById = {start: 0n, end: 1n, tracks: [], id: areaId};
globals.state = createEmptyState();
globals.state.currentSelection = {kind: 'AREA', areaId};
globals.state.areas[areaId] = latestArea;
@@ -35,7 +35,7 @@
test('UndefinedAreaAfterValidArea', () => {
const previousAreaId = '0';
const previous:
- AreaById = {startSec: 0, endSec: 1, tracks: [], id: previousAreaId};
+ AreaById = {start: 0n, end: 1n, tracks: [], id: previousAreaId};
globals.state = createEmptyState();
globals.state.currentSelection = {
kind: 'AREA',
@@ -71,7 +71,7 @@
test('validAreaAfterValidArea', () => {
const previousAreaId = '0';
const previous:
- AreaById = {startSec: 0, endSec: 1, tracks: [], id: previousAreaId};
+ AreaById = {start: 0n, end: 1n, tracks: [], id: previousAreaId};
globals.state = createEmptyState();
globals.state.currentSelection = {
kind: 'AREA',
@@ -82,8 +82,7 @@
areaSelectionHandler.getAreaChange();
const currentAreaId = '1';
- const current:
- AreaById = {startSec: 1, endSec: 2, tracks: [], id: currentAreaId};
+ const current: AreaById = {start: 1n, end: 2n, tracks: [], id: currentAreaId};
globals.state.currentSelection = {
kind: 'AREA',
areaId: currentAreaId,
@@ -98,7 +97,7 @@
test('sameAreaSelected', () => {
const previousAreaId = '0';
const previous:
- AreaById = {startSec: 0, endSec: 1, tracks: [], id: previousAreaId};
+ AreaById = {start: 0n, end: 1n, tracks: [], id: previousAreaId};
globals.state = createEmptyState();
globals.state.currentSelection = {
kind: 'AREA',
@@ -109,8 +108,7 @@
areaSelectionHandler.getAreaChange();
const currentAreaId = '0';
- const current:
- AreaById = {startSec: 0, endSec: 1, tracks: [], id: currentAreaId};
+ const current: AreaById = {start: 0n, end: 1n, tracks: [], id: currentAreaId};
globals.state.currentSelection = {
kind: 'AREA',
areaId: currentAreaId,
@@ -128,7 +126,7 @@
areaSelectionHandler.getAreaChange();
globals.state
- .currentSelection = {kind: 'COUNTER', leftTs: 0, rightTs: 0, id: 1};
+ .currentSelection = {kind: 'COUNTER', leftTs: 0n, rightTs: 0n, id: 1};
const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
expect(hasAreaChanged).toEqual(false);
diff --git a/ui/src/controller/flamegraph_controller.ts b/ui/src/controller/flamegraph_controller.ts
index e94531d..31a74c3 100644
--- a/ui/src/controller/flamegraph_controller.ts
+++ b/ui/src/controller/flamegraph_controller.ts
@@ -27,7 +27,7 @@
} from '../common/flamegraph_util';
import {NUM, STR} from '../common/query_result';
import {CallsiteInfo, FlamegraphState, ProfileType} from '../common/state';
-import {toNs} from '../common/time';
+import {tpDurationToSeconds, TPTime} from '../common/time';
import {FlamegraphDetails, globals} from '../frontend/globals';
import {publishFlamegraphDetails} from '../frontend/publish';
import {
@@ -145,8 +145,8 @@
}
globals.dispatch(Actions.openFlamegraph({
upids,
- startNs: toNs(area.startSec),
- endNs: toNs(area.endSec),
+ start: area.start,
+ end: area.end,
type: ProfileType.PERF_SAMPLE,
viewingOption: PERF_SAMPLES_KEY,
}));
@@ -169,8 +169,8 @@
const selectedFlamegraphState = {...selection};
const flamegraphMetadata = await this.getFlamegraphMetadata(
selection.type,
- selectedFlamegraphState.startNs,
- selectedFlamegraphState.endNs,
+ selectedFlamegraphState.start,
+ selectedFlamegraphState.end,
selectedFlamegraphState.upids);
if (flamegraphMetadata !== undefined) {
Object.assign(this.flamegraphDetails, flamegraphMetadata);
@@ -192,7 +192,7 @@
selectedFlamegraphState.expandedCallsite.totalSize;
const key = `${selectedFlamegraphState.upids};${
- selectedFlamegraphState.startNs};${selectedFlamegraphState.endNs}`;
+ selectedFlamegraphState.start};${selectedFlamegraphState.end}`;
try {
const flamegraphData = await this.getFlamegraphData(
@@ -200,15 +200,15 @@
selectedFlamegraphState.viewingOption ?
selectedFlamegraphState.viewingOption :
DEFAULT_VIEWING_OPTION,
- selection.startNs,
- selection.endNs,
+ selection.start,
+ selection.end,
selectedFlamegraphState.upids,
selectedFlamegraphState.type,
selectedFlamegraphState.focusRegex);
if (flamegraphData !== undefined && selection &&
selection.kind === selectedFlamegraphState.kind &&
- selection.startNs === selectedFlamegraphState.startNs &&
- selection.endNs === selectedFlamegraphState.endNs) {
+ selection.start === selectedFlamegraphState.start &&
+ selection.end === selectedFlamegraphState.end) {
const expandedFlamegraphData =
expandCallsites(flamegraphData, expandedId);
this.prepareAndMergeCallsites(
@@ -230,8 +230,8 @@
private shouldRequestData(selection: FlamegraphState) {
return selection.kind === 'FLAMEGRAPH_STATE' &&
(this.lastSelectedFlamegraphState === undefined ||
- (this.lastSelectedFlamegraphState.startNs !== selection.startNs ||
- this.lastSelectedFlamegraphState.endNs !== selection.endNs ||
+ (this.lastSelectedFlamegraphState.start !== selection.start ||
+ this.lastSelectedFlamegraphState.end !== selection.end ||
this.lastSelectedFlamegraphState.type !== selection.type ||
!FlamegraphController.areArraysEqual(
this.lastSelectedFlamegraphState.upids, selection.upids) ||
@@ -267,7 +267,7 @@
}
async getFlamegraphData(
- baseKey: string, viewingOption: string, startNs: number, endNs: number,
+ baseKey: string, viewingOption: string, start: TPTime, end: TPTime,
upids: number[], type: ProfileType,
focusRegex: string): Promise<CallsiteInfo[]> {
let currentData: CallsiteInfo[];
@@ -280,8 +280,8 @@
// Collecting data for drawing flamegraph for selected profile.
// Data needs to be in following format:
// id, name, parent_id, depth, total_size
- const tableName = await this.prepareViewsAndTables(
- startNs, endNs, upids, type, focusRegex);
+ const tableName =
+ await this.prepareViewsAndTables(start, end, upids, type, focusRegex);
currentData = await this.getFlamegraphDataFromTables(
tableName, viewingOption, focusRegex);
this.flamegraphDatasets.set(key, currentData);
@@ -413,7 +413,7 @@
}
private async prepareViewsAndTables(
- startNs: number, endNs: number, upids: number[], type: ProfileType,
+ start: TPTime, end: TPTime, upids: number[], type: ProfileType,
focusRegex: string): Promise<string> {
// Creating unique names for views so we can reuse and not delete them
// for each marker.
@@ -437,8 +437,8 @@
cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
size, alloc_size, count, alloc_count, source_file, line_number
from experimental_flamegraph
- where profile_type = '${flamegraphType}' and ${startNs} <= ts and
- ts <= ${endNs} and ${upidConditional}
+ where profile_type = '${flamegraphType}' and ${start} <= ts and
+ ts <= ${end} and ${upidConditional}
${focusRegexConditional}`);
}
return this.cache.getTableName(
@@ -447,7 +447,7 @@
size, alloc_size, count, alloc_count, source_file, line_number
from experimental_flamegraph
where profile_type = '${flamegraphType}'
- and ts = ${endNs}
+ and ts = ${end}
and upid = ${upids[0]}
${focusRegexConditional}`);
}
@@ -455,7 +455,8 @@
getMinSizeDisplayed(flamegraphData: CallsiteInfo[], rootSize?: number):
number {
const timeState = globals.state.frontendLocalState.visibleState;
- let width = (timeState.endSec - timeState.startSec) / timeState.resolution;
+ const dur = globals.stateVisibleTime().duration;
+ let width = tpDurationToSeconds(dur / timeState.resolution);
// TODO(168048193): Remove screen size hack:
width = Math.max(width, 800);
if (rootSize === undefined) {
@@ -465,11 +466,12 @@
}
async getFlamegraphMetadata(
- type: ProfileType, startNs: number, endNs: number, upids: number[]) {
+ type: ProfileType, start: TPTime, end: TPTime,
+ upids: number[]): Promise<FlamegraphDetails|undefined> {
// Don't do anything if selection of the marker stayed the same.
if ((this.lastSelectedFlamegraphState !== undefined &&
- ((this.lastSelectedFlamegraphState.startNs === startNs &&
- this.lastSelectedFlamegraphState.endNs === endNs &&
+ ((this.lastSelectedFlamegraphState.start === start &&
+ this.lastSelectedFlamegraphState.end === end &&
FlamegraphController.areArraysEqual(
this.lastSelectedFlamegraphState.upids, upids))))) {
return undefined;
@@ -486,7 +488,7 @@
for (let i = 0; it.valid(); ++i, it.next()) {
pids.push(it.pid);
}
- return {startNs, durNs: endNs - startNs, pids, upids, type};
+ return {start, dur: end - start, pids, upids, type};
}
private static areArraysEqual(a: number[], b: number[]) {
diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/controller/flow_events_controller.ts
index d89b514..07125e1 100644
--- a/ui/src/controller/flow_events_controller.ts
+++ b/ui/src/controller/flow_events_controller.ts
@@ -16,7 +16,7 @@
import {featureFlags} from '../common/feature_flags';
import {NUM, STR_NULL} from '../common/query_result';
import {Area} from '../common/state';
-import {fromNs, toNs} from '../common/time';
+import {fromNs} from '../common/time';
import {Flow, globals} from '../frontend/globals';
import {publishConnectedFlows, publishSelectedFlows} from '../frontend/publish';
import {
@@ -241,8 +241,8 @@
const area = globals.state.areas[areaId];
if (this.lastSelectedKind === 'AREA' && this.lastSelectedArea &&
this.lastSelectedArea.tracks.join(',') === area.tracks.join(',') &&
- this.lastSelectedArea.endSec === area.endSec &&
- this.lastSelectedArea.startSec === area.startSec) {
+ this.lastSelectedArea.end === area.end &&
+ this.lastSelectedArea.start === area.start) {
return;
}
@@ -268,8 +268,8 @@
const tracks = `(${trackIds.join(',')})`;
- const startNs = toNs(area.startSec);
- const endNs = toNs(area.endSec);
+ const startNs = area.start;
+ const endNs = area.end;
const query = `
select
diff --git a/ui/src/controller/ftrace_controller.ts b/ui/src/controller/ftrace_controller.ts
index f558a4a..a071ec5 100644
--- a/ui/src/controller/ftrace_controller.ts
+++ b/ui/src/controller/ftrace_controller.ts
@@ -13,9 +13,13 @@
// limitations under the License.
import {Engine} from '../common/engine';
-import {NUM, STR, STR_NULL} from '../common/query_result';
+import {
+ HighPrecisionTime,
+ HighPrecisionTimeSpan,
+} from '../common/high_precision_time';
+import {LONG, NUM, STR, STR_NULL} from '../common/query_result';
import {FtraceFilterState, Pagination} from '../common/state';
-import {TimeSpan, toNsCeil, toNsFloor} from '../common/time';
+import {Span} from '../common/time';
import {FtraceEvent, globals} from '../frontend/globals';
import {publishFtracePanelData} from '../frontend/publish';
import {ratelimit} from '../frontend/rate_limiters';
@@ -34,7 +38,7 @@
export class FtraceController extends Controller<'main'> {
private engine: Engine;
- private oldSpan: TimeSpan = new TimeSpan(0, 0);
+ private oldSpan: Span<HighPrecisionTime> = HighPrecisionTimeSpan.ZERO;
private oldFtraceFilter?: FtraceFilterState;
private oldPagination?: Pagination;
@@ -45,7 +49,7 @@
run() {
if (this.shouldUpdate()) {
- this.oldSpan = globals.frontendLocalState.visibleWindowTime.clone();
+ this.oldSpan = globals.frontendLocalState.visibleWindowTime;
this.oldFtraceFilter = globals.state.ftraceFilter;
this.oldPagination = globals.state.ftracePagination;
if (globals.state.ftracePagination.count > 0) {
@@ -66,8 +70,7 @@
private shouldUpdate(): boolean {
// Has the visible window moved?
const visibleWindow = globals.frontendLocalState.visibleWindowTime;
- if (this.oldSpan.start !== visibleWindow.start ||
- this.oldSpan.end !== visibleWindow.end) {
+ if (!this.oldSpan.equals(visibleWindow)) {
return true;
}
@@ -89,8 +92,8 @@
const frontendState = globals.frontendLocalState;
const {start, end} = frontendState.visibleWindowTime;
- const startNs = toNsFloor(start);
- const endNs = toNsCeil(end);
+ const startNs = start.nanos;
+ const endNs = end.nanos;
const excludeList = appState.ftraceFilter.excludedNames;
const excludeListSql = excludeList.map((s) => `'${s}'`).join(',');
@@ -132,7 +135,7 @@
const it = queryRes.iter(
{
id: NUM,
- ts: NUM,
+ ts: LONG,
name: STR,
cpu: NUM,
thread: STR_NULL,
diff --git a/ui/src/controller/logs_controller.ts b/ui/src/controller/logs_controller.ts
index d8f9b3a..c931ce0 100644
--- a/ui/src/controller/logs_controller.ts
+++ b/ui/src/controller/logs_controller.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {BigintMath} from '../base/bigint_math';
import {Engine} from '../common/engine';
import {
LogBounds,
@@ -20,62 +21,61 @@
LogEntriesKey,
LogExistsKey,
} from '../common/logs';
-import {NUM, STR} from '../common/query_result';
+import {LONG, LONG_NULL, NUM, STR} from '../common/query_result';
import {escapeGlob, escapeQuery} from '../common/query_utils';
import {LogFilteringCriteria} from '../common/state';
-import {fromNs, TimeSpan, toNsCeil, toNsFloor} from '../common/time';
+import {Span} from '../common/time';
+import {
+ TPTime,
+ TPTimeSpan,
+} from '../common/time';
import {globals} from '../frontend/globals';
import {publishTrackData} from '../frontend/publish';
import {Controller} from './controller';
async function updateLogBounds(
- engine: Engine, span: TimeSpan): Promise<LogBounds> {
- const vizStartNs = toNsFloor(span.start);
- const vizEndNs = toNsCeil(span.end);
+ engine: Engine, span: Span<TPTime>): Promise<LogBounds> {
+ const vizStartNs = span.start;
+ const vizEndNs = span.end;
- const countResult = await engine.query(`select
- ifnull(min(ts), 0) as minTs,
- ifnull(max(ts), 0) as maxTs,
- count(ts) as countTs
- from filtered_logs
- where ts >= ${vizStartNs}
- and ts <= ${vizEndNs}`);
+ const vizFilter = `ts between ${vizStartNs} and ${vizEndNs}`;
- const countRow = countResult.firstRow({minTs: NUM, maxTs: NUM, countTs: NUM});
+ const result = await engine.query(`select
+ min(ts) as minTs,
+ max(ts) as maxTs,
+ min(case when ${vizFilter} then ts end) as minVizTs,
+ max(case when ${vizFilter} then ts end) as maxVizTs,
+ count(case when ${vizFilter} then ts end) as countTs
+ from filtered_logs`);
- const firstRowNs = countRow.minTs;
- const lastRowNs = countRow.maxTs;
- const total = countRow.countTs;
+ const data = result.firstRow({
+ minTs: LONG_NULL,
+ maxTs: LONG_NULL,
+ minVizTs: LONG_NULL,
+ maxVizTs: LONG_NULL,
+ countTs: NUM,
+ });
- const minResult = await engine.query(`
- select ifnull(max(ts), 0) as maxTs from filtered_logs where ts < ${
- vizStartNs}`);
- const startNs = minResult.firstRow({maxTs: NUM}).maxTs;
+ const firstLogTs = data.minTs ?? 0n;
+ const lastLogTs = data.maxTs ?? BigintMath.INT64_MAX;
- const maxResult = await engine.query(`
- select ifnull(min(ts), 0) as minTs from filtered_logs where ts > ${
- vizEndNs}`);
- const endNs = maxResult.firstRow({minTs: NUM}).minTs;
-
- const startTs = startNs ? fromNs(startNs) : 0;
- const endTs = endNs ? fromNs(endNs) : Number.MAX_SAFE_INTEGER;
- const firstRowTs = firstRowNs ? fromNs(firstRowNs) : endTs;
- const lastRowTs = lastRowNs ? fromNs(lastRowNs) : startTs;
- return {
- startTs,
- endTs,
- firstRowTs,
- lastRowTs,
- total,
+ const bounds: LogBounds = {
+ firstLogTs,
+ lastLogTs,
+ firstVisibleLogTs: data.minVizTs ?? firstLogTs,
+ lastVisibleLogTs: data.maxVizTs ?? lastLogTs,
+ totalVisibleLogs: data.countTs,
};
+
+ return bounds;
}
async function updateLogEntries(
- engine: Engine, span: TimeSpan, pagination: Pagination):
+ engine: Engine, span: Span<TPTime>, pagination: Pagination):
Promise<LogEntries> {
- const vizStartNs = toNsFloor(span.start);
- const vizEndNs = toNsCeil(span.end);
+ const vizStartNs = span.start;
+ const vizEndNs = span.end;
const vizSqlBounds = `ts >= ${vizStartNs} and ts <= ${vizEndNs}`;
const rowsResult = await engine.query(`
@@ -101,7 +101,7 @@
const processName = [];
const it = rowsResult.iter({
- ts: NUM,
+ ts: LONG,
prio: NUM,
tag: STR,
msg: STR,
@@ -179,7 +179,7 @@
*/
export class LogsController extends Controller<'main'> {
private engine: Engine;
- private span: TimeSpan;
+ private span: Span<TPTime>;
private pagination: Pagination;
private hasLogs = false;
private logFilteringCriteria?: LogFilteringCriteria;
@@ -189,7 +189,7 @@
constructor(args: LogsControllerArgs) {
super('main');
this.engine = args.engine;
- this.span = new TimeSpan(0, 10);
+ this.span = new TPTimeSpan(0n, BigInt(10e9));
this.pagination = new Pagination(0, 0);
this.hasAnyLogs().then((exists) => {
this.hasLogs = exists;
@@ -226,8 +226,7 @@
}
private async updateLogTracks() {
- const traceTime = globals.state.frontendLocalState.visibleState;
- const newSpan = new TimeSpan(traceTime.startSec, traceTime.endSec);
+ const newSpan = globals.stateVisibleTime();
const oldSpan = this.span;
const pagination = globals.state.logsPagination;
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index d1df094..c5968c2 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -12,13 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {BigintMath} from '../base/bigint_math';
import {sqliteString} from '../base/string_utils';
import {Engine} from '../common/engine';
import {NUM, STR} from '../common/query_result';
import {escapeSearchQuery} from '../common/query_utils';
import {CurrentSearchResults, SearchSummary} from '../common/search_data';
-import {TimeSpan} from '../common/time';
-import {toNs} from '../common/time';
+import {Span} from '../common/time';
+import {
+ TPDuration,
+ TPTime,
+ TPTimeSpan,
+} from '../common/time';
import {globals} from '../frontend/globals';
import {publishSearch, publishSearchResult} from '../frontend/publish';
@@ -30,8 +35,8 @@
export class SearchController extends Controller<'main'> {
private engine: Engine;
- private previousSpan: TimeSpan;
- private previousResolution: number;
+ private previousSpan: Span<TPTime>;
+ private previousResolution: TPDuration;
private previousSearch: string;
private updateInProgress: boolean;
private setupInProgress: boolean;
@@ -39,11 +44,11 @@
constructor(args: SearchControllerArgs) {
super('main');
this.engine = args.engine;
- this.previousSpan = new TimeSpan(0, 1);
+ this.previousSpan = new TPTimeSpan(0n, 1n);
this.previousSearch = '';
this.updateInProgress = false;
this.setupInProgress = true;
- this.previousResolution = 1;
+ this.previousResolution = 1n;
this.setup().finally(() => {
this.setupInProgress = false;
this.run();
@@ -70,9 +75,9 @@
omniboxState.mode === 'COMMAND') {
return;
}
- const newSpan = new TimeSpan(visibleState.startSec, visibleState.endSec);
+ const newSpan = globals.stateVisibleTime();
const newSearch = omniboxState.omnibox;
- let newResolution = visibleState.resolution;
+ const newResolution = visibleState.resolution;
if (this.previousSpan.contains(newSpan) &&
this.previousResolution === newResolution &&
newSearch === this.previousSearch) {
@@ -83,9 +88,8 @@
// TODO(hjd): We should restrict this to the start of the trace but
// that is not easily available here.
// N.B. Timestamps can be negative.
- const start = newSpan.start - newSpan.duration;
- const end = newSpan.end + newSpan.duration;
- this.previousSpan = new TimeSpan(start, end);
+ const {start, end} = newSpan.pad(newSpan.duration);
+ this.previousSpan = new TPTimeSpan(start, end);
this.previousResolution = newResolution;
this.previousSearch = newSearch;
if (newSearch === '' || newSearch.length < 4) {
@@ -105,25 +109,12 @@
return;
}
- let startNs = toNs(newSpan.start);
- let endNs = toNs(newSpan.end);
-
- // TODO(hjd): We shouldn't need to be so defensive here:
- if (!Number.isFinite(startNs)) {
- startNs = 0;
- }
- if (!Number.isFinite(endNs)) {
- endNs = 1;
- }
- if (!Number.isFinite(newResolution)) {
- newResolution = 1;
- }
-
this.updateInProgress = true;
- const computeSummary = this.update(newSearch, startNs, endNs, newResolution)
- .then((summary) => {
- publishSearch(summary);
- });
+ const computeSummary =
+ this.update(newSearch, newSpan.start, newSpan.end, newResolution)
+ .then((summary) => {
+ publishSearch(summary);
+ });
const computeResults =
this.specificSearch(newSearch).then((searchResults) => {
@@ -140,15 +131,14 @@
onDestroy() {}
private async update(
- search: string, startNs: number, endNs: number,
- resolution: number): Promise<SearchSummary> {
- const quantumNs = Math.round(resolution * 10 * 1e9);
-
+ search: string, startNs: TPTime, endNs: TPTime,
+ resolution: TPDuration): Promise<SearchSummary> {
const searchLiteral = escapeSearchQuery(search);
- startNs = Math.floor(startNs / quantumNs) * quantumNs;
+ const quantumNs = resolution * 10n;
+ startNs = BigintMath.quantizeFloor(startNs, quantumNs);
- const windowDur = Math.max(endNs - startNs, 1);
+ const windowDur = BigintMath.max(endNs - startNs, 1n);
await this.query(`update search_summary_window set
window_start=${startNs},
window_dur=${windowDur},
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 66cf3d0..b5a3907 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -16,14 +16,23 @@
import {Arg, Args} from '../common/arg_types';
import {Engine} from '../common/engine';
import {
+ LONG,
NUM,
NUM_NULL,
STR,
STR_NULL,
} from '../common/query_result';
import {ChromeSliceSelection} from '../common/state';
-import {fromNs, toNs} from '../common/time';
-import {SliceDetails, ThreadStateDetails} from '../frontend/globals';
+import {
+ tpDurationFromSql,
+ TPTime,
+ tpTimeFromSql,
+} from '../common/time';
+import {
+ CounterDetails,
+ SliceDetails,
+ ThreadStateDetails,
+} from '../frontend/globals';
import {globals} from '../frontend/globals';
import {
publishCounterDetails,
@@ -176,10 +185,10 @@
case 'id':
break;
case 'ts':
- ts = fromNs(Number(v)) - globals.state.traceTime.startSec;
+ ts = tpTimeFromSql(v);
break;
case 'thread_ts':
- threadTs = fromNs(Number(v));
+ threadTs = tpTimeFromSql(v);
break;
case 'absTime':
if (v) absTime = `${v}`;
@@ -188,10 +197,10 @@
name = `${v}`;
break;
case 'dur':
- dur = fromNs(Number(v));
+ dur = tpDurationFromSql(v);
break;
case 'thread_dur':
- threadDur = fromNs(Number(v));
+ threadDur = tpDurationFromSql(v);
break;
case 'category':
case 'cat':
@@ -326,13 +335,13 @@
const selection = globals.state.currentSelection;
if (result.numRows() > 0 && selection) {
const row = result.firstRow({
- ts: NUM,
- dur: NUM,
+ ts: LONG,
+ dur: LONG,
});
- const ts = row.ts;
- const timeFromStart = fromNs(ts) - globals.state.traceTime.startSec;
- const dur = fromNs(row.dur);
- const selected: ThreadStateDetails = {ts: timeFromStart, dur};
+ const selected: ThreadStateDetails = {
+ ts: row.ts,
+ dur: row.dur,
+ };
publishThreadStateDetails(selected);
}
}
@@ -353,8 +362,8 @@
const selection = globals.state.currentSelection;
if (result.numRows() > 0 && selection) {
const row = result.firstRow({
- ts: NUM,
- dur: NUM,
+ ts: LONG,
+ dur: LONG,
priority: NUM,
endState: STR_NULL,
utid: NUM,
@@ -362,15 +371,14 @@
threadStateId: NUM_NULL,
});
const ts = row.ts;
- const timeFromStart = fromNs(ts) - globals.state.traceTime.startSec;
- const dur = fromNs(row.dur);
+ const dur = row.dur;
const priority = row.priority;
const endState = row.endState;
const utid = row.utid;
const cpu = row.cpu;
const threadStateId = row.threadStateId || undefined;
const selected: SliceDetails = {
- ts: timeFromStart,
+ ts,
dur,
priority,
endState,
@@ -391,7 +399,8 @@
}
}
- async counterDetails(ts: number, rightTs: number, id: number) {
+ async counterDetails(ts: TPTime, rightTs: TPTime, id: number):
+ Promise<CounterDetails> {
const counter = await this.args.engine.query(
`SELECT value, track_id as trackId FROM counter WHERE id = ${id}`);
const row = counter.iter({
@@ -407,17 +416,15 @@
IFNULL(value, 0) as value
FROM counter WHERE ts < ${ts} and track_id = ${trackId}`);
const previousValue = previous.firstRow({value: NUM}).value;
- const endTs =
- rightTs !== -1 ? rightTs : toNs(globals.state.traceTime.endSec);
+ const endTs = rightTs !== -1n ? rightTs : globals.state.traceTime.end;
const delta = value - previousValue;
const duration = endTs - ts;
- const startTime = fromNs(ts) - globals.state.traceTime.startSec;
const uiTrackId = globals.state.uiTrackIdByTraceTrackId[trackId];
const name = uiTrackId ? globals.state.tracks[uiTrackId].name : undefined;
- return {startTime, value, delta, duration, name};
+ return {startTime: ts, value, delta, duration, name};
}
- async schedulingDetails(ts: number, utid: number|Long) {
+ async schedulingDetails(ts: TPTime, utid: number|Long) {
// Find the ts of the first wakeup before the current slice.
const wakeResult = await this.args.engine.query(`
select ts, waker_utid as wakerUtid
@@ -430,7 +437,7 @@
return undefined;
}
- const wakeFirstRow = wakeResult.firstRow({ts: NUM, wakerUtid: NUM_NULL});
+ const wakeFirstRow = wakeResult.firstRow({ts: LONG, wakerUtid: NUM_NULL});
const wakeupTs = wakeFirstRow.ts;
const wakerUtid = wakeFirstRow.wakerUtid;
if (wakerUtid === null) {
@@ -449,7 +456,7 @@
// If this is the first sched slice for this utid or if the wakeup found
// was after the previous slice then we know the wakeup was for this slice.
if (prevSchedResult.numRows() !== 0 &&
- wakeupTs < prevSchedResult.firstRow({ts: NUM}).ts) {
+ wakeupTs < prevSchedResult.firstRow({ts: LONG}).ts) {
return undefined;
}
@@ -468,7 +475,7 @@
}
const wakerRow = wakerResult.firstRow({cpu: NUM});
- return {wakeupTs: fromNs(wakeupTs), wakerUtid, wakerCpu: wakerRow.cpu};
+ return {wakeupTs, wakerUtid, wakerCpu: wakerRow.cpu};
}
async computeThreadDetails(utid: number):
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 264ce93..f0e2d02 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {BigintMath} from '../base/bigint_math';
import {assertExists, assertTrue} from '../base/logging';
import {
Actions,
@@ -20,15 +21,30 @@
import {cacheTrace} from '../common/cache_manager';
import {Engine} from '../common/engine';
import {featureFlags, Flag, PERF_SAMPLE_FLAG} from '../common/feature_flags';
+import {
+ HighPrecisionTime,
+ HighPrecisionTimeSpan,
+} from '../common/high_precision_time';
import {HttpRpcEngine} from '../common/http_rpc_engine';
import {
getEnabledMetatracingCategories,
isMetatracingEnabled,
} from '../common/metatracing';
-import {NUM, NUM_NULL, QueryError, STR, STR_NULL} from '../common/query_result';
+import {
+ LONG,
+ NUM,
+ NUM_NULL,
+ QueryError,
+ STR,
+ STR_NULL,
+} from '../common/query_result';
import {onSelectionChanged} from '../common/selection_observer';
import {defaultTraceTime, EngineMode, ProfileType} from '../common/state';
-import {TimeSpan, toNs, toNsCeil, toNsFloor} from '../common/time';
+import {Span} from '../common/time';
+import {
+ TPTime,
+ TPTimeSpan,
+} from '../common/time';
import {resetEngineWorker, WasmEngineProxy} from '../common/wasm_engine_proxy';
import {BottomTabList} from '../frontend/bottom_tab';
import {
@@ -416,11 +432,11 @@
const traceUuid = await this.cacheCurrentTrace();
const traceTime = await this.engine.getTraceTimeBounds();
- const startSec = traceTime.start;
- const endSec = traceTime.end;
+ const start = traceTime.start;
+ const end = traceTime.end;
const traceTimeState = {
- startSec,
- endSec,
+ start,
+ end,
};
const shownJsonWarning =
@@ -453,16 +469,16 @@
Actions.setTraceTime(traceTimeState),
];
- const [startVisibleTime, endVisibleTime] =
- await computeVisibleTime(startSec, endSec, isJsonTrace, this.engine);
+ const visibleTimeSpan = await computeVisibleTime(
+ traceTime.start, traceTime.end, isJsonTrace, this.engine);
// We don't know the resolution at this point. However this will be
// replaced in 50ms so a guess is fine.
- const resolution = (endVisibleTime - startVisibleTime) / 1000;
+ const resolution = visibleTimeSpan.duration.divide(1000).toTPTime();
actions.push(Actions.setVisibleTraceTime({
- startSec: startVisibleTime,
- endSec: endVisibleTime,
+ start: visibleTimeSpan.start.toTPTime(),
+ end: visibleTimeSpan.end.toTPTime(),
lastUpdate: Date.now() / 1000,
- resolution,
+ resolution: BigintMath.max(resolution, 1n),
}));
globals.dispatchMultiple(actions);
@@ -540,8 +556,8 @@
if (profile.numRows() !== 1) return;
const row = profile.firstRow({upid: NUM});
const upid = row.upid;
- const leftTs = toNs(globals.state.traceTime.startSec);
- const rightTs = toNs(globals.state.traceTime.endSec);
+ const leftTs = globals.state.traceTime.start;
+ const rightTs = globals.state.traceTime.end;
globals.dispatch(Actions.selectPerfSamples(
{id: 0, upid, leftTs, rightTs, type: ProfileType.PERF_SAMPLE}));
}
@@ -560,7 +576,7 @@
order by ts limit 1`;
const profile = await assertExists(this.engine).query(query);
if (profile.numRows() !== 1) return;
- const row = profile.firstRow({ts: NUM, type: STR, upid: NUM});
+ const row = profile.firstRow({ts: LONG, type: STR, upid: NUM});
const ts = row.ts;
const type = profileType(row.type);
const upid = row.upid;
@@ -610,33 +626,32 @@
publishThreads(threads);
}
- private async loadTimelineOverview(traceTime: TimeSpan) {
+ private async loadTimelineOverview(trace: Span<TPTime>) {
clearOverviewData();
const engine = assertExists<Engine>(this.engine);
- const numSteps = 100;
- const stepSec = traceTime.duration / numSteps;
+ const stepSize = BigintMath.max(1n, trace.duration / 100n);
let hasSchedOverview = false;
- for (let step = 0; step < numSteps; step++) {
+ for (let start = trace.start; start < trace.end; start += stepSize) {
+ const progress = start - trace.start;
+ const ratio = Number(progress) / Number(trace.duration);
this.updateStatus(
- 'Loading overview ' +
- `${Math.round((step + 1) / numSteps * 1000) / 10}%`);
- const startSec = traceTime.start + step * stepSec;
- const startNs = toNsFloor(startSec);
- const endSec = startSec + stepSec;
- const endNs = toNsCeil(endSec);
+ 'Loading overview ' +
+ `${Math.round(ratio * 100)}%`);
+ const end = start + stepSize;
// Sched overview.
const schedResult = await engine.query(
- `select sum(dur)/${stepSec}/1e9 as load, cpu from sched ` +
- `where ts >= ${startNs} and ts < ${endNs} and utid != 0 ` +
- 'group by cpu order by cpu');
+ `select cast(sum(dur) as float)/${
+ stepSize} as load, cpu from sched ` +
+ `where ts >= ${start} and ts < ${end} and utid != 0 ` +
+ 'group by cpu order by cpu');
const schedData: {[key: string]: QuantizedLoad} = {};
const it = schedResult.iter({load: NUM, cpu: NUM});
for (; it.valid(); it.next()) {
const load = it.load;
const cpu = it.cpu;
- schedData[cpu] = {startSec, endSec, load};
+ schedData[cpu] = {start, end, load};
hasSchedOverview = true;
}
publishOverviewData(schedData);
@@ -647,16 +662,15 @@
}
// Slices overview.
- const traceStartNs = toNs(traceTime.start);
- const stepSecNs = toNs(stepSec);
const sliceResult = await engine.query(`select
bucket,
upid,
- ifnull(sum(utid_sum) / cast(${stepSecNs} as float), 0) as load
+ ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
from thread
inner join (
select
- ifnull(cast((ts - ${traceStartNs})/${stepSecNs} as int), 0) as bucket,
+ ifnull(cast((ts - ${trace.start})/${
+ stepSize} as int), 0) as bucket,
sum(dur) as utid_sum,
utid
from slice
@@ -667,21 +681,21 @@
group by bucket, upid`);
const slicesData: {[key: string]: QuantizedLoad[]} = {};
- const it = sliceResult.iter({bucket: NUM, upid: NUM, load: NUM});
+ const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM});
for (; it.valid(); it.next()) {
const bucket = it.bucket;
const upid = it.upid;
const load = it.load;
- const startSec = traceTime.start + stepSec * bucket;
- const endSec = startSec + stepSec;
+ const start = trace.start + stepSize * bucket;
+ const end = start + stepSize;
const upidStr = upid.toString();
let loadArray = slicesData[upidStr];
if (loadArray === undefined) {
loadArray = slicesData[upidStr] = [];
}
- loadArray.push({startSec, endSec, load});
+ loadArray.push({start, end, load});
}
publishOverviewData(slicesData);
}
@@ -892,48 +906,48 @@
}
}
-async function computeTraceReliableRangeStart(engine: Engine): Promise<number> {
+async function computeTraceReliableRangeStart(engine: Engine): Promise<TPTime> {
const result =
await engine.query(`SELECT RUN_METRIC('chrome/chrome_reliable_range.sql');
SELECT start FROM chrome_reliable_range`);
- const bounds = result.firstRow({start: NUM});
- return bounds.start / 1e9;
+ const bounds = result.firstRow({start: LONG});
+ return bounds.start;
}
async function computeVisibleTime(
- traceStartSec: number,
- traceEndSec: number,
- isJsonTrace: boolean,
- engine: Engine): Promise<[number, number]> {
+ traceStart: TPTime, traceEnd: TPTime, isJsonTrace: boolean, engine: Engine):
+ Promise<Span<HighPrecisionTime>> {
// if we have non-default visible state, update the visible time to it
- const previousVisibleState = globals.state.frontendLocalState.visibleState;
- if (!(previousVisibleState.startSec === defaultTraceTime.startSec &&
- previousVisibleState.endSec === defaultTraceTime.endSec) &&
- (previousVisibleState.startSec >= traceStartSec &&
- previousVisibleState.endSec <= traceEndSec)) {
- return [previousVisibleState.startSec, previousVisibleState.endSec];
+ const previousVisibleState = globals.stateVisibleTime();
+ const defaultTraceSpan =
+ new TPTimeSpan(defaultTraceTime.start, defaultTraceTime.end);
+ if (!(previousVisibleState.start === defaultTraceSpan.start &&
+ previousVisibleState.end === defaultTraceSpan.end) &&
+ (previousVisibleState.start >= traceStart &&
+ previousVisibleState.end <= traceEnd)) {
+ return HighPrecisionTimeSpan.fromTpTime(
+ previousVisibleState.start, previousVisibleState.end);
}
// initialise visible time to the trace time bounds
- let visibleStartSec = traceStartSec;
- let visibleEndSec = traceEndSec;
+ let visibleStartSec = traceStart;
+ let visibleEndSec = traceEnd;
// compare start and end with metadata computed by the trace processor
const mdTime = await engine.getTracingMetadataTimeBounds();
// make sure the bounds hold
- if (Math.max(visibleStartSec, mdTime.start) <
- Math.min(visibleEndSec, mdTime.end)) {
- visibleStartSec =
- Math.max(visibleStartSec, mdTime.start);
- visibleEndSec = Math.min(visibleEndSec, mdTime.end);
+ if (BigintMath.max(visibleStartSec, mdTime.start) <
+ BigintMath.min(visibleEndSec, mdTime.end)) {
+ visibleStartSec = BigintMath.max(visibleStartSec, mdTime.start);
+ visibleEndSec = BigintMath.min(visibleEndSec, mdTime.end);
}
// Trace Processor doesn't support the reliable range feature for JSON
// traces.
if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG.get()) {
const reliableRangeStart = await computeTraceReliableRangeStart(engine);
- visibleStartSec = Math.max(visibleStartSec, reliableRangeStart);
+ visibleStartSec = BigintMath.max(visibleStartSec, reliableRangeStart);
}
- return [visibleStartSec, visibleEndSec];
+ return HighPrecisionTimeSpan.fromTpTime(visibleStartSec, visibleEndSec);
}
diff --git a/ui/src/controller/track_controller.ts b/ui/src/controller/track_controller.ts
index 46d1f1f..73b9896 100644
--- a/ui/src/controller/track_controller.ts
+++ b/ui/src/controller/track_controller.ts
@@ -12,11 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {BigintMath} from '../base/bigint_math';
import {assertExists, assertTrue} from '../base/logging';
import {Engine} from '../common/engine';
import {Registry} from '../common/registry';
import {TraceTime, TrackState} from '../common/state';
-import {fromNs, toNs} from '../common/time';
+import {
+ TPDuration,
+ TPTime,
+ tpTimeFromSeconds,
+ TPTimeSpan,
+} from '../common/time';
import {LIMIT, TrackData} from '../common/track_data';
import {globals} from '../frontend/globals';
import {publishTrackData} from '../frontend/publish';
@@ -70,7 +76,7 @@
// Must be overridden by the track implementation. Is invoked when the track
// frontend runs out of cached data. The derived track controller is expected
// to publish new track data in response to this call.
- abstract onBoundsChange(start: number, end: number, resolution: number):
+ abstract onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data>;
get trackState(): TrackState {
@@ -129,6 +135,7 @@
}
shouldRequestData(traceTime: TraceTime): boolean {
+ const tspan = new TPTimeSpan(traceTime.start, traceTime.end);
if (this.data === undefined) return true;
if (this.shouldReload()) return true;
@@ -137,15 +144,14 @@
if (atLimit) {
// We request more data than the window, so add window duration to find
// the previous window.
- const prevWindowStart =
- this.data.start + (traceTime.startSec - traceTime.endSec);
- return traceTime.startSec !== prevWindowStart;
+ const prevWindowStart = this.data.start + tspan.duration;
+ return tspan.start !== prevWindowStart;
}
// Otherwise request more data only when out of range of current data or
// resolution has changed.
- const inRange = traceTime.startSec >= this.data.start &&
- traceTime.endSec <= this.data.end;
+ const inRange =
+ tspan.start >= this.data.start && tspan.end <= this.data.end;
return !inRange ||
this.data.resolution !==
globals.state.frontendLocalState.visibleState.resolution;
@@ -162,8 +168,7 @@
return undefined;
}
- const bounds = globals.state.traceTime;
- const traceDurNs = toNs(bounds.endSec - bounds.startSec);
+ const traceDuration = globals.stateTraceTime().duration;
// For large traces, going through the raw table in the most zoomed-out
// states can be very expensive as this can involve going through O(millions
@@ -198,7 +203,7 @@
// Compute the outermost bucket size. This acts as a starting point for
// computing the cached size.
const outermostResolutionLevel =
- Math.ceil(Math.log2(traceDurNs / approxWidthPx));
+ Math.ceil(Math.log2(traceDuration.nanos / approxWidthPx));
const outermostBucketNs = Math.pow(2, outermostResolutionLevel);
// This constant decides how many resolution levels down from our outermost
@@ -232,11 +237,11 @@
run() {
const visibleState = globals.state.frontendLocalState.visibleState;
- if (visibleState === undefined || visibleState.resolution === undefined ||
- visibleState.resolution === Infinity) {
+ if (visibleState === undefined) {
return;
}
- const dur = visibleState.endSec - visibleState.startSec;
+ const visibleTimeSpan = globals.stateVisibleTime();
+ const dur = visibleTimeSpan.duration;
if (globals.state.visibleTracks.includes(this.trackId) &&
this.shouldRequestData(visibleState)) {
if (this.requestingData) {
@@ -253,16 +258,14 @@
.then(() => {
this.isSetup = true;
let resolution = visibleState.resolution;
- // TODO(hjd): We shouldn't have to be so defensive here.
- if (Math.log2(toNs(resolution)) % 1 !== 0) {
- // resolution is in pixels per second so 1000 means
- // 1px = 1ms.
- resolution =
- fromNs(Math.pow(2, Math.floor(Math.log2(toNs(1000)))));
+
+ if (BigintMath.popcount(resolution) !== 1) {
+ resolution = BigintMath.bitFloor(tpTimeFromSeconds(1000));
}
+
return this.onBoundsChange(
- visibleState.startSec - dur,
- visibleState.endSec + dur,
+ visibleTimeSpan.start - dur,
+ visibleTimeSpan.end + dur,
resolution);
})
.then((data) => {
diff --git a/ui/src/frontend/aggregation_panel.ts b/ui/src/frontend/aggregation_panel.ts
index ddcb6ba..6bd6422 100644
--- a/ui/src/frontend/aggregation_panel.ts
+++ b/ui/src/frontend/aggregation_panel.ts
@@ -22,6 +22,7 @@
} from '../common/aggregation_data';
import {colorForState, textColorForState} from '../common/colorizer';
import {translateState} from '../common/thread_state';
+import {tpTimeToMillis} from '../common/time';
import {globals} from './globals';
import {Panel} from './panel';
@@ -111,7 +112,8 @@
const selection = globals.state.currentSelection;
if (selection === null || selection.kind !== 'AREA') return undefined;
const selectedArea = globals.state.areas[selection.areaId];
- const rangeDurationMs = (selectedArea.endSec - selectedArea.startSec) * 1e3;
+ const rangeDurationMs =
+ tpTimeToMillis(selectedArea.end - selectedArea.start);
return m('.time-range', `Selected range: ${rangeDurationMs.toFixed(6)} ms`);
}
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index ed261a1..77c6e6d 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -22,7 +22,12 @@
} from '../common/colorizer';
import {NUM} from '../common/query_result';
import {Selection, SelectionKind} from '../common/state';
-import {fromNs, toNs} from '../common/time';
+import {
+ fromNs,
+ tpDurationFromNanos,
+ TPTime,
+ tpTimeFromNanos,
+} from '../common/time';
import {checkerboardExcept} from './checkerboard';
import {globals} from './globals';
@@ -45,7 +50,7 @@
// Exposed and standalone to allow for testing without making this
// visible to subclasses.
function filterVisibleSlices<S extends Slice>(
- slices: S[], startS: number, endS: number): S[] {
+ slices: S[], start: TPTime, end: TPTime): S[] {
// Here we aim to reduce the number of slices we have to draw
// by ignoring those that are not visible. A slice is visible iff:
// slice.start + slice.duration >= start && slice.start <= end
@@ -89,7 +94,7 @@
// For all slice in slices: slice.startS > endS (e.g. all slices are to the
// right). Since the slices are sorted by startS we can check this easily:
const maybeFirstSlice: S|undefined = slices[0];
- if (maybeFirstSlice && maybeFirstSlice.startS > endS) {
+ if (maybeFirstSlice && maybeFirstSlice.start > end) {
return [];
}
// It's not possible to easily check the analogous edge case where all slices
@@ -108,15 +113,15 @@
let endIdx = slices.length;
for (; startIdx < endIdx; ++startIdx) {
const slice = slices[startIdx];
- const sliceEndS = slice.startS + slice.durationS;
- if (sliceEndS >= startS && slice.startS <= endS) {
+ const sliceEndS = slice.start + slice.duration;
+ if (sliceEndS >= start && slice.start <= end) {
break;
}
}
for (; startIdx < endIdx; --endIdx) {
const slice = slices[endIdx - 1];
- const sliceEndS = slice.startS + slice.durationS;
- if (sliceEndS >= startS && slice.startS <= endS) {
+ const sliceEndS = slice.start + slice.duration;
+ if (sliceEndS >= start && slice.start <= end) {
break;
}
}
@@ -272,13 +277,16 @@
renderCanvas(ctx: CanvasRenderingContext2D): void {
// TODO(hjd): fonts and colors should come from the CSS and not hardcoded
// here.
- const timeScale = globals.frontendLocalState.timeScale;
- const vizTime = globals.frontendLocalState.visibleWindowTime;
+ const {
+ visibleTimeScale: timeScale,
+ visibleWindowTime: vizTime,
+ } = globals.frontendLocalState;
{
- const windowSizePx = Math.max(1, timeScale.endPx - timeScale.startPx);
- const rawStartNs = toNs(vizTime.start);
- const rawEndNs = toNs(vizTime.end);
+ const windowSizePx = Math.max(1, timeScale.pxSpan.delta);
+ // TODO(stevegolton): Keep these guys as bigints
+ const rawStartNs = vizTime.start.nanos;
+ const rawEndNs = vizTime.end.nanos;
const rawSlicesKey = CacheKey.create(rawStartNs, rawEndNs, windowSizePx);
// If the visible time range is outside the cached area, requests
@@ -298,7 +306,8 @@
// Filter only the visible slices. |this.slices| will have more slices than
// needed because maybeRequestData() over-fetches to handle small pan/zooms.
// We don't want to waste time drawing slices that are off screen.
- const vizSlices = this.getVisibleSlicesInternal(vizTime.start, vizTime.end);
+ const vizSlices = this.getVisibleSlicesInternal(
+ vizTime.start.toTPTime('floor'), vizTime.end.toTPTime('ceil'));
let selection = globals.state.currentSelection;
@@ -321,15 +330,15 @@
// pxEnd is the last visible pixel in the visible viewport. Drawing
// anything < 0 or > pxEnd doesn't produce any visible effect as it goes
// beyond the visible portion of the canvas.
- const pxEnd = Math.floor(timeScale.timeToPx(vizTime.end));
+ const pxEnd = Math.floor(timeScale.hpTimeToPx(vizTime.end));
for (const slice of vizSlices) {
// Compute the basic geometry for any visible slice, even if only
// partially visible. This might end up with a negative x if the
// slice starts before the visible time or with a width that overflows
// pxEnd.
- slice.x = timeScale.timeToPx(slice.startS);
- slice.w = timeScale.deltaTimeToPx(slice.durationS);
+ slice.x = timeScale.tpTimeToPx(slice.start);
+ slice.w = timeScale.durationToPx(slice.duration);
if (slice.flags & SLICE_FLAGS_INSTANT) {
// In the case of an instant slice, set the slice geometry on the
// bounding box that will contain the chevron.
@@ -429,10 +438,10 @@
checkerboardExcept(
ctx,
this.getHeight(),
- timeScale.timeToPx(vizTime.start),
- timeScale.timeToPx(vizTime.end),
- timeScale.timeToPx(fromNs(this.slicesKey.startNs)),
- timeScale.timeToPx(fromNs(this.slicesKey.endNs)));
+ timeScale.hpTimeToPx(vizTime.start),
+ timeScale.hpTimeToPx(vizTime.end),
+ timeScale.secondsToPx(fromNs(this.slicesKey.startNs)),
+ timeScale.secondsToPx(fromNs(this.slicesKey.endNs)));
// TODO(hjd): Remove this.
// The only thing this does is drawing the sched latency arrow. We should
@@ -623,8 +632,8 @@
return {
id: row.id,
- startS: fromNs(startNsQ),
- durationS: fromNs(endNsQ - startNsQ),
+ start: tpTimeFromNanos(startNsQ),
+ duration: tpDurationFromNanos(endNsQ - startNsQ),
flags,
depth: row.depth,
title: '',
@@ -701,10 +710,10 @@
return true;
}
- private getVisibleSlicesInternal(startS: number, endS: number):
+ private getVisibleSlicesInternal(start: TPTime, end: TPTime):
Array<CastInternal<T['slice']>> {
return filterVisibleSlices<CastInternal<T['slice']>>(
- this.slices, startS, endS);
+ this.slices, start, end);
}
private updateSliceAndTrackHeight() {
diff --git a/ui/src/frontend/base_slice_track_unittest.ts b/ui/src/frontend/base_slice_track_unittest.ts
index 7dd109d..e9202a2 100644
--- a/ui/src/frontend/base_slice_track_unittest.ts
+++ b/ui/src/frontend/base_slice_track_unittest.ts
@@ -19,11 +19,11 @@
} from './base_slice_track';
import {Slice} from './slice';
-function slice(startS: number, durationS: number): Slice {
+function slice(start: number, duration: number): Slice {
return {
id: 42,
- startS,
- durationS,
+ start: BigInt(start),
+ duration: BigInt(duration),
depth: 0,
flags: 0,
title: '',
@@ -36,24 +36,24 @@
const s = slice;
test('filterVisibleSlices', () => {
- expect(filterVisibleSlices([], 0, 100)).toEqual([]);
- expect(filterVisibleSlices([s(10, 80)], 0, 100)).toEqual([s(10, 80)]);
- expect(filterVisibleSlices([s(0, 20)], 10, 100)).toEqual([s(0, 20)]);
- expect(filterVisibleSlices([s(0, 10)], 10, 100)).toEqual([s(0, 10)]);
- expect(filterVisibleSlices([s(100, 10)], 10, 100)).toEqual([s(100, 10)]);
- expect(filterVisibleSlices([s(10, 0)], 10, 100)).toEqual([s(10, 0)]);
- expect(filterVisibleSlices([s(100, 0)], 10, 100)).toEqual([s(100, 0)]);
- expect(filterVisibleSlices([s(0, 5)], 10, 90)).toEqual([]);
- expect(filterVisibleSlices([s(95, 5)], 10, 90)).toEqual([]);
- expect(filterVisibleSlices([s(0, 5), s(95, 5)], 10, 90)).toEqual([]);
+ expect(filterVisibleSlices([], 0n, 100n)).toEqual([]);
+ expect(filterVisibleSlices([s(10, 80)], 0n, 100n)).toEqual([s(10, 80)]);
+ expect(filterVisibleSlices([s(0, 20)], 10n, 100n)).toEqual([s(0, 20)]);
+ expect(filterVisibleSlices([s(0, 10)], 10n, 100n)).toEqual([s(0, 10)]);
+ expect(filterVisibleSlices([s(100, 10)], 10n, 100n)).toEqual([s(100, 10)]);
+ expect(filterVisibleSlices([s(10, 0)], 10n, 100n)).toEqual([s(10, 0)]);
+ expect(filterVisibleSlices([s(100, 0)], 10n, 100n)).toEqual([s(100, 0)]);
+ expect(filterVisibleSlices([s(0, 5)], 10n, 90n)).toEqual([]);
+ expect(filterVisibleSlices([s(95, 5)], 10n, 90n)).toEqual([]);
+ expect(filterVisibleSlices([s(0, 5), s(95, 5)], 10n, 90n)).toEqual([]);
expect(filterVisibleSlices(
[
s(0, 5),
s(50, 0),
s(95, 5),
],
- 10,
- 90))
+ 10n,
+ 90n))
.toEqual([
s(50, 0),
]);
@@ -63,8 +63,8 @@
s(1, 9),
s(6, 3),
],
- 10,
- 90))
+ 10n,
+ 90n))
.toContainEqual(s(1, 9));
expect(filterVisibleSlices(
[
@@ -73,16 +73,16 @@
s(6, 3),
s(50, 0),
],
- 10,
- 90))
+ 10n,
+ 90n))
.toContainEqual(s(1, 9));
expect(filterVisibleSlices(
[
s(85, 10),
s(100, 10),
],
- 10,
- 90))
+ 10n,
+ 90n))
.toEqual([
s(85, 10),
]);
@@ -91,8 +91,8 @@
s(0, 100),
],
- 10,
- 90))
+ 10n,
+ 90n))
.toEqual([
s(0, 100),
]);
@@ -109,7 +109,7 @@
s(8, 1),
s(9, 1),
],
- 10,
- 90))
+ 10n,
+ 90n))
.toContainEqual(s(5, 10));
});
diff --git a/ui/src/frontend/chrome_slice_panel.ts b/ui/src/frontend/chrome_slice_panel.ts
index 7c4dd0d..50e3d1c 100644
--- a/ui/src/frontend/chrome_slice_panel.ts
+++ b/ui/src/frontend/chrome_slice_panel.ts
@@ -19,7 +19,7 @@
import {Arg, ArgsTree, isArgTreeArray, isArgTreeMap} from '../common/arg_types';
import {EngineProxy} from '../common/engine';
import {runQuery} from '../common/queries';
-import {timeToCode} from '../common/time';
+import {timeToCode, tpDurationToSeconds, tpTimeToCode} from '../common/time';
import {FlowPoint, globals, SliceDetails} from './globals';
import {PanelSize} from './panel';
@@ -295,7 +295,9 @@
!sliceInfo.category || sliceInfo.category === '[NULL]' ?
'N/A' :
sliceInfo.category);
- defaultBuilder.add('Start time', timeToCode(sliceInfo.ts));
+ defaultBuilder.add(
+ 'Start time',
+ tpTimeToCode(sliceInfo.ts - globals.state.traceTime.start));
if (sliceInfo.absTime !== undefined) {
defaultBuilder.add('Absolute Time', sliceInfo.absTime);
}
@@ -305,9 +307,11 @@
sliceInfo.threadDur !== undefined) {
// If we have valid thread duration, also display a percentage of
// |threadDur| compared to |dur|.
- const threadDurFractionSuffix = sliceInfo.threadDur === -1 ?
+ const ratio = tpDurationToSeconds(sliceInfo.threadDur) /
+ tpDurationToSeconds(sliceInfo.dur);
+ const threadDurFractionSuffix = sliceInfo.threadDur === -1n ?
'' :
- ` (${(sliceInfo.threadDur / sliceInfo.dur * 100).toFixed(2)}%)`;
+ ` (${(ratio * 100).toFixed(2)}%)`;
defaultBuilder.add(
'Thread duration',
this.computeDuration(sliceInfo.threadTs, sliceInfo.threadDur) +
diff --git a/ui/src/frontend/counter_panel.ts b/ui/src/frontend/counter_panel.ts
index 99d3841..4237773 100644
--- a/ui/src/frontend/counter_panel.ts
+++ b/ui/src/frontend/counter_panel.ts
@@ -14,8 +14,7 @@
import m from 'mithril';
-import {fromNs, timeToCode} from '../common/time';
-
+import {tpTimeToCode} from '../common/time';
import {globals} from './globals';
import {Panel} from './panel';
@@ -37,7 +36,11 @@
m('tr', m('th', `Name`), m('td', `${counterInfo.name}`)),
m('tr',
m('th', `Start time`),
- m('td', `${timeToCode(counterInfo.startTime)}`)),
+ m('td',
+ `${
+ tpTimeToCode(
+ counterInfo.startTime -
+ globals.state.traceTime.start)}`)),
m('tr',
m('th', `Value`),
m('td', `${counterInfo.value.toLocaleString()}`)),
@@ -46,7 +49,7 @@
m('td', `${counterInfo.delta.toLocaleString()}`)),
m('tr',
m('th', `Duration`),
- m('td', `${timeToCode(fromNs(counterInfo.duration))}`)),
+ m('td', `${tpTimeToCode(counterInfo.duration)}`)),
])],
));
} else {
diff --git a/ui/src/frontend/debug.ts b/ui/src/frontend/debug.ts
index fae7a83..7e9c9e5 100644
--- a/ui/src/frontend/debug.ts
+++ b/ui/src/frontend/debug.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {produce} from 'immer';
import m from 'mithril';
import {Actions} from '../common/actions';
@@ -19,12 +20,14 @@
import {globals} from './globals';
+
declare global {
interface Window {
m: typeof m;
getSchema: typeof getSchema;
globals: typeof globals;
Actions: typeof Actions;
+ produce: typeof produce;
}
}
@@ -33,4 +36,5 @@
window.m = m;
window.globals = globals;
window.Actions = Actions;
+ window.produce = produce;
}
diff --git a/ui/src/frontend/drag/border_drag_strategy.ts b/ui/src/frontend/drag/border_drag_strategy.ts
index df450fc..564ffc3 100644
--- a/ui/src/frontend/drag/border_drag_strategy.ts
+++ b/ui/src/frontend/drag/border_drag_strategy.ts
@@ -12,28 +12,27 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {TimeScale} from '../time_scale';
-
import {DragStrategy} from './drag_strategy';
export class BorderDragStrategy extends DragStrategy {
private moveStart = false;
- constructor(timeScale: TimeScale, private pixelBounds: [number, number]) {
- super(timeScale);
+ constructor(map: TimeScale, private pixelBounds: [number, number]) {
+ super(map);
}
onDrag(x: number) {
- let tStart =
- this.timeScale.pxToTime(this.moveStart ? x : this.pixelBounds[0]);
- let tEnd =
- this.timeScale.pxToTime(!this.moveStart ? x : this.pixelBounds[1]);
- if (tStart > tEnd) {
+ let tStart = this.map.pxToHpTime(this.moveStart ? x : this.pixelBounds[0]);
+ let tEnd = this.map.pxToHpTime(!this.moveStart ? x : this.pixelBounds[1]);
+ if (tStart.isGreaterThan(tEnd)) {
this.moveStart = !this.moveStart;
[tEnd, tStart] = [tStart, tEnd];
}
super.updateGlobals(tStart, tEnd);
- this.pixelBounds =
- [this.timeScale.timeToPx(tStart), this.timeScale.timeToPx(tEnd)];
+ this.pixelBounds = [
+ this.map.hpTimeToPx(tStart),
+ this.map.hpTimeToPx(tEnd),
+ ];
}
onDragStart(x: number) {
diff --git a/ui/src/frontend/drag/drag_strategy.ts b/ui/src/frontend/drag/drag_strategy.ts
index 2896849..afb83e1 100644
--- a/ui/src/frontend/drag/drag_strategy.ts
+++ b/ui/src/frontend/drag/drag_strategy.ts
@@ -11,19 +11,22 @@
// 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 {TimeSpan} from '../../common/time';
+import {
+ HighPrecisionTime,
+ HighPrecisionTimeSpan,
+} from '../../common/high_precision_time';
import {globals} from '../globals';
import {TimeScale} from '../time_scale';
export abstract class DragStrategy {
- constructor(protected timeScale: TimeScale) {}
+ constructor(protected map: TimeScale) {}
abstract onDrag(x: number): void;
abstract onDragStart(x: number): void;
- protected updateGlobals(tStart: number, tEnd: number) {
- const vizTime = new TimeSpan(tStart, tEnd);
+ protected updateGlobals(tStart: HighPrecisionTime, tEnd: HighPrecisionTime) {
+ const vizTime = new HighPrecisionTimeSpan(tStart, tEnd);
globals.frontendLocalState.updateVisibleTime(vizTime);
globals.rafScheduler.scheduleRedraw();
}
diff --git a/ui/src/frontend/drag/inner_drag_strategy.ts b/ui/src/frontend/drag/inner_drag_strategy.ts
index 2af1b39..7be7f7b 100644
--- a/ui/src/frontend/drag/inner_drag_strategy.ts
+++ b/ui/src/frontend/drag/inner_drag_strategy.ts
@@ -23,8 +23,8 @@
onDrag(x: number) {
const move = x - this.dragStartPx;
- const tStart = this.timeScale.pxToTime(this.pixelBounds[0] + move);
- const tEnd = this.timeScale.pxToTime(this.pixelBounds[1] + move);
+ const tStart = this.map.pxToHpTime(this.pixelBounds[0] + move);
+ const tEnd = this.map.pxToHpTime(this.pixelBounds[1] + move);
super.updateGlobals(tStart, tEnd);
}
diff --git a/ui/src/frontend/drag/outer_drag_strategy.ts b/ui/src/frontend/drag/outer_drag_strategy.ts
index 648b50d..f8269fc 100644
--- a/ui/src/frontend/drag/outer_drag_strategy.ts
+++ b/ui/src/frontend/drag/outer_drag_strategy.ts
@@ -11,16 +11,19 @@
// 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 {
+ HighPrecisionTime,
+} from '../../common/high_precision_time';
import {DragStrategy} from './drag_strategy';
export class OuterDragStrategy extends DragStrategy {
private dragStartPx = 0;
onDrag(x: number) {
- const dragBeginTime = this.timeScale.pxToTime(this.dragStartPx);
- const dragEndTime = this.timeScale.pxToTime(x);
- const tStart = Math.min(dragBeginTime, dragEndTime);
- const tEnd = Math.max(dragBeginTime, dragEndTime);
+ const dragBeginTime = this.map.pxToHpTime(this.dragStartPx);
+ const dragEndTime = this.map.pxToHpTime(x);
+ const tStart = HighPrecisionTime.min(dragBeginTime, dragEndTime);
+ const tEnd = HighPrecisionTime.max(dragBeginTime, dragEndTime);
super.updateGlobals(tStart, tEnd);
}
diff --git a/ui/src/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index dbf30f0..0ac5f79 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -28,7 +28,7 @@
FlamegraphStateViewingOption,
ProfileType,
} from '../common/state';
-import {timeToCode} from '../common/time';
+import {tpTimeToCode} from '../common/time';
import {profileType} from '../controller/flamegraph_controller';
import {Flamegraph, NodeRendering} from './flamegraph';
@@ -64,7 +64,7 @@
export class FlamegraphDetailsPanel extends Panel<FlamegraphDetailsPanelAttrs> {
private profileType?: ProfileType = undefined;
- private ts = 0;
+ private ts = 0n;
private pids: number[] = [];
private flamegraph: Flamegraph = new Flamegraph([]);
private focusRegex = '';
@@ -76,12 +76,12 @@
view() {
const flamegraphDetails = globals.flamegraphDetails;
if (flamegraphDetails && flamegraphDetails.type !== undefined &&
- flamegraphDetails.startNs !== undefined &&
- flamegraphDetails.durNs !== undefined &&
+ flamegraphDetails.start !== undefined &&
+ flamegraphDetails.dur !== undefined &&
flamegraphDetails.pids !== undefined &&
flamegraphDetails.upids !== undefined) {
this.profileType = profileType(flamegraphDetails.type);
- this.ts = flamegraphDetails.startNs + flamegraphDetails.durNs;
+ this.ts = flamegraphDetails.start + flamegraphDetails.dur;
this.pids = flamegraphDetails.pids;
if (flamegraphDetails.flamegraph) {
this.flamegraph.updateDataIfChanged(
@@ -108,7 +108,7 @@
toSelectedCallsite(
flamegraphDetails.expandedCallsite)}`),
m('div.time',
- `Snapshot time: ${timeToCode(flamegraphDetails.durNs)}`),
+ `Snapshot time: ${tpTimeToCode(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_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 909dcb7..fdb91e0 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -140,7 +140,7 @@
}
private getXCoordinate(ts: number): number {
- return globals.frontendLocalState.timeScale.timeToPx(ts);
+ return globals.frontendLocalState.visibleTimeScale.secondsToPx(ts);
}
private getSliceRect(args: FlowEventsRendererArgs, point: FlowPoint):
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index 34663d8..f4d95ca 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -14,6 +14,10 @@
import {assertTrue} from '../base/logging';
import {Actions} from '../common/actions';
+import {
+ HighPrecisionTime,
+ HighPrecisionTimeSpan,
+} from '../common/high_precision_time';
import {HttpRpcState} from '../common/http_rpc_engine';
import {
Area,
@@ -21,11 +25,15 @@
Timestamped,
VisibleState,
} from '../common/state';
-import {TimeSpan} from '../common/time';
+import {Span} from '../common/time';
+import {
+ TPTime,
+ TPTimeSpan,
+} from '../common/time';
import {globals} from './globals';
import {ratelimit} from './rate_limiters';
-import {TimeScale} from './time_scale';
+import {PxSpan, TimeScale} from './time_scale';
interface Range {
start?: number;
@@ -42,10 +50,6 @@
return current;
}
-function capBetween(t: number, start: number, end: number) {
- return Math.min(Math.max(t, start), end);
-}
-
// Calculate the space a scrollbar takes up so that we can subtract it from
// the canvas width.
function calculateScrollbarWidth() {
@@ -60,13 +64,99 @@
return width;
}
+export class TimeWindow {
+ private readonly MIN_DURATION_NS = 10;
+ private _start: HighPrecisionTime = new HighPrecisionTime();
+ private _durationNanos: number = 10e9;
+
+ private get _end(): HighPrecisionTime {
+ return this._start.addNanos(this._durationNanos);
+ }
+
+ update(span: Span<HighPrecisionTime>) {
+ this._start = span.start;
+ this._durationNanos = Math.max(this.MIN_DURATION_NS, span.duration.nanos);
+ this.preventClip();
+ }
+
+ // Pan the window by certain number of seconds
+ pan(offset: HighPrecisionTime) {
+ this._start = this._start.add(offset);
+ this.preventClip();
+ }
+
+ // Zoom in or out a bit centered on a specific offset from the root
+ // Offset represents the center of the zoom as a normalized value between 0
+ // and 1 where 0 is the start of the time window and 1 is the end
+ zoom(ratio: number, offset: number) {
+ // TODO(stevegolton): Handle case where trace time < MIN_DURATION_NS
+
+ const traceDuration = globals.stateTraceTime().duration;
+ const minDuration = Math.min(this.MIN_DURATION_NS, traceDuration.nanos);
+ const newDurationNanos = Math.max(this._durationNanos * ratio, minDuration);
+ // Delta between new and old duration
+ // +ve if new duration is shorter than old duration
+ const durationDeltaNanos = this._durationNanos - newDurationNanos;
+ // If offset is 0, don't move the start at all
+ // If offset if 1, move the start by the amount the duration has changed
+ // If new duration is shorter - move start to right
+ // If new duration is longer - move start to left
+ this._start = this._start.addNanos(durationDeltaNanos * offset);
+ this._durationNanos = newDurationNanos;
+ this.preventClip();
+ }
+
+ createTimeScale(startPx: number, endPx: number): TimeScale {
+ return new TimeScale(
+ this._start, this._durationNanos, new PxSpan(startPx, endPx));
+ }
+
+ // Get timespan covering entire range of the window
+ get timeSpan(): HighPrecisionTimeSpan {
+ return new HighPrecisionTimeSpan(this._start, this._end);
+ }
+
+ get timestampSpan(): Span<TPTime> {
+ return new TPTimeSpan(this.earliest, this.latest);
+ }
+
+ get earliest(): TPTime {
+ return this._start.toTPTime('floor');
+ }
+
+ get latest(): TPTime {
+ return this._start.addNanos(this._durationNanos).toTPTime('ceil');
+ }
+
+ // Limit the zoom and pan
+ private preventClip() {
+ const traceTimeSpan = globals.stateTraceTime();
+ const traceDurationNanos = traceTimeSpan.duration.nanos;
+
+ if (this._durationNanos > traceDurationNanos) {
+ this._start = traceTimeSpan.start;
+ this._durationNanos = traceDurationNanos;
+ }
+
+ if (this._start.isLessThan(traceTimeSpan.start)) {
+ this._start = traceTimeSpan.start;
+ }
+
+ const end = this._start.addNanos(this._durationNanos);
+ if (end.isGreaterThan(traceTimeSpan.end)) {
+ this._start = traceTimeSpan.end.subtractNanos(this._durationNanos);
+ }
+ }
+}
+
/**
* State that is shared between several frontend components, but not the
* controller. This state is updated at 60fps.
*/
export class FrontendLocalState {
- visibleWindowTime = new TimeSpan(0, 10);
- timeScale = new TimeScale(this.visibleWindowTime, [0, 0]);
+ visibleWindow = new TimeWindow();
+ startPx: number = 0;
+ endPx: number = 0;
showPanningHint = false;
showCookieConsent = false;
visibleTracks = new Set<string>();
@@ -82,9 +172,9 @@
private _visibleState: VisibleState = {
lastUpdate: 0,
- startSec: 0,
- endSec: 10,
- resolution: 1,
+ start: 0n,
+ end: BigInt(10e9),
+ resolution: 1n,
};
private _selectedArea?: Area;
@@ -125,6 +215,16 @@
}
}
+ zoomVisibleWindow(ratio: number, centerPoint: number) {
+ this.visibleWindow.zoom(ratio, centerPoint);
+ this.kickUpdateLocalState();
+ }
+
+ panVisibleWindow(delta: HighPrecisionTime) {
+ this.visibleWindow.pan(delta);
+ this.kickUpdateLocalState();
+ }
+
mergeState(state: FrontendState): void {
// This is unfortunately subtle. This class mutates this._visibleState.
// Since we may not mutate |state| (in order to make immer's immutable
@@ -137,19 +237,22 @@
this._visibleState = chooseLatest(this._visibleState, state.visibleState);
const visibleStateWasUpdated = previousVisibleState !== this._visibleState;
if (visibleStateWasUpdated) {
- this.updateLocalTime(
- new TimeSpan(this._visibleState.startSec, this._visibleState.endSec));
+ this.updateLocalTime(new HighPrecisionTimeSpan(
+ HighPrecisionTime.fromTPTime(this._visibleState.start),
+ HighPrecisionTime.fromTPTime(this._visibleState.end),
+ ));
}
}
+ // Set the highlight box to draw
selectArea(
- startSec: number, endSec: number,
+ start: TPTime, end: TPTime,
tracks = this._selectedArea ? this._selectedArea.tracks : []) {
assertTrue(
- endSec >= startSec,
- `Impossible select area: startSec [${startSec}] >= endSec [${endSec}]`);
+ end >= start,
+ `Impossible select area: start [${start}] >= end [${end}]`);
this.showPanningHint = true;
- this._selectedArea = {startSec, endSec, tracks};
+ this._selectedArea = {start, end, tracks},
globals.rafScheduler.scheduleFullRedraw();
}
@@ -166,12 +269,11 @@
globals.dispatch(Actions.setVisibleTraceTime(this._visibleState));
}, 50);
- private updateLocalTime(ts: TimeSpan) {
- const traceTime = globals.state.traceTime;
- const startSec = capBetween(ts.start, traceTime.startSec, traceTime.endSec);
- const endSec = capBetween(ts.end, traceTime.startSec, traceTime.endSec);
- this.visibleWindowTime = new TimeSpan(startSec, endSec);
- this.timeScale.setTimeBounds(this.visibleWindowTime);
+ private updateLocalTime(ts: Span<HighPrecisionTime>) {
+ const traceBounds = globals.stateTraceTime();
+ const start = ts.start.clamp(traceBounds.start, traceBounds.end);
+ const end = ts.end.clamp(traceBounds.start, traceBounds.end);
+ this.visibleWindow.update(new HighPrecisionTimeSpan(start, end));
this.updateResolution();
}
@@ -181,17 +283,17 @@
this.ratelimitedUpdateVisible();
}
- updateVisibleTime(ts: TimeSpan) {
- this.updateLocalTime(ts);
+ private kickUpdateLocalState() {
this._visibleState.lastUpdate = Date.now() / 1000;
- this._visibleState.startSec = this.visibleWindowTime.start;
- this._visibleState.endSec = this.visibleWindowTime.end;
+ this._visibleState.start = this.visibleWindowTime.start.toTPTime();
+ this._visibleState.end = this.visibleWindowTime.end.toTPTime();
this._visibleState.resolution = globals.getCurResolution();
this.ratelimitedUpdateVisible();
}
- getVisibleStateBounds(): [number, number] {
- return [this.visibleWindowTime.start, this.visibleWindowTime.end];
+ updateVisibleTime(ts: Span<HighPrecisionTime>) {
+ this.updateLocalTime(ts);
+ this.kickUpdateLocalState();
}
// Whenever start/end px of the timeScale is changed, update
@@ -202,7 +304,28 @@
pxStart = Math.max(0, pxStart);
pxEnd = Math.max(0, pxEnd);
if (pxStart === pxEnd) pxEnd = pxStart + 1;
- this.timeScale.setLimitsPx(pxStart, pxEnd);
+ this.startPx = pxStart;
+ this.endPx = pxEnd;
this.updateResolution();
}
+
+ // Get the time scale for the visible window
+ get visibleTimeScale(): TimeScale {
+ return this.visibleWindow.createTimeScale(this.startPx, this.endPx);
+ }
+
+ // Produces a TimeScale object for this time window provided start and end px
+ getTimeScale(startPx: number, endPx: number): TimeScale {
+ return this.visibleWindow.createTimeScale(startPx, endPx);
+ }
+
+ // Get the bounds of the window in pixels
+ get windowSpan(): PxSpan {
+ return new PxSpan(this.startPx, this.endPx);
+ }
+
+ // Get the bounds of the visible time window as a time span
+ get visibleWindowTime(): Span<HighPrecisionTime> {
+ return this.visibleWindow.timeSpan;
+ }
}
diff --git a/ui/src/frontend/ftrace_panel.ts b/ui/src/frontend/ftrace_panel.ts
index 8de83cb..e45b5fe 100644
--- a/ui/src/frontend/ftrace_panel.ts
+++ b/ui/src/frontend/ftrace_panel.ts
@@ -18,7 +18,7 @@
import {assertExists} from '../base/logging';
import {Actions} from '../common/actions';
import {colorForString} from '../common/colorizer';
-import {formatTimestamp} from '../common/time';
+import {formatTPTime, TPTime} from '../common/time';
import {globals} from './globals';
import {Panel} from './panel';
@@ -117,12 +117,12 @@
this.recomputeVisibleRowsAndUpdate(scrollContainer);
};
- onRowOver(ts: number) {
+ onRowOver(ts: TPTime) {
globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
}
onRowOut() {
- globals.dispatch(Actions.setHoverCursorTimestamp({ts: -1}));
+ globals.dispatch(Actions.setHoverCursorTimestamp({ts: -1n}));
}
private renderRowsLabel() {
@@ -188,8 +188,7 @@
for (let i = 0; i < events.length; i++) {
const {ts, name, cpu, process, args} = events[i];
- const timestamp =
- formatTimestamp(ts / 1e9 - globals.state.traceTime.startSec);
+ const timestamp = formatTPTime(ts - globals.state.traceTime.start);
const rank = i + offset;
@@ -204,7 +203,7 @@
`.row`,
{
style: {top: `${(rank + 1.0) * ROW_H}px`},
- onmouseover: this.onRowOver.bind(this, ts / 1e9),
+ onmouseover: this.onRowOver.bind(this, ts),
onmouseout: this.onRowOut.bind(this),
},
m('.cell', timestamp),
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 0ca10a0..79ca58a 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {BigintMath} from '../base/bigint_math';
import {assertExists} from '../base/logging';
import {Actions, DeferredAction} from '../common/actions';
import {AggregateData} from '../common/aggregation_data';
@@ -22,10 +23,19 @@
} from '../common/conversion_jobs';
import {createEmptyState} from '../common/empty_state';
import {Engine} from '../common/engine';
+import {
+ HighPrecisionTime,
+ HighPrecisionTimeSpan,
+} from '../common/high_precision_time';
import {MetricResult} from '../common/metric_data';
import {CurrentSearchResults, SearchSummary} from '../common/search_data';
import {CallsiteInfo, EngineConfig, ProfileType, State} from '../common/state';
-import {fromNs, toNs} from '../common/time';
+import {Span, tpTimeFromSeconds} from '../common/time';
+import {
+ TPDuration,
+ TPTime,
+ TPTimeSpan,
+} from '../common/time';
import {Analytics, initAnalytics} from './analytics';
import {BottomTabList} from './bottom_tab';
@@ -33,6 +43,7 @@
import {RafScheduler} from './raf_scheduler';
import {Router} from './router';
import {ServiceWorkerController} from './service_worker_controller';
+import {PxSpan, TimeScale} from './time_scale';
type Dispatch = (action: DeferredAction) => void;
type TrackDataStore = Map<string, {}>;
@@ -41,18 +52,18 @@
type Description = Map<string, string>;
export interface SliceDetails {
- ts?: number;
+ ts?: TPTime;
absTime?: string;
- dur?: number;
- threadTs?: number;
- threadDur?: number;
+ dur?: TPDuration;
+ threadTs?: TPTime;
+ threadDur?: TPDuration;
priority?: number;
endState?: string|null;
cpu?: number;
id?: number;
threadStateId?: number;
utid?: number;
- wakeupTs?: number;
+ wakeupTs?: TPTime;
wakerUtid?: number;
wakerCpu?: number;
category?: string;
@@ -104,23 +115,23 @@
}
export interface CounterDetails {
- startTime?: number;
+ startTime?: TPTime;
value?: number;
delta?: number;
- duration?: number;
+ duration?: TPDuration;
name?: string;
}
export interface ThreadStateDetails {
- ts?: number;
- dur?: number;
+ ts?: TPTime;
+ dur?: TPDuration;
}
export interface FlamegraphDetails {
type?: ProfileType;
id?: number;
- startNs?: number;
- durNs?: number;
+ start?: TPTime;
+ dur?: TPDuration;
pids?: number[];
upids?: number[];
flamegraph?: CallsiteInfo[];
@@ -143,8 +154,8 @@
}
export interface QuantizedLoad {
- startSec: number;
- endSec: number;
+ start: TPTime;
+ end: TPTime;
load: number;
}
type OverviewStore = Map<string, QuantizedLoad[]>;
@@ -161,7 +172,7 @@
export interface FtraceEvent {
id: number;
- ts: number;
+ ts: TPTime;
name: string;
cpu: number;
thread: string|null;
@@ -530,7 +541,7 @@
this.aggregateDataStore.set(kind, data);
}
- getCurResolution() {
+ getCurResolution(): TPDuration {
// Truncate the resolution to the closest power of 2 (in nanosecond space).
// We choose to work in ns space because resolution is consumed be track
// controllers for quantization and they rely on resolution to be a power
@@ -541,24 +552,18 @@
// levels. Logic: each zoom level represents a delta of 0.1 * (visible
// window span). Therefore, zooming out by six levels is 1.1^6 ~= 2.
// Similarily, zooming in six levels is 0.9^6 ~= 0.5.
- const pxToSec = this.frontendLocalState.timeScale.deltaPxToDuration(1);
+ const timeScale = this.frontendLocalState.visibleTimeScale;
// TODO(b/186265930): Remove once fixed:
- if (!isFinite(pxToSec)) {
- // Resolution is in pixels per second so 1000 means 1px = 1ms.
- console.error(`b/186265930: Bad pxToSec suppressed ${pxToSec}`);
- return fromNs(Math.pow(2, Math.floor(Math.log2(toNs(1000)))));
+ if (timeScale.pxSpan.delta === 0) {
+ console.error(`b/186265930: Bad pxToSec suppressed`);
+ return BigintMath.bitFloor(tpTimeFromSeconds(1000));
}
- const pxToNs = Math.max(toNs(pxToSec), 1);
- const resolution = fromNs(Math.pow(2, Math.floor(Math.log2(pxToNs))));
- const log2 = Math.log2(toNs(resolution));
- if (log2 % 1 !== 0) {
- throw new Error(`Resolution should be a power of two.
- pxToSec: ${pxToSec},
- pxToNs: ${pxToNs},
- resolution: ${resolution},
- log2: ${Math.log2(toNs(resolution))}`);
- }
- return resolution;
+
+ const timePerPx = HighPrecisionTime.max(
+ timeScale.pxDeltaToDuration(1), new HighPrecisionTime(1n));
+
+ const resolutionBig = BigintMath.bitFloor(timePerPx.toTPTime());
+ return resolutionBig;
}
getCurrentEngine(): EngineConfig|undefined {
@@ -637,6 +642,30 @@
shutdown() {
this._rafScheduler!.shutdown();
}
+
+ // Get a timescale that covers the entire trace
+ getTraceTimeScale(pxSpan: PxSpan): TimeScale {
+ const {start, end} = this.state.traceTime;
+ const traceTime = HighPrecisionTimeSpan.fromTpTime(start, end);
+ return new TimeScale(traceTime.start, traceTime.duration.nanos, pxSpan);
+ }
+
+ // Get the trace time bounds
+ stateTraceTime(): Span<HighPrecisionTime> {
+ const {start, end} = this.state.traceTime;
+ return HighPrecisionTimeSpan.fromTpTime(start, end);
+ }
+
+ stateTraceTimeTP(): Span<TPTime> {
+ const {start, end} = this.state.traceTime;
+ return new TPTimeSpan(start, end);
+ }
+
+ // Get the state version of the visible time bounds
+ stateVisibleTime(): Span<TPTime> {
+ const {start, end} = this.state.frontendLocalState.visibleState;
+ return new TPTimeSpan(start, end);
+ }
}
export const globals = new Globals();
diff --git a/ui/src/frontend/gridline_helper.ts b/ui/src/frontend/gridline_helper.ts
index 1c9dbfe..6581c81 100644
--- a/ui/src/frontend/gridline_helper.ts
+++ b/ui/src/frontend/gridline_helper.ts
@@ -13,49 +13,91 @@
// limitations under the License.
import {assertTrue} from '../base/logging';
-import {roundDownNearest} from '../base/math_utils';
+import {Span, tpDurationToSeconds} from '../common/time';
+import {TPDuration, TPTime, TPTimeSpan} from '../common/time';
+
import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
import {globals} from './globals';
import {TimeScale} from './time_scale';
-// Returns the optimal step size (in seconds) and tick pattern of ticks within
-// the step. The returned step size has two properties: (1) It is 1, 2, or 5,
-// multiplied by some integer power of 10. (2) It is maximised given the
-// constraint: |range| / stepSize <= |maxNumberOfSteps|.
-export function getStepSize(
- range: number, maxNumberOfSteps: number): [number, string] {
- // First, get the largest possible power of 10 that is smaller than the
- // desired step size, and use it as our initial step size.
- // For example, if the range is 2345ms and the desired steps is 10, then the
- // minimum step size is 234.5ms so the step size will initialise to 100.
- const minStepSize = range / maxNumberOfSteps;
- const zeros = Math.floor(Math.log10(minStepSize));
- const initialStepSize = Math.pow(10, zeros);
+const micros = 1000n;
+const millis = 1000n * micros;
+const seconds = 1000n * millis;
+const minutes = 60n * seconds;
+const hours = 60n * minutes;
+const days = 24n * hours;
- // We know that |initialStepSize| is a power of 10, and
- // initialStepSize <= desiredStepSize <= 10 * initialStepSize. There are four
- // possible candidates for final step size: 1, 2, 5 or 10 * initialStepSize.
- // For our example above, this would result in a step size of 500ms, as both
- // 100ms and 200ms are smaller than the minimum step size of 234.5ms.
- // We pick the candidate that minimizes the step size without letting the
- // number of steps exceed |maxNumberOfSteps|. The factor we pick to also
- // determines the pattern of ticks. This pattern is represented using a string
- // where:
- // | = Major tick
- // : = Medium tick
- // . = Minor tick
- const stepSizeMultipliers: [number, string][] =
- [[1, '|....:....'], [2, '|.:.'], [5, '|....'], [10, '|....:....']];
+// These patterns cover the entire range of 0 - 2^63-1 nanoseconds
+const patterns: [bigint, string][] = [
+ [1n, '|'],
+ [2n, '|:'],
+ [5n, '|....'],
+ [10n, '|....:....'],
+ [20n, '|.:.'],
+ [50n, '|....'],
+ [100n, '|....:....'],
+ [200n, '|.:.'],
+ [500n, '|....'],
+ [1n * micros, '|....:....'],
+ [2n * micros, '|.:.'],
+ [5n * micros, '|....'],
+ [10n * micros, '|....:....'],
+ [20n * micros, '|.:.'],
+ [50n * micros, '|....'],
+ [100n * micros, '|....:....'],
+ [200n * micros, '|.:.'],
+ [500n * micros, '|....'],
+ [1n * millis, '|....:....'],
+ [2n * millis, '|.:.'],
+ [5n * millis, '|....'],
+ [10n * millis, '|....:....'],
+ [20n * millis, '|.:.'],
+ [50n * millis, '|....'],
+ [100n * millis, '|....:....'],
+ [200n * millis, '|.:.'],
+ [500n * millis, '|....'],
+ [1n * seconds, '|....:....'],
+ [2n * seconds, '|.:.'],
+ [5n * seconds, '|....'],
+ [10n * seconds, '|....:....'],
+ [30n * seconds, '|.:.:.'],
+ [1n * minutes, '|.....'],
+ [2n * minutes, '|.:.'],
+ [5n * minutes, '|.....'],
+ [10n * minutes, '|....:....'],
+ [30n * minutes, '|.:.:.'],
+ [1n * hours, '|.....'],
+ [2n * hours, '|.:.'],
+ [6n * hours, '|.....'],
+ [12n * hours, '|.....:.....'],
+ [1n * days, '|.:.'],
+ [2n * days, '|.:.'],
+ [5n * days, '|....'],
+ [10n * days, '|....:....'],
+ [20n * days, '|.:.'],
+ [50n * days, '|....'],
+ [100n * days, '|....:....'],
+ [200n * days, '|.:.'],
+ [500n * days, '|....'],
+ [1000n * days, '|....:....'],
+ [2000n * days, '|.:.'],
+ [5000n * days, '|....'],
+ [10000n * days, '|....:....'],
+ [20000n * days, '|.:.'],
+ [50000n * days, '|....'],
+ [100000n * days, '|....:....'],
+ [200000n * days, '|.:.'],
+];
- for (const [multiplier, pattern] of stepSizeMultipliers) {
- const newStepSize = multiplier * initialStepSize;
- const numberOfNewSteps = range / newStepSize;
- if (numberOfNewSteps <= maxNumberOfSteps) {
- return [newStepSize, pattern];
+// Returns the optimal step size and pattern of ticks within the step.
+export function getPattern(minPatternSize: bigint): [TPDuration, string] {
+ for (const [size, pattern] of patterns) {
+ if (size >= minPatternSize) {
+ return [size, pattern];
}
}
- throw new Error('Something has gone horribly wrong with maths');
+ throw new Error('Pattern not defined for this minsize');
}
function tickPatternToArray(pattern: string): TickType[] {
@@ -75,21 +117,23 @@
});
}
-// Assuming a number only has one non-zero decimal digit, find the number of
-// decimal places required to accurately print that number. I.e. the parameter
-// we should pass to number.toFixed(x). To account for floating point
-// innaccuracies when representing numbers in base-10, we only take the first
-// nonzero fractional digit into account. E.g.
+// Get the number of decimal places we would have to print a time to for a given
+// min step size. For example, if we know the min step size is 0.1 and all
+// values are going to be aligned to integral multiples of 0.1, there's no
+// point printing these values with more than 1 decimal place.
+// Note: It's assumed that stepSize only has one significant figure.
+// E.g. 0.3 and 0.00002 are fine, but 0.123 will be treated as if it were 0.1.
+// Some examples: (seconds -> decimal places)
// 1.0 -> 0
// 0.5 -> 1
// 0.009 -> 3
// 0.00007 -> 5
// 30000 -> 0
// 0.30000000000000004 -> 1
-export function guessDecimalPlaces(val: number): number {
- const neglog10 = -Math.floor(Math.log10(val));
- const clamped = Math.max(0, neglog10);
- return clamped;
+export function guessDecimalPlaces(stepSize: TPDuration): number {
+ const stepSizeSeconds = tpDurationToSeconds(stepSize);
+ const decimalPlaces = -Math.floor(Math.log10(stepSizeSeconds));
+ return Math.max(0, decimalPlaces);
}
export enum TickType {
@@ -100,55 +144,58 @@
export interface Tick {
type: TickType;
- time: number;
- position: number;
+ time: TPTime;
}
const MIN_PX_PER_STEP = 80;
+export function getMaxMajorTicks(width: number) {
+ return Math.max(1, Math.floor(width / MIN_PX_PER_STEP));
+}
+
+function roundDownNearest(time: TPTime, stepSize: TPDuration): TPTime {
+ return stepSize * (time / stepSize);
+}
// An iterable which generates a series of ticks for a given timescale.
export class TickGenerator implements Iterable<Tick> {
private _tickPattern: TickType[];
- private _patternSize: number;
+ private _patternSize: TPDuration;
+ private _timeSpan: Span<TPTime>;
+ private _offset: TPTime;
- constructor(private scale: TimeScale, {minLabelPx = MIN_PX_PER_STEP} = {}) {
- assertTrue(minLabelPx > 0, 'minLabelPx cannot be lte 0');
- assertTrue(scale.widthPx > 0, 'widthPx cannot be lte 0');
- assertTrue(
- scale.timeSpan.duration > 0, 'timeSpan.duration cannot be lte 0');
+ constructor(
+ timeSpan: Span<TPTime>, maxMajorTicks: number, offset: TPTime = 0n) {
+ assertTrue(timeSpan.duration > 0n, 'timeSpan.duration cannot be lte 0');
+ assertTrue(maxMajorTicks > 0, 'maxMajorTicks cannot be lte 0');
- const desiredSteps = scale.widthPx / minLabelPx;
- const [size, pattern] = getStepSize(scale.timeSpan.duration, desiredSteps);
+ this._timeSpan = timeSpan.add(-offset);
+ this._offset = offset;
+ const minStepSize =
+ BigInt(Math.floor(Number(timeSpan.duration) / maxMajorTicks));
+ const [size, pattern] = getPattern(minStepSize);
this._patternSize = size;
this._tickPattern = tickPatternToArray(pattern);
}
// Returns an iterable, so this object can be iterated over directly using the
// `for x of y` notation. The use of a generator here is just to make things
- // more elegant than creating an array of ticks and building an iterator for
- // it.
+ // more elegant compared to creating an array of ticks and building an
+ // iterator for it.
* [Symbol.iterator](): Generator<Tick> {
- const span = this.scale.timeSpan;
- const stepSize = this._patternSize / this._tickPattern.length;
- const start = roundDownNearest(span.start, this._patternSize);
- const timeAtStep = (i: number) => start + (i * stepSize);
+ const stepSize = this._patternSize / BigInt(this._tickPattern.length);
+ const start = roundDownNearest(this._timeSpan.start, this._patternSize);
+ const end = this._timeSpan.end;
+ let patternIndex = 0;
- // Iterating using steps instead of
- // for (let s = start; s < span.end; s += stepSize) because if start is much
- // larger than stepSize we can enter an infinite loop due to floating
- // point precision errors.
- for (let i = 0; timeAtStep(i) < span.end; i++) {
- const time = timeAtStep(i);
- if (time >= span.start) {
- const position = Math.floor(this.scale.timeToPx(time));
- const type = this._tickPattern[i % this._tickPattern.length];
- yield {type, time, position};
+ for (let time = start; time < end; time += stepSize, patternIndex++) {
+ if (time >= this._timeSpan.start) {
+ patternIndex = patternIndex % this._tickPattern.length;
+ const type = this._tickPattern[patternIndex];
+ yield {type, time: time + this._offset};
}
}
}
- // The number of decimal places labels should be printed with, assuming labels
- // are only printed on major ticks.
get digits(): number {
return guessDecimalPlaces(this._patternSize);
}
@@ -157,9 +204,7 @@
// Gets the timescale associated with the current visible window.
export function timeScaleForVisibleWindow(
startPx: number, endPx: number): TimeScale {
- const span = globals.frontendLocalState.visibleWindowTime;
- const spanRelative = span.add(-globals.state.traceTime.startSec);
- return new TimeScale(spanRelative, [startPx, endPx]);
+ return globals.frontendLocalState.getTimeScale(startPx, endPx);
}
export function drawGridLines(
@@ -169,13 +214,18 @@
ctx.strokeStyle = TRACK_BORDER_COLOR;
ctx.lineWidth = 1;
- const timeScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width);
- if (timeScale.timeSpan.duration > 0 && timeScale.widthPx > 0) {
- for (const {type, position} of new TickGenerator(timeScale)) {
+ const {earliest, latest} = globals.frontendLocalState.visibleWindow;
+ const span = new TPTimeSpan(earliest, latest);
+ if (width > TRACK_SHELL_WIDTH && span.duration > 0n) {
+ const maxMajorTicks = getMaxMajorTicks(width - TRACK_SHELL_WIDTH);
+ const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width);
+ for (const {type, time} of new TickGenerator(
+ span, maxMajorTicks, globals.state.traceTime.start)) {
+ const px = Math.floor(map.tpTimeToPx(time));
if (type === TickType.MAJOR) {
ctx.beginPath();
- ctx.moveTo(position + 0.5, 0);
- ctx.lineTo(position + 0.5, height);
+ ctx.moveTo(px + 0.5, 0);
+ ctx.lineTo(px + 0.5, height);
ctx.stroke();
}
}
diff --git a/ui/src/frontend/gridline_helper_unittest.ts b/ui/src/frontend/gridline_helper_unittest.ts
index 3b6dcac..2454680 100644
--- a/ui/src/frontend/gridline_helper_unittest.ts
+++ b/ui/src/frontend/gridline_helper_unittest.ts
@@ -12,303 +12,93 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {TimeSpan} from '../common/time';
+import {TPTimeSpan} from '../common/time';
-import {getStepSize, Tick, TickGenerator, TickType} from './gridline_helper';
-import {TimeScale} from './time_scale';
-
-const pattern1 = '|....:....';
-const pattern2 = '|.:.';
-const pattern5 = '|....';
-const timeScale = new TimeScale(new TimeSpan(0, 1), [1, 2]);
+import {getPattern, TickGenerator, TickType} from './gridline_helper';
test('gridline helper to have sensible step sizes', () => {
- expect(getStepSize(10, 14)).toEqual([1, pattern1]);
- expect(getStepSize(30, 14)).toEqual([5, pattern5]);
- expect(getStepSize(60, 14)).toEqual([5, pattern5]);
- expect(getStepSize(100, 14)).toEqual([10, pattern1]);
+ expect(getPattern(1n)).toEqual([1n, '|']);
+ expect(getPattern(2n)).toEqual([2n, '|:']);
+ expect(getPattern(3n)).toEqual([5n, '|....']);
+ expect(getPattern(4n)).toEqual([5n, '|....']);
+ expect(getPattern(5n)).toEqual([5n, '|....']);
+ expect(getPattern(7n)).toEqual([10n, '|....:....']);
- expect(getStepSize(10, 21)).toEqual([0.5, pattern5]);
- expect(getStepSize(30, 21)).toEqual([2, pattern2]);
- expect(getStepSize(60, 21)).toEqual([5, pattern5]);
- expect(getStepSize(100, 21)).toEqual([5, pattern5]);
+ expect(getPattern(10n)).toEqual([10n, '|....:....']);
+ expect(getPattern(20n)).toEqual([20n, '|.:.']);
+ expect(getPattern(50n)).toEqual([50n, '|....']);
- expect(getStepSize(10, 3)).toEqual([5, pattern5]);
- expect(getStepSize(30, 3)).toEqual([10, pattern1]);
- expect(getStepSize(60, 3)).toEqual([20, pattern2]);
- expect(getStepSize(100, 3)).toEqual([50, pattern5]);
-
- expect(getStepSize(800, 4)).toEqual([200, pattern2]);
+ expect(getPattern(100n)).toEqual([100n, '|....:....']);
});
-test('gridline helper to scale to very small and very large values', () => {
- expect(getStepSize(.01, 14)).toEqual([.001, pattern1]);
- expect(getStepSize(10000, 14)).toEqual([1000, pattern1]);
-});
+describe('TickGenerator', () => {
+ it('can generate ticks with span starting at origin', () => {
+ const tickGen = new TickGenerator(new TPTimeSpan(0n, 10n), 1);
+ const expected = [
+ {type: TickType.MAJOR, time: 0n},
+ {type: TickType.MINOR, time: 1n},
+ {type: TickType.MINOR, time: 2n},
+ {type: TickType.MINOR, time: 3n},
+ {type: TickType.MINOR, time: 4n},
+ {type: TickType.MEDIUM, time: 5n},
+ {type: TickType.MINOR, time: 6n},
+ {type: TickType.MINOR, time: 7n},
+ {type: TickType.MINOR, time: 8n},
+ {type: TickType.MINOR, time: 9n},
+ ];
+ const actual = Array.from(tickGen!);
+ expect(actual).toStrictEqual(expected);
+ expect(tickGen!.digits).toEqual(8);
+ });
-test('gridline helper to always return a reasonable number of steps', () => {
- for (let i = 1; i <= 1000; i++) {
- const [stepSize, _] = getStepSize(i, 14);
- expect(Math.round(i / stepSize)).toBeGreaterThanOrEqual(6);
- expect(Math.round(i / stepSize)).toBeLessThanOrEqual(14);
- }
-});
+ it('can generate ticks when span has an offset', () => {
+ const tickGen = new TickGenerator(new TPTimeSpan(10n, 20n), 1);
+ const expected = [
+ {type: TickType.MAJOR, time: 10n},
+ {type: TickType.MINOR, time: 11n},
+ {type: TickType.MINOR, time: 12n},
+ {type: TickType.MINOR, time: 13n},
+ {type: TickType.MINOR, time: 14n},
+ {type: TickType.MEDIUM, time: 15n},
+ {type: TickType.MINOR, time: 16n},
+ {type: TickType.MINOR, time: 17n},
+ {type: TickType.MINOR, time: 18n},
+ {type: TickType.MINOR, time: 19n},
+ ];
+ const actual = Array.from(tickGen!);
+ expect(actual).toStrictEqual(expected);
+ expect(tickGen!.digits).toEqual(8);
+ });
-describe('TickGenerator with range 0.0-1.0 and room for 2 labels', () => {
- let tickGen: TickGenerator|undefined = undefined;
- beforeAll(() => {
- const timeSpan = new TimeSpan(0.0, 1.0);
- const timeScale = new TimeScale(timeSpan, [0, 200]);
- tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
- });
- it('should produce major ticks at 0.5s and minor ticks at 0.1s starting at 0',
- () => {
- const expected = [
- {type: TickType.MAJOR, time: 0.0},
- {type: TickType.MINOR, time: 0.1},
- {type: TickType.MINOR, time: 0.2},
- {type: TickType.MINOR, time: 0.3},
- {type: TickType.MINOR, time: 0.4},
- {type: TickType.MAJOR, time: 0.5},
- {type: TickType.MINOR, time: 0.6},
- {type: TickType.MINOR, time: 0.7},
- {type: TickType.MINOR, time: 0.8},
- {type: TickType.MINOR, time: 0.9},
- ];
- const actual = Array.from(tickGen!);
- expectTicksEqual(actual, expected);
- });
- it('should tell us to use 1 decimal place for labels', () => {
- expect(tickGen!.digits).toEqual(1);
- });
-});
-
-describe('TickGenerator with range 0.3-1.3 and room for 2 labels', () => {
- let tickGen: TickGenerator|undefined = undefined;
- beforeAll(() => {
- const timeSpan = new TimeSpan(0.3, 1.3);
- const timeScale = new TimeScale(timeSpan, [0, 200]);
- tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
- });
- it('should produce major ticks at 0.5s and minor ticks at 0.1s starting at 0',
- () => {
- const expected = [
- {type: TickType.MINOR, time: 0.3},
- {type: TickType.MINOR, time: 0.4},
- {type: TickType.MAJOR, time: 0.5},
- {type: TickType.MINOR, time: 0.6},
- {type: TickType.MINOR, time: 0.7},
- {type: TickType.MINOR, time: 0.8},
- {type: TickType.MINOR, time: 0.9},
- {type: TickType.MAJOR, time: 1.0},
- {type: TickType.MINOR, time: 1.1},
- {type: TickType.MINOR, time: 1.2},
- ];
- const actual = Array.from(tickGen!);
- expectTicksEqual(actual, expected);
- });
- it('should tell us to use 1 decimal place for labels', () => {
- expect(tickGen!.digits).toEqual(1);
- });
-});
-
-describe('TickGenerator with range 0.0-0.2 and room for 1 label', () => {
- let tickGen: TickGenerator|undefined = undefined;
- beforeAll(() => {
- const timeSpan = new TimeSpan(0.0, 0.2);
- const timeScale = new TimeScale(timeSpan, [0, 100]);
- tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
- });
- it('should produce major ticks at 0.2s and minor ticks at 0.1s starting at 0',
- () => {
- const expected = [
- {type: TickType.MAJOR, time: 0.0},
- {type: TickType.MINOR, time: 0.05},
- {type: TickType.MEDIUM, time: 0.1},
- {type: TickType.MINOR, time: 0.15},
- ];
- const actual = Array.from(tickGen!);
- expectTicksEqual(actual, expected);
- });
- it('should tell us to use 1 decimal place for labels', () => {
- expect(tickGen!.digits).toEqual(1);
- });
-});
-
-describe('TickGenerator with range 0.0-0.1 and room for 1 label', () => {
- let tickGen: TickGenerator|undefined = undefined;
- beforeAll(() => {
- const timeSpan = new TimeSpan(0.0, 0.1);
- const timeScale = new TimeScale(timeSpan, [0, 100]);
- tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
- });
- it('should produce major ticks at 0.1s & minor ticks at 0.02s starting at 0',
- () => {
- const expected = [
- {type: TickType.MAJOR, time: 0.0},
- {type: TickType.MINOR, time: 0.01},
- {type: TickType.MINOR, time: 0.02},
- {type: TickType.MINOR, time: 0.03},
- {type: TickType.MINOR, time: 0.04},
- {type: TickType.MEDIUM, time: 0.05},
- {type: TickType.MINOR, time: 0.06},
- {type: TickType.MINOR, time: 0.07},
- {type: TickType.MINOR, time: 0.08},
- {type: TickType.MINOR, time: 0.09},
- ];
- const actual = Array.from(tickGen!);
- expect(tickGen!.digits).toEqual(1);
- expectTicksEqual(actual, expected);
- });
- it('should tell us to use 1 decimal place for labels', () => {
- expect(tickGen!.digits).toEqual(1);
- });
-});
-
-describe('TickGenerator with a very small timespan', () => {
- let tickGen: TickGenerator|undefined = undefined;
- beforeAll(() => {
- const timeSpan = new TimeSpan(0.0, 1e-9);
- const timeScale = new TimeScale(timeSpan, [0, 100]);
- tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
- });
- it('should generate minor ticks at 2e-10s and one major tick at the start',
- () => {
- const expected = [
- {type: TickType.MAJOR, time: 0.0},
- {type: TickType.MINOR, time: 1e-10},
- {type: TickType.MINOR, time: 2e-10},
- {type: TickType.MINOR, time: 3e-10},
- {type: TickType.MINOR, time: 4e-10},
- {type: TickType.MEDIUM, time: 5e-10},
- {type: TickType.MINOR, time: 6e-10},
- {type: TickType.MINOR, time: 7e-10},
- {type: TickType.MINOR, time: 8e-10},
- {type: TickType.MINOR, time: 9e-10},
- ];
- const actual = Array.from(tickGen!);
- expectTicksEqual(actual, expected);
- });
- it('should tell us to use 9 decimal places for labels', () => {
- expect(tickGen!.digits).toEqual(9);
- });
-});
-
-describe('TickGenerator with a very large timespan', () => {
- let tickGen: TickGenerator|undefined = undefined;
- beforeAll(() => {
- const timeSpan = new TimeSpan(0.0, 1e9);
- const timeScale = new TimeScale(timeSpan, [0, 100]);
- tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
- });
- it('should generate minor ticks at 2e8 and one major tick at the start',
- () => {
- const expected = [
- {type: TickType.MAJOR, time: 0.0},
- {type: TickType.MINOR, time: 1e8},
- {type: TickType.MINOR, time: 2e8},
- {type: TickType.MINOR, time: 3e8},
- {type: TickType.MINOR, time: 4e8},
- {type: TickType.MEDIUM, time: 5e8},
- {type: TickType.MINOR, time: 6e8},
- {type: TickType.MINOR, time: 7e8},
- {type: TickType.MINOR, time: 8e8},
- {type: TickType.MINOR, time: 9e8},
- ];
- const actual = Array.from(tickGen!);
- expectTicksEqual(actual, expected);
- });
- it('should tell us to use 0 decimal places for labels', () => {
+ it('can generate ticks when span is large', () => {
+ const tickGen =
+ new TickGenerator(new TPTimeSpan(1000000000n, 2000000000n), 1);
+ const expected = [
+ {type: TickType.MAJOR, time: 1000000000n},
+ {type: TickType.MINOR, time: 1100000000n},
+ {type: TickType.MINOR, time: 1200000000n},
+ {type: TickType.MINOR, time: 1300000000n},
+ {type: TickType.MINOR, time: 1400000000n},
+ {type: TickType.MEDIUM, time: 1500000000n},
+ {type: TickType.MINOR, time: 1600000000n},
+ {type: TickType.MINOR, time: 1700000000n},
+ {type: TickType.MINOR, time: 1800000000n},
+ {type: TickType.MINOR, time: 1900000000n},
+ ];
+ const actual = Array.from(tickGen!);
+ expect(actual).toStrictEqual(expected);
expect(tickGen!.digits).toEqual(0);
});
-});
-describe('TickGenerator where the timespan has a dynamic range of 1e12', () => {
- // This is the equivalent of zooming in to the nanosecond level, 1000 seconds
- // into a trace Note: this is about the limit of what this generator can
- // handle.
- let tickGen: TickGenerator|undefined = undefined;
- beforeAll(() => {
- const timeSpan = new TimeSpan(1000, 1000.000000001);
- const timeScale = new TimeScale(timeSpan, [0, 100]);
- tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+ it('throws an error when timespan duration is 0', () => {
+ expect(() => {
+ new TickGenerator(new TPTimeSpan(0n, 0n), 1);
+ }).toThrow(Error);
});
- it('should generate minor ticks at 1e-10s and one major tick at the start',
- () => {
- const expected = [
- {type: TickType.MAJOR, time: 1000.0000000000},
- {type: TickType.MINOR, time: 1000.0000000001},
- {type: TickType.MINOR, time: 1000.0000000002},
- {type: TickType.MINOR, time: 1000.0000000003},
- {type: TickType.MINOR, time: 1000.0000000004},
- {type: TickType.MEDIUM, time: 1000.0000000005},
- {type: TickType.MINOR, time: 1000.0000000006},
- {type: TickType.MINOR, time: 1000.0000000007},
- {type: TickType.MINOR, time: 1000.0000000008},
- {type: TickType.MINOR, time: 1000.0000000009},
- ];
- const actual = Array.from(tickGen!);
- expectTicksEqual(actual, expected);
- });
- it('should tell us to use 9 decimal places for labels', () => {
- expect(tickGen!.digits).toEqual(9);
+
+ it('throws an error when max ticks is 0', () => {
+ expect(() => {
+ new TickGenerator(new TPTimeSpan(0n, 1n), 0);
+ }).toThrow(Error);
});
});
-
-describe(
- 'TickGenerator where the timespan has a ridiculously huge dynamic range',
- () => {
- // We don't expect this to work, just wanna make sure it doesn't crash or
- // get stuck
- it('should not crash or get stuck in an infinite loop', () => {
- const timeSpan = new TimeSpan(1000, 1000.000000000001);
- const timeScale = new TimeScale(timeSpan, [0, 100]);
- new TickGenerator(timeScale);
- });
- });
-
-describe(
- 'TickGenerator where the timespan has a ridiculously huge dynamic range',
- () => {
- // We don't expect this to work, just wanna make sure it doesn't crash or
- // get stuck
- it('should not crash or get stuck in an infinite loop', () => {
- const timeSpan = new TimeSpan(1000, 1000.000000000001);
- const timeScale = new TimeScale(timeSpan, [0, 100]);
- new TickGenerator(timeScale);
- });
- });
-
-test('TickGenerator constructed with a 0 width throws an error', () => {
- expect(() => {
- const timeScale = new TimeScale(new TimeSpan(0.0, 1.0), [0, 0]);
- new TickGenerator(timeScale);
- }).toThrow(Error);
-});
-
-test(
- 'TickGenerator constructed with desiredPxPerStep of 0 throws an error',
- () => {
- expect(() => {
- new TickGenerator(timeScale, {minLabelPx: 0});
- }).toThrow(Error);
- });
-
-test('TickGenerator constructed with a 0 duration throws an error', () => {
- expect(() => {
- const timeScale = new TimeScale(new TimeSpan(0.0, 0.0), [0, 1]);
- new TickGenerator(timeScale);
- }).toThrow(Error);
-});
-
-function expectTicksEqual(actual: Tick[], expected: any[]) {
- // TODO(stevegolton) We could write a custom matcher for this; this approach
- // produces cryptic error messages.
- expect(actual.length).toEqual(expected.length);
- for (let i = 0; i < actual.length; ++i) {
- const ex = expected[i];
- const ac = actual[i];
- expect(ac.type).toEqual(ex.type);
- expect(ac.time).toBeCloseTo(ex.time, 9);
- }
-}
diff --git a/ui/src/frontend/keyboard_event_handler.ts b/ui/src/frontend/keyboard_event_handler.ts
index e757c12..f25bf52 100644
--- a/ui/src/frontend/keyboard_event_handler.ts
+++ b/ui/src/frontend/keyboard_event_handler.ts
@@ -14,6 +14,7 @@
import {Actions} from '../common/actions';
import {Area} from '../common/state';
+import {TPTime} from '../common/time';
import {Flow, globals} from './globals';
import {toggleHelp} from './help_modal';
@@ -23,7 +24,8 @@
} from './scroll_helper';
import {executeSearch} from './search_handler';
-const INSTANT_FOCUS_DURATION_S = 1 / 1e9; // 1 ns.
+const INSTANT_FOCUS_DURATION = 1n;
+const INCOMPLETE_SLICE_DURATION = 30_000n;
type Direction = 'Forward'|'Backward';
// Handles all key events than are not handled by the
@@ -55,8 +57,8 @@
if (selection !== null && selection.kind === 'AREA') {
const area = globals.state.areas[selection.areaId];
const coversEntireTimeRange =
- globals.state.traceTime.startSec === area.startSec &&
- globals.state.traceTime.endSec === area.endSec;
+ globals.state.traceTime.start === area.start &&
+ globals.state.traceTime.end === area.end;
if (!coversEntireTimeRange) {
// If the current selection is an area which does not cover the entire
// time range, preserve the list of selected tracks and expand the time
@@ -71,10 +73,11 @@
// If the current selection is not an area, select all.
tracksToSelect = Object.keys(globals.state.tracks);
}
+ const {start, end} = globals.state.traceTime;
globals.dispatch(Actions.selectArea({
area: {
- startSec: globals.state.traceTime.startSec,
- endSec: globals.state.traceTime.endSec,
+ start,
+ end,
tracks: tracksToSelect,
},
}));
@@ -201,29 +204,29 @@
}
}
-function findTimeRangeOfSelection(): {startTs: number, endTs: number} {
+function findTimeRangeOfSelection(): {startTs: TPTime, endTs: TPTime} {
const selection = globals.state.currentSelection;
- let startTs = -1;
- let endTs = -1;
+ let startTs = -1n;
+ let endTs = -1n;
if (selection === null) {
return {startTs, endTs};
} else if (selection.kind === 'SLICE' || selection.kind === 'CHROME_SLICE') {
const slice = globals.sliceDetails;
if (slice.ts && slice.dur !== undefined && slice.dur > 0) {
- startTs = slice.ts + globals.state.traceTime.startSec;
+ startTs = slice.ts;
endTs = startTs + slice.dur;
} else if (slice.ts) {
- startTs = slice.ts + globals.state.traceTime.startSec;
+ startTs = slice.ts;
// This will handle either:
// a)slice.dur === -1 -> unfinished slice
// b)slice.dur === 0 -> instant event
- endTs = slice.dur === -1 ? globals.state.traceTime.endSec :
- startTs + INSTANT_FOCUS_DURATION_S;
+ endTs = slice.dur === -1n ? startTs + INCOMPLETE_SLICE_DURATION :
+ startTs + INSTANT_FOCUS_DURATION;
}
} else if (selection.kind === 'THREAD_STATE') {
const threadState = globals.threadStateDetails;
if (threadState.ts && threadState.dur) {
- startTs = threadState.ts + globals.state.traceTime.startSec;
+ startTs = threadState.ts;
endTs = startTs + threadState.dur;
}
} else if (selection.kind === 'COUNTER') {
@@ -232,8 +235,8 @@
} else if (selection.kind === 'AREA') {
const selectedArea = globals.state.areas[selection.areaId];
if (selectedArea) {
- startTs = selectedArea.startSec;
- endTs = selectedArea.endSec;
+ startTs = selectedArea.start;
+ endTs = selectedArea.end;
}
} else if (selection.kind === 'NOTE') {
const selectedNote = globals.state.notes[selection.id];
@@ -241,18 +244,18 @@
// above in the AREA case.
if (selectedNote && selectedNote.noteType === 'DEFAULT') {
startTs = selectedNote.timestamp;
- endTs = selectedNote.timestamp + INSTANT_FOCUS_DURATION_S;
+ endTs = selectedNote.timestamp + INSTANT_FOCUS_DURATION;
}
} else if (selection.kind === 'LOG') {
// TODO(hjd): Make focus selection work for logs.
} else if (
selection.kind === 'DEBUG_SLICE' ||
selection.kind === 'TOP_LEVEL_SCROLL') {
- startTs = selection.startS;
- if (selection.durationS > 0) {
- endTs = startTs + selection.durationS;
+ startTs = selection.start;
+ if (selection.duration > 0) {
+ endTs = startTs + selection.duration;
} else {
- endTs = startTs + INSTANT_FOCUS_DURATION_S;
+ endTs = startTs + INSTANT_FOCUS_DURATION;
}
}
@@ -262,12 +265,12 @@
function lockSliceSpan(persistent = false) {
const range = findTimeRangeOfSelection();
- if (range.startTs !== -1 && range.endTs !== -1 &&
+ if (range.startTs !== -1n && range.endTs !== -1n &&
globals.state.currentSelection !== null) {
const tracks = globals.state.currentSelection.trackId ?
[globals.state.currentSelection.trackId] :
[];
- const area: Area = {startSec: range.startTs, endSec: range.endTs, tracks};
+ const area: Area = {start: range.startTs, end: range.endTs, tracks};
globals.dispatch(Actions.markArea({area, persistent}));
}
}
@@ -277,7 +280,7 @@
if (selection === null) return;
const range = findTimeRangeOfSelection();
- if (range.startTs !== -1 && range.endTs !== -1) {
+ if (range.startTs !== -1n && range.endTs !== -1n) {
focusHorizontalRange(range.startTs, range.endTs);
}
diff --git a/ui/src/frontend/logs_panel.ts b/ui/src/frontend/logs_panel.ts
index 18ed325..88baa1c 100644
--- a/ui/src/frontend/logs_panel.ts
+++ b/ui/src/frontend/logs_panel.ts
@@ -16,14 +16,14 @@
import {assertExists} from '../base/logging';
import {Actions} from '../common/actions';
+import {HighPrecisionTimeSpan} from '../common/high_precision_time';
import {
LogBounds,
LogBoundsKey,
LogEntries,
LogEntriesKey,
} from '../common/logs';
-import {formatTimestamp} from '../common/time';
-import {TimeSpan} from '../common/time';
+import {formatTPTime, TPTime} from '../common/time';
import {SELECTED_LOG_ROWS_COLOR} from './css_constants';
import {globals} from './globals';
@@ -62,12 +62,16 @@
dom.parentElement!.parentElement!.parentElement as HTMLElement);
this.scrollContainer.addEventListener(
'scroll', this.onScroll.bind(this), {passive: true});
+ // TODO(stevegolton): Type assersions are a source of bugs.
+ // Let's try to find another way of doing this.
this.bounds = globals.trackDataStore.get(LogBoundsKey) as LogBounds;
this.entries = globals.trackDataStore.get(LogEntriesKey) as LogEntries;
this.recomputeVisibleRowsAndUpdate();
}
onbeforeupdate(_: m.CVnodeDOM) {
+ // TODO(stevegolton): Type assersions are a source of bugs.
+ // Let's try to find another way of doing this.
this.bounds = globals.trackDataStore.get(LogBoundsKey) as LogBounds;
this.entries = globals.trackDataStore.get(LogEntriesKey) as LogEntries;
this.recomputeVisibleRowsAndUpdate();
@@ -79,12 +83,12 @@
globals.rafScheduler.scheduleFullRedraw();
}
- onRowOver(ts: number) {
+ onRowOver(ts: TPTime) {
globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
}
onRowOut() {
- globals.dispatch(Actions.setHoverCursorTimestamp({ts: -1}));
+ globals.dispatch(Actions.setHoverCursorTimestamp({ts: -1n}));
}
private totalRows():
@@ -92,17 +96,19 @@
if (!this.bounds) {
return {isStale: false, total: 0, offset: 0, count: 0};
}
- const {total, startTs, endTs, firstRowTs, lastRowTs} = this.bounds;
+ const {
+ totalVisibleLogs,
+ firstVisibleLogTs,
+ lastVisibleLogTs,
+ } = this.bounds;
const vis = globals.frontendLocalState.visibleWindowTime;
- const leftSpan = new TimeSpan(startTs, firstRowTs);
- const rightSpan = new TimeSpan(lastRowTs, endTs);
- const isStaleLeft = !leftSpan.isInBounds(vis.start);
- const isStaleRight = !rightSpan.isInBounds(vis.end);
- const isStale = isStaleLeft || isStaleRight;
- const offset = Math.min(this.visibleRowOffset, total);
- const visCount = Math.min(total - offset, this.visibleRowCount);
- return {isStale, total, count: visCount, offset};
+ const visibleLogSpan =
+ new HighPrecisionTimeSpan(firstVisibleLogTs, lastVisibleLogTs);
+ const isStale = !vis.contains(visibleLogSpan);
+ const offset = Math.min(this.visibleRowOffset, totalVisibleLogs);
+ const visCount = Math.min(totalVisibleLogs - offset, this.visibleRowCount);
+ return {isStale, total: totalVisibleLogs, count: visCount, offset};
}
view(_: m.CVnode<{}>) {
@@ -146,11 +152,10 @@
{
'class': isStale ? 'stale' : '',
style,
- 'onmouseover': this.onRowOver.bind(this, ts / 1e9),
+ 'onmouseover': this.onRowOver.bind(this, ts),
'onmouseout': this.onRowOut.bind(this),
},
- m('.cell',
- formatTimestamp(ts / 1e9 - globals.state.traceTime.startSec)),
+ m('.cell', formatTPTime(ts - globals.state.traceTime.start)),
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 d3eec07..27d9ec8 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -17,7 +17,9 @@
import {Actions} from '../common/actions';
import {randomColor} from '../common/colorizer';
import {AreaNote, Note} from '../common/state';
-import {timeToString} from '../common/time';
+import {
+ tpTimeToString,
+} from '../common/time';
import {
BottomTab,
@@ -28,6 +30,7 @@
import {PerfettoMouseEvent} from './events';
import {globals} from './globals';
import {
+ getMaxMajorTicks,
TickGenerator,
TickType,
timeScaleForVisibleWindow,
@@ -46,7 +49,7 @@
function getStartTimestamp(note: Note|AreaNote) {
if (note.noteType === 'AREA') {
- return globals.state.areas[note.areaId].startSec;
+ return globals.state.areas[note.areaId].start;
} else {
return note.timestamp;
}
@@ -66,7 +69,7 @@
});
dom.addEventListener('mouseout', () => {
this.hoveredX = null;
- globals.dispatch(Actions.setHoveredNoteTimestamp({ts: -1}));
+ globals.dispatch(Actions.setHoveredNoteTimestamp({ts: -1n}));
}, {passive: true});
}
@@ -110,15 +113,27 @@
}
renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
- const timeScale = globals.frontendLocalState.timeScale;
let aNoteIsHovered = false;
ctx.fillStyle = '#999';
ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
- const relScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
- if (relScale.timeSpan.duration > 0 && relScale.widthPx > 0) {
- for (const {type, position} of new TickGenerator(relScale)) {
- if (type === TickType.MAJOR) ctx.fillRect(position, 0, 1, size.height);
+
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height);
+ ctx.clip();
+
+ const span = globals.frontendLocalState.visibleWindow.timestampSpan;
+ const {visibleTimeScale} = globals.frontendLocalState;
+ 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);
+ for (const {type, time} of new TickGenerator(
+ span, maxMajorTicks, globals.state.traceTime.start)) {
+ const px = Math.floor(map.tpTimeToPx(time));
+ if (type === TickType.MAJOR) {
+ ctx.fillRect(px, 0, 1, size.height);
+ }
}
}
@@ -129,11 +144,10 @@
const timestamp = getStartTimestamp(note);
// TODO(hjd): We should still render area selection marks in viewport is
// *within* the area (e.g. both lhs and rhs are out of bounds).
- if ((note.noteType !== 'AREA' && !timeScale.timeInBounds(timestamp)) ||
+ if ((note.noteType !== 'AREA' && !span.contains(timestamp)) ||
(note.noteType === 'AREA' &&
- !timeScale.timeInBounds(globals.state.areas[note.areaId].endSec) &&
- !timeScale.timeInBounds(
- globals.state.areas[note.areaId].startSec))) {
+ !span.contains(globals.state.areas[note.areaId].end) &&
+ !span.contains(globals.state.areas[note.areaId].start))) {
continue;
}
const currentIsHovered =
@@ -144,7 +158,7 @@
const isSelected = selection !== null &&
((selection.kind === 'NOTE' && selection.id === note.id) ||
(selection.kind === 'AREA' && selection.noteId === note.id));
- const x = timeScale.timeToPx(timestamp);
+ const x = visibleTimeScale.tpTimeToPx(timestamp);
const left = Math.floor(x + TRACK_SHELL_WIDTH);
// Draw flag or marker.
@@ -153,7 +167,8 @@
this.drawAreaMarker(
ctx,
left,
- Math.floor(timeScale.timeToPx(area.endSec) + TRACK_SHELL_WIDTH),
+ Math.floor(
+ visibleTimeScale.tpTimeToPx(area.end) + TRACK_SHELL_WIDTH),
note.color,
isSelected);
} else {
@@ -175,19 +190,21 @@
// A real note is hovered so we don't need to see the preview line.
// TODO(hjd): Change cursor to pointer here.
if (aNoteIsHovered) {
- globals.dispatch(Actions.setHoveredNoteTimestamp({ts: -1}));
+ globals.dispatch(Actions.setHoveredNoteTimestamp({ts: -1n}));
}
// View preview note flag when hovering on notes panel.
if (!aNoteIsHovered && this.hoveredX !== null) {
- const timestamp = timeScale.pxToTime(this.hoveredX);
- if (timeScale.timeInBounds(timestamp)) {
+ const timestamp = visibleTimeScale.pxToHpTime(this.hoveredX).toTPTime();
+ if (span.contains(timestamp)) {
globals.dispatch(Actions.setHoveredNoteTimestamp({ts: timestamp}));
- const x = timeScale.timeToPx(timestamp);
+ const x = visibleTimeScale.tpTimeToPx(timestamp);
const left = Math.floor(x + TRACK_SHELL_WIDTH);
this.drawFlag(ctx, left, size.height, '#aaa', /* fill */ true);
}
}
+
+ ctx.restore();
}
private drawAreaMarker(
@@ -197,7 +214,7 @@
ctx.strokeStyle = color;
const topOffset = 10;
// Don't draw in the track shell section.
- if (x >= globals.frontendLocalState.timeScale.startPx + TRACK_SHELL_WIDTH) {
+ if (x >= globals.frontendLocalState.windowSpan.start + TRACK_SHELL_WIDTH) {
// Draw left triangle.
ctx.beginPath();
ctx.moveTo(x, topOffset);
@@ -218,7 +235,7 @@
// Start line after track shell section, join triangles.
const startDraw = Math.max(
- x, globals.frontendLocalState.timeScale.startPx + TRACK_SHELL_WIDTH);
+ x, globals.frontendLocalState.windowSpan.start + TRACK_SHELL_WIDTH);
ctx.beginPath();
ctx.moveTo(startDraw, topOffset);
ctx.lineTo(xEnd, topOffset);
@@ -250,8 +267,8 @@
private onClick(x: number, _: number) {
if (x < 0) return;
- const timeScale = globals.frontendLocalState.timeScale;
- const timestamp = timeScale.pxToTime(x);
+ const {visibleTimeScale} = globals.frontendLocalState;
+ const timestamp = visibleTimeScale.pxToHpTime(x).toTPTime();
for (const note of Object.values(globals.state.notes)) {
if (this.hoveredX && this.mouseOverNote(this.hoveredX, note)) {
if (note.noteType === 'AREA') {
@@ -268,13 +285,13 @@
}
private mouseOverNote(x: number, note: AreaNote|Note): boolean {
- const timeScale = globals.frontendLocalState.timeScale;
- const noteX = timeScale.timeToPx(getStartTimestamp(note));
+ const timeScale = globals.frontendLocalState.visibleTimeScale;
+ const noteX = timeScale.tpTimeToPx(getStartTimestamp(note));
if (note.noteType === 'AREA') {
const noteArea = globals.state.areas[note.areaId];
return (noteX <= x && x < noteX + AREA_TRIANGLE_WIDTH) ||
- (timeScale.timeToPx(noteArea.endSec) > x &&
- x > timeScale.timeToPx(noteArea.endSec) - AREA_TRIANGLE_WIDTH);
+ (timeScale.tpTimeToPx(noteArea.end) > x &&
+ x > timeScale.tpTimeToPx(noteArea.end) - AREA_TRIANGLE_WIDTH);
} else {
const width = FLAG_WIDTH;
return noteX <= x && x < noteX + width;
@@ -308,13 +325,12 @@
if (note === undefined) {
return m('.', `No Note with id ${this.config.id}`);
}
- const startTime =
- getStartTimestamp(note) - globals.state.traceTime.startSec;
+ const startTime = getStartTimestamp(note) - globals.state.traceTime.start;
return m(
'.notes-editor-panel',
m('.notes-editor-panel-heading-bar',
m('.notes-editor-panel-heading',
- `Annotation at ${timeToString(startTime)}`),
+ `Annotation at ${tpTimeToString(startTime)}`),
m('input[type=text]', {
onkeydown: (e: Event) => {
e.stopImmediatePropagation();
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index 76b4515..54f932e 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -14,9 +14,12 @@
import m from 'mithril';
-import {assertExists} from '../base/logging';
import {hueForCpu} from '../common/colorizer';
-import {TimeSpan} from '../common/time';
+import {
+ Span,
+ TPTime,
+ tpTimeToSeconds,
+} from '../common/time';
import {
OVERVIEW_TIMELINE_NON_VISIBLE_COLOR,
@@ -29,9 +32,9 @@
import {OuterDragStrategy} from './drag/outer_drag_strategy';
import {DragGestureHandler} from './drag_gesture_handler';
import {globals} from './globals';
-import {TickGenerator, TickType} from './gridline_helper';
+import {getMaxMajorTicks, TickGenerator, TickType} from './gridline_helper';
import {Panel, PanelSize} from './panel';
-import {TimeScale} from './time_scale';
+import {PxSpan, TimeScale} from './time_scale';
export class OverviewTimelinePanel extends Panel {
private static HANDLE_SIZE_PX = 5;
@@ -39,7 +42,7 @@
private width = 0;
private gesture?: DragGestureHandler;
private timeScale?: TimeScale;
- private totTime = new TimeSpan(0, 0);
+ private traceTime?: Span<TPTime>;
private dragStrategy?: DragStrategy;
private readonly boundOnMouseMove = this.onMouseMove.bind(this);
@@ -47,11 +50,11 @@
// https://github.com/Microsoft/TypeScript/issues/1373
onupdate({dom}: m.CVnodeDOM) {
this.width = dom.getBoundingClientRect().width;
- this.totTime = new TimeSpan(
- globals.state.traceTime.startSec, globals.state.traceTime.endSec);
- this.timeScale = new TimeScale(
- this.totTime, [TRACK_SHELL_WIDTH, assertExists(this.width)]);
-
+ this.traceTime = globals.stateTraceTimeTP();
+ const traceTime = globals.stateTraceTime();
+ const pxSpan = new PxSpan(TRACK_SHELL_WIDTH, this.width);
+ this.timeScale =
+ new TimeScale(traceTime.start, traceTime.duration.nanos, pxSpan);
if (this.gesture === undefined) {
this.gesture = new DragGestureHandler(
dom as HTMLElement,
@@ -78,26 +81,27 @@
renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
if (this.width === undefined) return;
+ if (this.traceTime === undefined) return;
if (this.timeScale === undefined) return;
const headerHeight = 20;
const tracksHeight = size.height - headerHeight;
- const timeSpan = new TimeSpan(0, this.totTime.duration);
- const timeScale = new TimeScale(timeSpan, [TRACK_SHELL_WIDTH, this.width]);
-
- if (timeScale.timeSpan.duration > 0 && timeScale.widthPx > 0) {
- const tickGen = new TickGenerator(timeScale);
+ if (size.width > TRACK_SHELL_WIDTH && this.traceTime.duration > 0n) {
+ const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH);
+ const tickGen = new TickGenerator(
+ this.traceTime, maxMajorTicks, globals.state.traceTime.start);
// Draw time labels on the top header.
ctx.font = '10px Roboto Condensed';
ctx.fillStyle = '#999';
- for (const {type, time, position} of tickGen) {
- const xPos = Math.round(position);
+ for (const {type, time} of tickGen) {
+ const xPos = Math.floor(this.timeScale.tpTimeToPx(time));
if (xPos <= 0) continue;
if (xPos > this.width) break;
if (type === TickType.MAJOR) {
ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
- ctx.fillText(time.toFixed(tickGen.digits) + ' s', xPos + 5, 18);
+ const sec = tpTimeToSeconds(time - globals.state.traceTime.start);
+ ctx.fillText(sec.toFixed(tickGen.digits) + ' s', xPos + 5, 18);
} else if (type == TickType.MEDIUM) {
ctx.fillRect(xPos - 1, 0, 1, 8);
} else if (type == TickType.MINOR) {
@@ -114,8 +118,8 @@
for (const key of globals.overviewStore.keys()) {
const loads = globals.overviewStore.get(key)!;
for (let i = 0; i < loads.length; i++) {
- const xStart = Math.floor(this.timeScale.timeToPx(loads[i].startSec));
- const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].endSec));
+ const xStart = Math.floor(this.timeScale.tpTimeToPx(loads[i].start));
+ const xEnd = Math.ceil(this.timeScale.tpTimeToPx(loads[i].end));
const yOff = Math.floor(headerHeight + y * trackHeight);
const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100);
ctx.fillStyle = `hsl(${hueForCpu(y)}, 50%, ${lightness}%)`;
@@ -210,10 +214,10 @@
}
private static extractBounds(timeScale: TimeScale): [number, number] {
- const vizTime = globals.frontendLocalState.getVisibleStateBounds();
+ const vizTime = globals.frontendLocalState.visibleWindowTime;
return [
- Math.floor(timeScale.timeToPx(vizTime[0])),
- Math.ceil(timeScale.timeToPx(vizTime[1])),
+ Math.floor(timeScale.hpTimeToPx(vizTime.start)),
+ Math.ceil(timeScale.hpTimeToPx(vizTime.end)),
];
}
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 4c6576f..b7841d7 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -135,11 +135,13 @@
return;
}
+ const {visibleTimeScale} = globals.frontendLocalState;
+
// The Y value is given from the top of the pan and zoom region, we want it
// from the top of the panel container. The parent offset corrects that.
const panels = this.getPanelsInRegion(
- globals.frontendLocalState.timeScale.timeToPx(area.startSec),
- globals.frontendLocalState.timeScale.timeToPx(area.endSec),
+ visibleTimeScale.tpTimeToPx(area.start),
+ visibleTimeScale.tpTimeToPx(area.end),
globals.frontendLocalState.areaY.start + TOPBAR_HEIGHT,
globals.frontendLocalState.areaY.end + TOPBAR_HEIGHT);
// Get the track ids from the panels.
@@ -160,7 +162,7 @@
}
}
}
- globals.frontendLocalState.selectArea(area.startSec, area.endSec, tracks);
+ globals.frontendLocalState.selectArea(area.start, area.end, tracks);
}
constructor(vnode: m.CVnode<Attrs>) {
@@ -449,8 +451,9 @@
return;
}
- const startX = globals.frontendLocalState.timeScale.timeToPx(area.startSec);
- const endX = globals.frontendLocalState.timeScale.timeToPx(area.endSec);
+ const {visibleTimeScale} = globals.frontendLocalState;
+ const startX = visibleTimeScale.tpTimeToPx(area.start);
+ const endX = visibleTimeScale.tpTimeToPx(area.end);
// To align with where to draw on the canvas subtract the first panel Y.
selectedTracksMinY -= this.panelContainerTop;
selectedTracksMaxY -= this.panelContainerTop;
diff --git a/ui/src/frontend/pivot_table_query_generator.ts b/ui/src/frontend/pivot_table_query_generator.ts
index 0c61f56..dffa6e4 100644
--- a/ui/src/frontend/pivot_table_query_generator.ts
+++ b/ui/src/frontend/pivot_table_query_generator.ts
@@ -20,7 +20,6 @@
PivotTableQuery,
PivotTableState,
} from '../common/state';
-import {toNs} from '../common/time';
import {
getSelectedTrackIds,
} from '../controller/aggregation/slice_aggregation_controller';
@@ -100,8 +99,8 @@
export function areaFilter(area: Area): string {
return `
- ts + dur > ${toNs(area.startSec)}
- and ts < ${toNs(area.endSec)}
+ ts + dur > ${area.start}
+ and ts < ${area.end}
and track_id in (${getSelectedTrackIds(area).join(', ')})
`;
}
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index c27ecc2..a58d1d6 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -14,13 +14,15 @@
import m from 'mithril';
+import {BigintMath} from '../base/bigint_math';
import {Actions} from '../common/actions';
import {QueryResponse} from '../common/queries';
import {ColumnType, Row} from '../common/query_result';
-import {fromNs} from '../common/time';
-import {Anchor} from './anchor';
+import {TPTime, tpTimeFromNanos} from '../common/time';
+import {TPDuration} from '../common/time';
+import {Anchor} from './anchor';
import {copyToClipboard, queryResponseToClipboard} from './clipboard';
import {downloadData} from './download_utils';
import {globals} from './globals';
@@ -38,6 +40,16 @@
}
// Convert column value to number if it's a bigint or a number, otherwise throw
+function colToTimestamp(colValue: ColumnType): TPTime {
+ if (typeof colValue === 'bigint') {
+ return colValue;
+ } else if (typeof colValue === 'number') {
+ return tpTimeFromNanos(colValue);
+ } else {
+ throw Error('Value is not a number or a bigint');
+ }
+}
+
function colToNumber(colValue: ColumnType): number {
if (typeof colValue === 'bigint') {
return Number(colValue);
@@ -48,6 +60,15 @@
}
}
+function colToDuration(colValue: ColumnType): TPDuration {
+ return colToTimestamp(colValue);
+}
+
+function clampDurationLower(
+ dur: TPDuration, lowerClamp: TPDuration): TPDuration {
+ return BigintMath.max(dur, lowerClamp);
+}
+
class QueryTableRow implements m.ClassComponent<QueryTableRowAttrs> {
static columnsContainsSliceLocation(columns: string[]) {
const requiredColumns = ['ts', 'dur', 'track_id'];
@@ -65,15 +86,14 @@
// the slice.
event.stopPropagation();
- const sliceStart = fromNs(colToNumber(row.ts));
+ const sliceStart = colToTimestamp(row.ts);
// row.dur can be negative. Clamp to 1ns.
- const sliceDur = fromNs(Math.max(colToNumber(row.dur), 1));
+ const sliceDur = clampDurationLower(colToDuration(row.dur), 1n);
const sliceEnd = sliceStart + sliceDur;
- const trackId: number = colToNumber(row.track_id);
+ const trackId = colToNumber(row.track_id);
const uiTrackId = globals.state.uiTrackIdByTraceTrackId[trackId];
if (uiTrackId === undefined) return;
verticalScrollToTrack(uiTrackId, true);
- // TODO(stevegolton) Soon this function will only accept Bigints
focusHorizontalRange(sliceStart, sliceEnd);
let sliceId: number|undefined;
diff --git a/ui/src/frontend/scroll_helper.ts b/ui/src/frontend/scroll_helper.ts
index 18a9a78..f177a65 100644
--- a/ui/src/frontend/scroll_helper.ts
+++ b/ui/src/frontend/scroll_helper.ts
@@ -13,23 +13,28 @@
// limitations under the License.
import {Actions} from '../common/actions';
+import {
+ HighPrecisionTime,
+ HighPrecisionTimeSpan,
+} from '../common/high_precision_time';
import {getContainingTrackId} from '../common/state';
-import {fromNs, TimeSpan, toNs} from '../common/time';
+import {TPTime} from '../common/time';
import {globals} from './globals';
-const INCOMPLETE_SLICE_TIME_S = 0.00003;
// Given a timestamp, if |ts| is not currently in view move the view to
// center |ts|, keeping the same zoom level.
-export function horizontalScrollToTs(ts: number) {
- const startNs = toNs(globals.frontendLocalState.visibleWindowTime.start);
- const endNs = toNs(globals.frontendLocalState.visibleWindowTime.end);
- const currentViewNs = endNs - startNs;
- if (ts < startNs || ts > endNs) {
+// TODO(stevegolton): Remove me!
+export function horizontalScrollToTs(ts: TPTime) {
+ console.log('horizontalScrollToTs', ts);
+ const time = HighPrecisionTime.fromTPTime(ts);
+ const {start, end, duration} = globals.frontendLocalState.visibleWindowTime;
+ const halfDuration = duration.nanos / 2;
+ if (time.isLessThan(start) || time.isGreaterThan(end)) {
// TODO(hjd): This is an ugly jump, we should do a smooth pan instead.
- globals.frontendLocalState.updateVisibleTime(new TimeSpan(
- fromNs(ts - currentViewNs / 2), fromNs(ts + currentViewNs / 2)));
+ globals.frontendLocalState.updateVisibleTime(new HighPrecisionTimeSpan(
+ time.subtractNanos(halfDuration), time.addNanos(halfDuration)));
}
}
@@ -46,16 +51,11 @@
// to cover 1/5 of the viewport.
// - Otherwise, preserve the zoom range.
export function focusHorizontalRange(
- startTs: number, endTs: number, viewPercentage?: number) {
- const visibleDur = globals.frontendLocalState.visibleWindowTime.end -
- globals.frontendLocalState.visibleWindowTime.start;
- let selectDur = endTs - startTs;
- // TODO(altimin): We go from `ts` and `dur` to `startTs` and `endTs` and back
- // to `dur`. We should fix that.
- if (toNs(selectDur) === -1) { // Unfinished slice
- selectDur = INCOMPLETE_SLICE_TIME_S;
- endTs = startTs;
- }
+ start: TPTime, end: TPTime, viewPercentage?: number) {
+ console.log('focusHorizontalRange', start, end);
+ const visible = globals.frontendLocalState.visibleWindowTime;
+ const trace = globals.stateTraceTime();
+ const select = HighPrecisionTimeSpan.fromTpTime(start, end);
if (viewPercentage !== undefined) {
if (viewPercentage <= 0.0 || viewPercentage > 1.0) {
@@ -67,51 +67,43 @@
viewPercentage = 0.5;
}
const paddingPercentage = 1.0 - viewPercentage;
- const paddingTime = selectDur * paddingPercentage;
- const halfPaddingTime = paddingTime / 2;
- globals.frontendLocalState.updateVisibleTime(
- new TimeSpan(startTs - halfPaddingTime, endTs + halfPaddingTime));
+ const paddingTime = select.duration.multiply(paddingPercentage);
+ const halfPaddingTime = paddingTime.divide(2);
+ globals.frontendLocalState.updateVisibleTime(select.pad(halfPaddingTime));
return;
}
-
// If the range is too large to fit on the current zoom level, resize.
- if (selectDur > 0.5 * visibleDur) {
- globals.frontendLocalState.updateVisibleTime(
- new TimeSpan(startTs - (selectDur * 2), endTs + (selectDur * 2)));
+ if (select.duration.isGreaterThan(visible.duration.multiply(0.5))) {
+ const paddedRange = select.pad(select.duration.multiply(2));
+ globals.frontendLocalState.updateVisibleTime(paddedRange);
return;
}
- const midpointTs = (endTs + startTs) / 2;
// Calculate the new visible window preserving the zoom level.
- let newStartTs = midpointTs - visibleDur / 2;
- let newEndTs = midpointTs + visibleDur / 2;
+ let newStart = select.midpoint.subtract(visible.duration.divide(2));
+ let newEnd = select.midpoint.add(visible.duration.divide(2));
// Adjust the new visible window if it intersects with the trace boundaries.
// It's needed to make the "update the zoom level if visible window doesn't
// change" logic reliable.
- if (newEndTs > globals.state.traceTime.endSec) {
- newStartTs = globals.state.traceTime.endSec - visibleDur;
- newEndTs = globals.state.traceTime.endSec;
+ if (newEnd.isGreaterThan(trace.end)) {
+ newStart = trace.end.subtract(visible.duration);
+ newEnd = trace.end;
}
- if (newStartTs < globals.state.traceTime.startSec) {
- newStartTs = globals.state.traceTime.startSec;
- newEndTs = globals.state.traceTime.startSec + visibleDur;
+ if (newStart.isLessThan(trace.start)) {
+ newStart = trace.start;
+ newEnd = trace.start.add(visible.duration);
}
- const newStartNs = toNs(newStartTs);
- const newEndNs = toNs(newEndTs);
-
- const viewStartNs = toNs(globals.frontendLocalState.visibleWindowTime.start);
- const viewEndNs = toNs(globals.frontendLocalState.visibleWindowTime.end);
+ const view = new HighPrecisionTimeSpan(newStart, newEnd);
// If preserving the zoom doesn't change the visible window, update the zoom
// level.
- if (newStartNs === viewStartNs && newEndNs === viewEndNs) {
- globals.frontendLocalState.updateVisibleTime(
- new TimeSpan(startTs - (selectDur * 2), endTs + (selectDur * 2)));
- return;
+ if (view.start.equals(visible.start) && view.end.equals(visible.end)) {
+ const padded = select.pad(select.duration.multiply(2));
+ globals.frontendLocalState.updateVisibleTime(padded);
+ } else {
+ globals.frontendLocalState.updateVisibleTime(view);
}
- globals.frontendLocalState.updateVisibleTime(
- new TimeSpan(newStartTs, newEndTs));
}
// Given a track id, find a track with that id and scroll it into view. If the
@@ -155,7 +147,7 @@
// Scroll vertically and horizontally to reach track (|trackId|) at |ts|.
export function scrollToTrackAndTs(
- trackId: string|number|undefined, ts: number, openGroup = false) {
+ trackId: string|number|undefined, ts: TPTime, openGroup = false) {
if (trackId !== undefined) {
verticalScrollToTrack(trackId, openGroup);
}
diff --git a/ui/src/frontend/search_handler.ts b/ui/src/frontend/search_handler.ts
index 1622a7e..f995617 100644
--- a/ui/src/frontend/search_handler.ts
+++ b/ui/src/frontend/search_handler.ts
@@ -14,8 +14,6 @@
import {searchSegment} from '../base/binary_search';
import {Actions} from '../common/actions';
-import {toNs} from '../common/time';
-
import {globals} from './globals';
function setToPrevious(current: number) {
@@ -34,8 +32,9 @@
export function executeSearch(reverse = false) {
const index = globals.state.searchIndex;
- const startNs = toNs(globals.frontendLocalState.visibleWindowTime.start);
- const endNs = toNs(globals.frontendLocalState.visibleWindowTime.end);
+ const vizWindow = globals.frontendLocalState.visibleWindowTime;
+ const startNs = vizWindow.start.nanos;
+ const endNs = vizWindow.end.nanos;
const currentTs = globals.currentSearchResults.tsStarts[index];
// If the value of |globals.currentSearchResults.totalResults| is 0,
diff --git a/ui/src/frontend/slice.ts b/ui/src/frontend/slice.ts
index 5b660ef..7587a27 100644
--- a/ui/src/frontend/slice.ts
+++ b/ui/src/frontend/slice.ts
@@ -13,13 +13,14 @@
// limitations under the License.
import {Color} from '../common/colorizer';
+import {TPDuration, TPTime} from '../common/time';
export interface Slice {
// These properties are updated only once per query result when the Slice
// object is created and don't change afterwards.
readonly id: number;
- readonly startS: number;
- readonly durationS: number;
+ readonly start: TPTime;
+ readonly duration: TPDuration;
readonly depth: number;
readonly flags: number;
diff --git a/ui/src/frontend/slice_details_panel.ts b/ui/src/frontend/slice_details_panel.ts
index 9b018dd..13e3dd5 100644
--- a/ui/src/frontend/slice_details_panel.ts
+++ b/ui/src/frontend/slice_details_panel.ts
@@ -16,7 +16,7 @@
import {Actions} from '../common/actions';
import {translateState} from '../common/thread_state';
-import {timeToCode, toNs} from '../common/time';
+import {tpTimeToCode} from '../common/time';
import {globals, SliceDetails, ThreadDesc} from './globals';
import {scrollToTrackAndTs} from './scroll_helper';
import {SlicePanel} from './slice_panel';
@@ -60,9 +60,8 @@
if (!threadInfo) {
return null;
}
- const timestamp = timeToCode(
- sliceInfo.wakeupTs! - globals.state.traceTime.startSec,
- );
+ const timestamp =
+ tpTimeToCode(sliceInfo.wakeupTs! - globals.state.traceTime.start);
return m(
'.slice-details-wakeup-text',
m('', `Wakeup @ ${timestamp} on CPU ${sliceInfo.wakerCpu} by`),
@@ -76,9 +75,7 @@
return null;
}
- const latency = timeToCode(
- sliceInfo.ts - (sliceInfo.wakeupTs - globals.state.traceTime.startSec),
- );
+ const latency = tpTimeToCode(sliceInfo.ts - sliceInfo.wakeupTs);
return m(
'.slice-details-latency-text',
m('', `Scheduling latency: ${latency}`),
@@ -111,7 +108,10 @@
{onclick: () => this.goToThread(), title: 'Go to thread'},
'call_made'))),
m('tr', m('th', `Cmdline`), m('td', threadInfo.cmdline)),
- m('tr', m('th', `Start time`), m('td', `${timeToCode(sliceInfo.ts)}`)),
+ m('tr',
+ m('th', `Start time`),
+ m('td',
+ `${tpTimeToCode(sliceInfo.ts - globals.state.traceTime.start)}`)),
m('tr',
m('th', `Duration`),
m('td', this.computeDuration(sliceInfo.ts, sliceInfo.dur))),
@@ -172,8 +172,7 @@
trackId: trackId.toString(),
}));
- scrollToTrackAndTs(
- trackId, toNs(sliceInfo.ts + globals.state.traceTime.startSec), true);
+ scrollToTrackAndTs(trackId, sliceInfo.ts, true);
}
}
diff --git a/ui/src/frontend/slice_panel.ts b/ui/src/frontend/slice_panel.ts
index 17b4aeb..9d6f542 100644
--- a/ui/src/frontend/slice_panel.ts
+++ b/ui/src/frontend/slice_panel.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {timeToCode, toNs} from '../common/time';
+import {TPDuration, TPTime, tpTimeToCode} from '../common/time';
import {globals, SliceDetails} from './globals';
import {Panel} from './panel';
@@ -34,10 +34,9 @@
}
export abstract class SlicePanel extends Panel {
- protected computeDuration(ts: number, dur: number): string {
- return toNs(dur) === -1 ?
- `${globals.state.traceTime.endSec - ts} (Did not end)` :
- timeToCode(dur);
+ protected computeDuration(ts: TPTime, dur: TPDuration): string {
+ return dur === -1n ? `${globals.state.traceTime.end - ts} (Did not end)` :
+ tpTimeToCode(dur);
}
protected getProcessThreadDetails(sliceInfo: SliceDetails) {
diff --git a/ui/src/frontend/sql_types.ts b/ui/src/frontend/sql_types.ts
index f7135fa..b7e2df2 100644
--- a/ui/src/frontend/sql_types.ts
+++ b/ui/src/frontend/sql_types.ts
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {ColumnType} from 'src/common/query_result';
-import {fromNs, toNs} from '../common/time';
+import {TPTime} from '../common/time';
+
import {globals} from './globals';
// Type-safe aliases for various flavours of ints Trace Processor exposes
@@ -26,37 +26,19 @@
// Timestamp (in nanoseconds) in the same time domain as Trace Processor is
// exposing.
-export type TPTimestamp = bigint&{
+export type TPTimestamp = TPTime&{
__type: 'TPTimestamp'
}
-// Create a timestamp from a bigint in nanos.
-// Use this when we know the type is a bigint.
-export function timestampFromNanos(nanos: bigint) {
- return nanos as TPTimestamp;
-}
-
-// Create a timestamp from an arbitrary SQL value.
-// Throws if the value cannot be reasonably converted to a timestamp.
-// Assumes the input will be in units of nanoseconds.
-export function timestampFromSqlNanos(nanos: ColumnType): TPTimestamp {
- if (typeof nanos === 'bigint') {
- return nanos as TPTimestamp;
- } else if (typeof nanos === 'number') {
- // Note - this will throw if the number is something which cannot be
- // represented by an integer - i.e. decimals, infinity, or NaN.
- return BigInt(nanos) as TPTimestamp;
- } else {
- throw Error('Refusing to create TPTimestamp from unrelated type');
- }
+export function asTPTimestamp(v: bigint): TPTimestamp;
+export function asTPTimestamp(v?: bigint): TPTimestamp|undefined;
+export function asTPTimestamp(v?: bigint): TPTimestamp|undefined {
+ return v as (TPTimestamp | undefined);
}
// TODO: unify this with common/time.ts.
-// TODO(stevegolton): Return a bigint, or a new TPDuration object rather than
-// convert to number which could lose precision.
-export function toTraceTime(ts: TPTimestamp): number {
- const traceStartNs = toNs(globals.state.traceTime.startSec);
- return fromNs(Number(ts - BigInt(traceStartNs)));
+export function toTraceTime(ts: TPTimestamp): TPTime {
+ return ts - globals.state.traceTime.start;
}
// Unique id for a process, id into |process| table.
diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts
index c7031f2..0bc4082 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -16,7 +16,11 @@
import {EngineProxy} from '../common/engine';
import {LONG, NUM, NUM_NULL, STR_NULL} from '../common/query_result';
import {translateState} from '../common/thread_state';
-import {fromNs, timeToCode} from '../common/time';
+import {
+ TPDuration,
+ TPTime,
+ tpTimeToCode,
+} from '../common/time';
import {copyToClipboard} from './clipboard';
import {globals} from './globals';
@@ -26,9 +30,6 @@
asUtid,
SchedSqlId,
ThreadStateSqlId,
- timestampFromNanos,
- toTraceTime,
- TPTimestamp,
} from './sql_types';
import {
constraintsToQueryFragment,
@@ -50,10 +51,10 @@
threadStateSqlId: ThreadStateSqlId;
// Id of the corresponding entry in the |sched| table.
schedSqlId?: SchedSqlId;
- // Timestamp of the beginning of this thread state in nanoseconds.
- ts: TPTimestamp;
+ // Timestamp of the the beginning of this thread state in nanoseconds.
+ ts: TPTime;
// Duration of this thread state in nanoseconds.
- dur: number;
+ dur: TPDuration;
// CPU id if this thread state corresponds to a thread running on the CPU.
cpu?: number;
// Human-readable name of this thread state.
@@ -90,7 +91,7 @@
threadStateSqlId: NUM,
schedSqlId: NUM_NULL,
ts: LONG,
- dur: NUM,
+ dur: LONG,
cpu: NUM_NULL,
state: STR_NULL,
blockedFunction: STR_NULL,
@@ -110,7 +111,7 @@
result.push({
threadStateSqlId: it.threadStateSqlId as ThreadStateSqlId,
schedSqlId: fromNumNull(it.schedSqlId) as (SchedSqlId | undefined),
- ts: timestampFromNanos(it.ts),
+ ts: it.ts,
dur: it.dur,
cpu: fromNumNull(it.cpu),
state: translateState(it.state || undefined, ioWait),
@@ -137,7 +138,7 @@
return result[0];
}
-export function goToSchedSlice(cpu: number, id: SchedSqlId, ts: TPTimestamp) {
+export function goToSchedSlice(cpu: number, id: SchedSqlId, ts: TPTime) {
let trackId: string|undefined;
for (const track of Object.values(globals.state.tracks)) {
if (track.kind === 'CpuSliceTrack' &&
@@ -149,15 +150,12 @@
return;
}
globals.makeSelection(Actions.selectSlice({id, trackId}));
- // TODO(stevegolton): scrollToTrackAndTs() should take a TPTimestamp
- scrollToTrackAndTs(trackId, Number(ts));
+ scrollToTrackAndTs(trackId, ts);
}
function stateToValue(
- state: string,
- cpu: number|undefined,
- id: SchedSqlId|undefined,
- ts: TPTimestamp): Value|null {
+ state: string, cpu: number|undefined, id: SchedSqlId|undefined, ts: TPTime):
+ Value|null {
if (!state) {
return null;
}
@@ -177,8 +175,9 @@
export function threadStateToDict(state: ThreadState): Dict {
const result: {[name: string]: Value|null} = {};
- result['Start time'] = value(timeToCode(toTraceTime(state.ts)));
- result['Duration'] = value(timeToCode(fromNs(state.dur)));
+ 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);
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index 00612eb..f1579b9 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -19,6 +19,7 @@
import {TRACK_SHELL_WIDTH} from './css_constants';
import {globals} from './globals';
import {
+ getMaxMajorTicks,
TickGenerator,
TickType,
timeScaleForVisibleWindow,
@@ -32,14 +33,26 @@
}
renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+ const {visibleWindowTime, visibleTimeScale} = globals.frontendLocalState;
ctx.fillStyle = '#999';
ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
- const relScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
- if (relScale.timeSpan.duration > 0 && relScale.widthPx > 0) {
- for (const {type, position} of new TickGenerator(relScale)) {
- if (type === TickType.MAJOR) ctx.fillRect(position, 0, 1, size.height);
+
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height);
+ ctx.clip();
+
+ const span = globals.frontendLocalState.visibleWindow.timestampSpan;
+ 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);
+ for (const {type, time} of new TickGenerator(
+ span, maxMajorTicks, globals.state.traceTime.start)) {
+ const px = Math.floor(map.tpTimeToPx(time));
+ if (type === TickType.MAJOR) {
+ ctx.fillRect(px, 0, 1, size.height);
+ }
}
}
@@ -47,12 +60,13 @@
for (let i = 0; i < data.tsStarts.length; i++) {
const tStart = data.tsStarts[i];
const tEnd = data.tsEnds[i];
- if (tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end) {
+ if (tEnd <= visibleWindowTime.start.seconds ||
+ tStart >= visibleWindowTime.end.seconds) {
continue;
}
const rectStart =
- Math.max(timeScale.timeToPx(tStart), 0) + TRACK_SHELL_WIDTH;
- const rectEnd = timeScale.timeToPx(tEnd) + TRACK_SHELL_WIDTH;
+ Math.max(visibleTimeScale.secondsToPx(tStart), 0) + TRACK_SHELL_WIDTH;
+ const rectEnd = visibleTimeScale.secondsToPx(tEnd) + TRACK_SHELL_WIDTH;
ctx.fillStyle = '#ffe263';
ctx.fillRect(
Math.floor(rectStart),
@@ -61,16 +75,21 @@
size.height);
}
const index = globals.state.searchIndex;
- const startSec = fromNs(globals.currentSearchResults.tsStarts[index]);
- const triangleStart =
- Math.max(timeScale.timeToPx(startSec), 0) + TRACK_SHELL_WIDTH;
- ctx.fillStyle = '#000';
- ctx.beginPath();
- ctx.moveTo(triangleStart, size.height);
- ctx.lineTo(triangleStart - 3, 0);
- ctx.lineTo(triangleStart + 3, 0);
- ctx.lineTo(triangleStart, size.height);
- ctx.fill();
- ctx.closePath();
+ if (index !== -1) {
+ const startSec = fromNs(globals.currentSearchResults.tsStarts[index]);
+ const triangleStart =
+ Math.max(visibleTimeScale.secondsToPx(startSec), 0) +
+ TRACK_SHELL_WIDTH;
+ ctx.fillStyle = '#000';
+ ctx.beginPath();
+ ctx.moveTo(triangleStart, size.height);
+ ctx.lineTo(triangleStart - 3, 0);
+ ctx.lineTo(triangleStart + 3, 0);
+ ctx.lineTo(triangleStart, size.height);
+ ctx.fill();
+ ctx.closePath();
+ }
+
+ ctx.restore();
}
}
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index 3f6dc64..4819d54 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -14,11 +14,15 @@
import m from 'mithril';
-import {timeToString} from '../common/time';
+import {
+ tpTimeToSeconds,
+ tpTimeToString,
+} from '../common/time';
import {TRACK_SHELL_WIDTH} from './css_constants';
import {globals} from './globals';
import {
+ getMaxMajorTicks,
TickGenerator,
TickType,
timeScaleForVisibleWindow,
@@ -35,21 +39,33 @@
ctx.font = '10px Roboto Condensed';
ctx.textAlign = 'left';
- const startTime = timeToString(globals.state.traceTime.startSec);
+ const startTime = tpTimeToString(globals.state.traceTime.start);
ctx.fillText(startTime + ' +', 6, 11);
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height);
+ ctx.clip();
+
// Draw time axis.
- const timeScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
- if (timeScale.timeSpan.duration > 0 && timeScale.widthPx > 0) {
- const tickGen = new TickGenerator(timeScale);
- for (const {type, time, position} of tickGen) {
+ const span = globals.frontendLocalState.visibleWindow.timestampSpan;
+ 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);
+ 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) {
ctx.fillRect(position, 0, 1, size.height);
- ctx.fillText(time.toFixed(tickGen.digits) + ' s', position + 5, 10);
+ ctx.fillText(sec.toFixed(tickGen.digits) + ' s', position + 5, 10);
}
}
}
+ ctx.restore();
+
ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
}
}
diff --git a/ui/src/frontend/time_scale.ts b/ui/src/frontend/time_scale.ts
index 6f0307a..7804348 100644
--- a/ui/src/frontend/time_scale.ts
+++ b/ui/src/frontend/time_scale.ts
@@ -12,95 +12,92 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {assertFalse, assertTrue} from '../base/logging';
-import {TimeSpan} from '../common/time';
+import {assertTrue} from '../base/logging';
+import {
+ HighPrecisionTime,
+ HighPrecisionTimeSpan,
+} from '../common/high_precision_time';
+import {Span} from '../common/time';
+import {
+ TPDuration,
+ TPTime,
+} from '../common/time';
-const MAX_ZOOM_SPAN_SEC = 1e-8; // 10 ns.
-
-/**
- * Defines a mapping between number and seconds for the entire application.
- * Linearly scales time values from boundsMs to pixel values in boundsPx and
- * back.
- */
export class TimeScale {
- private timeBounds: TimeSpan;
- private _startPx: number;
- private _endPx: number;
- private secPerPx = 0;
+ private _start: HighPrecisionTime;
+ private _durationNanos: number;
+ readonly pxSpan: PxSpan;
+ private _nanosPerPx = 0;
+ private _startSec: number;
- constructor(timeBounds: TimeSpan, boundsPx: [number, number]) {
- this.timeBounds = timeBounds;
- this._startPx = boundsPx[0];
- this._endPx = boundsPx[1];
- this.updateSlope();
+ constructor(start: HighPrecisionTime, durationNanos: number, pxSpan: PxSpan) {
+ // TODO(stevegolton): Ensure duration & pxSpan > 0.
+ // assertTrue(pxSpan.start < pxSpan.end, 'Px start >= end');
+ // assertTrue(durationNanos < 0, 'Duration <= 0');
+ this.pxSpan = pxSpan;
+ this._start = start;
+ this._durationNanos = durationNanos;
+ if (durationNanos <= 0 || pxSpan.delta <= 0) {
+ this._nanosPerPx = 1;
+ } else {
+ this._nanosPerPx = durationNanos / (pxSpan.delta);
+ }
+ this._startSec = this._start.seconds;
}
- private updateSlope() {
- this.secPerPx = this.timeBounds.duration / (this._endPx - this._startPx);
+ get timeSpan(): Span<HighPrecisionTime> {
+ const end = this._start.addNanos(this._durationNanos);
+ return new HighPrecisionTimeSpan(this._start, end);
}
- deltaTimeToPx(time: number): number {
- return Math.round(time / this.secPerPx);
+ tpTimeToPx(ts: TPTime): number {
+ // WARNING: Number(bigint) can be surprisingly slow. Avoid in hotpath.
+ const timeOffsetNanos = Number(ts - this._start.base) - this._start.offset;
+ return this.pxSpan.start + timeOffsetNanos / this._nanosPerPx;
}
- timeToPx(time: number): number {
- return this._startPx + (time - this.timeBounds.start) / this.secPerPx;
+ secondsToPx(seconds: number): number {
+ const timeOffset = (seconds - this._startSec) * 1e9;
+ return this.pxSpan.start + timeOffset / this._nanosPerPx;
}
- pxToTime(px: number): number {
- return this.timeBounds.start + (px - this._startPx) * this.secPerPx;
+ hpTimeToPx(time: HighPrecisionTime): number {
+ const timeOffsetNanos = time.subtract(this._start).nanos;
+ return this.pxSpan.start + timeOffsetNanos / this._nanosPerPx;
}
- deltaPxToDuration(px: number): number {
- return px * this.secPerPx;
+ // Convert pixels to a high precision time object, which can be futher
+ // converted to other time formats.
+ pxToHpTime(px: number): HighPrecisionTime {
+ const offsetNanos = (px - this.pxSpan.start) * this._nanosPerPx;
+ return this._start.addNanos(offsetNanos);
}
- setTimeBounds(timeBounds: TimeSpan) {
- this.timeBounds = timeBounds;
- this.updateSlope();
+ durationToPx(dur: TPDuration): number {
+ // WARNING: Number(bigint) can be surprisingly slow. Avoid in hotpath.
+ return Number(dur) / this._nanosPerPx;
}
- setLimitsPx(pxStart: number, pxEnd: number) {
- assertFalse(pxStart === pxEnd);
- assertTrue(pxStart >= 0 && pxEnd >= 0);
- this._startPx = pxStart;
- this._endPx = pxEnd;
- this.updateSlope();
- }
-
- timeInBounds(time: number): boolean {
- return this.timeBounds.isInBounds(time);
- }
-
- get startPx(): number {
- return this._startPx;
- }
-
- get endPx(): number {
- return this._endPx;
- }
-
- get widthPx(): number {
- return this._endPx - this._startPx;
- }
-
- get timeSpan(): TimeSpan {
- return this.timeBounds;
+ pxDeltaToDuration(pxDelta: number): HighPrecisionTime {
+ const time = pxDelta * this._nanosPerPx;
+ return HighPrecisionTime.fromNanos(time);
}
}
-export function computeZoom(
- scale: TimeScale, span: TimeSpan, zoomFactor: number, zoomPx: number):
- TimeSpan {
- const startPx = scale.startPx;
- const endPx = scale.endPx;
- const deltaPx = endPx - startPx;
- const deltaTime = span.end - span.start;
- const newDeltaTime = Math.max(deltaTime * zoomFactor, MAX_ZOOM_SPAN_SEC);
- const clampedZoomPx = Math.max(startPx, Math.min(endPx, zoomPx));
- const zoomTime = scale.pxToTime(clampedZoomPx);
- const r = (clampedZoomPx - startPx) / deltaPx;
- const newStartTime = zoomTime - newDeltaTime * r;
- const newEndTime = newStartTime + newDeltaTime;
- return new TimeSpan(newStartTime, newEndTime);
+export class PxSpan {
+ constructor(private _start: number, private _end: number) {
+ assertTrue(_start <= _end, 'PxSpan start > end');
+ }
+
+ get start(): number {
+ return this._start;
+ }
+
+ get end(): number {
+ return this._end;
+ }
+
+ get delta(): number {
+ return this._end - this._start;
+ }
}
diff --git a/ui/src/frontend/time_scale_unittest.ts b/ui/src/frontend/time_scale_unittest.ts
index 7a1be03..f7046de 100644
--- a/ui/src/frontend/time_scale_unittest.ts
+++ b/ui/src/frontend/time_scale_unittest.ts
@@ -12,66 +12,62 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {TimeSpan} from '../common/time';
+import {HighPrecisionTime} from '../common/high_precision_time';
-import {computeZoom, TimeScale} from './time_scale';
+import {PxSpan, TimeScale} from './time_scale';
-test('time scale to work', () => {
- const scale = new TimeScale(new TimeSpan(0, 100), [200, 1000]);
+describe('TimeScale', () => {
+ const ts =
+ new TimeScale(new HighPrecisionTime(40n), 100, new PxSpan(200, 1000));
- expect(scale.timeToPx(0)).toEqual(200);
- expect(scale.timeToPx(100)).toEqual(1000);
- expect(scale.timeToPx(50)).toEqual(600);
+ it('converts timescales to pixels', () => {
+ expect(ts.tpTimeToPx(40n)).toEqual(200);
+ expect(ts.tpTimeToPx(140n)).toEqual(1000);
+ expect(ts.tpTimeToPx(90n)).toEqual(600);
- expect(scale.pxToTime(200)).toEqual(0);
- expect(scale.pxToTime(1000)).toEqual(100);
- expect(scale.pxToTime(600)).toEqual(50);
+ expect(ts.tpTimeToPx(240n)).toEqual(1800);
+ expect(ts.tpTimeToPx(-60n)).toEqual(-600);
+ });
- expect(scale.deltaPxToDuration(400)).toEqual(50);
+ it('converts pixels to HPTime objects', () => {
+ let result = ts.pxToHpTime(200);
+ expect(result.base).toEqual(40n);
+ expect(result.offset).toBeCloseTo(0);
- expect(scale.timeInBounds(50)).toEqual(true);
- expect(scale.timeInBounds(0)).toEqual(true);
- expect(scale.timeInBounds(100)).toEqual(true);
- expect(scale.timeInBounds(-1)).toEqual(false);
- expect(scale.timeInBounds(101)).toEqual(false);
-});
+ result = ts.pxToHpTime(1000);
+ expect(result.base).toEqual(140n);
+ expect(result.offset).toBeCloseTo(0);
+ result = ts.pxToHpTime(600);
+ expect(result.base).toEqual(90n);
+ expect(result.offset).toBeCloseTo(0);
-test('time scale to be updatable', () => {
- const scale = new TimeScale(new TimeSpan(0, 100), [100, 1000]);
+ result = ts.pxToHpTime(1800);
+ expect(result.base).toEqual(240n);
+ expect(result.offset).toBeCloseTo(0);
- expect(scale.timeToPx(0)).toEqual(100);
+ result = ts.pxToHpTime(-600);
+ expect(result.base).toEqual(-60n);
+ expect(result.offset).toBeCloseTo(0);
+ });
- scale.setLimitsPx(200, 1000);
- expect(scale.timeToPx(0)).toEqual(200);
- expect(scale.timeToPx(100)).toEqual(1000);
+ it('converts durations to pixels', () => {
+ expect(ts.durationToPx(0n)).toEqual(0);
+ expect(ts.durationToPx(1n)).toEqual(8);
+ expect(ts.durationToPx(1000n)).toEqual(8000);
+ });
- scale.setTimeBounds(new TimeSpan(0, 200));
- expect(scale.timeToPx(0)).toEqual(200);
- expect(scale.timeToPx(100)).toEqual(600);
- expect(scale.timeToPx(200)).toEqual(1000);
-});
+ it('converts pxDeltaToDurations to HPTime durations', () => {
+ let result = ts.pxDeltaToDuration(0);
+ expect(result.base).toEqual(0n);
+ expect(result.offset).toBeCloseTo(0);
-test('it zooms', () => {
- const span = new TimeSpan(0, 20);
- const scale = new TimeScale(span, [0, 100]);
- const newSpan = computeZoom(scale, span, 0.5, 50);
- expect(newSpan.start).toEqual(5);
- expect(newSpan.end).toEqual(15);
-});
+ result = ts.pxDeltaToDuration(1);
+ expect(result.base).toEqual(0n);
+ expect(result.offset).toBeCloseTo(0.125);
-test('it zooms an offset scale and span', () => {
- const span = new TimeSpan(1000, 1020);
- const scale = new TimeScale(span, [200, 300]);
- const newSpan = computeZoom(scale, span, 0.5, 250);
- expect(newSpan.start).toEqual(1005);
- expect(newSpan.end).toEqual(1015);
-});
-
-test('it clamps zoom in', () => {
- const span = new TimeSpan(1000, 1040);
- const scale = new TimeScale(span, [200, 300]);
- const newSpan = computeZoom(scale, span, 0.0000000001, 225);
- expect((newSpan.end - newSpan.start) / 2 + newSpan.start).toBeCloseTo(1010);
- expect(newSpan.end - newSpan.start).toBeCloseTo(1e-8);
+ result = ts.pxDeltaToDuration(100);
+ expect(result.base).toEqual(12n);
+ expect(result.offset).toBeCloseTo(0.5);
+ });
});
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index 20f1c53..5711e6a 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -13,9 +13,13 @@
// limitations under the License.
import m from 'mithril';
+import {BigintMath} from '../base/bigint_math';
-import {timeToString} from '../common/time';
-import {TimeSpan} from '../common/time';
+import {Span, tpTimeToString} from '../common/time';
+import {
+ TPTime,
+ TPTimeSpan,
+} from '../common/time';
import {
BACKGROUND_COLOR,
@@ -24,6 +28,7 @@
} from './css_constants';
import {globals} from './globals';
import {
+ getMaxMajorTicks,
TickGenerator,
TickType,
timeScaleForVisibleWindow,
@@ -48,7 +53,7 @@
ctx.fillStyle = FOREGROUND_COLOR;
const xLeft = Math.floor(target.x);
- const xRight = Math.ceil(target.x + target.width);
+ const xRight = Math.floor(target.x + target.width);
const yMid = Math.floor(target.height / 2 + target.y);
const xWidth = xRight - xLeft;
@@ -130,11 +135,21 @@
renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
ctx.fillStyle = '#999';
ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
- const scale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
- if (scale.timeSpan.duration > 0 && scale.widthPx > 0) {
- for (const {position, type} of new TickGenerator(scale)) {
+
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height);
+ ctx.clip();
+
+ const span = globals.frontendLocalState.visibleWindow.timestampSpan;
+ 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);
+ for (const {type, time} of new TickGenerator(
+ span, maxMajorTicks, globals.state.traceTime.start)) {
+ const px = Math.floor(map.tpTimeToPx(time));
if (type === TickType.MAJOR) {
- ctx.fillRect(position, 0, 1, size.height);
+ ctx.fillRect(px, 0, 1, size.height);
}
}
}
@@ -142,17 +157,17 @@
const localArea = globals.frontendLocalState.selectedArea;
const selection = globals.state.currentSelection;
if (localArea !== undefined) {
- const start = Math.min(localArea.startSec, localArea.endSec);
- const end = Math.max(localArea.startSec, localArea.endSec);
- this.renderSpan(ctx, size, new TimeSpan(start, end));
+ const start = BigintMath.min(localArea.start, localArea.end);
+ const end = BigintMath.max(localArea.start, localArea.end);
+ this.renderSpan(ctx, size, new TPTimeSpan(start, end));
} else if (selection !== null && selection.kind === 'AREA') {
const selectedArea = globals.state.areas[selection.areaId];
- const start = Math.min(selectedArea.startSec, selectedArea.endSec);
- const end = Math.max(selectedArea.startSec, selectedArea.endSec);
- this.renderSpan(ctx, size, new TimeSpan(start, end));
+ const start = BigintMath.min(selectedArea.start, selectedArea.end);
+ const end = BigintMath.max(selectedArea.start, selectedArea.end);
+ this.renderSpan(ctx, size, new TPTimeSpan(start, end));
}
- if (globals.state.hoverCursorTimestamp !== -1) {
+ if (globals.state.hoverCursorTimestamp !== -1n) {
this.renderHover(ctx, size, globals.state.hoverCursorTimestamp);
}
@@ -162,27 +177,29 @@
if (note.noteType === 'AREA' && !noteIsSelected) {
const selectedArea = globals.state.areas[note.areaId];
this.renderSpan(
- ctx,
- size,
- new TimeSpan(selectedArea.startSec, selectedArea.endSec));
+ ctx, size, new TPTimeSpan(selectedArea.start, selectedArea.end));
}
}
+
+ ctx.restore();
}
- renderHover(ctx: CanvasRenderingContext2D, size: PanelSize, ts: number) {
- const timeScale = globals.frontendLocalState.timeScale;
- const xPos = TRACK_SHELL_WIDTH + Math.floor(timeScale.timeToPx(ts));
- const offsetTime = timeToString(ts - globals.state.traceTime.startSec);
- const timeFromStart = timeToString(ts);
+ renderHover(ctx: CanvasRenderingContext2D, size: PanelSize, ts: TPTime) {
+ 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})`;
drawIBar(ctx, xPos, this.bounds(size), label);
}
- renderSpan(ctx: CanvasRenderingContext2D, size: PanelSize, span: TimeSpan) {
- const timeScale = globals.frontendLocalState.timeScale;
- const xLeft = timeScale.timeToPx(span.start);
- const xRight = timeScale.timeToPx(span.end);
- const label = timeToString(span.duration);
+ renderSpan(
+ ctx: CanvasRenderingContext2D, size: PanelSize, span: Span<TPTime>) {
+ const {visibleTimeScale} = globals.frontendLocalState;
+ const xLeft = visibleTimeScale.tpTimeToPx(span.start);
+ const xRight = visibleTimeScale.tpTimeToPx(span.end);
+ const label = tpTimeToString(span.duration);
drawHBar(
ctx,
{
diff --git a/ui/src/frontend/trace_converter.ts b/ui/src/frontend/trace_converter.ts
index ca4ced3..0f5ca69 100644
--- a/ui/src/frontend/trace_converter.ts
+++ b/ui/src/frontend/trace_converter.ts
@@ -17,6 +17,7 @@
ConversionJobName,
ConversionJobStatus,
} from '../common/conversion_jobs';
+import {TPTime} from '../common/time';
import {download} from './clipboard';
import {maybeShowErrorDialog} from './error_dialog';
@@ -106,7 +107,7 @@
}
export function convertTraceToPprofAndDownload(
- trace: Blob, pid: number, ts: number) {
+ trace: Blob, pid: number, ts: TPTime) {
makeWorkerAndPost({
kind: 'ConvertTraceToPprof',
trace,
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index 2d5ab81..e826317 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -130,9 +130,11 @@
render(ctx: CanvasRenderingContext2D) {
globals.frontendLocalState.addVisibleTrack(this.trackState.id);
if (this.data() === undefined && !this.frontendOnly) {
- const {visibleWindowTime, timeScale} = globals.frontendLocalState;
- const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start));
- const endPx = Math.ceil(timeScale.timeToPx(visibleWindowTime.end));
+ const {visibleWindowTime, visibleTimeScale} = globals.frontendLocalState;
+ const startPx =
+ Math.floor(visibleTimeScale.hpTimeToPx(visibleWindowTime.start));
+ const endPx =
+ Math.ceil(visibleTimeScale.hpTimeToPx(visibleWindowTime.end));
checkerboard(ctx, this.getHeight(), startPx, endPx);
} else {
this.renderCanvas(ctx);
@@ -175,7 +177,7 @@
y -= 10;
// Ensure the box is on screen:
- const endPx = globals.frontendLocalState.timeScale.endPx;
+ const endPx = globals.frontendLocalState.visibleTimeScale.pxSpan.end;
if (x + width > endPx) {
x -= x + width - endPx;
}
diff --git a/ui/src/frontend/track_cache.ts b/ui/src/frontend/track_cache.ts
index 049cbf4..2adbd0c 100644
--- a/ui/src/frontend/track_cache.ts
+++ b/ui/src/frontend/track_cache.ts
@@ -52,6 +52,7 @@
// In other words the normal window is a superset of the data of the
// non-normal window at a higher resolution. Normalization is used to
// avoid re-fetching data on tiny zooms/moves/resizes.
+// TODO(stevegolton): Convert to bigint timestamps.
export class CacheKey {
readonly startNs: number;
readonly endNs: number;
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index c9d109f..dbab7f7 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -187,18 +187,17 @@
}
highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
- const localState = globals.frontendLocalState;
+ const {visibleTimeScale} = globals.frontendLocalState;
const selection = globals.state.currentSelection;
if (!selection || selection.kind !== 'AREA') return;
const selectedArea = globals.state.areas[selection.areaId];
+ const selectedAreaDuration = selectedArea.end - selectedArea.start;
if (selectedArea.tracks.includes(this.trackGroupId)) {
ctx.fillStyle = 'rgba(131, 152, 230, 0.3)';
ctx.fillRect(
- localState.timeScale.timeToPx(selectedArea.startSec) +
- this.shellWidth,
+ visibleTimeScale.tpTimeToPx(selectedArea.start) + this.shellWidth,
0,
- localState.timeScale.deltaTimeToPx(
- selectedArea.endSec - selectedArea.startSec),
+ visibleTimeScale.durationToPx(selectedAreaDuration),
size.height);
}
}
@@ -227,20 +226,20 @@
this.highlightIfTrackSelected(ctx, size);
- const localState = globals.frontendLocalState;
+ const {visibleTimeScale} = globals.frontendLocalState;
// Draw vertical line when hovering on the notes panel.
- if (globals.state.hoveredNoteTimestamp !== -1) {
+ if (globals.state.hoveredNoteTimestamp !== -1n) {
drawVerticalLineAtTime(
ctx,
- localState.timeScale,
+ visibleTimeScale,
globals.state.hoveredNoteTimestamp,
size.height,
`#aaa`);
}
- if (globals.state.hoverCursorTimestamp !== -1) {
+ if (globals.state.hoverCursorTimestamp !== -1n) {
drawVerticalLineAtTime(
ctx,
- localState.timeScale,
+ visibleTimeScale,
globals.state.hoverCursorTimestamp,
size.height,
`#344596`);
@@ -251,7 +250,7 @@
globals.sliceDetails.wakeupTs !== undefined) {
drawVerticalLineAtTime(
ctx,
- localState.timeScale,
+ visibleTimeScale,
globals.sliceDetails.wakeupTs,
size.height,
`black`);
@@ -265,21 +264,21 @@
'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
drawVerticalLineAtTime(
ctx,
- localState.timeScale,
- globals.state.areas[note.areaId].startSec,
+ visibleTimeScale,
+ globals.state.areas[note.areaId].start,
size.height,
transparentNoteColor,
1);
drawVerticalLineAtTime(
ctx,
- localState.timeScale,
- globals.state.areas[note.areaId].endSec,
+ visibleTimeScale,
+ globals.state.areas[note.areaId].end,
size.height,
transparentNoteColor,
1);
} else if (note.noteType === 'DEFAULT') {
drawVerticalLineAtTime(
- ctx, localState.timeScale, note.timestamp, size.height, note.color);
+ ctx, visibleTimeScale, note.timestamp, size.height, note.color);
}
}
}
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 9110c1d..278b73d 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -362,20 +362,20 @@
}
highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
- const localState = globals.frontendLocalState;
+ const {visibleTimeScale} = globals.frontendLocalState;
const selection = globals.state.currentSelection;
const trackState = this.trackState;
if (!selection || selection.kind !== 'AREA' || trackState === undefined) {
return;
}
const selectedArea = globals.state.areas[selection.areaId];
+ const selectedAreaDuration = selectedArea.end - selectedArea.start;
if (selectedArea.tracks.includes(trackState.id)) {
- const timeScale = localState.timeScale;
ctx.fillStyle = SELECTION_FILL_COLOR;
ctx.fillRect(
- timeScale.timeToPx(selectedArea.startSec) + TRACK_SHELL_WIDTH,
+ visibleTimeScale.tpTimeToPx(selectedArea.start) + TRACK_SHELL_WIDTH,
0,
- timeScale.deltaTimeToPx(selectedArea.endSec - selectedArea.startSec),
+ visibleTimeScale.durationToPx(selectedAreaDuration),
size.height);
}
}
@@ -396,20 +396,20 @@
this.highlightIfTrackSelected(ctx, size);
- const localState = globals.frontendLocalState;
+ const {visibleTimeScale} = globals.frontendLocalState;
// Draw vertical line when hovering on the notes panel.
- if (globals.state.hoveredNoteTimestamp !== -1) {
+ if (globals.state.hoveredNoteTimestamp !== -1n) {
drawVerticalLineAtTime(
ctx,
- localState.timeScale,
+ visibleTimeScale,
globals.state.hoveredNoteTimestamp,
size.height,
`#aaa`);
}
- if (globals.state.hoverCursorTimestamp !== -1) {
+ if (globals.state.hoverCursorTimestamp !== -1n) {
drawVerticalLineAtTime(
ctx,
- localState.timeScale,
+ visibleTimeScale,
globals.state.hoverCursorTimestamp,
size.height,
`#344596`);
@@ -420,7 +420,7 @@
globals.sliceDetails.wakeupTs !== undefined) {
drawVerticalLineAtTime(
ctx,
- localState.timeScale,
+ visibleTimeScale,
globals.sliceDetails.wakeupTs,
size.height,
`black`);
@@ -434,21 +434,21 @@
'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
drawVerticalLineAtTime(
ctx,
- localState.timeScale,
- globals.state.areas[note.areaId].startSec,
+ visibleTimeScale,
+ globals.state.areas[note.areaId].start,
size.height,
transparentNoteColor,
1);
drawVerticalLineAtTime(
ctx,
- localState.timeScale,
- globals.state.areas[note.areaId].endSec,
+ visibleTimeScale,
+ globals.state.areas[note.areaId].end,
size.height,
transparentNoteColor,
1);
} else if (note.noteType === 'DEFAULT') {
drawVerticalLineAtTime(
- ctx, localState.timeScale, note.timestamp, size.height, note.color);
+ ctx, visibleTimeScale, note.timestamp, size.height, note.color);
}
}
}
diff --git a/ui/src/frontend/vertical_line_helper.ts b/ui/src/frontend/vertical_line_helper.ts
index b1e2cfc..353d166 100644
--- a/ui/src/frontend/vertical_line_helper.ts
+++ b/ui/src/frontend/vertical_line_helper.ts
@@ -12,18 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {TPTime} from '../common/time';
import {TRACK_SHELL_WIDTH} from './css_constants';
import {TimeScale} from './time_scale';
-export function drawVerticalLineAtTime(ctx: CanvasRenderingContext2D,
- timeScale: TimeScale,
- time: number,
- height: number,
- color: string,
- lineWidth = 2) {
- const xPos = TRACK_SHELL_WIDTH + Math.floor(timeScale.timeToPx(time));
- drawVerticalLine(ctx, xPos, height, color, lineWidth);
- }
+export function drawVerticalLineAtTime(
+ ctx: CanvasRenderingContext2D,
+ timeScale: TimeScale,
+ time: TPTime,
+ height: number,
+ color: string,
+ lineWidth = 2) {
+ const xPos = TRACK_SHELL_WIDTH + Math.floor(timeScale.tpTimeToPx(time));
+ drawVerticalLine(ctx, xPos, height, color, lineWidth);
+}
function drawVerticalLine(ctx: CanvasRenderingContext2D,
xPos: number,
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index d3651e7..1a9e096 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -13,10 +13,10 @@
// limitations under the License.
import m from 'mithril';
+import {BigintMath} from '../base/bigint_math';
import {Actions} from '../common/actions';
import {featureFlags} from '../common/feature_flags';
-import {TimeSpan} from '../common/time';
import {TRACK_SHELL_WIDTH} from './css_constants';
import {DetailsPanel} from './details_panel';
@@ -28,7 +28,6 @@
import {AnyAttrsVnode, PanelContainer} from './panel_container';
import {TickmarkPanel} from './tickmark_panel';
import {TimeAxisPanel} from './time_axis_panel';
-import {computeZoom} from './time_scale';
import {TimeSelectionPanel} from './time_selection_panel';
import {DISMISSED_PANNING_HINT_KEY} from './topbar';
import {TrackGroupPanel} from './track_group_panel';
@@ -53,8 +52,9 @@
const area = globals.frontendLocalState.selectedArea ?
globals.frontendLocalState.selectedArea :
globals.state.areas[selection.areaId];
- const start = globals.frontendLocalState.timeScale.timeToPx(area.startSec);
- const end = globals.frontendLocalState.timeScale.timeToPx(area.endSec);
+ const {visibleTimeScale} = globals.frontendLocalState;
+ const start = visibleTimeScale.tpTimeToPx(area.start);
+ const end = visibleTimeScale.tpTimeToPx(area.end);
const startDrag = mousePos - TRACK_SHELL_WIDTH;
const startDistance = Math.abs(start - startDrag);
const endDistance = Math.abs(end - startDrag);
@@ -119,21 +119,14 @@
element: panZoomEl,
contentOffsetX: SIDEBAR_WIDTH,
onPanned: (pannedPx: number) => {
+ const {
+ visibleTimeScale,
+ } = globals.frontendLocalState;
+
this.keepCurrentSelection = true;
- const traceTime = globals.state.traceTime;
- const vizTime = globals.frontendLocalState.visibleWindowTime;
- const origDelta = vizTime.duration;
- const tDelta = frontendLocalState.timeScale.deltaPxToDuration(pannedPx);
- let tStart = vizTime.start + tDelta;
- let tEnd = vizTime.end + tDelta;
- if (tStart < traceTime.startSec) {
- tStart = traceTime.startSec;
- tEnd = tStart + origDelta;
- } else if (tEnd > traceTime.endSec) {
- tEnd = traceTime.endSec;
- tStart = tEnd - origDelta;
- }
- frontendLocalState.updateVisibleTime(new TimeSpan(tStart, tEnd));
+ const tDelta = visibleTimeScale.pxDeltaToDuration(pannedPx);
+ frontendLocalState.panVisibleWindow(tDelta);
+
// If the user has panned they no longer need the hint.
localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true');
globals.rafScheduler.scheduleRedraw();
@@ -141,11 +134,10 @@
onZoomed: (zoomedPositionPx: number, zoomRatio: number) => {
// TODO(hjd): Avoid hardcoding TRACK_SHELL_WIDTH.
// TODO(hjd): Improve support for zooming in overview timeline.
- const span = frontendLocalState.visibleWindowTime;
- const scale = frontendLocalState.timeScale;
const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH;
- const newSpan = computeZoom(scale, span, 1 - zoomRatio, zoomPx);
- frontendLocalState.updateVisibleTime(newSpan);
+ const rect = vnode.dom.getBoundingClientRect();
+ const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH);
+ frontendLocalState.zoomVisibleWindow(1 - zoomRatio, centerPoint);
globals.rafScheduler.scheduleRedraw();
},
editSelection: (currentPx: number) => {
@@ -159,7 +151,7 @@
currentY: number,
editing: boolean) => {
const traceTime = globals.state.traceTime;
- const scale = frontendLocalState.timeScale;
+ const {visibleTimeScale} = frontendLocalState;
this.keepCurrentSelection = true;
if (editing) {
const selection = globals.state.currentSelection;
@@ -167,33 +159,40 @@
const area = globals.frontendLocalState.selectedArea ?
globals.frontendLocalState.selectedArea :
globals.state.areas[selection.areaId];
- let newTime = scale.pxToTime(currentX - TRACK_SHELL_WIDTH);
+ let newTime =
+ visibleTimeScale.pxToHpTime(currentX - TRACK_SHELL_WIDTH)
+ .toTPTime();
// Have to check again for when one boundary crosses over the other.
const curBoundary = onTimeRangeBoundary(prevX);
if (curBoundary == null) return;
- const keepTime =
- curBoundary === 'START' ? area.endSec : area.startSec;
+ const keepTime = curBoundary === 'START' ? area.end : area.start;
// Don't drag selection outside of current screen.
if (newTime < keepTime) {
- newTime = Math.max(newTime, scale.pxToTime(scale.startPx));
+ newTime = BigintMath.max(
+ newTime, visibleTimeScale.timeSpan.start.toTPTime());
} else {
- newTime = Math.min(newTime, scale.pxToTime(scale.endPx));
+ newTime = BigintMath.max(
+ newTime, visibleTimeScale.timeSpan.end.toTPTime());
}
// When editing the time range we always use the saved tracks,
// since these will not change.
frontendLocalState.selectArea(
- Math.max(Math.min(keepTime, newTime), traceTime.startSec),
- Math.min(Math.max(keepTime, newTime), traceTime.endSec),
+ BigintMath.max(
+ BigintMath.min(keepTime, newTime), traceTime.start),
+ BigintMath.min(
+ BigintMath.max(keepTime, newTime), traceTime.end),
globals.state.areas[selection.areaId].tracks);
}
} else {
let startPx = Math.min(dragStartX, currentX) - TRACK_SHELL_WIDTH;
let endPx = Math.max(dragStartX, currentX) - TRACK_SHELL_WIDTH;
if (startPx < 0 && endPx < 0) return;
- startPx = Math.max(startPx, scale.startPx);
- endPx = Math.min(endPx, scale.endPx);
+ startPx = Math.max(startPx, visibleTimeScale.pxSpan.start);
+ endPx = Math.min(endPx, visibleTimeScale.pxSpan.end);
frontendLocalState.selectArea(
- scale.pxToTime(startPx), scale.pxToTime(endPx));
+ visibleTimeScale.pxToHpTime(startPx).toTPTime('floor'),
+ visibleTimeScale.pxToHpTime(endPx).toTPTime('ceil'),
+ );
frontendLocalState.areaY.start = dragStartY;
frontendLocalState.areaY.end = currentY;
}
@@ -271,6 +270,9 @@
m('.pan-and-zoom-content',
{
onclick: () => {
+ // TODO(stevegolton): Make it possible to click buttons and
+ // things on this element without deselecting the selected
+ // element!
// We don't want to deselect when panning/drag selecting.
if (this.keepCurrentSelection) {
this.keepCurrentSelection = false;
diff --git a/ui/src/frontend/widgets/duration.ts b/ui/src/frontend/widgets/duration.ts
index 6f36c95..cd18e02 100644
--- a/ui/src/frontend/widgets/duration.ts
+++ b/ui/src/frontend/widgets/duration.ts
@@ -14,14 +14,14 @@
import m from 'mithril';
-import {fromNs, timeToCode} from '../../common/time';
+import {TPDuration, tpTimeToCode} from '../../common/time';
interface DurationAttrs {
- dur: number;
+ dur: TPDuration;
}
export class Duration implements m.ClassComponent<DurationAttrs> {
view(vnode: m.Vnode<DurationAttrs>) {
- return timeToCode(fromNs(vnode.attrs.dur));
+ return tpTimeToCode(vnode.attrs.dur);
}
}
diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts
index 4552275..d8cc841 100644
--- a/ui/src/frontend/widgets/timestamp.ts
+++ b/ui/src/frontend/widgets/timestamp.ts
@@ -14,7 +14,7 @@
import m from 'mithril';
-import {timeToCode} from '../../common/time';
+import {tpTimeToCode} from '../../common/time';
import {toTraceTime, TPTimestamp} from '../sql_types';
interface TimestampAttrs {
@@ -23,6 +23,6 @@
export class Timestamp implements m.ClassComponent<TimestampAttrs> {
view(vnode: m.Vnode<TimestampAttrs>) {
- return timeToCode(toTraceTime(vnode.attrs.ts));
+ return tpTimeToCode(toTraceTime(vnode.attrs.ts));
}
}
diff --git a/ui/src/traceconv/index.ts b/ui/src/traceconv/index.ts
index 714666a..df3e8a0 100644
--- a/ui/src/traceconv/index.ts
+++ b/ui/src/traceconv/index.ts
@@ -18,6 +18,7 @@
ConversionJobName,
ConversionJobStatus,
} from '../common/conversion_jobs';
+import {TPTime} from '../common/time';
import traceconv from '../gen/traceconv';
const selfWorker = self as {} as Worker;
@@ -176,7 +177,7 @@
kind: 'ConvertTraceToPprof';
trace: Blob;
pid: number;
- ts: number;
+ ts: TPTime;
}
function isConvertTraceToPprof(msg: Args): msg is ConvertTraceToPprofArgs {
@@ -186,8 +187,7 @@
return true;
}
-async function ConvertTraceToPprof(
-trace: Blob, pid: number, ts: number) {
+async function ConvertTraceToPprof(trace: Blob, pid: number, ts: TPTime) {
const jobName = 'convert_pprof';
updateJobStatus(jobName, ConversionJobStatus.InProgress);
const args = [
diff --git a/ui/src/tracks/actual_frames/index.ts b/ui/src/tracks/actual_frames/index.ts
index 927dea4..7f5afb0 100644
--- a/ui/src/tracks/actual_frames/index.ts
+++ b/ui/src/tracks/actual_frames/index.ts
@@ -14,7 +14,7 @@
import {PluginContext} from '../../common/plugin_api';
import {NUM, NUM_NULL, STR} from '../../common/query_result';
-import {fromNs, toNs} from '../../common/time';
+import {fromNs, TPDuration, TPTime, tpTimeToNanos} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {TrackController} from '../../controller/track_controller';
import {NewTrackArgs, Track} from '../../frontend/track';
@@ -51,16 +51,17 @@
static readonly kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
private maxDurNs = 0;
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
- const startNs = toNs(start);
- const endNs = toNs(end);
+ const startNs = tpTimeToNanos(start);
+ const endNs = tpTimeToNanos(end);
const pxSize = this.pxSize();
// ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
// be an even number, so we can snap in the middle.
- const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
+ const bucketNs =
+ Math.max(Math.round(Number(resolution) * pxSize / 2) * 2, 1);
if (this.maxDurNs === 0) {
const maxDurResult = await this.query(`
diff --git a/ui/src/tracks/android_log/index.ts b/ui/src/tracks/android_log/index.ts
index b2a824b..6c8a1bc 100644
--- a/ui/src/tracks/android_log/index.ts
+++ b/ui/src/tracks/android_log/index.ts
@@ -14,7 +14,7 @@
import {PluginContext} from '../../common/plugin_api';
import {NUM} from '../../common/query_result';
-import {fromNs, toNsCeil, toNsFloor} from '../../common/time';
+import {fromNs, TPDuration, TPTime} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {LIMIT} from '../../common/track_data';
import {
@@ -61,21 +61,15 @@
class AndroidLogTrackController extends TrackController<Config, Data> {
static readonly kind = ANDROID_LOGS_TRACK_KIND;
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
- const startNs = toNsFloor(start);
- const endNs = toNsCeil(end);
-
- // |resolution| is in s/px the frontend wants.
- const quantNs = toNsCeil(resolution);
-
const queryRes = await this.query(`
select
- cast(ts / ${quantNs} as integer) * ${quantNs} as tsQuant,
+ cast(ts / ${resolution} as integer) * ${resolution} as tsQuant,
prio,
count(prio) as numEvents
from android_logs
- where ts >= ${startNs} and ts <= ${endNs}
+ where ts >= ${start} and ts <= ${end}
group by tsQuant, prio
order by tsQuant, prio limit ${LIMIT};`);
@@ -113,16 +107,16 @@
}
renderCanvas(ctx: CanvasRenderingContext2D): void {
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+ const {visibleTimeScale, windowSpan} = globals.frontendLocalState;
const data = this.data();
if (data === undefined) return; // Can't possibly draw anything.
- const dataStartPx = timeScale.timeToPx(data.start);
- const dataEndPx = timeScale.timeToPx(data.end);
- const visibleStartPx = timeScale.timeToPx(visibleWindowTime.start);
- const visibleEndPx = timeScale.timeToPx(visibleWindowTime.end);
+ const dataStartPx = visibleTimeScale.tpTimeToPx(data.start);
+ const dataEndPx = visibleTimeScale.tpTimeToPx(data.end);
+ const visibleStartPx = windowSpan.start;
+ const visibleEndPx = windowSpan.end;
checkerboardExcept(
ctx,
@@ -133,7 +127,7 @@
dataEndPx);
const quantWidth =
- Math.max(EVT_PX, timeScale.deltaTimeToPx(data.resolution));
+ Math.max(EVT_PX, visibleTimeScale.durationToPx(data.resolution));
const blockH = RECT_HEIGHT / LEVELS.length;
for (let i = 0; i < data.timestamps.length; i++) {
for (let lev = 0; lev < LEVELS.length; lev++) {
@@ -143,7 +137,7 @@
}
if (!hasEventsForCurColor) continue;
ctx.fillStyle = LEVELS[lev].color;
- const px = Math.floor(timeScale.timeToPx(data.timestamps[i]));
+ const px = Math.floor(visibleTimeScale.secondsToPx(data.timestamps[i]));
ctx.fillRect(px, MARGIN_TOP + blockH * lev, quantWidth, blockH);
} // for(lev)
} // for (timestamps)
diff --git a/ui/src/tracks/async_slices/index.ts b/ui/src/tracks/async_slices/index.ts
index 338e02a..997577f 100644
--- a/ui/src/tracks/async_slices/index.ts
+++ b/ui/src/tracks/async_slices/index.ts
@@ -13,8 +13,8 @@
// limitations under the License.
import {PluginContext} from '../../common/plugin_api';
-import {NUM, NUM_NULL, STR} from '../../common/query_result';
-import {fromNs, toNs} from '../../common/time';
+import {LONG_NULL, NUM, STR} from '../../common/query_result';
+import {fromNs, TPDuration, TPTime} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {
TrackController,
@@ -43,26 +43,24 @@
class AsyncSliceTrackController extends TrackController<Config, Data> {
static readonly kind = ASYNC_SLICE_TRACK_KIND;
- private maxDurNs = 0;
+ private maxDurNs: TPDuration = 0n;
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
- const startNs = toNs(start);
- const endNs = toNs(end);
-
const pxSize = this.pxSize();
// ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
// be an even number, so we can snap in the middle.
- const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
+ const bucketNs =
+ Math.max(Math.round(Number(resolution) * pxSize / 2) * 2, 1);
- if (this.maxDurNs === 0) {
+ if (this.maxDurNs === 0n) {
const maxDurResult = await this.query(`
select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
as maxDur from experimental_slice_layout
where filter_track_ids = '${this.config.trackIds.join(',')}'
`);
- this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
+ this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
}
const queryRes = await this.query(`
@@ -78,8 +76,8 @@
from experimental_slice_layout
where
filter_track_ids = '${this.config.trackIds.join(',')}' and
- ts >= ${startNs - this.maxDurNs} and
- ts <= ${endNs}
+ ts >= ${start - this.maxDurNs} and
+ ts <= ${end}
group by tsq, layout_depth
order by tsq, layout_depth
`);
diff --git a/ui/src/tracks/chrome_slices/index.ts b/ui/src/tracks/chrome_slices/index.ts
index 23f9a5e..4e70507 100644
--- a/ui/src/tracks/chrome_slices/index.ts
+++ b/ui/src/tracks/chrome_slices/index.ts
@@ -15,9 +15,16 @@
import {Actions} from '../../common/actions';
import {cropText, drawIncompleteSlice} from '../../common/canvas_utils';
import {colorForThreadIdleSlice, hslForSlice} from '../../common/colorizer';
+import {
+ HighPrecisionTime,
+} from '../../common/high_precision_time';
import {PluginContext} from '../../common/plugin_api';
-import {NUM, NUM_NULL, STR} from '../../common/query_result';
-import {fromNs, toNs} from '../../common/time';
+import {LONG_NULL, NUM, NUM_NULL, STR} from '../../common/query_result';
+import {
+ fromNs,
+ TPDuration,
+ TPTime,
+} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {
TrackController,
@@ -60,27 +67,25 @@
export class ChromeSliceTrackController extends TrackController<Config, Data> {
static kind = SLICE_TRACK_KIND;
- private maxDurNs = 0;
+ private maxDurNs: TPDuration = 0n;
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
- const startNs = toNs(start);
- const endNs = toNs(end);
-
const pxSize = this.pxSize();
// ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
// be an even number, so we can snap in the middle.
- const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
+ const bucketNs =
+ Math.max(Math.round(Number(resolution) * pxSize / 2) * 2, 1);
const tableName = this.namespaceTable('slice');
- if (this.maxDurNs === 0) {
+ if (this.maxDurNs === 0n) {
const query = `
SELECT max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
AS maxDur FROM ${tableName} WHERE track_id = ${this.config.trackId}`;
const queryRes = await this.query(query);
- this.maxDurNs = queryRes.firstRow({maxDur: NUM_NULL}).maxDur || 0;
+ this.maxDurNs = queryRes.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
}
// Buckets are always even and positive, don't quantize once we zoom to
@@ -103,8 +108,8 @@
thread_dur as threadDur
FROM ${tableName}
WHERE track_id = ${this.config.trackId} AND
- ts >= (${startNs - this.maxDurNs}) AND
- ts <= ${endNs}
+ ts >= (${start - this.maxDurNs}) AND
+ ts <= ${end}
GROUP BY depth, tsq`;
const queryRes = await this.query(query);
@@ -209,7 +214,7 @@
renderCanvas(ctx: CanvasRenderingContext2D): void {
// TODO: fonts and colors should come from the CSS and not hardcoded here.
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+ const {visibleTimeScale, visibleWindowTime} = globals.frontendLocalState;
const data = this.data();
if (data === undefined) return; // Can't possibly draw anything.
@@ -219,10 +224,10 @@
checkerboardExcept(
ctx,
this.getHeight(),
- timeScale.timeToPx(visibleWindowTime.start),
- timeScale.timeToPx(visibleWindowTime.end),
- timeScale.timeToPx(data.start),
- timeScale.timeToPx(data.end),
+ visibleTimeScale.hpTimeToPx(visibleWindowTime.start),
+ visibleTimeScale.hpTimeToPx(visibleWindowTime.end),
+ visibleTimeScale.tpTimeToPx(data.start),
+ visibleTimeScale.tpTimeToPx(data.end),
);
ctx.textAlign = 'center';
@@ -245,7 +250,7 @@
const title = data.strings[titleId];
const colorOverride = data.colors && data.strings[data.colors[i]];
if (isIncomplete) { // incomplete slice
- tEnd = visibleWindowTime.end;
+ tEnd = visibleWindowTime.end.seconds;
}
const rect = this.getSliceRect(tStart, tEnd, depth);
@@ -369,10 +374,14 @@
getSliceIndex({x, y}: {x: number, y: number}): number|void {
const data = this.data();
if (data === undefined) return;
- const {timeScale} = globals.frontendLocalState;
+ const {
+ visibleTimeScale: timeScale,
+ visibleWindowTime,
+ } = globals.frontendLocalState;
if (y < TRACK_PADDING) return;
- const instantWidthTime = timeScale.deltaPxToDuration(HALF_CHEVRON_WIDTH_PX);
- const t = timeScale.pxToTime(x);
+ const instantWidthTime = timeScale.pxDeltaToDuration(HALF_CHEVRON_WIDTH_PX);
+ const instantWidthTimeSec = instantWidthTime.seconds;
+ const t = timeScale.pxToHpTime(x).seconds;
const depth = Math.floor((y - TRACK_PADDING) / SLICE_HEIGHT);
for (let i = 0; i < data.starts.length; i++) {
if (depth !== data.depths[i]) {
@@ -380,13 +389,13 @@
}
const tStart = data.starts[i];
if (data.isInstant[i]) {
- if (Math.abs(tStart - t) < instantWidthTime) {
+ if (Math.abs(tStart - t) < instantWidthTimeSec) {
return i;
}
} else {
let tEnd = data.ends[i];
if (data.isIncomplete[i]) {
- tEnd = globals.frontendLocalState.visibleWindowTime.end;
+ tEnd = visibleWindowTime.end.seconds;
}
if (tStart <= t && t <= tEnd) {
return i;
@@ -435,17 +444,28 @@
getSliceRect(tStart: number, tEnd: number, depth: number): SliceRect
|undefined {
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
- const pxEnd = timeScale.timeToPx(visibleWindowTime.end);
- const left = Math.max(timeScale.timeToPx(tStart), 0);
- const right = Math.min(timeScale.timeToPx(tEnd), pxEnd);
+ const {
+ visibleTimeScale: timeScale,
+ visibleWindowTime,
+ windowSpan,
+ } = globals.frontendLocalState;
+
+ const pxEnd = windowSpan.end;
+ const left = Math.max(timeScale.secondsToPx(tStart), 0);
+ const right = Math.min(timeScale.secondsToPx(tEnd), pxEnd);
+
+ const visible =
+ !(visibleWindowTime.start.isGreaterThan(
+ HighPrecisionTime.fromSeconds(tEnd)) ||
+ visibleWindowTime.end.isLessThan(
+ HighPrecisionTime.fromSeconds(tStart)));
+
return {
left,
width: Math.max(right - left, 1),
top: TRACK_PADDING + depth * SLICE_HEIGHT,
height: SLICE_HEIGHT,
- visible:
- !(tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end),
+ visible,
};
}
}
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index 9aa8f74..f22e8c7 100644
--- a/ui/src/tracks/counter/index.ts
+++ b/ui/src/tracks/counter/index.ts
@@ -19,13 +19,18 @@
import {Actions} from '../../common/actions';
import {
EngineProxy,
+ LONG_NULL,
NUM,
- NUM_NULL,
PluginContext,
STR,
TrackInfo,
} from '../../common/plugin_api';
-import {fromNs, toNs} from '../../common/time';
+import {
+ fromNs,
+ TPDuration,
+ TPTime,
+ tpTimeFromSeconds,
+} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {
TrackController,
@@ -62,8 +67,8 @@
name: string;
maximumValue?: number;
minimumValue?: number;
- startTs?: number;
- endTs?: number;
+ startTs?: TPTime;
+ endTs?: TPTime;
namespace: string;
trackId: number;
scale?: CounterScaleOptions;
@@ -76,18 +81,16 @@
private minimumValueSeen = 0;
private maximumDeltaSeen = 0;
private minimumDeltaSeen = 0;
- private maxDurNs = 0;
+ private maxDurNs: TPDuration = 0n;
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
- const startNs = toNs(start);
- const endNs = toNs(end);
-
const pxSize = this.pxSize();
// ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
// be an even number, so we can snap in the middle.
- const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
+ const bucketNs =
+ Math.max(Math.round(Number(resolution) * pxSize / 2) * 2, 1);
if (!this.setup) {
if (this.config.namespace === undefined) {
@@ -123,7 +126,7 @@
) as maxDur
from ${this.tableName('counter_view')}
`);
- this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
+ this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
const queryRes = await this.query(`
select
@@ -151,7 +154,7 @@
value_at_max_ts(ts, id) as lastId,
value_at_max_ts(ts, value) as lastValue
from ${this.tableName('counter_view')}
- where ts >= ${startNs - this.maxDurNs} and ts <= ${endNs}
+ where ts >= ${start - this.maxDurNs} and ts <= ${end}
group by tsq
order by tsq
`);
@@ -285,7 +288,10 @@
renderCanvas(ctx: CanvasRenderingContext2D): void {
// TODO: fonts and colors should come from the CSS and not hardcoded here.
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+ const {
+ visibleTimeScale: timeScale,
+ windowSpan,
+ } = globals.frontendLocalState;
const data = this.data();
// Can't possibly draw anything.
@@ -321,7 +327,7 @@
minimumValue = data.minimumRate;
}
- const endPx = Math.floor(timeScale.timeToPx(visibleWindowTime.end));
+ const endPx = windowSpan.end;
const zeroY = MARGIN_TOP + RECT_HEIGHT / (minimumValue < 0 ? 2 : 1);
// Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
@@ -366,7 +372,7 @@
ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
const calculateX = (ts: number) => {
- return Math.floor(timeScale.timeToPx(ts));
+ return Math.floor(timeScale.secondsToPx(ts));
};
const calculateY = (value: number) => {
return MARGIN_TOP + RECT_HEIGHT -
@@ -427,10 +433,10 @@
ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
- const xStart = Math.floor(timeScale.timeToPx(this.hoveredTs));
+ const xStart = Math.floor(timeScale.secondsToPx(this.hoveredTs));
const xEnd = this.hoveredTsEnd === undefined ?
endPx :
- Math.floor(timeScale.timeToPx(this.hoveredTsEnd));
+ Math.floor(timeScale.secondsToPx(this.hoveredTsEnd));
const y = MARGIN_TOP + RECT_HEIGHT -
Math.round(((this.hoveredValue - yMin) / yRange) * RECT_HEIGHT);
@@ -463,9 +469,10 @@
// TODO(hjd): Refactor this into checkerboardExcept
{
- const endPx = timeScale.timeToPx(visibleWindowTime.end);
- const counterEndPx =
- Math.min(timeScale.timeToPx(this.config.endTs || Infinity), endPx);
+ let counterEndPx = Infinity;
+ if (this.config.endTs) {
+ counterEndPx = Math.min(timeScale.tpTimeToPx(this.config.endTs), endPx);
+ }
// Grey out RHS.
if (counterEndPx < endPx) {
@@ -479,18 +486,18 @@
checkerboardExcept(
ctx,
this.getHeight(),
- timeScale.timeToPx(visibleWindowTime.start),
- timeScale.timeToPx(visibleWindowTime.end),
- timeScale.timeToPx(data.start),
- timeScale.timeToPx(data.end));
+ windowSpan.start,
+ windowSpan.end,
+ timeScale.tpTimeToPx(data.start),
+ timeScale.tpTimeToPx(data.end));
}
onMouseMove(pos: {x: number, y: number}) {
const data = this.data();
if (data === undefined) return;
this.mousePos = pos;
- const {timeScale} = globals.frontendLocalState;
- const time = timeScale.pxToTime(pos.x);
+ const {visibleTimeScale} = globals.frontendLocalState;
+ const time = visibleTimeScale.pxToHpTime(pos.x).seconds;
const values = this.config.scale === 'DELTA_FROM_PREVIOUS' ?
data.totalDeltas :
@@ -509,8 +516,8 @@
onMouseClick({x}: {x: number}) {
const data = this.data();
if (data === undefined) return false;
- const {timeScale} = globals.frontendLocalState;
- const time = timeScale.pxToTime(x);
+ const {visibleTimeScale} = globals.frontendLocalState;
+ const time = visibleTimeScale.pxToHpTime(x).seconds;
const [left, right] = searchSegment(data.timestamps, time);
if (left === -1) {
return false;
@@ -518,8 +525,8 @@
const counterId = data.lastIds[left];
if (counterId === -1) return true;
globals.makeSelection(Actions.selectCounter({
- leftTs: toNs(data.timestamps[left]),
- rightTs: right !== -1 ? toNs(data.timestamps[right]) : -1,
+ leftTs: tpTimeFromSeconds(data.timestamps[left]),
+ rightTs: tpTimeFromSeconds(right !== -1 ? data.timestamps[right] : -1),
id: counterId,
trackId: this.trackState.id,
}));
diff --git a/ui/src/tracks/cpu_freq/index.ts b/ui/src/tracks/cpu_freq/index.ts
index 8bb2e3b..7956172 100644
--- a/ui/src/tracks/cpu_freq/index.ts
+++ b/ui/src/tracks/cpu_freq/index.ts
@@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {BigintMath} from '../../base/bigint_math';
import {searchSegment} from '../../base/binary_search';
import {assertTrue} from '../../base/logging';
import {hueForCpu} from '../../common/colorizer';
import {PluginContext} from '../../common/plugin_api';
import {NUM, NUM_NULL, QueryResult} from '../../common/query_result';
-import {fromNs, toNs} from '../../common/time';
+import {fromNs, TPDuration, TPTime, tpTimeToNanos} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {
TrackController,
@@ -96,15 +97,17 @@
this.cachedBucketNs = bucketNs;
}
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
// The resolution should always be a power of two for the logic of this
// function to make sense.
- const resolutionNs = toNs(resolution);
- assertTrue(Math.log2(resolutionNs) % 1 === 0);
+ assertTrue(
+ BigintMath.popcount(resolution) === 1,
+ `${resolution} is not a power of 2`);
+ const resolutionNs = Number(resolution);
- const startNs = toNs(start);
- const endNs = toNs(end);
+ const startNs = tpTimeToNanos(start);
+ const endNs = tpTimeToNanos(end);
// ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
// be an even number, so we can snap in the middle.
@@ -289,7 +292,11 @@
renderCanvas(ctx: CanvasRenderingContext2D): void {
// TODO: fonts and colors should come from the CSS and not hardcoded here.
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+ const {
+ visibleTimeScale,
+ visibleWindowTime,
+ windowSpan,
+ } = globals.frontendLocalState;
const data = this.data();
if (data === undefined || data.timestamps.length === 0) {
@@ -302,7 +309,7 @@
assertTrue(data.timestamps.length === data.maxFreqKHz.length);
assertTrue(data.timestamps.length === data.lastIdleValues.length);
- const endPx = timeScale.timeToPx(visibleWindowTime.end);
+ const endPx = windowSpan.end;
const zeroY = MARGIN_TOP + RECT_HEIGHT;
// Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
@@ -326,17 +333,18 @@
ctx.strokeStyle = `hsl(${hue}, ${saturation}%, 55%)`;
const calculateX = (timestamp: number) => {
- return Math.floor(timeScale.timeToPx(timestamp));
+ return Math.floor(visibleTimeScale.secondsToPx(timestamp));
};
const calculateY = (value: number) => {
return zeroY - Math.round((value / yMax) * RECT_HEIGHT);
};
- const [rawStartIdx] =
- searchSegment(data.timestamps, visibleWindowTime.start);
+ const startSec = visibleWindowTime.start.seconds;
+ const endSec = visibleWindowTime.end.seconds;
+ const [rawStartIdx] = searchSegment(data.timestamps, startSec);
const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
- const [, rawEndIdx] = searchSegment(data.timestamps, visibleWindowTime.end);
+ const [, rawEndIdx] = searchSegment(data.timestamps, endSec);
const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx;
ctx.beginPath();
@@ -383,10 +391,10 @@
// coordinates. Instead we use floating point which prevents flickering as
// we pan and zoom; this relies on the browser anti-aliasing pixels
// correctly.
- const x = timeScale.timeToPx(data.timestamps[i]);
+ const x = visibleTimeScale.secondsToPx(data.timestamps[i]);
const xEnd = i === data.lastIdleValues.length - 1 ?
finalX :
- timeScale.timeToPx(data.timestamps[i + 1]);
+ visibleTimeScale.secondsToPx(data.timestamps[i + 1]);
const width = xEnd - x;
const height = calculateY(data.lastFreqKHz[i]) - zeroY;
@@ -402,10 +410,10 @@
ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
- const xStart = Math.floor(timeScale.timeToPx(this.hoveredTs));
+ const xStart = Math.floor(visibleTimeScale.secondsToPx(this.hoveredTs));
const xEnd = this.hoveredTsEnd === undefined ?
endPx :
- Math.floor(timeScale.timeToPx(this.hoveredTsEnd));
+ Math.floor(visibleTimeScale.secondsToPx(this.hoveredTsEnd));
const y = zeroY - Math.round((this.hoveredValue / yMax) * RECT_HEIGHT);
// Highlight line.
@@ -446,18 +454,18 @@
checkerboardExcept(
ctx,
this.getHeight(),
- timeScale.timeToPx(visibleWindowTime.start),
- timeScale.timeToPx(visibleWindowTime.end),
- timeScale.timeToPx(data.start),
- timeScale.timeToPx(data.end));
+ windowSpan.start,
+ windowSpan.end,
+ visibleTimeScale.tpTimeToPx(data.start),
+ visibleTimeScale.tpTimeToPx(data.end));
}
onMouseMove(pos: {x: number, y: number}) {
const data = this.data();
if (data === undefined) return;
this.mousePos = pos;
- const {timeScale} = globals.frontendLocalState;
- const time = timeScale.pxToTime(pos.x);
+ const {visibleTimeScale} = globals.frontendLocalState;
+ const time = visibleTimeScale.pxToHpTime(pos.x).seconds;
const [left, right] = searchSegment(data.timestamps, time);
this.hoveredTs = left === -1 ? undefined : data.timestamps[left];
diff --git a/ui/src/tracks/cpu_profile/index.ts b/ui/src/tracks/cpu_profile/index.ts
index eee7b17..028d5b1 100644
--- a/ui/src/tracks/cpu_profile/index.ts
+++ b/ui/src/tracks/cpu_profile/index.ts
@@ -18,7 +18,7 @@
import {hslForSlice} from '../../common/colorizer';
import {PluginContext} from '../../common/plugin_api';
import {NUM} from '../../common/query_result';
-import {fromNs, toNs} from '../../common/time';
+import {fromNs, TPDuration, TPTime} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {
TrackController,
@@ -46,7 +46,7 @@
class CpuProfileTrackController extends TrackController<Config, Data> {
static readonly kind = CPU_PROFILE_TRACK_KIND;
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
const query = `select
id,
@@ -105,7 +105,7 @@
renderCanvas(ctx: CanvasRenderingContext2D): void {
const {
- timeScale,
+ visibleTimeScale: timeScale,
} = globals.frontendLocalState;
const data = this.data();
@@ -120,7 +120,7 @@
const strokeWidth = isSelected ? 3 : 0;
this.drawMarker(
ctx,
- timeScale.timeToPx(fromNs(centerX)),
+ timeScale.secondsToPx(fromNs(centerX)),
this.centerY,
isHovered,
strokeWidth,
@@ -146,8 +146,8 @@
if (clusterStartIndex !== clusterEndIndex) {
const startX = data.tsStarts[clusterStartIndex];
const endX = data.tsStarts[clusterEndIndex];
- const leftPx = timeScale.timeToPx(fromNs(startX)) - this.markerWidth;
- const rightPx = timeScale.timeToPx(fromNs(endX)) + this.markerWidth;
+ const leftPx = timeScale.secondsToPx(fromNs(startX)) - this.markerWidth;
+ const rightPx = timeScale.secondsToPx(fromNs(endX)) + this.markerWidth;
const width = rightPx - leftPx;
ctx.fillStyle = colorForSample(callsiteId, false);
ctx.fillRect(leftPx, MARGIN_TOP, width, BAR_HEIGHT);
@@ -179,8 +179,10 @@
onMouseMove({x, y}: {x: number, y: number}) {
const data = this.data();
if (data === undefined) return;
- const {timeScale} = globals.frontendLocalState;
- const time = toNs(timeScale.pxToTime(x));
+ const {
+ visibleTimeScale: timeScale,
+ } = globals.frontendLocalState;
+ const time = timeScale.pxToHpTime(x).nanos;
const [left, right] = searchSegment(data.tsStarts, time);
const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
this.hoveredTs = index === -1 ? undefined : data.tsStarts[index];
@@ -193,9 +195,11 @@
onMouseClick({x, y}: {x: number, y: number}) {
const data = this.data();
if (data === undefined) return false;
- const {timeScale} = globals.frontendLocalState;
+ const {
+ visibleTimeScale: timeScale,
+ } = globals.frontendLocalState;
- const time = toNs(timeScale.pxToTime(x));
+ const time = timeScale.pxToHpTime(x).nanos;
const [left, right] = searchSegment(data.tsStarts, time);
const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
@@ -217,13 +221,13 @@
right: number): number {
let index = -1;
if (left !== -1) {
- const centerX = timeScale.timeToPx(fromNs(data.tsStarts[left]));
+ const centerX = timeScale.secondsToPx(fromNs(data.tsStarts[left]));
if (this.isInMarker(x, y, centerX)) {
index = left;
}
}
if (right !== -1) {
- const centerX = timeScale.timeToPx(fromNs(data.tsStarts[right]));
+ const centerX = timeScale.secondsToPx(fromNs(data.tsStarts[right]));
if (this.isInMarker(x, y, centerX)) {
index = right;
}
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 0953d2f..4264069 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {BigintMath} from '../../base/bigint_math';
import {search, searchEq, searchSegment} from '../../base/binary_search';
import {assertTrue} from '../../base/logging';
import {Actions} from '../../common/actions';
@@ -23,7 +24,15 @@
import {colorForThread} from '../../common/colorizer';
import {PluginContext} from '../../common/plugin_api';
import {NUM} from '../../common/query_result';
-import {fromNs, timeToString, toNs} from '../../common/time';
+import {
+ fromNs,
+ toNs,
+ TPDuration,
+ TPTime,
+ tpTimeFromSeconds,
+ tpTimeToNanos,
+ tpTimeToString,
+} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {
TrackController,
@@ -102,16 +111,19 @@
this.cachedBucketNs = bucketNs;
}
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
- const resolutionNs = toNs(resolution);
+ assertTrue(
+ BigintMath.popcount(resolution) === 1,
+ `${resolution} is not a power of 2`);
+ const resolutionNs = Number(resolution);
// The resolution should always be a power of two for the logic of this
// function to make sense.
assertTrue(Math.log2(resolutionNs) % 1 === 0);
- const boundStartNs = toNs(start);
- const boundEndNs = toNs(end);
+ const boundStartNs = tpTimeToNanos(start);
+ const boundEndNs = tpTimeToNanos(end);
// ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
// be an even number, so we can snap in the middle.
@@ -223,7 +235,7 @@
renderCanvas(ctx: CanvasRenderingContext2D): void {
// TODO: fonts and colors should come from the CSS and not hardcoded here.
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+ const {visibleTimeScale, windowSpan} = globals.frontendLocalState;
const data = this.data();
if (data === undefined) return; // Can't possibly draw anything.
@@ -233,28 +245,35 @@
checkerboardExcept(
ctx,
this.getHeight(),
- timeScale.timeToPx(visibleWindowTime.start),
- timeScale.timeToPx(visibleWindowTime.end),
- timeScale.timeToPx(data.start),
- timeScale.timeToPx(data.end));
+ windowSpan.start,
+ windowSpan.end,
+ visibleTimeScale.tpTimeToPx(data.start),
+ visibleTimeScale.tpTimeToPx(data.end));
this.renderSlices(ctx, data);
}
renderSlices(ctx: CanvasRenderingContext2D, data: Data): void {
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+ const {
+ visibleTimeScale,
+ visibleWindowTime,
+ } = globals.frontendLocalState;
assertTrue(data.starts.length === data.ends.length);
assertTrue(data.starts.length === data.utids.length);
+ const visWindowEndPx = visibleTimeScale.hpTimeToPx(visibleWindowTime.end);
+
ctx.textAlign = 'center';
ctx.font = '12px Roboto Condensed';
const charWidth = ctx.measureText('dbpqaouk').width / 8;
- const rawStartIdx =
- data.ends.findIndex((end) => end >= visibleWindowTime.start);
+ const startSec = visibleWindowTime.start.seconds;
+ const endSec = visibleWindowTime.end.seconds;
+
+ const rawStartIdx = data.ends.findIndex((end) => end >= startSec);
const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
- const [, rawEndIdx] = searchSegment(data.starts, visibleWindowTime.end);
+ const [, rawEndIdx] = searchSegment(data.starts, endSec);
const endIdx = rawEndIdx === -1 ? data.starts.length : rawEndIdx;
for (let i = startIdx; i < endIdx; i++) {
@@ -266,10 +285,10 @@
// window, else it might spill over the window and the end would not be
// visible as a zigzag line.
if (data.ids[i] === data.lastRowId && data.isIncomplete[i]) {
- tEnd = visibleWindowTime.end;
+ tEnd = visibleWindowTime.end.seconds;
}
- const rectStart = timeScale.timeToPx(tStart);
- const rectEnd = timeScale.timeToPx(tEnd);
+ const rectStart = visibleTimeScale.secondsToPx(tStart);
+ const rectEnd = visibleTimeScale.secondsToPx(tEnd);
const rectWidth = Math.max(1, rectEnd - rectStart);
const threadInfo = globals.threads.get(utid);
@@ -317,8 +336,7 @@
title = `${threadInfo.threadName} [${threadInfo.tid}]`;
}
}
- const right =
- Math.min(timeScale.timeToPx(visibleWindowTime.end), rectEnd);
+ const right = Math.min(visWindowEndPx, rectEnd);
const left = Math.max(rectStart, 0);
const visibleWidth = Math.max(right - left, 1);
title = cropText(title, charWidth, visibleWidth);
@@ -341,8 +359,8 @@
const tEnd = data.ends[startIndex];
const utid = data.utids[startIndex];
const color = colorForThread(globals.threads.get(utid));
- const rectStart = timeScale.timeToPx(tStart);
- const rectEnd = timeScale.timeToPx(tEnd);
+ const rectStart = visibleTimeScale.secondsToPx(tStart);
+ const rectEnd = visibleTimeScale.secondsToPx(tEnd);
const rectWidth = Math.max(1, rectEnd - rectStart);
// Draw a rectangle around the slice that is currently selected.
@@ -353,7 +371,7 @@
ctx.closePath();
// Draw arrow from wakeup time of current slice.
if (details.wakeupTs) {
- const wakeupPos = timeScale.timeToPx(details.wakeupTs);
+ const wakeupPos = visibleTimeScale.tpTimeToPx(details.wakeupTs);
const latencyWidth = rectStart - wakeupPos;
drawDoubleHeadedArrow(
ctx,
@@ -362,7 +380,8 @@
latencyWidth,
latencyWidth >= 20);
// Latency time with a white semi-transparent background.
- const displayText = timeToString(tStart - details.wakeupTs);
+ const latency = tpTimeFromSeconds(tStart) - details.wakeupTs;
+ const displayText = tpTimeToString(latency);
const measured = ctx.measureText(displayText);
if (latencyWidth >= measured.width + 2) {
ctx.fillStyle = 'rgba(255,255,255,0.7)';
@@ -383,7 +402,8 @@
// Draw diamond if the track being drawn is the cpu of the waker.
if (this.config.cpu === details.wakerCpu && details.wakeupTs) {
- const wakeupPos = Math.floor(timeScale.timeToPx(details.wakeupTs));
+ const wakeupPos =
+ Math.floor(visibleTimeScale.tpTimeToPx(details.wakeupTs));
ctx.beginPath();
ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
ctx.fillStyle = 'black';
@@ -411,13 +431,13 @@
const data = this.data();
this.mousePos = pos;
if (data === undefined) return;
- const {timeScale} = globals.frontendLocalState;
+ const {visibleTimeScale} = globals.frontendLocalState;
if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
this.utidHoveredInThisTrack = -1;
globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
return;
}
- const t = timeScale.pxToTime(pos.x);
+ const t = visibleTimeScale.pxToHpTime(pos.x).seconds;
let hoveredUtid = -1;
for (let i = 0; i < data.starts.length; i++) {
@@ -445,8 +465,8 @@
onMouseClick({x}: {x: number}) {
const data = this.data();
if (data === undefined) return false;
- const {timeScale} = globals.frontendLocalState;
- const time = timeScale.pxToTime(x);
+ const {visibleTimeScale} = globals.frontendLocalState;
+ const time = visibleTimeScale.pxToHpTime(x).seconds;
const index = search(data.starts, time);
const id = index === -1 ? undefined : data.ids[index];
if (!id || this.utidHoveredInThisTrack === -1) return false;
diff --git a/ui/src/tracks/debug/details_tab.ts b/ui/src/tracks/debug/details_tab.ts
index 9268c17..50c2319 100644
--- a/ui/src/tracks/debug/details_tab.ts
+++ b/ui/src/tracks/debug/details_tab.ts
@@ -15,13 +15,14 @@
import m from 'mithril';
import {ColumnType} from '../../common/query_result';
+import {tpDurationFromSql, tpTimeFromSql} from '../../common/time';
import {
BottomTab,
bottomTabRegistry,
NewBottomTabArgs,
} from '../../frontend/bottom_tab';
import {globals} from '../../frontend/globals';
-import {timestampFromSqlNanos} from '../../frontend/sql_types';
+import {asTPTimestamp} from '../../frontend/sql_types';
import {Duration} from '../../frontend/widgets/duration';
import {Timestamp} from '../../frontend/widgets/timestamp';
import {dictToTree} from '../../frontend/widgets/tree';
@@ -69,12 +70,11 @@
if (this.data === undefined) {
return m('h2', 'Loading');
}
- // TODO(stevegolton): These type assertions are dangerous, but no more
- // dangerous than they used to be before this change.
const left = dictToTree({
'Name': this.data['name'] as string,
- 'Start time': m(Timestamp, {ts: timestampFromSqlNanos(this.data['ts'])}),
- 'Duration': m(Duration, {dur: Number(this.data['dur'])}),
+ '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}]`,
});
const args: {[key: string]: m.Child} = {};
diff --git a/ui/src/tracks/debug/slice_track.ts b/ui/src/tracks/debug/slice_track.ts
index a871a9f..664840d 100644
--- a/ui/src/tracks/debug/slice_track.ts
+++ b/ui/src/tracks/debug/slice_track.ts
@@ -78,8 +78,8 @@
globals.dispatch(Actions.selectDebugSlice({
id: args.slice.id,
sqlTableName: this.config.sqlTableName,
- startS: args.slice.startS,
- durationS: args.slice.durationS,
+ start: args.slice.start,
+ duration: args.slice.duration,
trackId: this.trackId,
}));
}
diff --git a/ui/src/tracks/expected_frames/index.ts b/ui/src/tracks/expected_frames/index.ts
index f2ab086..f7e5121 100644
--- a/ui/src/tracks/expected_frames/index.ts
+++ b/ui/src/tracks/expected_frames/index.ts
@@ -19,8 +19,8 @@
import {NewTrackArgs, Track} from '../../frontend/track';
import {ChromeSliceTrack} from '../chrome_slices';
-import {NUM, NUM_NULL, STR} from '../../common/query_result';
-import {fromNs, toNs} from '../../common/time';
+import {LONG_NULL, NUM, STR} from '../../common/query_result';
+import {TPDuration, TPTime, fromNs} from '../../common/time';
import {
TrackController,
} from '../../controller/track_controller';
@@ -46,27 +46,25 @@
class ExpectedFramesSliceTrackController extends TrackController<Config, Data> {
static readonly kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
- private maxDurNs = 0;
+ private maxDurNs: TPDuration = 0n;
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
- const startNs = toNs(start);
- const endNs = toNs(end);
-
const pxSize = this.pxSize();
// ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
// be an even number, so we can snap in the middle.
- const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
+ const bucketNs =
+ Math.max(Math.round(Number(resolution) * pxSize / 2) * 2, 1);
- if (this.maxDurNs === 0) {
+ if (this.maxDurNs === 0n) {
const maxDurResult = await this.query(`
select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
as maxDur
from experimental_slice_layout
where filter_track_ids = '${this.config.trackIds.join(',')}'
`);
- this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
+ this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
}
const queryRes = await this.query(`
@@ -82,8 +80,8 @@
from experimental_slice_layout
where
filter_track_ids = '${this.config.trackIds.join(',')}' and
- ts >= ${startNs - this.maxDurNs} and
- ts <= ${endNs}
+ ts >= ${start - this.maxDurNs} and
+ ts <= ${end}
group by tsq, layout_depth
order by tsq, layout_depth
`);
diff --git a/ui/src/tracks/ftrace/index.ts b/ui/src/tracks/ftrace/index.ts
index e5913fa..8cd40f3 100644
--- a/ui/src/tracks/ftrace/index.ts
+++ b/ui/src/tracks/ftrace/index.ts
@@ -17,7 +17,8 @@
import {colorForString} from '../../common/colorizer';
import {PluginContext} from '../../common/plugin_api';
import {NUM, STR} from '../../common/query_result';
-import {fromNs, toNsCeil, toNsFloor} from '../../common/time';
+import {fromNs, TPDuration} from '../../common/time';
+import {TPTime} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {LIMIT} from '../../common/track_data';
import {
@@ -46,14 +47,8 @@
class FtraceRawTrackController extends TrackController<Config, Data> {
static readonly kind = FTRACE_RAW_TRACK_KIND;
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
- const startNs = toNsFloor(start);
- const endNs = toNsCeil(end);
-
- // |resolution| is in s/px the frontend wants.
- const quantNs = toNsCeil(resolution);
-
const excludeList = Array.from(globals.state.ftraceFilter.excludedNames);
const excludeListSql = excludeList.map((s) => `'${s}'`).join(',');
const cpuFilter =
@@ -61,13 +56,13 @@
const queryRes = await this.query(`
select
- cast(ts / ${quantNs} as integer) * ${quantNs} as tsQuant,
+ cast(ts / ${resolution} as integer) * ${resolution} as tsQuant,
type,
name
from ftrace_event
where
name not in (${excludeListSql}) and
- ts >= ${startNs} and ts <= ${endNs} ${cpuFilter}
+ ts >= ${start} and ts <= ${end} ${cpuFilter}
group by tsQuant
order by tsQuant limit ${LIMIT};`);
@@ -107,16 +102,19 @@
}
renderCanvas(ctx: CanvasRenderingContext2D): void {
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+ const {
+ visibleTimeScale,
+ windowSpan,
+ } = globals.frontendLocalState;
const data = this.data();
if (data === undefined) return; // Can't possibly draw anything.
- const dataStartPx = timeScale.timeToPx(data.start);
- const dataEndPx = timeScale.timeToPx(data.end);
- const visibleStartPx = timeScale.timeToPx(visibleWindowTime.start);
- const visibleEndPx = timeScale.timeToPx(visibleWindowTime.end);
+ const dataStartPx = visibleTimeScale.tpTimeToPx(data.start);
+ const dataEndPx = visibleTimeScale.tpTimeToPx(data.end);
+ const visibleStartPx = windowSpan.start;
+ const visibleEndPx = windowSpan.end;
checkerboardExcept(
ctx,
@@ -137,7 +135,7 @@
${Math.min(color.l + 10, 60)}%
)`;
ctx.fillStyle = hsl;
- const xPos = Math.floor(timeScale.timeToPx(data.timestamps[i]));
+ const xPos = Math.floor(visibleTimeScale.secondsToPx(data.timestamps[i]));
// Draw a diamond over the event
ctx.save();
diff --git a/ui/src/tracks/heap_profile/index.ts b/ui/src/tracks/heap_profile/index.ts
index b3eb40a..3e656f0 100644
--- a/ui/src/tracks/heap_profile/index.ts
+++ b/ui/src/tracks/heap_profile/index.ts
@@ -18,7 +18,13 @@
import {Actions} from '../../common/actions';
import {PluginContext} from '../../common/plugin_api';
import {NUM, STR} from '../../common/query_result';
-import {fromNs, toNs} from '../../common/time';
+import {
+ fromNs,
+ TPDuration,
+ TPTime,
+ tpTimeFromNanos,
+ tpTimeFromSeconds,
+} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {profileType} from '../../controller/flamegraph_controller';
import {
@@ -42,7 +48,7 @@
class HeapProfileTrackController extends TrackController<Config, Data> {
static readonly kind = HEAP_PROFILE_TRACK_KIND;
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
if (this.config.upid === undefined) {
return {
@@ -111,7 +117,7 @@
renderCanvas(ctx: CanvasRenderingContext2D): void {
const {
- timeScale,
+ visibleTimeScale: timeScale,
} = globals.frontendLocalState;
const data = this.data();
@@ -122,11 +128,12 @@
const selection = globals.state.currentSelection;
const isHovered = this.hoveredTs === centerX;
const isSelected = selection !== null &&
- selection.kind === 'HEAP_PROFILE' && selection.ts === centerX;
+ selection.kind === 'HEAP_PROFILE' &&
+ selection.ts === tpTimeFromSeconds(centerX);
const strokeWidth = isSelected ? 3 : 0;
this.drawMarker(
ctx,
- timeScale.timeToPx(fromNs(centerX)),
+ timeScale.secondsToPx(fromNs(centerX)),
this.centerY,
isHovered,
strokeWidth);
@@ -155,8 +162,10 @@
onMouseMove({x, y}: {x: number, y: number}) {
const data = this.data();
if (data === undefined) return;
- const {timeScale} = globals.frontendLocalState;
- const time = toNs(timeScale.pxToTime(x));
+ const {
+ visibleTimeScale: timeScale,
+ } = globals.frontendLocalState;
+ const time = timeScale.pxToHpTime(x).nanos;
const [left, right] = searchSegment(data.tsStarts, time);
const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
this.hoveredTs = index === -1 ? undefined : data.tsStarts[index];
@@ -169,15 +178,18 @@
onMouseClick({x, y}: {x: number, y: number}) {
const data = this.data();
if (data === undefined) return false;
- const {timeScale} = globals.frontendLocalState;
+ const {
+ visibleTimeScale: timeScale,
+ } = globals.frontendLocalState;
- const time = toNs(timeScale.pxToTime(x));
+ const time = timeScale.pxToHpTime(x).nanos;
const [left, right] = searchSegment(data.tsStarts, time);
const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
if (index !== -1) {
- const ts = data.tsStarts[index];
+ // TODO(stevegolton): Remove conversion from number to bigint.
+ const ts = tpTimeFromNanos(data.tsStarts[index]);
const type = data.types[index];
globals.makeSelection(Actions.selectHeapProfile(
{id: index, upid: this.config.upid, ts, type}));
@@ -192,13 +204,13 @@
right: number): number {
let index = -1;
if (left !== -1) {
- const centerX = timeScale.timeToPx(fromNs(data.tsStarts[left]));
+ const centerX = timeScale.secondsToPx(fromNs(data.tsStarts[left]));
if (this.isInMarker(x, y, centerX)) {
index = left;
}
}
if (right !== -1) {
- const centerX = timeScale.timeToPx(fromNs(data.tsStarts[right]));
+ const centerX = timeScale.secondsToPx(fromNs(data.tsStarts[right]));
if (this.isInMarker(x, y, centerX)) {
index = right;
}
diff --git a/ui/src/tracks/perf_samples_profile/index.ts b/ui/src/tracks/perf_samples_profile/index.ts
index cfc73dd..693e295 100644
--- a/ui/src/tracks/perf_samples_profile/index.ts
+++ b/ui/src/tracks/perf_samples_profile/index.ts
@@ -17,7 +17,12 @@
import {PluginContext} from '../../common/plugin_api';
import {NUM} from '../../common/query_result';
import {ProfileType} from '../../common/state';
-import {fromNs, toNs} from '../../common/time';
+import {
+ fromNs,
+ TPDuration,
+ TPTime,
+ tpTimeFromSeconds,
+} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {
TrackController,
@@ -39,7 +44,7 @@
class PerfSamplesProfileTrackController extends TrackController<Config, Data> {
static readonly kind = PERF_SAMPLES_PROFILE_TRACK_KIND;
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
if (this.config.upid === undefined) {
return {
@@ -99,7 +104,7 @@
renderCanvas(ctx: CanvasRenderingContext2D): void {
const {
- timeScale,
+ visibleTimeScale,
} = globals.frontendLocalState;
const data = this.data();
@@ -115,7 +120,7 @@
const strokeWidth = isSelected ? 3 : 0;
this.drawMarker(
ctx,
- timeScale.timeToPx(fromNs(centerX)),
+ visibleTimeScale.secondsToPx(fromNs(centerX)),
this.centerY,
isHovered,
strokeWidth);
@@ -144,10 +149,11 @@
onMouseMove({x, y}: {x: number, y: number}) {
const data = this.data();
if (data === undefined) return;
- const {timeScale} = globals.frontendLocalState;
- const time = toNs(timeScale.pxToTime(x));
+ const {visibleTimeScale} = globals.frontendLocalState;
+ const time = visibleTimeScale.pxToHpTime(x).nanos;
const [left, right] = searchSegment(data.tsStartsNs, time);
- const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
+ const index =
+ this.findTimestampIndex(left, visibleTimeScale, data, x, y, right);
this.hoveredTs = index === -1 ? undefined : data.tsStartsNs[index];
}
@@ -158,9 +164,11 @@
onMouseClick({x, y}: {x: number, y: number}) {
const data = this.data();
if (data === undefined) return false;
- const {timeScale} = globals.frontendLocalState;
+ const {
+ visibleTimeScale: timeScale,
+ } = globals.frontendLocalState;
- const time = toNs(timeScale.pxToTime(x));
+ const time = timeScale.pxToHpTime(x).nanos;
const [left, right] = searchSegment(data.tsStartsNs, time);
const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
@@ -170,8 +178,8 @@
globals.makeSelection(Actions.selectPerfSamples({
id: index,
upid: this.config.upid,
- leftTs: ts,
- rightTs: ts,
+ leftTs: tpTimeFromSeconds(ts),
+ rightTs: tpTimeFromSeconds(ts),
type: ProfileType.PERF_SAMPLE,
}));
return true;
@@ -185,13 +193,13 @@
right: number): number {
let index = -1;
if (left !== -1) {
- const centerX = timeScale.timeToPx(fromNs(data.tsStartsNs[left]));
+ const centerX = timeScale.secondsToPx(fromNs(data.tsStartsNs[left]));
if (this.isInMarker(x, y, centerX)) {
index = left;
}
}
if (right !== -1) {
- const centerX = timeScale.timeToPx(fromNs(data.tsStartsNs[right]));
+ const centerX = timeScale.secondsToPx(fromNs(data.tsStartsNs[right]));
if (this.isInMarker(x, y, centerX)) {
index = right;
}
diff --git a/ui/src/tracks/process_scheduling/index.ts b/ui/src/tracks/process_scheduling/index.ts
index 95302b6..d88bc71 100644
--- a/ui/src/tracks/process_scheduling/index.ts
+++ b/ui/src/tracks/process_scheduling/index.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 {searchEq, searchRange, searchSegment} from '../../base/binary_search';
import {assertTrue} from '../../base/logging';
import {Actions} from '../../common/actions';
import {colorForThread} from '../../common/colorizer';
import {PluginContext} from '../../common/plugin_api';
import {NUM, QueryResult} from '../../common/query_result';
-import {fromNs, toNs} from '../../common/time';
+import {
+ fromNs,
+ TPDuration,
+ TPTime,
+ tpTimeFromSeconds,
+ tpTimeToNanos,
+} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {
TrackController,
@@ -91,22 +98,23 @@
this.cachedBucketNs = bucketNs;
}
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
assertTrue(this.config.upid !== null);
// The resolution should always be a power of two for the logic of this
// function to make sense.
- const resolutionNs = toNs(resolution);
- assertTrue(Math.log2(resolutionNs) % 1 === 0);
+ assertTrue(
+ BigintMath.popcount(resolution) === 1,
+ `${resolution} is not a power of 2`);
- const startNs = toNs(start);
- const endNs = toNs(end);
+ const startNs = tpTimeToNanos(start);
+ const endNs = tpTimeToNanos(end);
// ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
// be an even number, so we can snap in the middle.
const bucketNs =
- Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1);
+ Math.max(Math.round(Number(resolution) * this.pxSize() / 2) * 2, 1);
const queryRes = await this.queryData(startNs, endNs, bucketNs);
const numRows = queryRes.numRows();
@@ -144,7 +152,8 @@
slices.ends[row] = fromNs(endNsQ);
slices.cpus[row] = it.cpu;
slices.utids[row] = it.utid;
- slices.end = Math.max(slices.ends[row], slices.end);
+ slices.end =
+ BigintMath.max(tpTimeFromSeconds(slices.ends[row]), slices.end);
}
return slices;
}
@@ -208,7 +217,10 @@
renderCanvas(ctx: CanvasRenderingContext2D): void {
// TODO: fonts and colors should come from the CSS and not hardcoded here.
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+ const {
+ visibleTimeScale,
+ visibleWindowTime,
+ } = globals.frontendLocalState;
const data = this.data();
if (data === undefined) return; // Can't possibly draw anything.
@@ -218,19 +230,20 @@
checkerboardExcept(
ctx,
this.getHeight(),
- timeScale.timeToPx(visibleWindowTime.start),
- timeScale.timeToPx(visibleWindowTime.end),
- timeScale.timeToPx(data.start),
- timeScale.timeToPx(data.end));
+ visibleTimeScale.hpTimeToPx(visibleWindowTime.start),
+ visibleTimeScale.hpTimeToPx(visibleWindowTime.end),
+ visibleTimeScale.tpTimeToPx(data.start),
+ visibleTimeScale.tpTimeToPx(data.end));
assertTrue(data.starts.length === data.ends.length);
assertTrue(data.starts.length === data.utids.length);
- const rawStartIdx =
- data.ends.findIndex((end) => end >= visibleWindowTime.start);
+ const startSeconds = visibleWindowTime.start.seconds;
+ const rawStartIdx = data.ends.findIndex((end) => end >= startSeconds);
const startIdx = rawStartIdx === -1 ? data.starts.length : rawStartIdx;
- const [, rawEndIdx] = searchSegment(data.starts, visibleWindowTime.end);
+ const [, rawEndIdx] =
+ searchSegment(data.starts, visibleWindowTime.end.seconds);
const endIdx = rawEndIdx === -1 ? data.starts.length : rawEndIdx;
const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
@@ -241,8 +254,8 @@
const utid = data.utids[i];
const cpu = data.cpus[i];
- const rectStart = timeScale.timeToPx(tStart);
- const rectEnd = timeScale.timeToPx(tEnd);
+ const rectStart = visibleTimeScale.secondsToPx(tStart);
+ const rectEnd = visibleTimeScale.secondsToPx(tEnd);
const rectWidth = rectEnd - rectStart;
if (rectWidth < 0.3) continue;
@@ -294,8 +307,8 @@
const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
const cpu = Math.floor((pos.y - MARGIN_TOP) / (cpuTrackHeight + 1));
- const {timeScale} = globals.frontendLocalState;
- const t = timeScale.pxToTime(pos.x);
+ const {visibleTimeScale} = globals.frontendLocalState;
+ const t = visibleTimeScale.pxToHpTime(pos.x).seconds;
const [i, j] = searchRange(data.starts, t, searchEq(data.cpus, cpu));
if (i === j || i >= data.starts.length || t > data.ends[i]) {
diff --git a/ui/src/tracks/process_summary/index.ts b/ui/src/tracks/process_summary/index.ts
index d2d0ee8..8b92511 100644
--- a/ui/src/tracks/process_summary/index.ts
+++ b/ui/src/tracks/process_summary/index.ts
@@ -15,7 +15,14 @@
import {colorForTid} from '../../common/colorizer';
import {PluginContext} from '../../common/plugin_api';
import {NUM} from '../../common/query_result';
-import {fromNs, toNs} from '../../common/time';
+import {
+ fromNs,
+ TPDuration,
+ TPTime,
+ tpTimeFromNanos,
+ tpTimeToNanos,
+ tpTimeToSeconds,
+} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {LIMIT} from '../../common/track_data';
import {
@@ -45,10 +52,10 @@
static readonly kind = PROCESS_SUMMARY_TRACK;
private setup = false;
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
- const startNs = toNs(start);
- const endNs = toNs(end);
+ const startNs = tpTimeToNanos(start);
+ const endNs = tpTimeToNanos(end);
if (this.setup === false) {
await this.query(
@@ -85,9 +92,9 @@
this.setup = true;
}
- // |resolution| is in s/px we want # ns for 10px window:
+ // |resolution| is in ns/px we want # ns for 10px window:
// Max value with 1 so we don't end up with resolution 0.
- const bucketSizeNs = Math.max(1, Math.round(resolution * 10 * 1e9));
+ const bucketSizeNs = Math.max(1, Math.round(Number(resolution) * 10));
const windowStartNs = Math.floor(startNs / bucketSizeNs) * bucketSizeNs;
const windowDurNs = Math.max(1, endNs - windowStartNs);
@@ -98,14 +105,14 @@
where rowid = 0;`);
return this.computeSummary(
- fromNs(windowStartNs), end, resolution, bucketSizeNs);
+ tpTimeFromNanos(windowStartNs), end, resolution, bucketSizeNs);
}
private async computeSummary(
- start: number, end: number, resolution: number,
+ start: TPTime, end: TPTime, resolution: TPDuration,
bucketSizeNs: number): Promise<Data> {
- const startNs = toNs(start);
- const endNs = toNs(end);
+ const startNs = Number(start);
+ const endNs = Number(end);
const numBuckets =
Math.min(Math.ceil((endNs - startNs) / bucketSizeNs), LIMIT);
@@ -167,25 +174,28 @@
}
renderCanvas(ctx: CanvasRenderingContext2D): void {
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+ const {
+ visibleTimeScale,
+ windowSpan,
+ } = globals.frontendLocalState;
const data = this.data();
if (data === undefined) return; // Can't possibly draw anything.
checkerboardExcept(
ctx,
this.getHeight(),
- timeScale.timeToPx(visibleWindowTime.start),
- timeScale.timeToPx(visibleWindowTime.end),
- timeScale.timeToPx(data.start),
- timeScale.timeToPx(data.end));
+ windowSpan.start,
+ windowSpan.end,
+ visibleTimeScale.tpTimeToPx(data.start),
+ visibleTimeScale.tpTimeToPx(data.end));
this.renderSummary(ctx, data);
}
// TODO(dproy): Dedup with CPU slices.
renderSummary(ctx: CanvasRenderingContext2D, data: Data): void {
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
- const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start));
+ const {visibleTimeScale, windowSpan} = globals.frontendLocalState;
+ const startPx = windowSpan.start;
const bottomY = TRACK_HEIGHT;
let lastX = startPx;
@@ -202,9 +212,10 @@
for (let i = 0; i < data.utilizations.length; i++) {
// TODO(dproy): Investigate why utilization is > 1 sometimes.
const utilization = Math.min(data.utilizations[i], 1);
- const startTime = i * data.bucketSizeSeconds + data.start;
+ const startTime =
+ i * data.bucketSizeSeconds + tpTimeToSeconds(data.start);
- lastX = Math.floor(timeScale.timeToPx(startTime));
+ lastX = Math.floor(visibleTimeScale.secondsToPx(startTime));
ctx.lineTo(lastX, lastY);
lastY = MARGIN_TOP + Math.round(SUMMARY_HEIGHT * (1 - utilization));
diff --git a/ui/src/tracks/scroll_jank/scroll_details_tab.ts b/ui/src/tracks/scroll_jank/scroll_details_tab.ts
index e7c257c..b13fcae 100644
--- a/ui/src/tracks/scroll_jank/scroll_details_tab.ts
+++ b/ui/src/tracks/scroll_jank/scroll_details_tab.ts
@@ -18,13 +18,14 @@
import m from 'mithril';
import {ColumnType} from '../../common/query_result';
+import {tpDurationFromSql, tpTimeFromSql} from '../../common/time';
import {
BottomTab,
bottomTabRegistry,
NewBottomTabArgs,
} from '../../frontend/bottom_tab';
import {globals} from '../../frontend/globals';
-import {timestampFromSqlNanos} from '../../frontend/sql_types';
+import {asTPTimestamp} from '../../frontend/sql_types';
import {Duration} from '../../frontend/widgets/duration';
import {Timestamp} from '../../frontend/widgets/timestamp';
import {dictToTree} from '../../frontend/widgets/tree';
@@ -63,8 +64,9 @@
const left = dictToTree({
'Scroll Id (gesture_scroll_id)': `${this.data['id']}`,
- 'Start time': m(Timestamp, {ts: timestampFromSqlNanos(this.data['ts'])}),
- 'Duration': m(Duration, {dur: Number(this.data['dur'])}),
+ 'Start time':
+ m(Timestamp, {ts: asTPTimestamp(tpTimeFromSql(this.data['ts']))}),
+ 'Duration': m(Duration, {dur: tpDurationFromSql(this.data['dur'])}),
});
return m(
'.details-panel',
diff --git a/ui/src/tracks/scroll_jank/scroll_track.ts b/ui/src/tracks/scroll_jank/scroll_track.ts
index 8706b9d..8532a45 100644
--- a/ui/src/tracks/scroll_jank/scroll_track.ts
+++ b/ui/src/tracks/scroll_jank/scroll_track.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {TPTime} from 'src/common/time';
import {v4 as uuidv4} from 'uuid';
import {Actions} from '../../common/actions';
@@ -39,8 +40,8 @@
kind: 'TOP_LEVEL_SCROLL';
id: number;
sqlTableName: string;
- startS: number;
- durationS: number;
+ start: TPTime;
+ duration: TPTime;
}
export {Data} from '../chrome_slices';
@@ -86,8 +87,8 @@
globals.dispatch(Actions.selectTopLevelScrollSlice({
id: args.slice.id,
sqlTableName: this.tableName,
- startS: args.slice.startS,
- durationS: args.slice.durationS,
+ start: args.slice.start,
+ duration: args.slice.duration,
trackId: this.trackId,
}));
}
diff --git a/ui/src/tracks/thread_state/index.ts b/ui/src/tracks/thread_state/index.ts
index 3713999..042a7cf 100644
--- a/ui/src/tracks/thread_state/index.ts
+++ b/ui/src/tracks/thread_state/index.ts
@@ -17,10 +17,18 @@
import {Actions} from '../../common/actions';
import {cropText} from '../../common/canvas_utils';
import {colorForState} from '../../common/colorizer';
+import {
+ HighPrecisionTime,
+ HighPrecisionTimeSpan,
+} from '../../common/high_precision_time';
import {PluginContext} from '../../common/plugin_api';
-import {NUM, NUM_NULL, STR_NULL} from '../../common/query_result';
+import {LONG, NUM, NUM_NULL, STR_NULL} from '../../common/query_result';
import {translateState} from '../../common/thread_state';
-import {fromNs, toNs} from '../../common/time';
+import {
+ fromNs,
+ TPDuration,
+ TPTime,
+} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {TrackController} from '../../controller/track_controller';
import {checkerboardExcept} from '../../frontend/checkerboard';
@@ -46,7 +54,7 @@
class ThreadStateTrackController extends TrackController<Config, Data> {
static readonly kind = THREAD_STATE_TRACK_KIND;
- private maxDurNs = 0;
+ private maxDurNs: TPDuration = 0n;
async onSetup() {
await this.query(`
@@ -66,19 +74,15 @@
select ifnull(max(dur), 0) as maxDur
from ${this.tableName('thread_state')}
`);
- this.maxDurNs = queryRes.firstRow({maxDur: NUM}).maxDur;
+ this.maxDurNs = queryRes.firstRow({maxDur: LONG}).maxDur;
}
- async onBoundsChange(start: number, end: number, resolution: number):
+ async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
Promise<Data> {
- const resolutionNs = toNs(resolution);
- const startNs = toNs(start);
- const endNs = toNs(end);
-
// ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
// be an even number, so we can snap in the middle.
const bucketNs =
- Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1);
+ Math.max(Math.round(Number(resolution) * this.pxSize() / 2) * 2, 1);
const query = `
select
@@ -92,8 +96,8 @@
ifnull(id, -1) as id
from ${this.tableName('thread_state')}
where
- ts >= ${startNs - this.maxDurNs} and
- ts <= ${endNs}
+ ts >= ${start - this.maxDurNs} and
+ ts <= ${end}
group by tsq, is_sleep
order by tsq
`;
@@ -186,7 +190,11 @@
}
renderCanvas(ctx: CanvasRenderingContext2D): void {
- const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+ const {
+ visibleTimeScale: timeScale,
+ visibleWindowTime,
+ windowSpan,
+ } = globals.frontendLocalState;
const data = this.data();
const charWidth = ctx.measureText('dbpqaouk').width / 8;
@@ -199,10 +207,10 @@
checkerboardExcept(
ctx,
this.getHeight(),
- timeScale.timeToPx(visibleWindowTime.start),
- timeScale.timeToPx(visibleWindowTime.end),
- timeScale.timeToPx(data.start),
- timeScale.timeToPx(data.end),
+ windowSpan.start,
+ windowSpan.end,
+ timeScale.tpTimeToPx(data.start),
+ timeScale.tpTimeToPx(data.end),
);
ctx.textAlign = 'center';
@@ -220,14 +228,19 @@
const tStart = data.starts[i];
const tEnd = data.ends[i];
const state = data.strings[data.state[i]];
- if (tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end) {
+ const timeSpan = new HighPrecisionTimeSpan(
+ HighPrecisionTime.fromSeconds(tStart),
+ HighPrecisionTime.fromSeconds(tEnd),
+ );
+
+ if (!visibleWindowTime.intersects(timeSpan)) {
continue;
}
// Don't display a slice for Task Dead.
if (state === 'x') continue;
- const rectStart = timeScale.timeToPx(tStart);
- const rectEnd = timeScale.timeToPx(tEnd);
+ const rectStart = timeScale.secondsToPx(tStart);
+ const rectEnd = timeScale.secondsToPx(tEnd);
const rectWidth = rectEnd - rectStart;
const currentSelection = globals.state.currentSelection;
@@ -255,10 +268,9 @@
if (isSelected) {
drawRectOnSelected = () => {
const rectStart =
- Math.max(0 - EXCESS_WIDTH, timeScale.timeToPx(tStart));
+ Math.max(0 - EXCESS_WIDTH, timeScale.secondsToPx(tStart));
const rectEnd = Math.min(
- timeScale.timeToPx(visibleWindowTime.end) + EXCESS_WIDTH,
- timeScale.timeToPx(tEnd));
+ windowSpan.end + EXCESS_WIDTH, timeScale.secondsToPx(tEnd));
const color = colorForState(state);
ctx.strokeStyle = `hsl(${color.h},${color.s}%,${color.l * 0.7}%)`;
ctx.beginPath();
@@ -278,9 +290,9 @@
onMouseClick({x}: {x: number}) {
const data = this.data();
if (data === undefined) return false;
- const {timeScale} = globals.frontendLocalState;
- const time = timeScale.pxToTime(x);
- const index = search(data.starts, time);
+ const {visibleTimeScale} = globals.frontendLocalState;
+ const time = visibleTimeScale.pxToHpTime(x);
+ const index = search(data.starts, time.nanos);
if (index === -1) return false;
const id = data.ids[index];