Merge "trace_processor: fix sorting of events in sched tracker"
diff --git a/Android.bp b/Android.bp
index 541e66f..480e618 100644
--- a/Android.bp
+++ b/Android.bp
@@ -32,6 +32,7 @@
":perfetto_protos_perfetto_trace_trusted_lite_gen",
":perfetto_protos_perfetto_trace_zero_gen",
":perfetto_src_ipc_wire_protocol_gen",
+ "src/base/event.cc",
"src/base/file_utils.cc",
"src/base/metatrace.cc",
"src/base/page_allocator.cc",
@@ -157,6 +158,7 @@
":perfetto_src_ipc_wire_protocol_gen",
":perfetto_src_perfetto_cmd_protos_gen",
"src/base/android_task_runner.cc",
+ "src/base/event.cc",
"src/base/file_utils.cc",
"src/base/metatrace.cc",
"src/base/page_allocator.cc",
@@ -297,6 +299,7 @@
":perfetto_protos_perfetto_trace_zero_gen",
":perfetto_src_ipc_wire_protocol_gen",
"src/base/android_task_runner.cc",
+ "src/base/event.cc",
"src/base/file_utils.cc",
"src/base/metatrace.cc",
"src/base/page_allocator.cc",
@@ -3612,6 +3615,7 @@
":perfetto_protos_perfetto_trace_trusted_lite_gen",
":perfetto_protos_perfetto_trace_zero_gen",
":perfetto_src_ipc_wire_protocol_gen",
+ "src/base/event.cc",
"src/base/file_utils.cc",
"src/base/metatrace.cc",
"src/base/page_allocator.cc",
@@ -3800,6 +3804,7 @@
":perfetto_src_traced_probes_ftrace_test_messages_lite_gen",
":perfetto_src_traced_probes_ftrace_test_messages_zero_gen",
"src/base/android_task_runner.cc",
+ "src/base/event.cc",
"src/base/file_utils.cc",
"src/base/metatrace.cc",
"src/base/page_allocator.cc",
@@ -4033,6 +4038,7 @@
":perfetto_protos_perfetto_trace_minimal_lite_gen",
":perfetto_protos_perfetto_trace_ps_lite_gen",
":perfetto_protos_perfetto_trace_sys_stats_lite_gen",
+ "src/base/event.cc",
"src/base/file_utils.cc",
"src/base/metatrace.cc",
"src/base/page_allocator.cc",
diff --git a/README.chromium b/README.chromium
index a8a7cb6..33541c5 100644
--- a/README.chromium
+++ b/README.chromium
@@ -2,7 +2,7 @@
URL: https://android.googlesource.com/platform/external/perfetto/
Version: unknown
License: Apache2
-License File: MODULE_LICENSE_APACHE2
+License File: NOTICE
Security Critical: yes
License Android Compatible: yes
Description: Performance instrumentation and logging for Google client platforms
diff --git a/include/perfetto/base/BUILD.gn b/include/perfetto/base/BUILD.gn
index 10da3d0..5ee76ba 100644
--- a/include/perfetto/base/BUILD.gn
+++ b/include/perfetto/base/BUILD.gn
@@ -17,6 +17,7 @@
source_set("base") {
sources = [
"build_config.h",
+ "event.h",
"export.h",
"file_utils.h",
"logging.h",
diff --git a/include/perfetto/base/android_task_runner.h b/include/perfetto/base/android_task_runner.h
index 5087330..5aebbac 100644
--- a/include/perfetto/base/android_task_runner.h
+++ b/include/perfetto/base/android_task_runner.h
@@ -17,6 +17,7 @@
#ifndef INCLUDE_PERFETTO_BASE_ANDROID_TASK_RUNNER_H_
#define INCLUDE_PERFETTO_BASE_ANDROID_TASK_RUNNER_H_
+#include "perfetto/base/event.h"
#include "perfetto/base/scoped_file.h"
#include "perfetto/base/task_runner.h"
#include "perfetto/base/thread_checker.h"
@@ -69,7 +70,7 @@
void ScheduleDelayedWakeUp(TimeMillis time);
ALooper* const looper_;
- ScopedFile immediate_event_;
+ Event immediate_event_;
ScopedFile delayed_timer_;
ThreadChecker thread_checker_;
diff --git a/include/perfetto/base/event.h b/include/perfetto/base/event.h
new file mode 100644
index 0000000..bca6c00
--- /dev/null
+++ b/include/perfetto/base/event.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#ifndef INCLUDE_PERFETTO_BASE_EVENT_H_
+#define INCLUDE_PERFETTO_BASE_EVENT_H_
+
+#include "perfetto/base/build_config.h"
+#include "perfetto/base/scoped_file.h"
+
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
+ PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+#define PERFETTO_USE_EVENTFD() 1
+#else
+#define PERFETTO_USE_EVENTFD() 0
+#endif
+
+namespace perfetto {
+namespace base {
+
+// A waitable event that can be used with poll/select.
+// This is really a wrapper around eventfd_create with a pipe-based fallback
+// for other platforms where eventfd is not supported.
+class Event {
+ public:
+ Event();
+ ~Event();
+ Event(Event&&) noexcept = default;
+ Event& operator=(Event&&) = default;
+
+ // The non-blocking file descriptor that can be polled to wait for the event.
+ int fd() const { return fd_.get(); }
+
+ // Can be called from any thread.
+ void Notify();
+
+ // Can be called from any thread. If more Notify() are queued a Clear() call
+ // can clear all of them (up to 16 per call).
+ void Clear();
+
+ private:
+ // The eventfd, when eventfd is supported, otherwise this is the read end of
+ // the pipe for fallback mode.
+ ScopedFile fd_;
+
+#if !PERFETTO_USE_EVENTFD()
+ // The write end of the wakeup pipe.
+ ScopedFile write_fd_;
+#endif
+};
+
+} // namespace base
+} // namespace perfetto
+
+#endif // INCLUDE_PERFETTO_BASE_EVENT_H_
diff --git a/include/perfetto/base/file_utils.h b/include/perfetto/base/file_utils.h
index e23add1..71f716f 100644
--- a/include/perfetto/base/file_utils.h
+++ b/include/perfetto/base/file_utils.h
@@ -17,14 +17,27 @@
#ifndef INCLUDE_PERFETTO_BASE_FILE_UTILS_H_
#define INCLUDE_PERFETTO_BASE_FILE_UTILS_H_
+#include <stddef.h>
+
#include <string>
+#include "perfetto/base/utils.h" // For ssize_t on Windows.
+
namespace perfetto {
namespace base {
bool ReadFileDescriptor(int fd, std::string* out);
bool ReadFile(const std::string& path, std::string* out);
+// Call write until all data is written or an error is detected.
+//
+// man 2 write:
+// If a write() is interrupted by a signal handler before any bytes are
+// written, then the call fails with the error EINTR; if it is
+// interrupted after at least one byte has been written, the call
+// succeeds, and returns the number of bytes written.
+ssize_t WriteAll(int fd, const void* buf, size_t count);
+
} // namespace base
} // namespace perfetto
diff --git a/include/perfetto/base/scoped_file.h b/include/perfetto/base/scoped_file.h
index 7e380f0..59ca75f 100644
--- a/include/perfetto/base/scoped_file.h
+++ b/include/perfetto/base/scoped_file.h
@@ -24,6 +24,7 @@
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
#include <corecrt_io.h>
+typedef int mode_t;
#else
#include <dirent.h>
#include <unistd.h>
@@ -36,6 +37,8 @@
namespace perfetto {
namespace base {
+constexpr mode_t kInvalidMode = static_cast<mode_t>(-1);
+
// RAII classes for auto-releasing fds and dirs.
template <typename T,
int (*CloseFunction)(T),
@@ -81,8 +84,8 @@
using ScopedFile = ScopedResource<int, close, -1>;
inline static ScopedFile OpenFile(const std::string& path,
int flags,
- mode_t mode = 0) {
- PERFETTO_DCHECK((flags & O_CREAT) == 0 || mode != 0);
+ mode_t mode = kInvalidMode) {
+ PERFETTO_DCHECK((flags & O_CREAT) == 0 || mode != kInvalidMode);
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
ScopedFile fd(open(path.c_str(), flags, mode));
#else
diff --git a/include/perfetto/base/unix_task_runner.h b/include/perfetto/base/unix_task_runner.h
index 65c8361..3d1a896 100644
--- a/include/perfetto/base/unix_task_runner.h
+++ b/include/perfetto/base/unix_task_runner.h
@@ -18,6 +18,7 @@
#define INCLUDE_PERFETTO_BASE_UNIX_TASK_RUNNER_H_
#include "perfetto/base/build_config.h"
+#include "perfetto/base/event.h"
#include "perfetto/base/scoped_file.h"
#include "perfetto/base/task_runner.h"
#include "perfetto/base/thread_checker.h"
@@ -30,13 +31,6 @@
#include <mutex>
#include <vector>
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
- PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define PERFETTO_USE_EVENTFD() 1
-#else
-#define PERFETTO_USE_EVENTFD() 0
-#endif
-
namespace perfetto {
namespace base {
@@ -75,11 +69,7 @@
// On Linux, an eventfd(2) used to waking up the task runner when a new task
// is posted. Otherwise the read end of a pipe used for the same purpose.
- ScopedFile event_;
-#if !PERFETTO_USE_EVENTFD()
- // The write end of the wakeup pipe.
- ScopedFile event_write_;
-#endif
+ Event event_;
std::vector<struct pollfd> poll_fds_;
diff --git a/protos/perfetto/trace/chrome/chrome_trace_event.proto b/protos/perfetto/trace/chrome/chrome_trace_event.proto
index a004d92..7911f42 100644
--- a/protos/perfetto/trace/chrome/chrome_trace_event.proto
+++ b/protos/perfetto/trace/chrome/chrome_trace_event.proto
@@ -41,7 +41,7 @@
// Takes precedence over |name| if set,
// and is an index into |string_table|.
- optional int32 name_index = 9;
+ optional uint32 name_index = 9;
}
optional string name = 1;
@@ -63,8 +63,8 @@
// Takes precedence over respectively |name| and
// |category_group_name_index| if set,
// and are indices into |string_table|.
- optional int32 name_index = 15;
- optional int32 category_group_name_index = 16;
+ optional uint32 name_index = 15;
+ optional uint32 category_group_name_index = 16;
}
message ChromeMetadata {
diff --git a/protos/perfetto/trace_processor/raw_query.proto b/protos/perfetto/trace_processor/raw_query.proto
index 673994f..2192e41 100644
--- a/protos/perfetto/trace_processor/raw_query.proto
+++ b/protos/perfetto/trace_processor/raw_query.proto
@@ -45,4 +45,5 @@
optional uint64 num_records = 2;
repeated ColumnValues columns = 3;
optional string error = 4;
+ optional uint64 execution_time_ns = 5;
}
diff --git a/src/base/BUILD.gn b/src/base/BUILD.gn
index 019cd7b..ad3f170 100644
--- a/src/base/BUILD.gn
+++ b/src/base/BUILD.gn
@@ -37,6 +37,7 @@
# TODO(brucedawson): Enable these for Windows when possible.
if (!is_win) {
sources += [
+ "event.cc",
"temp_file.cc",
"unix_task_runner.cc",
]
diff --git a/src/base/android_task_runner.cc b/src/base/android_task_runner.cc
index d8e7cc7..23697c8 100644
--- a/src/base/android_task_runner.cc
+++ b/src/base/android_task_runner.cc
@@ -17,7 +17,6 @@
#include "perfetto/base/android_task_runner.h"
#include <errno.h>
-#include <sys/eventfd.h>
#include <sys/timerfd.h>
namespace perfetto {
@@ -25,13 +24,11 @@
AndroidTaskRunner::AndroidTaskRunner()
: looper_(ALooper_prepare(0 /* require callbacks */)),
- immediate_event_(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)),
delayed_timer_(
timerfd_create(kWallTimeClockSource, TFD_NONBLOCK | TFD_CLOEXEC)) {
ALooper_acquire(looper_);
- PERFETTO_CHECK(immediate_event_);
PERFETTO_CHECK(delayed_timer_);
- AddFileDescriptorWatch(immediate_event_.get(),
+ AddFileDescriptorWatch(immediate_event_.fd(),
std::bind(&AndroidTaskRunner::RunImmediateTask, this));
AddFileDescriptorWatch(delayed_timer_.get(),
std::bind(&AndroidTaskRunner::RunDelayedTask, this));
@@ -81,11 +78,7 @@
}
void AndroidTaskRunner::RunImmediateTask() {
- uint64_t unused = 0;
- if (read(immediate_event_.get(), &unused, sizeof(unused)) != sizeof(unused) &&
- errno != EAGAIN) {
- PERFETTO_DPLOG("read");
- }
+ immediate_event_.Clear();
// If locking overhead becomes an issue, add a separate work queue.
bool has_next;
@@ -133,11 +126,7 @@
}
void AndroidTaskRunner::ScheduleImmediateWakeUp() {
- uint64_t value = 1;
- if (write(immediate_event_.get(), &value, sizeof(value)) == -1 &&
- errno != EAGAIN) {
- PERFETTO_DPLOG("write");
- }
+ immediate_event_.Notify();
}
void AndroidTaskRunner::ScheduleDelayedWakeUp(TimeMillis time) {
diff --git a/src/base/debug_crash_stack_trace.cc b/src/base/debug_crash_stack_trace.cc
index c9cc762..3220d9d 100644
--- a/src/base/debug_crash_stack_trace.cc
+++ b/src/base/debug_crash_stack_trace.cc
@@ -28,6 +28,7 @@
#include <unwind.h>
#include "perfetto/base/build_config.h"
+#include "perfetto/base/file_utils.h"
// Some glibc headers hit this when using signals.
#pragma GCC diagnostic push
@@ -66,7 +67,7 @@
template <typename T>
void Print(const T& str) {
- write(STDERR_FILENO, str, sizeof(str));
+ perfetto::base::WriteAll(STDERR_FILENO, str, sizeof(str));
}
template <typename T>
@@ -74,7 +75,7 @@
for (unsigned i = 0; i < sizeof(n) * 8; i += 4) {
char nibble = static_cast<char>(n >> (sizeof(n) * 8 - i - 4)) & 0x0F;
char c = (nibble < 10) ? '0' + nibble : 'A' + nibble - 10;
- write(STDERR_FILENO, &c, 1);
+ perfetto::base::WriteAll(STDERR_FILENO, &c, 1);
}
}
@@ -198,14 +199,16 @@
// might be moved.
g_demangled_name = demangled;
}
- write(STDERR_FILENO, sym.sym_name, strlen(sym.sym_name));
+ perfetto::base::WriteAll(STDERR_FILENO, sym.sym_name,
+ strlen(sym.sym_name));
} else {
Print("0x");
PrintHex(frames[i]);
}
if (sym.file_name[0]) {
Print("\n ");
- write(STDERR_FILENO, sym.file_name, strlen(sym.file_name));
+ perfetto::base::WriteAll(STDERR_FILENO, sym.file_name,
+ strlen(sym.file_name));
}
Print("\n");
}
diff --git a/src/base/event.cc b/src/base/event.cc
new file mode 100644
index 0000000..4085618
--- /dev/null
+++ b/src/base/event.cc
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#include <stdint.h>
+#include <unistd.h>
+
+#include "perfetto/base/event.h"
+#include "perfetto/base/logging.h"
+
+#if PERFETTO_USE_EVENTFD()
+#include <sys/eventfd.h>
+#endif
+
+namespace perfetto {
+namespace base {
+
+Event::Event() {
+#if PERFETTO_USE_EVENTFD()
+ fd_.reset(eventfd(/* start value */ 0, EFD_CLOEXEC | EFD_NONBLOCK));
+ PERFETTO_CHECK(fd_);
+#else
+ int pipe_fds[2];
+ PERFETTO_CHECK(pipe(pipe_fds) == 0);
+
+ // Make the pipe non-blocking so that we never block the waking thread (either
+ // the main thread or another one) when scheduling a wake-up.
+ for (auto fd : pipe_fds) {
+ int flags = fcntl(fd, F_GETFL, 0);
+ PERFETTO_CHECK(flags != -1);
+ PERFETTO_CHECK(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0);
+ PERFETTO_CHECK(fcntl(fd, F_SETFD, FD_CLOEXEC) == 0);
+ }
+ fd_.reset(pipe_fds[0]);
+ write_fd_.reset(pipe_fds[1]);
+#endif // !PERFETTO_USE_EVENTFD()
+}
+
+Event::~Event() = default;
+
+void Event::Notify() {
+ const uint64_t value = 1;
+
+#if PERFETTO_USE_EVENTFD()
+ ssize_t ret = write(fd_.get(), &value, sizeof(value));
+#else
+ ssize_t ret = write(write_fd_.get(), &value, sizeof(uint8_t));
+#endif
+
+ if (ret <= 0 && errno != EAGAIN) {
+ PERFETTO_DPLOG("write()");
+ PERFETTO_DCHECK(false);
+ }
+}
+
+void Event::Clear() {
+#if PERFETTO_USE_EVENTFD()
+ uint64_t value;
+ ssize_t ret = read(fd_.get(), &value, sizeof(value));
+#else
+ // Drain the byte(s) written to the wake-up pipe. We can potentially read
+ // more than one byte if several wake-ups have been scheduled.
+ char buffer[16];
+ ssize_t ret = read(fd_.get(), &buffer[0], sizeof(buffer));
+#endif
+ if (ret <= 0 && errno != EAGAIN)
+ PERFETTO_DPLOG("read()");
+}
+
+} // namespace base
+} // namespace perfetto
diff --git a/src/base/file_utils.cc b/src/base/file_utils.cc
index a51b9b2..44b27f7 100644
--- a/src/base/file_utils.cc
+++ b/src/base/file_utils.cc
@@ -16,11 +16,17 @@
#include <sys/stat.h>
+#include "perfetto/base/build_config.h"
#include "perfetto/base/file_utils.h"
-
#include "perfetto/base/logging.h"
#include "perfetto/base/scoped_file.h"
+#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+#include <unistd.h>
+#else
+#include <corecrt_io.h>
+#endif
+
namespace perfetto {
namespace base {
namespace {
@@ -53,12 +59,26 @@
}
bool ReadFile(const std::string& path, std::string* out) {
- base::ScopedFile fd = base::OpenFile(path.c_str(), O_RDONLY);
+ base::ScopedFile fd = base::OpenFile(path, O_RDONLY);
if (!fd)
return false;
return ReadFileDescriptor(*fd, out);
}
+ssize_t WriteAll(int fd, const void* buf, size_t count) {
+ size_t written = 0;
+ while (written < count) {
+ ssize_t wr = PERFETTO_EINTR(
+ write(fd, static_cast<const char*>(buf) + written, count - written));
+ if (wr == 0)
+ break;
+ if (wr < 0)
+ return wr;
+ written += static_cast<size_t>(wr);
+ }
+ return static_cast<ssize_t>(written);
+}
+
} // namespace base
} // namespace perfetto
diff --git a/src/base/metatrace.cc b/src/base/metatrace.cc
index 045d6fb..1585e43 100644
--- a/src/base/metatrace.cc
+++ b/src/base/metatrace.cc
@@ -20,6 +20,7 @@
#include <stdlib.h>
#include "perfetto/base/build_config.h"
+#include "perfetto/base/file_utils.h"
#include "perfetto/base/time.h"
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
@@ -49,7 +50,7 @@
"{\"ts\": %f, \"cat\": \"PERF\", \"ph\": \"%c\", \"name\": "
"\"%s\", \"pid\": %zu},\n",
GetWallTimeNs().count() / 1000.0, type, evt_name, cpu);
- ignore_result(write(fd, json, static_cast<size_t>(len)));
+ ignore_result(WriteAll(fd, json, static_cast<size_t>(len)));
}
} // namespace base
diff --git a/src/base/task_runner_unittest.cc b/src/base/task_runner_unittest.cc
index 19a3dd5..0bde467 100644
--- a/src/base/task_runner_unittest.cc
+++ b/src/base/task_runner_unittest.cc
@@ -27,6 +27,8 @@
#include <thread>
+#include "perfetto/base/file_utils.h"
+
namespace perfetto {
namespace base {
namespace {
@@ -62,7 +64,7 @@
void Write() {
const char b = '?';
- PERFETTO_DCHECK(write(write_fd.get(), &b, 1) == 1);
+ PERFETTO_DCHECK(WriteAll(write_fd.get(), &b, 1) == 1);
}
ScopedFile read_fd;
diff --git a/src/base/unix_socket.cc b/src/base/unix_socket.cc
index fb4f5ff..bb56248 100644
--- a/src/base/unix_socket.cc
+++ b/src/base/unix_socket.cc
@@ -65,7 +65,8 @@
#endif
void ShiftMsgHdr(size_t n, struct msghdr* msg) {
- for (size_t i = 0; i < msg->msg_iovlen; ++i) {
+ using LenType = decltype(msg->msg_iovlen); // Mac and Linux don't agree.
+ for (LenType i = 0; i < msg->msg_iovlen; ++i) {
struct iovec* vec = &msg->msg_iov[i];
if (n < vec->iov_len) {
// We sent a part of this iovec.
diff --git a/src/base/unix_socket_unittest.cc b/src/base/unix_socket_unittest.cc
index 382f9bc..531f1e4 100644
--- a/src/base/unix_socket_unittest.cc
+++ b/src/base/unix_socket_unittest.cc
@@ -16,6 +16,7 @@
#include "perfetto/base/unix_socket.h"
+#include <signal.h>
#include <sys/mman.h>
#include <list>
@@ -24,6 +25,7 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "perfetto/base/build_config.h"
+#include "perfetto/base/file_utils.h"
#include "perfetto/base/logging.h"
#include "perfetto/base/temp_file.h"
#include "perfetto/base/utils.h"
@@ -200,8 +202,8 @@
ASSERT_TRUE(srv_conn);
ASSERT_TRUE(cli->is_connected());
- ScopedFile null_fd(open("/dev/null", O_RDONLY));
- ScopedFile zero_fd(open("/dev/zero", O_RDONLY));
+ ScopedFile null_fd(base::OpenFile("/dev/null", O_RDONLY));
+ ScopedFile zero_fd(base::OpenFile("/dev/zero", O_RDONLY));
auto cli_did_recv = task_runner_.CreateCheckpoint("cli_did_recv");
EXPECT_CALL(event_listener_, OnDataAvailable(cli.get()))
@@ -351,7 +353,7 @@
auto srv = UnixSocket::Listen(kSocketName, &event_listener_, &task_runner_);
ASSERT_TRUE(srv->is_listening());
// Signal the other process that it can connect.
- ASSERT_EQ(1, PERFETTO_EINTR(write(pipes[1], ".", 1)));
+ ASSERT_EQ(1, base::WriteAll(pipes[1], ".", 1));
auto checkpoint = task_runner_.CreateCheckpoint("change_seen_by_server");
EXPECT_CALL(event_listener_, OnNewIncomingConnection(srv.get(), _))
.WillOnce(Invoke(
diff --git a/src/base/unix_task_runner.cc b/src/base/unix_task_runner.cc
index 006b7af..44501df 100644
--- a/src/base/unix_task_runner.cc
+++ b/src/base/unix_task_runner.cc
@@ -24,32 +24,11 @@
#include <limits>
-#if PERFETTO_USE_EVENTFD()
-#include <sys/eventfd.h>
-#endif
-
namespace perfetto {
namespace base {
UnixTaskRunner::UnixTaskRunner() {
-#if PERFETTO_USE_EVENTFD()
- event_.reset(eventfd(/* start value */ 0, EFD_CLOEXEC | EFD_NONBLOCK));
-#else
- int pipe_fds[2];
- PERFETTO_CHECK(pipe(pipe_fds) == 0);
-
- // Make the pipe non-blocking so that we never block the waking thread (either
- // the main thread or another one) when scheduling a wake-up.
- for (auto fd : pipe_fds) {
- int flags = fcntl(fd, F_GETFL, 0);
- PERFETTO_CHECK(flags != -1);
- PERFETTO_CHECK(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0);
- PERFETTO_CHECK(fcntl(fd, F_SETFD, FD_CLOEXEC) == 0);
- }
- event_.reset(pipe_fds[0]);
- event_write_.reset(pipe_fds[1]);
-#endif // !PERFETTO_USE_EVENTFD()
- AddFileDescriptorWatch(event_.get(), [] {
+ AddFileDescriptorWatch(event_.fd(), [] {
// Not reached -- see PostFileDescriptorWatches().
PERFETTO_DCHECK(false);
});
@@ -58,14 +37,7 @@
UnixTaskRunner::~UnixTaskRunner() = default;
void UnixTaskRunner::WakeUp() {
- const uint64_t value = 1;
-#if PERFETTO_USE_EVENTFD()
- ssize_t ret = write(event_.get(), &value, sizeof(value));
-#else
- ssize_t ret = write(event_write_.get(), &value, sizeof(uint8_t));
-#endif
- if (ret <= 0 && errno != EAGAIN)
- PERFETTO_DPLOG("write()");
+ event_.Notify();
}
void UnixTaskRunner::Run() {
@@ -153,18 +125,8 @@
// The wake-up event is handled inline to avoid an infinite recursion of
// posted tasks.
- if (poll_fds_[i].fd == event_.get()) {
-#if PERFETTO_USE_EVENTFD()
- uint64_t value;
- ssize_t ret = read(event_.get(), &value, sizeof(value));
-#else
- // Drain the byte(s) written to the wake-up pipe. We can potentially read
- // more than one byte if several wake-ups have been scheduled.
- char buffer[16];
- ssize_t ret = read(event_.get(), &buffer[0], sizeof(buffer));
-#endif
- if (ret <= 0 && errno != EAGAIN)
- PERFETTO_DPLOG("read()");
+ if (poll_fds_[i].fd == event_.fd()) {
+ event_.Clear();
continue;
}
diff --git a/src/base/utils_unittest.cc b/src/base/utils_unittest.cc
index 459b420..1f556d6 100644
--- a/src/base/utils_unittest.cc
+++ b/src/base/utils_unittest.cc
@@ -23,6 +23,8 @@
#include "gtest/gtest.h"
+#include "perfetto/base/file_utils.h"
+
namespace perfetto {
namespace base {
namespace {
@@ -79,7 +81,7 @@
if (pid == 0 /* child */) {
usleep(5000);
kill(parent_pid, SIGUSR2);
- ignore_result(write(pipe_fd[1], "foo\0", 4));
+ ignore_result(WriteAll(pipe_fd[1], "foo\0", 4));
_exit(0);
}
diff --git a/src/base/watchdog_posix.cc b/src/base/watchdog_posix.cc
index d8c064c..85cbb96 100644
--- a/src/base/watchdog_posix.cc
+++ b/src/base/watchdog_posix.cc
@@ -127,7 +127,7 @@
}
void Watchdog::ThreadMain() {
- base::ScopedFile stat_fd(open("/proc/self/stat", O_RDONLY));
+ base::ScopedFile stat_fd(base::OpenFile("/proc/self/stat", O_RDONLY));
if (!stat_fd) {
PERFETTO_ELOG("Failed to open stat file to enforce resource limits.");
return;
diff --git a/src/ipc/client_impl_unittest.cc b/src/ipc/client_impl_unittest.cc
index 050fcca..61c7768 100644
--- a/src/ipc/client_impl_unittest.cc
+++ b/src/ipc/client_impl_unittest.cc
@@ -23,6 +23,7 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h"
+#include "perfetto/base/file_utils.h"
#include "perfetto/base/temp_file.h"
#include "perfetto/base/unix_socket.h"
#include "perfetto/base/utils.h"
@@ -349,7 +350,8 @@
base::TempFile tx_file = base::TempFile::CreateUnlinked();
static constexpr char kFileContent[] = "shared file";
- base::ignore_result(write(tx_file.fd(), kFileContent, sizeof(kFileContent)));
+ ASSERT_EQ(base::WriteAll(tx_file.fd(), kFileContent, sizeof(kFileContent)),
+ sizeof(kFileContent));
host_->next_reply_fd = tx_file.fd();
EXPECT_CALL(*host_method, OnInvoke(_, _))
@@ -393,7 +395,8 @@
base::TempFile tx_file = base::TempFile::CreateUnlinked();
static constexpr char kFileContent[] = "shared file";
- base::ignore_result(write(tx_file.fd(), kFileContent, sizeof(kFileContent)));
+ ASSERT_EQ(base::WriteAll(tx_file.fd(), kFileContent, sizeof(kFileContent)),
+ sizeof(kFileContent));
EXPECT_CALL(*host_method, OnInvoke(_, _))
.WillOnce(Invoke(
[](const Frame::InvokeMethod&, Frame::InvokeMethodReply* reply) {
diff --git a/src/ipc/host_impl_unittest.cc b/src/ipc/host_impl_unittest.cc
index 26b56c8..8247935 100644
--- a/src/ipc/host_impl_unittest.cc
+++ b/src/ipc/host_impl_unittest.cc
@@ -20,6 +20,7 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h"
+#include "perfetto/base/file_utils.h"
#include "perfetto/base/scoped_file.h"
#include "perfetto/base/temp_file.h"
#include "perfetto/base/unix_socket.h"
@@ -330,7 +331,8 @@
cli_->InvokeMethod(cli_->last_bound_service_id_, 1, req_args);
auto on_reply_sent = task_runner_->CreateCheckpoint("on_reply_sent");
base::TempFile tx_file = base::TempFile::CreateUnlinked();
- base::ignore_result(write(tx_file.fd(), kFileContent, sizeof(kFileContent)));
+ ASSERT_EQ(base::WriteAll(tx_file.fd(), kFileContent, sizeof(kFileContent)),
+ sizeof(kFileContent));
EXPECT_CALL(*fake_service, OnFakeMethod1(_, _))
.WillOnce(Invoke([on_reply_sent, &tx_file](const RequestProto&,
DeferredBase* reply) {
@@ -370,7 +372,8 @@
static constexpr char kFileContent[] = "shared file";
RequestProto req_args;
base::TempFile tx_file = base::TempFile::CreateUnlinked();
- base::ignore_result(write(tx_file.fd(), kFileContent, sizeof(kFileContent)));
+ ASSERT_EQ(base::WriteAll(tx_file.fd(), kFileContent, sizeof(kFileContent)),
+ sizeof(kFileContent));
cli_->InvokeMethod(cli_->last_bound_service_id_, 1, req_args, false,
tx_file.fd());
EXPECT_CALL(*cli_, OnInvokeMethodReply(_));
diff --git a/src/perfetto_cmd/perfetto_cmd.cc b/src/perfetto_cmd/perfetto_cmd.cc
index bd166ba..8c8f20e 100644
--- a/src/perfetto_cmd/perfetto_cmd.cc
+++ b/src/perfetto_cmd/perfetto_cmd.cc
@@ -333,7 +333,7 @@
// violation (about system_server ending up with a writable FD to our dir).
char fdpath[64];
sprintf(fdpath, "/proc/self/fd/%d", fileno(*trace_out_stream_));
- base::ScopedFile read_only_fd(open(fdpath, O_RDONLY));
+ base::ScopedFile read_only_fd(base::OpenFile(fdpath, O_RDONLY));
PERFETTO_CHECK(read_only_fd);
trace_out_stream_.reset();
android::binder::Status status =
@@ -359,7 +359,7 @@
// If we are tracing to DropBox, there's no need to make a
// filesystem-visible temporary file.
// TODO(skyostil): Fall back to base::TempFile for older devices.
- fd.reset(open(kTempDropBoxTraceDir, O_TMPFILE | O_RDWR, 0600));
+ fd = base::OpenFile(kTempDropBoxTraceDir, O_TMPFILE | O_RDWR, 0600);
if (!fd) {
PERFETTO_ELOG("Could not create a temporary trace file in %s",
kTempDropBoxTraceDir);
@@ -371,7 +371,7 @@
} else if (trace_out_path_ == "-") {
fd.reset(dup(STDOUT_FILENO));
} else {
- fd.reset(open(trace_out_path_.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0600));
+ fd = base::OpenFile(trace_out_path_, O_RDWR | O_CREAT | O_TRUNC, 0600);
}
trace_out_stream_.reset(fdopen(fd.release(), "wb"));
PERFETTO_CHECK(trace_out_stream_);
@@ -396,8 +396,8 @@
sa.sa_handler = [](int) {
PERFETTO_LOG("SIGINT received: disabling tracing");
char one = '1';
- PERFETTO_CHECK(PERFETTO_EINTR(write(g_consumer_cmd->ctrl_c_pipe_wr(), &one,
- sizeof(one))) == 1);
+ PERFETTO_CHECK(base::WriteAll(g_consumer_cmd->ctrl_c_pipe_wr(), &one,
+ sizeof(one)) == 1);
};
sa.sa_flags = static_cast<decltype(sa.sa_flags)>(SA_RESETHAND | SA_RESTART);
#pragma GCC diagnostic pop
diff --git a/src/perfetto_cmd/rate_limiter.cc b/src/perfetto_cmd/rate_limiter.cc
index 081b6ed..2b789af 100644
--- a/src/perfetto_cmd/rate_limiter.cc
+++ b/src/perfetto_cmd/rate_limiter.cc
@@ -20,6 +20,7 @@
#include <sys/types.h>
#include <unistd.h>
+#include "perfetto/base/file_utils.h"
#include "perfetto/base/logging.h"
#include "perfetto/base/scoped_file.h"
#include "perfetto/base/utils.h"
@@ -157,8 +158,7 @@
}
bool RateLimiter::LoadState(PerfettoCmdState* state) {
- base::ScopedFile in_fd;
- in_fd.reset(open(GetStateFilePath().c_str(), O_RDONLY));
+ base::ScopedFile in_fd(base::OpenFile(GetStateFilePath(), O_RDONLY));
if (!in_fd)
return false;
@@ -170,15 +170,14 @@
}
bool RateLimiter::SaveState(const PerfettoCmdState& state) {
- base::ScopedFile out_fd;
// Rationale for 0666: the cmdline client can be executed under two
// different Unix UIDs: shell and statsd. If we run one after the
// other and the file has 0600 permissions, then the 2nd run won't
// be able to read the file and will clear it, aborting the trace.
// SELinux still prevents that anything other than the perfetto
// executable can change the guardrail file.
- out_fd.reset(
- open(GetStateFilePath().c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666));
+ base::ScopedFile out_fd(
+ base::OpenFile(GetStateFilePath(), O_WRONLY | O_CREAT | O_TRUNC, 0666));
if (!out_fd)
return false;
char buf[1024];
@@ -186,7 +185,7 @@
PERFETTO_CHECK(size < sizeof(buf));
if (!state.SerializeToArray(&buf, static_cast<int>(size)))
return false;
- ssize_t written = PERFETTO_EINTR(write(out_fd.get(), &buf, size));
+ ssize_t written = base::WriteAll(out_fd.get(), &buf, size);
return written >= 0 && static_cast<size_t>(written) == size;
}
diff --git a/src/perfetto_cmd/rate_limiter_unittest.cc b/src/perfetto_cmd/rate_limiter_unittest.cc
index 753e0e6..988c7b9 100644
--- a/src/perfetto_cmd/rate_limiter_unittest.cc
+++ b/src/perfetto_cmd/rate_limiter_unittest.cc
@@ -18,6 +18,7 @@
#include <stdio.h>
+#include "perfetto/base/file_utils.h"
#include "perfetto/base/scoped_file.h"
#include "perfetto/base/temp_file.h"
#include "perfetto/base/utils.h"
@@ -69,10 +70,9 @@
};
void WriteGarbageToFile(const std::string& path) {
- base::ScopedFile fd;
- fd.reset(open(path.c_str(), O_WRONLY | O_CREAT, 0600));
+ base::ScopedFile fd(base::OpenFile(path, O_WRONLY | O_CREAT, 0600));
constexpr char data[] = "Some random bytes.";
- if (write(fd.get(), data, sizeof(data)) != sizeof(data))
+ if (base::WriteAll(fd.get(), data, sizeof(data)) != sizeof(data))
ADD_FAILURE() << "Could not write garbage";
}
diff --git a/src/profiling/memory/client.cc b/src/profiling/memory/client.cc
index 083dea4..1d63c83 100644
--- a/src/profiling/memory/client.cc
+++ b/src/profiling/memory/client.cc
@@ -35,8 +35,10 @@
#include <unwindstack/RegsGetLocal.h>
#include "perfetto/base/logging.h"
+#include "perfetto/base/scoped_file.h"
#include "perfetto/base/unix_socket.h"
#include "perfetto/base/utils.h"
+#include "src/profiling/memory/sampler.h"
#include "src/profiling/memory/wire_protocol.h"
namespace perfetto {
@@ -73,6 +75,27 @@
return getpid() == gettid();
}
+// TODO(b/117203899): Remove this after making bionic implementation safe to
+// use.
+char* FindMainThreadStack() {
+ base::ScopedFstream maps(fopen("/proc/self/maps", "r"));
+ if (!maps) {
+ return nullptr;
+ }
+ while (!feof(*maps)) {
+ char line[1024];
+ char* data = fgets(line, sizeof(line), *maps);
+ if (data != nullptr && strstr(data, "[stack]")) {
+ char* sep = strstr(data, "-");
+ if (sep == nullptr)
+ continue;
+ sep++;
+ return reinterpret_cast<char*>(strtoll(sep, nullptr, 16));
+ }
+ }
+ return nullptr;
+}
+
} // namespace
void FreePage::Add(const uint64_t addr,
@@ -92,6 +115,7 @@
void FreePage::FlushLocked(SocketPool* pool) {
WireMessage msg = {};
msg.record_type = RecordType::Free;
+ free_page_.num_entries = offset_;
msg.free_header = &free_page_;
BorrowedSocket fd(pool->Borrow());
SendWireMessage(*fd, msg);
@@ -141,11 +165,17 @@
}
Client::Client(std::vector<base::ScopedFile> socks)
- : socket_pool_(std::move(socks)) {
+ : pthread_key_(ThreadLocalSamplingData::KeyDestructor),
+ socket_pool_(std::move(socks)),
+ main_thread_stack_base_(FindMainThreadStack()) {
+ PERFETTO_DCHECK(pthread_key_.valid());
+
uint64_t size = 0;
+ base::ScopedFile maps(base::OpenFile("/proc/self/maps", O_RDONLY));
+ base::ScopedFile mem(base::OpenFile("/proc/self/mem", O_RDONLY));
int fds[2];
- fds[0] = open("/proc/self/maps", O_RDONLY | O_CLOEXEC);
- fds[1] = open("/proc/self/mem", O_RDONLY | O_CLOEXEC);
+ fds[0] = *maps;
+ fds[1] = *mem;
auto fd = socket_pool_.Borrow();
// Send an empty record to transfer fds for /proc/self/maps and
// /proc/self/mem.
@@ -201,6 +231,7 @@
metadata.sequence_number = ++sequence_number_;
WireMessage msg{};
+ msg.record_type = RecordType::Malloc;
msg.alloc_header = &metadata;
msg.payload = const_cast<char*>(stacktop);
msg.payload_size = static_cast<size_t>(stack_size);
@@ -214,4 +245,11 @@
free_page_.Add(alloc_address, ++sequence_number_, &socket_pool_);
}
+bool Client::ShouldSampleAlloc(uint64_t alloc_size,
+ void* (*unhooked_malloc)(size_t),
+ void (*unhooked_free)(void*)) {
+ return ShouldSample(pthread_key_.get(), alloc_size, client_config_.rate,
+ unhooked_malloc, unhooked_free);
+}
+
} // namespace perfetto
diff --git a/src/profiling/memory/client.h b/src/profiling/memory/client.h
index 93a262f..980196f 100644
--- a/src/profiling/memory/client.h
+++ b/src/profiling/memory/client.h
@@ -17,6 +17,7 @@
#ifndef SRC_PROFILING_MEMORY_CLIENT_H_
#define SRC_PROFILING_MEMORY_CLIENT_H_
+#include <pthread.h>
#include <stddef.h>
#include <mutex>
@@ -95,6 +96,30 @@
const char* GetThreadStackBase();
+// RAII wrapper around pthread_key_t. This is different from a ScopedResource
+// because it needs a separate boolean indicating validity.
+class PThreadKey {
+ public:
+ PThreadKey(const PThreadKey&) = delete;
+ PThreadKey& operator=(const PThreadKey&) = delete;
+
+ PThreadKey(void (*destructor)(void*)) noexcept
+ : valid_(pthread_key_create(&key_, destructor) == 0) {}
+ ~PThreadKey() noexcept {
+ if (valid_)
+ pthread_key_delete(key_);
+ }
+ bool valid() const { return valid_; }
+ pthread_key_t get() const {
+ PERFETTO_DCHECK(valid_);
+ return key_;
+ }
+
+ private:
+ pthread_key_t key_;
+ bool valid_;
+};
+
// This is created and owned by the malloc hooks.
class Client {
public:
@@ -102,6 +127,9 @@
Client(const std::string& sock_name, size_t conns);
void RecordMalloc(uint64_t alloc_size, uint64_t alloc_address);
void RecordFree(uint64_t alloc_address);
+ bool ShouldSampleAlloc(uint64_t alloc_size,
+ void* (*unhooked_malloc)(size_t),
+ void (*unhooked_free)(void*));
ClientConfiguration client_config_for_testing() { return client_config_; }
@@ -109,6 +137,7 @@
const char* GetStackBase();
ClientConfiguration client_config_;
+ PThreadKey pthread_key_;
SocketPool socket_pool_;
FreePage free_page_;
const char* main_thread_stack_base_ = nullptr;
diff --git a/src/profiling/memory/client_unittest.cc b/src/profiling/memory/client_unittest.cc
index df848da..8b5d79b 100644
--- a/src/profiling/memory/client_unittest.cc
+++ b/src/profiling/memory/client_unittest.cc
@@ -25,15 +25,15 @@
TEST(SocketPoolTest, Basic) {
std::vector<base::ScopedFile> files;
- files.emplace_back(open("/dev/null", O_RDONLY));
+ files.emplace_back(base::OpenFile("/dev/null", O_RDONLY));
SocketPool pool(std::move(files));
BorrowedSocket sock = pool.Borrow();
}
TEST(SocketPoolTest, Multiple) {
std::vector<base::ScopedFile> files;
- files.emplace_back(open("/dev/null", O_RDONLY));
- files.emplace_back(open("/dev/null", O_RDONLY));
+ files.emplace_back(base::OpenFile("/dev/null", O_RDONLY));
+ files.emplace_back(base::OpenFile("/dev/null", O_RDONLY));
SocketPool pool(std::move(files));
BorrowedSocket sock = pool.Borrow();
BorrowedSocket sock_2 = pool.Borrow();
@@ -41,7 +41,7 @@
TEST(SocketPoolTest, Blocked) {
std::vector<base::ScopedFile> files;
- files.emplace_back(open("/dev/null", O_RDONLY));
+ files.emplace_back(base::OpenFile("/dev/null", O_RDONLY));
SocketPool pool(std::move(files));
BorrowedSocket sock = pool.Borrow();
std::thread t([&pool] { pool.Borrow(); });
@@ -54,7 +54,7 @@
TEST(SocketPoolTest, MultipleBlocked) {
std::vector<base::ScopedFile> files;
- files.emplace_back(open("/dev/null", O_RDONLY));
+ files.emplace_back(base::OpenFile("/dev/null", O_RDONLY));
SocketPool pool(std::move(files));
BorrowedSocket sock = pool.Borrow();
std::thread t([&pool] { pool.Borrow(); });
diff --git a/src/profiling/memory/sampler_unittest.cc b/src/profiling/memory/sampler_unittest.cc
index d598e71..fc91b74 100644
--- a/src/profiling/memory/sampler_unittest.cc
+++ b/src/profiling/memory/sampler_unittest.cc
@@ -20,43 +20,39 @@
#include <thread>
+#include "src/profiling/memory/client.h" // For PThreadKey.
+
namespace perfetto {
namespace {
TEST(SamplerTest, TestLarge) {
- pthread_key_t key;
- ASSERT_EQ(pthread_key_create(&key, ThreadLocalSamplingData::KeyDestructor),
- 0);
- EXPECT_EQ(ShouldSample(key, 1024, 512, malloc, free), 1);
- pthread_key_delete(key);
+ PThreadKey key(ThreadLocalSamplingData::KeyDestructor);
+ ASSERT_TRUE(key.valid());
+ EXPECT_EQ(ShouldSample(key.get(), 1024, 512, malloc, free), 1);
}
TEST(SamplerTest, TestSmall) {
- pthread_key_t key;
- ASSERT_EQ(pthread_key_create(&key, ThreadLocalSamplingData::KeyDestructor),
- 0);
+ PThreadKey key(ThreadLocalSamplingData::KeyDestructor);
+ ASSERT_TRUE(key.valid());
// As we initialize interval_to_next_sample_ with 0, the first sample
// should always get sampled.
- EXPECT_EQ(ShouldSample(key, 1, 512, malloc, free), 1);
- pthread_key_delete(key);
+ EXPECT_EQ(ShouldSample(key.get(), 1, 512, malloc, free), 1);
}
TEST(SamplerTest, TestSmallFromThread) {
- pthread_key_t key;
- ASSERT_EQ(pthread_key_create(&key, ThreadLocalSamplingData::KeyDestructor),
- 0);
- std::thread th([key] {
+ PThreadKey key(ThreadLocalSamplingData::KeyDestructor);
+ ASSERT_TRUE(key.valid());
+ std::thread th([&key] {
// As we initialize interval_to_next_sample_ with 0, the first sample
// should always get sampled.
- EXPECT_EQ(ShouldSample(key, 1, 512, malloc, free), 1);
+ EXPECT_EQ(ShouldSample(key.get(), 1, 512, malloc, free), 1);
});
- std::thread th2([key] {
+ std::thread th2([&key] {
// The threads should have separate state.
- EXPECT_EQ(ShouldSample(key, 1, 512, malloc, free), 1);
+ EXPECT_EQ(ShouldSample(key.get(), 1, 512, malloc, free), 1);
});
th.join();
th2.join();
- pthread_key_delete(key);
}
} // namespace
diff --git a/src/profiling/memory/socket_listener_unittest.cc b/src/profiling/memory/socket_listener_unittest.cc
index 65d7455..f40cb9f 100644
--- a/src/profiling/memory/socket_listener_unittest.cc
+++ b/src/profiling/memory/socket_listener_unittest.cc
@@ -68,8 +68,9 @@
task_runner.RunUntilCheckpoint("connected");
uint64_t size = 1;
- base::ScopedFile fds[2] = {base::ScopedFile(open("/dev/null", O_RDONLY)),
- base::ScopedFile(open("/dev/null", O_RDONLY))};
+ base::ScopedFile fds[2] = {
+ base::ScopedFile(base::OpenFile("/dev/null", O_RDONLY)),
+ base::ScopedFile(base::OpenFile("/dev/null", O_RDONLY))};
int raw_fds[2] = {*fds[0], *fds[1]};
ASSERT_TRUE(client_socket->Send(&size, sizeof(size), raw_fds,
base::ArraySize(raw_fds),
diff --git a/src/profiling/memory/unwinding.cc b/src/profiling/memory/unwinding.cc
index 8ed2780..f43743b 100644
--- a/src/profiling/memory/unwinding.cc
+++ b/src/profiling/memory/unwinding.cc
@@ -145,7 +145,7 @@
flags |= unwindstack::MAPS_FLAGS_DEVICE_MAP;
}
maps_.push_back(
- new unwindstack::MapInfo(start, end, pgoff, flags, name));
+ new unwindstack::MapInfo(nullptr, start, end, pgoff, flags, name));
});
}
diff --git a/src/profiling/memory/unwinding_unittest.cc b/src/profiling/memory/unwinding_unittest.cc
index f80a0ee..3993692 100644
--- a/src/profiling/memory/unwinding_unittest.cc
+++ b/src/profiling/memory/unwinding_unittest.cc
@@ -33,7 +33,7 @@
namespace {
TEST(UnwindingTest, StackMemoryOverlay) {
- base::ScopedFile proc_mem(open("/proc/self/mem", O_RDONLY));
+ base::ScopedFile proc_mem(base::OpenFile("/proc/self/mem", O_RDONLY));
ASSERT_TRUE(proc_mem);
uint8_t fake_stack[1] = {120};
StackMemory memory(*proc_mem, 0u, fake_stack, 1);
@@ -45,7 +45,7 @@
TEST(UnwindingTest, StackMemoryNonOverlay) {
uint8_t value = 52;
- base::ScopedFile proc_mem(open("/proc/self/mem", O_RDONLY));
+ base::ScopedFile proc_mem(base::OpenFile("/proc/self/mem", O_RDONLY));
ASSERT_TRUE(proc_mem);
uint8_t fake_stack[1] = {120};
StackMemory memory(*proc_mem, 0u, fake_stack, 1);
@@ -55,7 +55,7 @@
}
TEST(UnwindingTest, FileDescriptorMapsParse) {
- base::ScopedFile proc_maps(open("/proc/self/maps", O_RDONLY));
+ base::ScopedFile proc_maps(base::OpenFile("/proc/self/maps", O_RDONLY));
ASSERT_TRUE(proc_maps);
FileDescriptorMaps maps(std::move(proc_maps));
ASSERT_TRUE(maps.Parse());
@@ -119,8 +119,8 @@
// TODO(fmayer): Investigate why this fails out of tree.
TEST(UnwindingTest, MAYBE_DoUnwind) {
- base::ScopedFile proc_maps(open("/proc/self/maps", O_RDONLY));
- base::ScopedFile proc_mem(open("/proc/self/mem", O_RDONLY));
+ base::ScopedFile proc_maps(base::OpenFile("/proc/self/maps", O_RDONLY));
+ base::ScopedFile proc_mem(base::OpenFile("/proc/self/mem", O_RDONLY));
GlobalCallstackTrie callsites;
ProcessMetadata metadata(getpid(), std::move(proc_maps), std::move(proc_mem),
&callsites);
diff --git a/src/trace_processor/trace_processor.cc b/src/trace_processor/trace_processor.cc
index 61c1b80..5e07842 100644
--- a/src/trace_processor/trace_processor.cc
+++ b/src/trace_processor/trace_processor.cc
@@ -19,6 +19,7 @@
#include <sqlite3.h>
#include <functional>
+#include "perfetto/base/time.h"
#include "src/trace_processor/counters_table.h"
#include "src/trace_processor/json_trace_parser.h"
#include "src/trace_processor/process_table.h"
@@ -102,6 +103,8 @@
protos::RawQueryResult proto;
query_interrupted_.store(false, std::memory_order_relaxed);
+ base::TimeNanos t_start = base::GetWallTimeNs();
+
const auto& sql = args.sql_query();
sqlite3_stmt* raw_stmt;
int err = sqlite3_prepare_v2(*db_, sql.c_str(), static_cast<int>(sql.size()),
@@ -175,6 +178,8 @@
query_interrupted_ = false;
}
+ base::TimeNanos t_end = base::GetWallTimeNs();
+ proto.set_execution_time_ns(static_cast<uint64_t>((t_end - t_start).count()));
callback(proto);
}
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index 487ac8f..05540e1 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -202,8 +202,7 @@
TraceProcessor::Config config;
config.optimization_mode = OptimizationMode::kMaxBandwidth;
TraceProcessor tp(config);
- base::ScopedFile fd;
- fd.reset(open(trace_file_path, O_RDONLY));
+ base::ScopedFile fd(base::OpenFile(trace_file_path, O_RDONLY));
PERFETTO_CHECK(fd);
// Load the trace in chunks using async IO. We create a simple pipeline where,
diff --git a/src/traced/probes/filesystem/fs_mount_unittest.cc b/src/traced/probes/filesystem/fs_mount_unittest.cc
index da544e8..6c6b489 100644
--- a/src/traced/probes/filesystem/fs_mount_unittest.cc
+++ b/src/traced/probes/filesystem/fs_mount_unittest.cc
@@ -24,6 +24,7 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "perfetto/base/build_config.h"
+#include "perfetto/base/file_utils.h"
#include "perfetto/base/scoped_file.h"
#include "perfetto/base/temp_file.h"
#include "perfetto/base/utils.h"
@@ -52,7 +53,8 @@
)";
base::TempFile tmp_file = base::TempFile::Create();
- base::ignore_result(write(tmp_file.fd(), kMounts, sizeof(kMounts)));
+ ASSERT_EQ(base::WriteAll(tmp_file.fd(), kMounts, sizeof(kMounts)),
+ sizeof(kMounts));
std::multimap<BlockDeviceID, std::string> mounts =
ParseMounts(tmp_file.path().c_str());
struct stat dev_stat = {}, root_stat = {};
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index 5cff028..abc5d09 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -29,6 +29,7 @@
#include <utility>
#include "perfetto/base/build_config.h"
+#include "perfetto/base/file_utils.h"
#include "perfetto/base/logging.h"
#include "perfetto/base/time.h"
#include "perfetto/tracing/core/trace_writer.h"
@@ -73,18 +74,14 @@
}
void WriteToFile(const char* path, const char* str) {
- int fd = open(path, O_WRONLY);
- if (fd == -1)
+ auto fd = base::OpenFile(path, O_WRONLY);
+ if (!fd)
return;
- perfetto::base::ignore_result(write(fd, str, strlen(str)));
- perfetto::base::ignore_result(close(fd));
+ base::ignore_result(base::WriteAll(*fd, str, strlen(str)));
}
void ClearFile(const char* path) {
- int fd = open(path, O_WRONLY | O_TRUNC);
- if (fd == -1)
- return;
- perfetto::base::ignore_result(close(fd));
+ auto fd = base::OpenFile(path, O_WRONLY | O_TRUNC);
}
} // namespace
diff --git a/src/traced/probes/ftrace/ftrace_controller_unittest.cc b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
index 9e74a0a..aa059f8 100644
--- a/src/traced/probes/ftrace/ftrace_controller_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
@@ -163,7 +163,7 @@
}
base::ScopedFile OpenPipeForCpu(size_t /*cpu*/) override {
- return base::ScopedFile(open("/dev/null", O_RDONLY));
+ return base::ScopedFile(base::OpenFile("/dev/null", O_RDONLY));
}
MOCK_METHOD2(WriteToFile,
diff --git a/src/traced/probes/ftrace/ftrace_procfs.cc b/src/traced/probes/ftrace/ftrace_procfs.cc
index 87095cf..cb84dd7 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs.cc
@@ -46,7 +46,7 @@
void KernelLogWrite(const char* s) {
PERFETTO_DCHECK(*s && s[strlen(s) - 1] == '\n');
if (FtraceProcfs::g_kmesg_fd != -1)
- base::ignore_result(write(FtraceProcfs::g_kmesg_fd, s, strlen(s)));
+ base::ignore_result(base::WriteAll(FtraceProcfs::g_kmesg_fd, s, strlen(s)));
}
} // namespace
@@ -224,7 +224,7 @@
base::ScopedFile fd = base::OpenFile(path, O_WRONLY);
if (!fd)
return false;
- ssize_t written = PERFETTO_EINTR(write(fd.get(), str.c_str(), str.length()));
+ ssize_t written = base::WriteAll(fd.get(), str.c_str(), str.length());
ssize_t length = static_cast<ssize_t>(str.length());
// This should either fail or write fully.
PERFETTO_CHECK(written == length || written == -1);
diff --git a/src/traced/probes/sys_stats/sys_stats_data_source.cc b/src/traced/probes/sys_stats/sys_stats_data_source.cc
index cd6efd1..00ab945 100644
--- a/src/traced/probes/sys_stats/sys_stats_data_source.cc
+++ b/src/traced/probes/sys_stats/sys_stats_data_source.cc
@@ -45,8 +45,7 @@
constexpr size_t kReadBufSize = 1024 * 16;
base::ScopedFile OpenReadOnly(const char* path) {
- base::ScopedFile fd;
- fd.reset(open(path, O_RDONLY | O_CLOEXEC));
+ base::ScopedFile fd(base::OpenFile(path, O_RDONLY));
if (!fd)
PERFETTO_PLOG("Failed opening %s", path);
return fd;
diff --git a/src/tracing/core/tracing_service_impl.cc b/src/tracing/core/tracing_service_impl.cc
index 9cab744..80b924c 100644
--- a/src/tracing/core/tracing_service_impl.cc
+++ b/src/tracing/core/tracing_service_impl.cc
@@ -31,6 +31,7 @@
#include <algorithm>
#include "perfetto/base/build_config.h"
+#include "perfetto/base/file_utils.h"
#include "perfetto/base/task_runner.h"
#include "perfetto/base/utils.h"
#include "perfetto/tracing/core/consumer.h"
@@ -80,7 +81,7 @@
ssize_t writev(int fd, const struct iovec* iov, int iovcnt) {
ssize_t total_size = 0;
for (int i = 0; i < iovcnt; ++i) {
- ssize_t current_size = write(fd, iov[i].iov_base, iov[i].iov_len);
+ ssize_t current_size = base::WriteAll(fd, iov[i].iov_base, iov[i].iov_len);
if (current_size != static_cast<ssize_t>(iov[i].iov_len))
return -1;
total_size += current_size;
diff --git a/src/tracing/ipc/posix_shared_memory_unittest.cc b/src/tracing/ipc/posix_shared_memory_unittest.cc
index b07cb00..e6739bf 100644
--- a/src/tracing/ipc/posix_shared_memory_unittest.cc
+++ b/src/tracing/ipc/posix_shared_memory_unittest.cc
@@ -25,6 +25,7 @@
#include "gtest/gtest.h"
#include "perfetto/base/build_config.h"
+#include "perfetto/base/file_utils.h"
#include "perfetto/base/scoped_file.h"
#include "perfetto/base/temp_file.h"
#include "perfetto/base/utils.h"
@@ -69,7 +70,7 @@
base::TempFile tmp_file = base::TempFile::CreateUnlinked();
const int fd_num = tmp_file.fd();
ASSERT_EQ(0, ftruncate(fd_num, base::kPageSize));
- ASSERT_EQ(7, PERFETTO_EINTR(write(fd_num, "foobar", 7)));
+ ASSERT_EQ(7, base::WriteAll(fd_num, "foobar", 7));
std::unique_ptr<PosixSharedMemory> shm =
PosixSharedMemory::AttachToFd(tmp_file.ReleaseFD());
diff --git a/tools/install-build-deps b/tools/install-build-deps
index 7770631..1efaf33 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -163,7 +163,7 @@
# These dependencies are for libunwindstack, which is used by src/profiling.
('buildtools/android-core',
'https://android.googlesource.com/platform/system/core.git',
- 'ec004eb1b376d2d9008829a25f47ac3fcfd728ab',
+ 'd3a7ddcf8dd97e162d7d4a9b3f87b4f1ef797d5f',
'all'
),
@@ -175,7 +175,7 @@
('buildtools/bionic',
'https://android.googlesource.com/platform/bionic.git',
- '3fd45bba4857fdbf320b6e89d2ae0569d9463bf5',
+ '4b7c5cca7fbd0330cdfef41c97f1401824e78fba',
'all'
),
diff --git a/tools/pipestats.cc b/tools/pipestats.cc
index d82ecbb..15fbe27 100644
--- a/tools/pipestats.cc
+++ b/tools/pipestats.cc
@@ -47,7 +47,7 @@
int PipestatsMain(int argc, char** argv) {
PERFETTO_CHECK(argc == 2);
- base::ScopedFile trace_fd(open(argv[1], O_RDONLY));
+ base::ScopedFile trace_fd(base::OpenFile(argv[1], O_RDONLY));
PERFETTO_CHECK(trace_fd);
std::thread reader(ReadLoop, trace_fd.get());
diff --git a/ui/package-lock.json b/ui/package-lock.json
index d5a0e39..67ee3c9 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -3256,6 +3256,11 @@
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==",
"dev": true
},
+ "immer": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-1.7.2.tgz",
+ "integrity": "sha512-4Urocwu9+XLDJw4Tc6ZCg7APVjjLInCFvO4TwGsAYV5zT6YYSor14dsZR0+0tHlDIN92cFUOq+i7fC00G5vTxA=="
+ },
"immutable": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
@@ -5974,20 +5979,12 @@
"requires": {
"@types/estree": "0.0.39",
"@types/node": "*"
- },
- "dependencies": {
- "@types/node": {
- "version": "8.10.15",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.15.tgz",
- "integrity": "sha512-qNb+m5Cuj6YUMK7YFcvuSgcHCKfVg1uXAUOP91SWvAakZlZTzbGmJaBi99CgDWEAyfZo51NlUhXkuP5WtXsgjg==",
- "dev": true
- }
}
},
"rollup-plugin-commonjs": {
- "version": "9.1.3",
- "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.3.tgz",
- "integrity": "sha512-g91ZZKZwTW7F7vL6jMee38I8coj/Q9GBdTmXXeFL7ldgC1Ky5WJvHgbKlAiXXTh762qvohhExwUgeQGFh9suGg==",
+ "version": "9.1.8",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.8.tgz",
+ "integrity": "sha512-c3nAfVVyEwbq9OohIeQudfQQdGV9Cl1RE8MUc90fH9UdtCiWAYpI+au3HxGwNf1DdV51HfBjCDbT4fwjsZEUUg==",
"dev": true,
"requires": {
"estree-walker": "^0.5.1",
@@ -5997,9 +5994,9 @@
}
},
"rollup-plugin-node-resolve": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.3.0.tgz",
- "integrity": "sha512-9zHGr3oUJq6G+X0oRMYlzid9fXicBdiydhwGChdyeNRGPcN/majtegApRKHLR5drboUvEWU+QeUmGTyEZQs3WA==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.4.0.tgz",
+ "integrity": "sha512-PJcd85dxfSBWih84ozRtBkB731OjXk0KnzN0oGp7WOWcarAFkVa71cV5hTJg2qpVsV2U8EUwrzHP3tvy9vS3qg==",
"dev": true,
"requires": {
"builtin-modules": "^2.0.0",
@@ -6015,6 +6012,17 @@
}
}
},
+ "rollup-plugin-replace": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.0.0.tgz",
+ "integrity": "sha512-pK9mTd/FNrhtBxcTBXoh0YOwRIShV0gGhv9qvUtNcXHxIMRZMXqfiZKVBmCRGp8/2DJRy62z2JUE7/5tP6WxOQ==",
+ "dev": true,
+ "requires": {
+ "magic-string": "^0.22.4",
+ "minimatch": "^3.0.2",
+ "rollup-pluginutils": "^2.0.1"
+ }
+ },
"rollup-pluginutils": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.3.0.tgz",
diff --git a/ui/package.json b/ui/package.json
index b28879f..148426d 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -13,6 +13,7 @@
"dependencies": {
"@types/mithril": "^1.1.12",
"@types/uuid": "^3.4.3",
+ "immer": "^1.7.2",
"mithril": "^1.1.6",
"protobufjs": "^6.8.6",
"uuid": "^3.3.2"
@@ -26,8 +27,9 @@
"node-sass": "^4.9.2",
"puppeteer": "^1.5.0",
"rollup": "^0.59.4",
- "rollup-plugin-commonjs": "^9.1.3",
- "rollup-plugin-node-resolve": "^3.3.0",
+ "rollup-plugin-commonjs": "^9.1.8",
+ "rollup-plugin-node-resolve": "^3.4.0",
+ "rollup-plugin-replace": "^2.0.0",
"sorcery": "^0.10.0",
"tslib": "^1.9.3",
"tslint": "^5.10.0",
diff --git a/ui/rollup.config.js b/ui/rollup.config.js
index 168f320..8c884f1 100644
--- a/ui/rollup.config.js
+++ b/ui/rollup.config.js
@@ -1,5 +1,6 @@
import commonjs from 'rollup-plugin-commonjs';
import nodeResolve from 'rollup-plugin-node-resolve';
+import replace from 'rollup-plugin-replace';
export default {
output: {name: 'perfetto'},
@@ -15,6 +16,11 @@
'fs',
'path',
]
+ }),
+
+ replace({
+ 'immer_1.produce': 'immer_1',
})
+
]
}
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index cac3d75..8ef1161 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -312,6 +312,19 @@
height: 25px;
}
+header.overview {
+ display: flex;
+ justify-content: space-between;
+}
+
+.query-error {
+ user-select: text;
+}
+
+span.code {
+ user-select: text;
+}
+
.text-column {
font-size: 115%;
// 2-3 alphabets per line is comfortable for reading.
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index bd7cada..5ae4598 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -12,125 +12,215 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {State} from './state';
-import {TimeSpan} from './time';
+import {DraftObject} from 'immer';
-export interface Action { type: string; }
+import {defaultTraceTime, State, Status, TraceTime} from './state';
-export function openTraceFromUrl(url: string) {
- return {
- type: 'OPEN_TRACE_FROM_URL',
- url,
- };
+type StateDraft = DraftObject<State>;
+
+
+function clearTraceState(state: StateDraft) {
+ state.traceTime = defaultTraceTime;
+ state.visibleTraceTime = defaultTraceTime;
+ state.pinnedTracks = [];
+ state.scrollingTracks = [];
}
-export function openTraceFromFile(file: File) {
- return {
- type: 'OPEN_TRACE_FROM_FILE',
- file,
- };
+export const StateActions = {
+
+ navigate(state: StateDraft, args: {route: string}): void {
+ state.route = args.route;
+ },
+
+ openTraceFromFile(state: StateDraft, args: {file: File}): void {
+ clearTraceState(state);
+ const id = `${state.nextId++}`;
+ state.engines[id] = {
+ id,
+ ready: false,
+ source: args.file,
+ };
+ state.route = `/viewer`;
+ },
+
+ openTraceFromUrl(state: StateDraft, args: {url: string}): void {
+ clearTraceState(state);
+ const id = `${state.nextId++}`;
+ state.engines[id] = {
+ id,
+ ready: false,
+ source: args.url,
+ };
+ state.route = `/viewer`;
+ },
+
+ addTrack(
+ state: StateDraft,
+ args: {engineId: string; kind: string; name: string; config: {};}): void {
+ const id = `${state.nextId++}`;
+ state.tracks[id] = {
+ id,
+ engineId: args.engineId,
+ kind: args.kind,
+ name: args.name,
+ config: args.config,
+ };
+ state.scrollingTracks.push(id);
+ },
+
+ reqTrackData(state: StateDraft, args: {
+ trackId: string; start: number; end: number; resolution: number;
+ }): void {
+ const id = args.trackId;
+ state.tracks[id].dataReq = {
+ start: args.start,
+ end: args.end,
+ resolution: args.resolution
+ };
+ },
+
+ clearTrackDataReq(state: StateDraft, args: {trackId: string}): void {
+ const id = args.trackId;
+ state.tracks[id].dataReq = undefined;
+ },
+
+ executeQuery(
+ state: StateDraft,
+ args: {queryId: string; engineId: string; query: string}): void {
+ state.queries[args.queryId] = {
+ id: args.queryId,
+ engineId: args.engineId,
+ query: args.query,
+ };
+ },
+
+ deleteQuery(state: StateDraft, args: {queryId: string}): void {
+ delete state.queries[args.queryId];
+ },
+
+ moveTrack(
+ state: StateDraft, args: {trackId: string; direction: 'up' | 'down';}):
+ void {
+ const id = args.trackId;
+ const isPinned = state.pinnedTracks.includes(id);
+ const isScrolling = state.scrollingTracks.includes(id);
+ if (!isScrolling && !isPinned) {
+ throw new Error(`No track with id ${id}`);
+ }
+ const tracks = isPinned ? state.pinnedTracks : state.scrollingTracks;
+
+ const oldIndex: number = tracks.indexOf(id);
+ const newIndex = args.direction === 'up' ? oldIndex - 1 : oldIndex + 1;
+ const swappedTrackId = tracks[newIndex];
+ if (isPinned && newIndex === state.pinnedTracks.length) {
+ // Move from last element of pinned to first element of scrolling.
+ state.scrollingTracks.unshift(state.pinnedTracks.pop()!);
+ } else if (isScrolling && newIndex === -1) {
+ // Move first element of scrolling to last element of pinned.
+ state.pinnedTracks.push(state.scrollingTracks.shift()!);
+ } else if (swappedTrackId) {
+ tracks[newIndex] = id;
+ tracks[oldIndex] = swappedTrackId;
+ }
+ },
+
+ toggleTrackPinned(state: StateDraft, args: {trackId: string}): void {
+ const id = args.trackId;
+ const isPinned = state.pinnedTracks.includes(id);
+
+ if (isPinned) {
+ state.pinnedTracks.splice(state.pinnedTracks.indexOf(id), 1);
+ state.scrollingTracks.unshift(id);
+ } else {
+ state.scrollingTracks.splice(state.scrollingTracks.indexOf(id), 1);
+ state.pinnedTracks.push(id);
+ }
+ },
+
+ setEngineReady(state: StateDraft, args: {engineId: string; ready: boolean}):
+ void {
+ state.engines[args.engineId].ready = args.ready;
+ },
+
+ createPermalink(state: StateDraft, args: {requestId: string}): void {
+ state.permalink = {requestId: args.requestId, hash: undefined};
+ },
+
+ setPermalink(state: StateDraft, args: {requestId: string; hash: string}):
+ void {
+ // Drop any links for old requests.
+ if (state.permalink.requestId !== args.requestId) return;
+ state.permalink = args;
+ },
+
+ loadPermalink(state: StateDraft, args: {requestId: string; hash: string}):
+ void {
+ state.permalink = args;
+ },
+
+ setTraceTime(state: StateDraft, args: TraceTime): void {
+ state.traceTime = args;
+ },
+
+ setVisibleTraceTime(state: StateDraft, args: TraceTime): void {
+ state.visibleTraceTime = args;
+ },
+
+ updateStatus(state: StateDraft, args: Status): void {
+ state.status = args;
+ },
+
+ // TODO(hjd): Remove setState - it causes problems due to reuse of ids.
+ setState(_state: StateDraft, _args: {newState: State}): void {
+ // This has to be handled at a higher level since we can't
+ // replace the whole tree here however we still need a method here
+ // so it appears on the proxy Actions class.
+ throw new Error('Called setState on StateActions.');
+ },
+};
+
+
+// When we are on the frontend side, we don't really want to execute the
+// actions above, we just want to serialize them and marshal their
+// arguments, send them over to the controller side and have them being
+// executed there. The magic below takes care of turning each action into a
+// function that returns the marshaled args.
+
+// A DeferredAction is a bundle of Args and a method name. This is the marshaled
+// version of a StateActions method call.
+export interface DeferredAction<Args = {}> {
+ type: string;
+ args: Args;
}
-// TODO(hjd): Remove CPU and add a generic way to handle track specific state.
-export function addTrack(
- engineId: string, trackKind: string, name: string, config: {}) {
- return {
- type: 'ADD_TRACK',
- engineId,
- trackKind,
- name,
- config,
- };
-}
+// This type magic creates a type function DeferredActions<T> which takes a type
+// T and 'maps' its attributes. For each attribute on T matching the signature:
+// (state: StateDraft, args: Args) => void
+// DeferredActions<T> has an attribute:
+// (args: Args) => DeferredAction<Args>
+type ActionFunction<Args> = (state: StateDraft, args: Args) => void;
+type DeferredActionFunc<T> = T extends ActionFunction<infer Args>?
+ (args: Args) => DeferredAction<Args>:
+ never;
+type DeferredActions<C> = {
+ [P in keyof C]: DeferredActionFunc<C[P]>;
+};
-export function requestTrackData(
- trackId: string, start: number, end: number, resolution: number) {
- return {type: 'REQ_TRACK_DATA', trackId, start, end, resolution};
-}
-
-export function clearTrackDataRequest(trackId: string) {
- return {type: 'CLEAR_TRACK_DATA_REQ', trackId};
-}
-
-export function executeQuery(engineId: string, queryId: string, query: string) {
- return {
- type: 'EXECUTE_QUERY',
- engineId,
- queryId,
- query,
- };
-}
-
-export function deleteQuery(queryId: string) {
- return {
- type: 'DELETE_QUERY',
- queryId,
- };
-}
-
-export function navigate(route: string) {
- return {
- type: 'NAVIGATE',
- route,
- };
-}
-
-export function moveTrack(trackId: string, direction: 'up'|'down') {
- return {
- type: 'MOVE_TRACK',
- trackId,
- direction,
- };
-}
-
-export function toggleTrackPinned(trackId: string) {
- return {
- type: 'TOGGLE_TRACK_PINNED',
- trackId,
- };
-}
-
-export function setEngineReady(engineId: string, ready = true) {
- return {type: 'SET_ENGINE_READY', engineId, ready};
-}
-
-export function createPermalink() {
- return {type: 'CREATE_PERMALINK', requestId: new Date().toISOString()};
-}
-
-export function setPermalink(requestId: string, hash: string) {
- return {type: 'SET_PERMALINK', requestId, hash};
-}
-
-export function loadPermalink(hash: string) {
- return {type: 'LOAD_PERMALINK', requestId: new Date().toISOString(), hash};
-}
-
-export function setState(newState: State) {
- return {
- type: 'SET_STATE',
- newState,
- };
-}
-
-export function setTraceTime(ts: TimeSpan) {
- return {
- type: 'SET_TRACE_TIME',
- startSec: ts.start,
- endSec: ts.end,
- lastUpdate: Date.now() / 1000,
- };
-}
-
-export function setVisibleTraceTime(ts: TimeSpan) {
- return {
- type: 'SET_VISIBLE_TRACE_TIME',
- startSec: ts.start,
- endSec: ts.end,
- lastUpdate: Date.now() / 1000,
- };
-}
-
-export function updateStatus(msg: string) {
- return {type: 'UPDATE_STATUS', msg, timestamp: Date.now() / 1000};
-}
+// Actions is an implementation of DeferredActions<typeof StateActions>.
+// (since StateActions is a variable not a type we have to do
+// 'typeof StateActions' to access the (unnamed) type of StateActions).
+// It's a Proxy such that any attribute access returns a function:
+// (args) => {return {type: ATTRIBUTE_NAME, args};}
+export const Actions =
+ // tslint:disable-next-line no-any
+ new Proxy<DeferredActions<typeof StateActions>>({} as any, {
+ // tslint:disable-next-line no-any
+ get(_: any, prop: string, _2: any) {
+ return (args: {}): DeferredAction<{}> => {
+ return {
+ type: prop,
+ args,
+ };
+ };
+ },
+ });
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
new file mode 100644
index 0000000..7a3dc94
--- /dev/null
+++ b/ui/src/common/actions_unittest.ts
@@ -0,0 +1,238 @@
+// Copyright (C) 2018 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 {produce} from 'immer';
+import {StateActions} from './actions';
+import {createEmptyState, State, TrackState} from './state';
+
+function fakeTrack(state: State, id: string): TrackState {
+ const track: TrackState = {
+ id,
+ engineId: '1',
+ kind: 'SOME_TRACK_KIND',
+ name: 'A track',
+ config: {},
+ };
+ state.tracks[id] = track;
+ return track;
+}
+
+test('navigate', () => {
+ const after = produce(createEmptyState(), draft => {
+ StateActions.navigate(draft, {route: '/foo'});
+ });
+ expect(after.route).toBe('/foo');
+});
+
+test('add tracks', () => {
+ const once = produce(createEmptyState(), draft => {
+ StateActions.addTrack(draft, {
+ engineId: '1',
+ kind: 'cpu',
+ name: 'Cpu 1',
+ config: {},
+ });
+ });
+ const twice = produce(once, draft => {
+ StateActions.addTrack(draft, {
+ engineId: '2',
+ kind: 'cpu',
+ name: 'Cpu 2',
+ config: {},
+ });
+ });
+
+ expect(Object.values(twice.tracks).length).toBe(2);
+ expect(twice.scrollingTracks.length).toBe(2);
+});
+
+test('reorder tracks', () => {
+ const once = produce(createEmptyState(), draft => {
+ StateActions.addTrack(draft, {
+ engineId: '1',
+ kind: 'cpu',
+ name: 'Cpu 1',
+ config: {},
+ });
+ StateActions.addTrack(draft, {
+ engineId: '2',
+ kind: 'cpu',
+ name: 'Cpu 2',
+ config: {},
+ });
+ });
+
+ const firstTrackId = once.scrollingTracks[0];
+ const secondTrackId = once.scrollingTracks[1];
+
+ const twice = produce(once, draft => {
+ StateActions.moveTrack(draft, {
+ trackId: `${firstTrackId}`,
+ direction: 'down',
+ });
+ });
+
+ expect(twice.scrollingTracks[0]).toBe(secondTrackId);
+ expect(twice.scrollingTracks[1]).toBe(firstTrackId);
+});
+
+test('reorder pinned to scrolling', () => {
+ const state = createEmptyState();
+ fakeTrack(state, 'a');
+ fakeTrack(state, 'b');
+ fakeTrack(state, 'c');
+ state.pinnedTracks = ['a', 'b'];
+ state.scrollingTracks = ['c'];
+
+ const after = produce(state, draft => {
+ StateActions.moveTrack(draft, {
+ trackId: 'b',
+ direction: 'down',
+ });
+ });
+
+ expect(after.pinnedTracks).toEqual(['a']);
+ expect(after.scrollingTracks).toEqual(['b', 'c']);
+});
+
+test('reorder scrolling to pinned', () => {
+ const state = createEmptyState();
+ fakeTrack(state, 'a');
+ fakeTrack(state, 'b');
+ fakeTrack(state, 'c');
+ state.pinnedTracks = ['a'];
+ state.scrollingTracks = ['b', 'c'];
+
+ const after = produce(state, draft => {
+ StateActions.moveTrack(draft, {
+ trackId: 'b',
+ direction: 'up',
+ });
+ });
+
+ expect(after.pinnedTracks).toEqual(['a', 'b']);
+ expect(after.scrollingTracks).toEqual(['c']);
+});
+
+test('reorder clamp bottom', () => {
+ const state = createEmptyState();
+ fakeTrack(state, 'a');
+ fakeTrack(state, 'b');
+ fakeTrack(state, 'c');
+ state.pinnedTracks = ['a', 'b'];
+ state.scrollingTracks = ['c'];
+
+ const after = produce(state, draft => {
+ StateActions.moveTrack(draft, {
+ trackId: 'a',
+ direction: 'up',
+ });
+ });
+ expect(after).toEqual(state);
+});
+
+test('reorder clamp top', () => {
+ const state = createEmptyState();
+ fakeTrack(state, 'a');
+ fakeTrack(state, 'b');
+ fakeTrack(state, 'c');
+ state.pinnedTracks = ['a'];
+ state.scrollingTracks = ['b', 'c'];
+
+ const after = produce(state, draft => {
+ StateActions.moveTrack(draft, {
+ trackId: 'c',
+ direction: 'down',
+ });
+ });
+ expect(after).toEqual(state);
+});
+
+test('pin', () => {
+ const state = createEmptyState();
+ fakeTrack(state, 'a');
+ fakeTrack(state, 'b');
+ fakeTrack(state, 'c');
+ state.pinnedTracks = ['a'];
+ state.scrollingTracks = ['b', 'c'];
+
+ const after = produce(state, draft => {
+ StateActions.toggleTrackPinned(draft, {
+ trackId: 'c',
+ });
+ });
+ expect(after.pinnedTracks).toEqual(['a', 'c']);
+ expect(after.scrollingTracks).toEqual(['b']);
+});
+
+test('unpin', () => {
+ const state = createEmptyState();
+ fakeTrack(state, 'a');
+ fakeTrack(state, 'b');
+ fakeTrack(state, 'c');
+ state.pinnedTracks = ['a', 'b'];
+ state.scrollingTracks = ['c'];
+
+ const after = produce(state, draft => {
+ StateActions.toggleTrackPinned(draft, {
+ trackId: 'a',
+ });
+ });
+ expect(after.pinnedTracks).toEqual(['b']);
+ expect(after.scrollingTracks).toEqual(['a', 'c']);
+});
+
+test('open trace', () => {
+ const after = produce(createEmptyState(), draft => {
+ StateActions.openTraceFromUrl(draft, {
+ url: 'https://example.com/bar',
+ });
+ });
+
+ const engineKeys = Object.keys(after.engines);
+ expect(engineKeys.length).toBe(1);
+ expect(after.engines[engineKeys[0]].source).toBe('https://example.com/bar');
+ expect(after.route).toBe('/viewer');
+});
+
+test('open second trace from file', () => {
+ const once = produce(createEmptyState(), draft => {
+ StateActions.openTraceFromUrl(draft, {
+ url: 'https://example.com/bar',
+ });
+ });
+
+ const twice = produce(once, draft => {
+ StateActions.addTrack(draft, {
+ engineId: '1',
+ kind: 'cpu',
+ name: 'Cpu 1',
+ config: {},
+ });
+ });
+
+ const thrice = produce(twice, draft => {
+ StateActions.openTraceFromUrl(draft, {
+ url: 'https://example.com/foo',
+ });
+ });
+
+ const engineKeys = Object.keys(thrice.engines);
+ expect(engineKeys.length).toBe(2);
+ expect(thrice.engines[engineKeys[0]].source).toBe('https://example.com/bar');
+ expect(thrice.engines[engineKeys[1]].source).toBe('https://example.com/foo');
+ expect(thrice.pinnedTracks.length).toBe(0);
+ expect(thrice.scrollingTracks.length).toBe(0);
+ expect(thrice.route).toBe('/viewer');
+});
diff --git a/ui/src/controller/globals.ts b/ui/src/controller/globals.ts
index 8a336fa..c46ffbf 100644
--- a/ui/src/controller/globals.ts
+++ b/ui/src/controller/globals.ts
@@ -12,13 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {produce} from 'immer';
+
import {assertExists} from '../base/logging';
import {Remote} from '../base/remote';
-import {Action} from '../common/actions';
+import {DeferredAction, StateActions} from '../common/actions';
import {createEmptyState, State} from '../common/state';
+
import {ControllerAny} from './controller';
import {Engine} from './engine';
-import {rootReducer} from './reducer';
import {WasmEngineProxy} from './wasm_engine_proxy';
import {
createWasmEngine,
@@ -33,19 +35,19 @@
private _rootController?: ControllerAny;
private _frontend?: Remote;
private _runningControllers = false;
- private _queuedActions = new Array<Action>();
+ private _queuedActions = new Array<DeferredAction>();
initialize(rootController: ControllerAny, frontendProxy: Remote) {
- this._state = createEmptyState();
this._rootController = rootController;
this._frontend = frontendProxy;
+ this._state = createEmptyState();
}
- dispatch(action: Action): void {
+ dispatch(action: DeferredAction): void {
this.dispatchMultiple([action]);
}
- dispatchMultiple(actions: Action[]): void {
+ dispatchMultiple(actions: DeferredAction[]): void {
this._queuedActions = this._queuedActions.concat(actions);
// If we are in the middle of running the controllers, queue the actions
@@ -67,10 +69,10 @@
for (let iter = 0; runAgain || this._queuedActions.length > 0; iter++) {
if (iter > 100) throw new Error('Controllers are stuck in a livelock');
const actions = this._queuedActions;
- this._queuedActions = new Array<Action>();
+ this._queuedActions = new Array<DeferredAction>();
for (const action of actions) {
console.debug('Applying action', action);
- this._state = rootReducer(this.state, action);
+ this.applyAction(action);
}
this._runningControllers = true;
try {
@@ -102,8 +104,21 @@
return assertExists(this._state);
}
- set state(state: State) {
- this._state = state;
+ applyAction(action: DeferredAction): void {
+ assertExists(this._state);
+ // We need a special case for when we want to replace the whole tree.
+ if (action.type === 'setState') {
+ const args = (action as DeferredAction<{newState: State}>).args;
+ this._state = args.newState;
+ return;
+ }
+ // 'produce' creates a immer proxy which wraps the current state turning
+ // all imperative mutations of the state done in the callback into
+ // immutable changes to the returned state.
+ this._state = produce(this.state, draft => {
+ // tslint:disable-next-line no-any
+ (StateActions as any)[action.type](draft, action.args);
+ });
}
resetForTesting() {
diff --git a/ui/src/controller/permalink_controller.ts b/ui/src/controller/permalink_controller.ts
index 0146c56..0388934 100644
--- a/ui/src/controller/permalink_controller.ts
+++ b/ui/src/controller/permalink_controller.ts
@@ -15,7 +15,7 @@
import * as uuidv4 from 'uuid/v4';
import {assertExists} from '../base/logging';
-import {setPermalink, setState} from '../common/actions';
+import {Actions} from '../common/actions';
import {EngineConfig, State} from '../common/state';
import {Controller} from './controller';
@@ -40,14 +40,14 @@
// if the |link| is not set, this is a request to create a permalink.
if (globals.state.permalink.hash === undefined) {
PermalinkController.createPermalink().then(hash => {
- globals.dispatch(setPermalink(requestId, hash));
+ globals.dispatch(Actions.setPermalink({requestId, hash}));
});
return;
}
// Otherwise, this is a request to load the permalink.
PermalinkController.loadState(globals.state.permalink.hash).then(state => {
- globals.dispatch(setState(state));
+ globals.dispatch(Actions.setState({newState: state}));
this.lastRequestId = state.permalink.requestId;
});
}
diff --git a/ui/src/controller/query_controller.ts b/ui/src/controller/query_controller.ts
index a700f71..14bbf6e 100644
--- a/ui/src/controller/query_controller.ts
+++ b/ui/src/controller/query_controller.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import {assertExists} from '../base/logging';
-import {deleteQuery} from '../common/actions';
+import {Actions} from '../common/actions';
import {rawQueryResultColumns, rawQueryResultIter, Row} from '../common/protos';
import {QueryResponse} from '../common/queries';
import {Controller} from './controller';
@@ -37,7 +37,7 @@
this.runQuery(config.query).then(result => {
console.log(`Query ${config.query} took ${result.durationMs} ms`);
globals.publish('QueryResult', {id: this.args.queryId, data: result});
- globals.dispatch(deleteQuery(this.args.queryId));
+ globals.dispatch(Actions.deleteQuery({queryId: this.args.queryId}));
});
this.setState('querying');
break;
diff --git a/ui/src/controller/reducer.ts b/ui/src/controller/reducer.ts
deleted file mode 100644
index 8e6cc9b..0000000
--- a/ui/src/controller/reducer.ts
+++ /dev/null
@@ -1,229 +0,0 @@
-// Copyright (C) 2018 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 {defaultTraceTime, State} from '../common/state';
-
-// TODO(hjd): Type check this better.
-// tslint:disable-next-line no-any
-export function rootReducer(state: State, action: any): State {
- switch (action.type) {
- case 'NAVIGATE': {
- const nextState = {...state};
- nextState.route = action.route;
- return nextState;
- }
-
- case 'OPEN_TRACE_FROM_FILE': {
- const nextState = {...state};
- nextState.traceTime = {...defaultTraceTime};
- nextState.visibleTraceTime = {...defaultTraceTime};
- const id = `${nextState.nextId++}`;
- // Reset displayed tracks.
- nextState.pinnedTracks = [];
- nextState.scrollingTracks = [];
- nextState.engines[id] = {
- id,
- ready: false,
- source: action.file,
- };
- nextState.route = `/viewer`;
-
- return nextState;
- }
-
- case 'OPEN_TRACE_FROM_URL': {
- const nextState = {...state};
- nextState.traceTime = {...defaultTraceTime};
- nextState.visibleTraceTime = {...defaultTraceTime};
- const id = `${nextState.nextId++}`;
- // Reset displayed tracks.
- nextState.pinnedTracks = [];
- nextState.scrollingTracks = [];
- nextState.engines[id] = {
- id,
- ready: false,
- source: action.url,
- };
- nextState.route = `/viewer`;
- return nextState;
- }
-
- case 'ADD_TRACK': {
- const nextState = {...state};
- nextState.tracks = {...state.tracks};
- nextState.scrollingTracks = [...state.scrollingTracks];
- const id = `${nextState.nextId++}`;
- nextState.tracks[id] = {
- id,
- engineId: action.engineId,
- kind: action.trackKind,
- name: action.name,
- config: action.config,
- };
- nextState.scrollingTracks.push(id);
- return nextState;
- }
-
- case 'REQ_TRACK_DATA': {
- const id = action.trackId;
- const nextState = {...state};
- const nextTracks = nextState.tracks = {...state.tracks};
- const nextTrack = nextTracks[id] = {...nextTracks[id]};
- nextTrack.dataReq = {
- start: action.start,
- end: action.end,
- resolution: action.resolution
- };
- return nextState;
- }
-
- case 'CLEAR_TRACK_DATA_REQ': {
- const id = action.trackId;
- const nextState = {...state};
- const nextTracks = nextState.tracks = {...state.tracks};
- const nextTrack = nextTracks[id] = {...nextTracks[id]};
- nextTrack.dataReq = undefined;
- return nextState;
- }
-
- case 'EXECUTE_QUERY': {
- const nextState = {...state};
- nextState.queries = {...state.queries};
- nextState.queries[action.queryId] = {
- id: action.queryId,
- engineId: action.engineId,
- query: action.query,
- };
- return nextState;
- }
-
- case 'DELETE_QUERY': {
- const nextState = {...state};
- nextState.queries = {...state.queries};
- delete nextState.queries[action.queryId];
- return nextState;
- }
-
- case 'MOVE_TRACK': {
- if (!action.direction) {
- throw new Error('No direction given');
- }
- const id = action.trackId;
- const isPinned = state.pinnedTracks.includes(id);
- const isScrolling = state.scrollingTracks.includes(id);
- if (!isScrolling && !isPinned) {
- throw new Error(`No track with id ${id}`);
- }
- const nextState = {...state};
- const scrollingTracks = nextState.scrollingTracks =
- state.scrollingTracks.slice();
- const pinnedTracks = nextState.pinnedTracks = state.pinnedTracks.slice();
-
- const tracks = isPinned ? pinnedTracks : scrollingTracks;
-
- const oldIndex = tracks.indexOf(id);
- const newIndex = action.direction === 'up' ? oldIndex - 1 : oldIndex + 1;
- const swappedTrackId = tracks[newIndex];
- if (isPinned && newIndex === pinnedTracks.length) {
- // Move from last element of pinned to first element of scrolling.
- scrollingTracks.unshift(pinnedTracks.pop()!);
- } else if (isScrolling && newIndex === -1) {
- // Move first element of scrolling to last element of pinned.
- pinnedTracks.push(scrollingTracks.shift()!);
- } else if (swappedTrackId) {
- tracks[newIndex] = id;
- tracks[oldIndex] = swappedTrackId;
- } else {
- return state;
- }
- return nextState;
- }
-
- case 'TOGGLE_TRACK_PINNED': {
- const id = action.trackId;
- const isPinned = state.pinnedTracks.includes(id);
-
- const nextState = {...state};
- const pinnedTracks = nextState.pinnedTracks = [...state.pinnedTracks];
- const scrollingTracks = nextState.scrollingTracks =
- [...state.scrollingTracks];
- if (isPinned) {
- pinnedTracks.splice(pinnedTracks.indexOf(id), 1);
- scrollingTracks.unshift(id);
- } else {
- scrollingTracks.splice(scrollingTracks.indexOf(id), 1);
- pinnedTracks.push(id);
- }
- return nextState;
- }
-
- case 'SET_ENGINE_READY': {
- const nextState = {...state}; // Creates a shallow copy.
- nextState.engines = {...state.engines};
- nextState.engines[action.engineId].ready = action.ready;
- return nextState;
- }
-
- case 'CREATE_PERMALINK': {
- const nextState = {...state};
- nextState.permalink = {requestId: action.requestId, hash: undefined};
- return nextState;
- }
-
- case 'SET_PERMALINK': {
- // Drop any links for old requests.
- if (state.permalink.requestId !== action.requestId) return state;
-
- const nextState = {...state};
- nextState.permalink = {requestId: action.requestId, hash: action.hash};
- return nextState;
- }
-
- case 'LOAD_PERMALINK': {
- const nextState = {...state};
- nextState.permalink = {requestId: action.requestId, hash: action.hash};
- return nextState;
- }
-
- case 'SET_STATE': {
- return action.newState;
- }
-
- case 'SET_TRACE_TIME': {
- const nextState = {...state};
- nextState.traceTime.startSec = action.startSec;
- nextState.traceTime.endSec = action.endSec;
- nextState.traceTime.lastUpdate = action.lastUpdate;
- return nextState;
- }
-
- case 'SET_VISIBLE_TRACE_TIME': {
- const nextState = {...state};
- nextState.visibleTraceTime.startSec = action.startSec;
- nextState.visibleTraceTime.endSec = action.endSec;
- nextState.visibleTraceTime.lastUpdate = action.lastUpdate;
- return nextState;
- }
-
- case 'UPDATE_STATUS': {
- const nextState = {...state};
- nextState.status = {msg: action.msg, timestamp: action.timestamp};
- return nextState;
- }
-
- default:
- break;
- }
- return state;
-}
diff --git a/ui/src/controller/reducer_unittest.ts b/ui/src/controller/reducer_unittest.ts
deleted file mode 100644
index fc0f246..0000000
--- a/ui/src/controller/reducer_unittest.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-// Copyright (C) 2018 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 {moveTrack, toggleTrackPinned} from '../common/actions';
-import {createEmptyState, State, TrackState} from '../common/state';
-
-import {rootReducer} from './reducer';
-
-function fakeTrack(state: State, id: string): TrackState {
- const track: TrackState = {
- id,
- engineId: '1',
- kind: 'SOME_TRACK_KIND',
- name: 'A track',
- config: {},
- };
- state.tracks[id] = track;
- return track;
-}
-
-test('navigate', async () => {
- const before = createEmptyState();
- const after = rootReducer(before, {type: 'NAVIGATE', route: '/foo'});
- expect(after.route).toBe('/foo');
-});
-
-test('add tracks', () => {
- const empty = createEmptyState();
- const step1 = rootReducer(empty, {
- type: 'ADD_TRACK',
- engineId: '1',
- trackKind: 'cpu',
- cpu: '1',
- });
- const state = rootReducer(step1, {
- type: 'ADD_TRACK',
- engineId: '2',
- trackKind: 'cpu',
- cpu: '2',
- });
- expect(Object.values(state.tracks).length).toBe(2);
- expect(state.scrollingTracks.length).toBe(2);
-});
-
-test('reorder tracks', () => {
- const empty = createEmptyState();
- const step1 = rootReducer(empty, {
- type: 'ADD_TRACK',
- engineId: '1',
- trackKind: 'cpu',
- config: {},
- });
- const before = rootReducer(step1, {
- type: 'ADD_TRACK',
- engineId: '2',
- trackKind: 'cpu',
- config: {},
- });
-
- const firstTrackId = before.scrollingTracks[0];
- const secondTrackId = before.scrollingTracks[1];
-
- const after = rootReducer(before, {
- type: 'MOVE_TRACK',
- trackId: `${firstTrackId}`,
- direction: 'down',
- });
-
- // Ensure the order is swapped. This test would fail to detect side effects
- // if the before state was modified, so other tests are needed as well.
- expect(after.scrollingTracks[0]).toBe(secondTrackId);
- expect(after.scrollingTracks[1]).toBe(firstTrackId);
-
- // Ensure the track state contents have actually swapped places in the new
- // state, but not in the old one.
- expect(before.tracks[before.scrollingTracks[0]].engineId).toBe('1');
- expect(before.tracks[before.scrollingTracks[1]].engineId).toBe('2');
- expect(after.tracks[after.scrollingTracks[0]].engineId).toBe('2');
- expect(after.tracks[after.scrollingTracks[1]].engineId).toBe('1');
-});
-
-test('reorder pinned to scrolling', () => {
- const before = createEmptyState();
-
- fakeTrack(before, 'a');
- fakeTrack(before, 'b');
- fakeTrack(before, 'c');
-
- before.pinnedTracks = ['a', 'b'];
- before.scrollingTracks = ['c'];
-
- const after = rootReducer(before, moveTrack('b', 'down'));
- expect(after.pinnedTracks).toEqual(['a']);
- expect(after.scrollingTracks).toEqual(['b', 'c']);
-});
-
-test('reorder scrolling to pinned', () => {
- const before = createEmptyState();
- fakeTrack(before, 'a');
- fakeTrack(before, 'b');
- fakeTrack(before, 'c');
-
- before.pinnedTracks = ['a'];
- before.scrollingTracks = ['b', 'c'];
-
- const after = rootReducer(before, moveTrack('b', 'up'));
- expect(after.pinnedTracks).toEqual(['a', 'b']);
- expect(after.scrollingTracks).toEqual(['c']);
-});
-
-test('reorder clamp bottom', () => {
- const before = createEmptyState();
- fakeTrack(before, 'a');
- fakeTrack(before, 'b');
- fakeTrack(before, 'c');
-
- before.pinnedTracks = ['a', 'b'];
- before.scrollingTracks = ['c'];
-
- const after = rootReducer(before, moveTrack('a', 'up'));
- expect(after).toEqual(before);
-});
-
-test('reorder clamp top', () => {
- const before = createEmptyState();
- fakeTrack(before, 'a');
- fakeTrack(before, 'b');
- fakeTrack(before, 'c');
-
- before.pinnedTracks = ['a'];
- before.scrollingTracks = ['b', 'c'];
-
- const after = rootReducer(before, moveTrack('c', 'down'));
- expect(after).toEqual(before);
-});
-
-test('pin', () => {
- const before = createEmptyState();
- fakeTrack(before, 'a');
- fakeTrack(before, 'b');
- fakeTrack(before, 'c');
-
- before.pinnedTracks = ['a'];
- before.scrollingTracks = ['b', 'c'];
-
- const after = rootReducer(before, toggleTrackPinned('c'));
- expect(after.pinnedTracks).toEqual(['a', 'c']);
- expect(after.scrollingTracks).toEqual(['b']);
-});
-
-test('unpin', () => {
- const before = createEmptyState();
- fakeTrack(before, 'a');
- fakeTrack(before, 'b');
- fakeTrack(before, 'c');
-
- before.pinnedTracks = ['a', 'b'];
- before.scrollingTracks = ['c'];
-
- const after = rootReducer(before, toggleTrackPinned('a'));
- expect(after.pinnedTracks).toEqual(['b']);
- expect(after.scrollingTracks).toEqual(['a', 'c']);
-});
-
-test('open trace', async () => {
- const before = createEmptyState();
- const after = rootReducer(before, {
- type: 'OPEN_TRACE_FROM_URL',
- url: 'https://example.com/bar',
- });
- const engineKeys = Object.keys(after.engines);
- expect(engineKeys.length).toBe(1);
- expect(after.engines[engineKeys[0]].source).toBe('https://example.com/bar');
- expect(after.route).toBe('/viewer');
-});
-
-test('set state', async () => {
- const newState = createEmptyState();
- const before = createEmptyState();
- const after = rootReducer(before, {
- type: 'SET_STATE',
- newState,
- });
- expect(after).toBe(newState);
-});
-
-test('open second trace from file', () => {
- const before = createEmptyState();
- const afterFirst = rootReducer(before, {
- type: 'OPEN_TRACE_FROM_URL',
- url: 'https://example.com/bar',
- });
- afterFirst.scrollingTracks = ['track1', 'track2'];
- afterFirst.pinnedTracks = ['track3', 'track4'];
- const after = rootReducer(afterFirst, {
- type: 'OPEN_TRACE_FROM_URL',
- url: 'https://example.com/foo',
- });
- const engineKeys = Object.keys(after.engines);
- expect(engineKeys.length).toBe(2);
- expect(after.engines[engineKeys[0]].source).toBe('https://example.com/bar');
- expect(after.engines[engineKeys[1]].source).toBe('https://example.com/foo');
- expect(after.pinnedTracks.length).toBe(0);
- expect(after.scrollingTracks.length).toBe(0);
- expect(after.route).toBe('/viewer');
-});
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 06f570f..e68d26c 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -16,13 +16,8 @@
import {assertExists, assertTrue} from '../base/logging';
import {
- Action,
- addTrack,
- navigate,
- setEngineReady,
- setTraceTime,
- setVisibleTraceTime,
- updateStatus
+ Actions,
+ DeferredAction,
} from '../common/actions';
import {TimeSpan} from '../common/time';
import {QuantizedLoad, ThreadDesc} from '../frontend/globals';
@@ -64,11 +59,17 @@
const engineCfg = assertExists(globals.state.engines[this.engineId]);
switch (this.state) {
case 'init':
- globals.dispatch(setEngineReady(this.engineId, false));
+ globals.dispatch(Actions.setEngineReady({
+ engineId: this.engineId,
+ ready: false,
+ }));
this.loadTrace().then(() => {
- globals.dispatch(setEngineReady(this.engineId, true));
+ globals.dispatch(Actions.setEngineReady({
+ engineId: this.engineId,
+ ready: true,
+ }));
});
- globals.dispatch(updateStatus('Opening trace'));
+ this.updateStatus('Opening trace');
this.setState('loading_trace');
break;
@@ -110,7 +111,7 @@
}
private async loadTrace() {
- globals.dispatch(updateStatus('Creating trace processor'));
+ this.updateStatus('Creating trace processor');
const engineCfg = assertExists(globals.state.engines[this.engineId]);
this.engine = globals.createEngine();
@@ -124,12 +125,12 @@
const arrBuf = reader.readAsArrayBuffer(slice);
await this.engine.parse(new Uint8Array(arrBuf));
const progress = Math.round((off + slice.size) / blob.size * 100);
- globals.dispatch(updateStatus(`${statusHeader} ${progress} %`));
+ this.updateStatus(`${statusHeader} ${progress} %`);
}
} else {
const resp = await fetch(engineCfg.source);
if (resp.status !== 200) {
- globals.dispatch(updateStatus(`HTTP error ${resp.status}`));
+ this.updateStatus(`HTTP error ${resp.status}`);
throw new Error(`fetch() failed with HTTP error ${resp.status}`);
}
// tslint:disable-next-line no-any
@@ -152,7 +153,7 @@
const tElapsed = (nowMs - tStartMs) / 1e3;
let status = `${statusHeader} ${mb.toFixed(1)} MB `;
status += `(${(mb / tElapsed).toFixed(1)} MB/s)`;
- globals.dispatch(updateStatus(status));
+ this.updateStatus(status);
}
if (readRes.done) break;
}
@@ -161,13 +162,18 @@
await this.engine.notifyEof();
const traceTime = await this.engine.getTraceTimeBounds();
+ const traceTimeState = {
+ startSec: traceTime.start,
+ endSec: traceTime.end,
+ lastUpdate: Date.now() / 1000,
+ };
const actions = [
- setTraceTime(traceTime),
- navigate('/viewer'),
+ Actions.setTraceTime(traceTimeState),
+ Actions.navigate({route: '/viewer'}),
];
if (globals.state.visibleTraceTime.lastUpdate === 0) {
- actions.push(setVisibleTraceTime(traceTime));
+ actions.push(Actions.setVisibleTraceTime(traceTimeState));
}
globals.dispatchMultiple(actions);
@@ -178,20 +184,28 @@
}
private async listTracks() {
- globals.dispatch(updateStatus('Loading tracks'));
+ this.updateStatus('Loading tracks');
const engine = assertExists<Engine>(this.engine);
- const addToTrackActions: Action[] = [];
+ const addToTrackActions: DeferredAction[] = [];
const numCpus = await engine.getNumberOfCpus();
for (let cpu = 0; cpu < numCpus; cpu++) {
- addToTrackActions.push(
- addTrack(this.engineId, CPU_SLICE_TRACK_KIND, `Cpu ${cpu}`, {
- cpu,
- }));
+ addToTrackActions.push(Actions.addTrack({
+ engineId: this.engineId,
+ kind: CPU_SLICE_TRACK_KIND,
+ name: `Cpu ${cpu}`,
+ config: {
+ cpu,
+ }
+ }));
}
- const threadQuery = await engine.query(
- 'select upid, utid, tid, thread.name, max(slices.depth) ' +
- 'from thread inner join slices using(utid) group by utid');
+ const threadQuery = await engine.query(`
+ select upid, utid, tid, thread.name, depth
+ from thread inner join (
+ select utid, max(slices.depth) as depth
+ from slices
+ group by utid
+ ) using(utid)`);
for (let i = 0; i < threadQuery.numRecords; i++) {
const upid = threadQuery.columns[0].longValues![i];
const utid = threadQuery.columns[1].longValues![i];
@@ -199,18 +213,22 @@
let threadName = threadQuery.columns[3].stringValues![i];
threadName += `[${threadId}]`;
const maxDepth = threadQuery.columns[4].longValues![i];
- addToTrackActions.push(
- addTrack(this.engineId, SLICE_TRACK_KIND, threadName, {
- upid: upid as number,
- utid: utid as number,
- maxDepth: maxDepth as number,
- }));
+ addToTrackActions.push(Actions.addTrack({
+ engineId: this.engineId,
+ kind: SLICE_TRACK_KIND,
+ name: threadName,
+ config: {
+ upid: upid as number,
+ utid: utid as number,
+ maxDepth: maxDepth as number,
+ }
+ }));
}
globals.dispatchMultiple(addToTrackActions);
}
private async listThreads() {
- globals.dispatch(updateStatus('Reading thread list'));
+ this.updateStatus('Reading thread list');
const sqlQuery = 'select utid, tid, pid, thread.name, process.name ' +
'from thread inner join process using(upid)';
const threadRows = await assertExists(this.engine).query(sqlQuery);
@@ -231,9 +249,9 @@
const numSteps = 100;
const stepSec = traceTime.duration / numSteps;
for (let step = 0; step < numSteps; step++) {
- globals.dispatch(updateStatus(
+ this.updateStatus(
'Loading overview ' +
- `${Math.round((step + 1) / numSteps * 1000) / 10}%`));
+ `${Math.round((step + 1) / numSteps * 1000) / 10}%`);
const startSec = traceTime.start + step * stepSec;
const startNs = Math.floor(startSec * 1e9);
const endSec = startSec + stepSec;
@@ -270,4 +288,11 @@
globals.publish('OverviewData', slicesData);
} // for (step ...)
}
+
+ private updateStatus(msg: string): void {
+ globals.dispatch(Actions.updateStatus({
+ msg,
+ timestamp: Date.now() / 1000,
+ }));
+ }
}
diff --git a/ui/src/controller/track_controller.ts b/ui/src/controller/track_controller.ts
index cbf444c..4340847 100644
--- a/ui/src/controller/track_controller.ts
+++ b/ui/src/controller/track_controller.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import {assertExists} from '../base/logging';
-import {clearTrackDataRequest} from '../common/actions';
+import {Actions} from '../common/actions';
import {Registry} from '../common/registry';
import {TrackState} from '../common/state';
@@ -55,7 +55,7 @@
run() {
const dataReq = this.trackState.dataReq;
if (dataReq === undefined) return;
- globals.dispatch(clearTrackDataRequest(this.trackId));
+ globals.dispatch(Actions.clearTrackDataReq({trackId: this.trackId}));
this.onBoundsChange(dataReq.start, dataReq.end, dataReq.resolution);
}
}
diff --git a/ui/src/frontend/clipboard.ts b/ui/src/frontend/clipboard.ts
new file mode 100644
index 0000000..28d70be
--- /dev/null
+++ b/ui/src/frontend/clipboard.ts
@@ -0,0 +1,23 @@
+// Copyright (C) 2018 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 async function copyToClipboard(text: string): Promise<void> {
+ try {
+ // TODO(hjd): Fix typescript type for navigator.
+ // tslint:disable-next-line no-any
+ await(navigator as any).clipboard.writeText(text);
+ } catch (err) {
+ console.error(`Failed to copy "${text}" to clipboard: ${err}`);
+ }
+}
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index b9aa740..47911ea 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {setVisibleTraceTime} from '../common/actions';
+import {Actions} from '../common/actions';
import {TimeSpan} from '../common/time';
import {globals} from './globals';
@@ -44,8 +44,12 @@
this.pendingGlobalTimeUpdate = this.visibleWindowTime;
if (alreadyPosted) return;
setTimeout(() => {
- globals.dispatch(setVisibleTraceTime(this.pendingGlobalTimeUpdate!));
this._visibleTimeLastUpdate = Date.now() / 1000;
+ globals.dispatch(Actions.setVisibleTraceTime({
+ startSec: this.pendingGlobalTimeUpdate!.start,
+ endSec: this.pendingGlobalTimeUpdate!.end,
+ lastUpdate: this._visibleTimeLastUpdate,
+ }));
this.pendingGlobalTimeUpdate = undefined;
}, 100);
}
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index e370127..ea0c248 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -13,13 +13,13 @@
// limitations under the License.
import {assertExists} from '../base/logging';
-import {Action} from '../common/actions';
+import {DeferredAction} from '../common/actions';
import {createEmptyState, State} from '../common/state';
import {FrontendLocalState} from './frontend_local_state';
import {RafScheduler} from './raf_scheduler';
-type Dispatch = (action: Action) => void;
+type Dispatch = (action: DeferredAction) => void;
type TrackDataStore = Map<string, {}>;
type QueryResultsStore = Map<string, {}>;
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 0b92765..0905de4 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -17,7 +17,7 @@
import * as m from 'mithril';
import {forwardRemoteCalls} from '../base/remote';
-import {loadPermalink} from '../common/actions';
+import {Actions} from '../common/actions';
import {State} from '../common/state';
import {TimeSpan} from '../common/time';
import {globals, QuantizedLoad, ThreadDesc} from './globals';
@@ -120,7 +120,11 @@
// /?s=xxxx for permalinks.
const stateHash = router.param('s');
if (stateHash) {
- globals.dispatch(loadPermalink(stateHash));
+ // TODO(hjd): Should requestId not be set to nextId++ in the controller?
+ globals.dispatch(Actions.loadPermalink({
+ requestId: new Date().toISOString(),
+ hash: stateHash,
+ }));
}
// Prevent pinch zoom.
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 4fb18f5..9329053 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -32,10 +32,12 @@
*/
const SCROLLING_CANVAS_OVERDRAW_FACTOR = 2;
+// We need any here so we can accept vnodes with arbitrary attrs.
+// tslint:disable-next-line:no-any
+export type AnyAttrsVnode = m.Vnode<any, {}>;
+
interface Attrs {
- // Panels with non-empty attrs does not work without any.
- // tslint:disable-next-line:no-any
- panels: Array<m.Vnode<any, {}>>;
+ panels: AnyAttrsVnode[];
doesScroll: boolean;
}
@@ -55,26 +57,29 @@
renderStats: new RunningStatistics(10),
};
- // attrs received in the most recent mithril redraw.
- private attrs?: Attrs;
+ // Attrs received in the most recent mithril redraw. We receive a new vnode
+ // with new attrs on every redraw, and we cache it here so that resize
+ // listeners and canvas redraw callbacks can access it.
+ private attrs: Attrs;
- private canvasOverdrawFactor: number;
private ctx?: CanvasRenderingContext2D;
private onResize: () => void = () => {};
private parentOnScroll: () => void = () => {};
private canvasRedrawer: () => void;
+ get canvasOverdrawFactor() {
+ return this.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
+ }
+
constructor(vnode: m.CVnode<Attrs>) {
- this.canvasOverdrawFactor =
- vnode.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
+ this.attrs = vnode.attrs;
this.canvasRedrawer = () => this.redrawCanvas();
globals.rafScheduler.addRedrawCallback(this.canvasRedrawer);
perfDisplay.addContainer(this);
}
oncreate(vnodeDom: m.CVnodeDOM<Attrs>) {
- const attrs = vnodeDom.attrs;
// Save the canvas context in the state.
const canvas =
vnodeDom.dom.querySelector('.main-canvas') as HTMLCanvasElement;
@@ -89,21 +94,18 @@
this.parentWidth = clientRect.width;
this.parentHeight = clientRect.height;
- this.updatePanelHeightsFromDom(vnodeDom);
+ this.readPanelHeightsFromDom(vnodeDom.dom);
(vnodeDom.dom as HTMLElement).style.height = `${this.totalPanelHeight}px`;
- this.canvasHeight = this.getCanvasHeight(attrs.doesScroll);
- this.updateCanvasDimensions(vnodeDom);
+ this.updateCanvasDimensions();
+ this.repositionCanvas();
// Save the resize handler in the state so we can remove it later.
// TODO: Encapsulate resize handling better.
this.onResize = () => {
- const clientRect =
- assertExists(vnodeDom.dom.parentElement).getBoundingClientRect();
- this.parentWidth = clientRect.width;
- this.parentHeight = clientRect.height;
- this.canvasHeight = this.getCanvasHeight(attrs.doesScroll);
- this.updateCanvasDimensions(vnodeDom);
+ this.readParentSizeFromDom(vnodeDom.dom);
+ this.updateCanvasDimensions();
+ this.repositionCanvas();
globals.rafScheduler.scheduleFullRedraw();
};
@@ -111,10 +113,10 @@
window.addEventListener('resize', this.onResize);
// TODO(dproy): Handle change in doesScroll attribute.
- if (vnodeDom.attrs.doesScroll) {
+ if (this.attrs.doesScroll) {
this.parentOnScroll = () => {
- this.scrollTop = vnodeDom.dom.parentElement!.scrollTop;
- this.repositionCanvas(vnodeDom);
+ this.scrollTop = assertExists(vnodeDom.dom.parentElement).scrollTop;
+ this.repositionCanvas();
globals.rafScheduler.scheduleRedraw();
};
vnodeDom.dom.parentElement!.addEventListener(
@@ -132,8 +134,6 @@
}
view({attrs}: m.CVnode<Attrs>) {
- // We receive a new vnode object with new attrs on every mithril redraw. We
- // store the latest attrs so redrawCanvas can use it.
this.attrs = attrs;
const renderPanel = (panel: m.Vnode) => perfDebug() ?
m('.panel', panel, m('.debug-panel-border')) :
@@ -146,25 +146,27 @@
}
onupdate(vnodeDom: m.CVnodeDOM<Attrs>) {
- this.repositionCanvas(vnodeDom);
+ const totalPanelHeightChanged = this.readPanelHeightsFromDom(vnodeDom.dom);
+ const parentSizeChanged = this.readParentSizeFromDom(vnodeDom.dom);
- if (this.updatePanelHeightsFromDom(vnodeDom)) {
+ if (totalPanelHeightChanged) {
(vnodeDom.dom as HTMLElement).style.height = `${this.totalPanelHeight}px`;
}
- // In non-scrolling case, canvas height can change if panel heights changed.
- const canvasHeight = this.getCanvasHeight(vnodeDom.attrs.doesScroll);
- if (this.canvasHeight !== canvasHeight) {
- this.canvasHeight = canvasHeight;
- this.updateCanvasDimensions(vnodeDom);
+ const canvasSizeShouldChange =
+ this.attrs.doesScroll ? parentSizeChanged : totalPanelHeightChanged;
+ if (canvasSizeShouldChange) {
+ this.updateCanvasDimensions();
+ this.repositionCanvas();
}
}
- private updateCanvasDimensions(vnodeDom: m.CVnodeDOM<Attrs>) {
- const canvas =
- assertExists(vnodeDom.dom.querySelector('canvas.main-canvas')) as
- HTMLCanvasElement;
+ private updateCanvasDimensions() {
+ this.canvasHeight = this.attrs.doesScroll ?
+ this.parentHeight * this.canvasOverdrawFactor :
+ this.totalPanelHeight;
const ctx = assertExists(this.ctx);
+ const canvas = assertExists(ctx.canvas);
canvas.style.height = `${this.canvasHeight}px`;
const dpr = window.devicePixelRatio;
ctx.canvas.width = this.parentWidth * dpr;
@@ -172,13 +174,36 @@
ctx.scale(dpr, dpr);
}
- private updatePanelHeightsFromDom(vnodeDom: m.CVnodeDOM<Attrs>): boolean {
+ private repositionCanvas() {
+ const canvas = assertExists(assertExists(this.ctx).canvas);
+ const canvasYStart = this.scrollTop - this.getCanvasOverdrawHeightPerSide();
+ canvas.style.transform = `translateY(${canvasYStart}px)`;
+ }
+
+ /**
+ * Reads dimensions of parent node. Returns true if read dimensions are
+ * different from what was cached in the state.
+ */
+ private readParentSizeFromDom(dom: Element): boolean {
+ const oldWidth = this.parentWidth;
+ const oldHeight = this.parentHeight;
+ const clientRect = assertExists(dom.parentElement).getBoundingClientRect();
+ this.parentWidth = clientRect.width;
+ this.parentHeight = clientRect.height;
+ return this.parentHeight !== oldHeight || this.parentWidth !== oldWidth;
+ }
+
+ /**
+ * Reads dimensions of panels. Returns true if total panel height is different
+ * from what was cached in state.
+ */
+ private readPanelHeightsFromDom(dom: Element): boolean {
const prevHeight = this.totalPanelHeight;
this.panelHeights = [];
this.totalPanelHeight = 0;
- const panels = vnodeDom.dom.querySelectorAll('.panel');
- assertTrue(panels.length === vnodeDom.attrs.panels.length);
+ const panels = dom.querySelectorAll('.panel');
+ assertTrue(panels.length === this.attrs.panels.length);
for (let i = 0; i < panels.length; i++) {
const height = panels[i].getBoundingClientRect().height;
this.panelHeights[i] = height;
@@ -188,19 +213,6 @@
return this.totalPanelHeight !== prevHeight;
}
- private getCanvasHeight(doesScroll: boolean) {
- return doesScroll ? this.parentHeight * this.canvasOverdrawFactor :
- this.totalPanelHeight;
- }
-
- private repositionCanvas(vnodeDom: m.CVnodeDOM<Attrs>) {
- const canvas =
- assertExists(vnodeDom.dom.querySelector('canvas.main-canvas')) as
- HTMLCanvasElement;
- const canvasYStart = this.scrollTop - this.getCanvasOverdrawHeightPerSide();
- canvas.style.transform = `translateY(${canvasYStart}px)`;
- }
-
private overlapsCanvas(yStart: number, yEnd: number) {
return yEnd > 0 && yStart < this.canvasHeight;
}
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index 79d8417..21e295e 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -14,21 +14,12 @@
import * as m from 'mithril';
+import {copyToClipboard} from './clipboard';
import {createPage} from './pages';
const RECORD_COMMAND_LINE =
'echo CgYIgKAGIAESIwohCgxsaW51eC5mdHJhY2UQAKIGDhIFc2NoZWQSBWlucHV0GJBOMh0KFnBlcmZldHRvLnRyYWNlZF9wcm9iZXMQgCAYBEAASAA= | base64 --decode | adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace" && adb pull /data/misc/perfetto-traces/trace /tmp/trace';
-async function copyToClipboard(text: string): Promise<void> {
- try {
- // TODO(hjd): Fix typescript type for navigator.
- // tslint:disable-next-line no-any
- await(navigator as any).clipboard.writeText(text);
- } catch (err) {
- console.error(`Failed to copy "${text}" to clipboard: ${err}`);
- }
-}
-
interface CodeSampleAttrs {
text: string;
}
diff --git a/ui/src/frontend/router.ts b/ui/src/frontend/router.ts
index 98bf553..0830712 100644
--- a/ui/src/frontend/router.ts
+++ b/ui/src/frontend/router.ts
@@ -14,7 +14,7 @@
import * as m from 'mithril';
-import {Action, navigate} from '../common/actions';
+import {Actions, DeferredAction} from '../common/actions';
interface RouteMap {
[route: string]: m.Component;
@@ -25,7 +25,7 @@
export class Router {
constructor(
private defaultRoute: string, private routes: RouteMap,
- private dispatch: (a: Action) => void) {
+ private dispatch: (a: DeferredAction) => void) {
if (!(defaultRoute in routes)) {
throw Error('routes must define a component for defaultRoute.');
}
@@ -57,7 +57,7 @@
if (!(route in this.routes)) {
console.info(
`Route ${route} not known redirecting to ${this.defaultRoute}.`);
- this.dispatch(navigate(this.defaultRoute));
+ this.dispatch(Actions.navigate({route: this.defaultRoute}));
}
}
@@ -68,7 +68,7 @@
navigateToCurrentHash() {
const hashRoute = this.getRouteFromHash();
const newRoute = hashRoute in this.routes ? hashRoute : this.defaultRoute;
- this.dispatch(navigate(newRoute));
+ this.dispatch(Actions.navigate({route: newRoute}));
// TODO(dproy): Handle case when new route has a permalink.
}
diff --git a/ui/src/frontend/router_jsdomtest.ts b/ui/src/frontend/router_jsdomtest.ts
index 2d1c4ce..4b8aadb 100644
--- a/ui/src/frontend/router_jsdomtest.ts
+++ b/ui/src/frontend/router_jsdomtest.ts
@@ -14,7 +14,7 @@
import {dingus} from 'dingusjs';
-import {Action, navigate} from '../common/actions';
+import {Actions, DeferredAction} from '../common/actions';
import {Router} from './router';
@@ -49,7 +49,7 @@
});
test('Set valid route on hash', () => {
- const dispatch = dingus<(a: Action) => void>();
+ const dispatch = dingus<(a: DeferredAction) => void>();
const router = new Router(
'/',
{
@@ -67,19 +67,19 @@
});
test('Redirects to default for invalid route in setRouteOnHash ', () => {
- const dispatch = dingus<(a: Action) => void>();
+ const dispatch = dingus<(a: DeferredAction) => void>();
// const dispatch = () => {console.log("action received")};
const router = new Router('/', {'/': mockComponent}, dispatch);
router.setRouteOnHash('foo');
expect(dispatch.calls.length).toBe(1);
expect(dispatch.calls[0][1].length).toBeGreaterThanOrEqual(1);
- expect(dispatch.calls[0][1][0]).toEqual(navigate('/'));
+ expect(dispatch.calls[0][1][0]).toEqual(Actions.navigate({route: '/'}));
});
test('Navigate on hash change', done => {
- const mockDispatch = (a: Action) => {
- expect(a).toEqual(navigate('/viewer'));
+ const mockDispatch = (a: DeferredAction) => {
+ expect(a).toEqual(Actions.navigate({route: '/viewer'}));
done();
};
new Router(
@@ -93,8 +93,8 @@
});
test('Redirects to default when invalid route set in window location', done => {
- const mockDispatch = (a: Action) => {
- expect(a).toEqual(navigate('/'));
+ const mockDispatch = (a: DeferredAction) => {
+ expect(a).toEqual(Actions.navigate({route: '/'}));
done();
};
@@ -110,20 +110,20 @@
});
test('navigateToCurrentHash with valid current route', () => {
- const dispatch = dingus<(a: Action) => void>();
+ const dispatch = dingus<(a: DeferredAction) => void>();
window.location.hash = '#!/b';
const router =
new Router('/', {'/': mockComponent, '/b': mockComponent}, dispatch);
router.navigateToCurrentHash();
- expect(dispatch.calls[0][1][0]).toEqual(navigate('/b'));
+ expect(dispatch.calls[0][1][0]).toEqual(Actions.navigate({route: '/b'}));
});
test('navigateToCurrentHash with invalid current route', () => {
- const dispatch = dingus<(a: Action) => void>();
+ const dispatch = dingus<(a: DeferredAction) => void>();
window.location.hash = '#!/invalid';
const router = new Router('/', {'/': mockComponent}, dispatch);
router.navigateToCurrentHash();
- expect(dispatch.calls[0][1][0]).toEqual(navigate('/'));
+ expect(dispatch.calls[0][1][0]).toEqual(Actions.navigate({route: '/'}));
});
test('Params parsing', () => {
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 6f8e5d2..543c78c 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -14,13 +14,7 @@
import * as m from 'mithril';
-import {
- createPermalink,
- executeQuery,
- navigate,
- openTraceFromFile,
- openTraceFromUrl
-} from '../common/actions';
+import {Actions} from '../common/actions';
import {globals} from './globals';
@@ -60,7 +54,11 @@
function createCannedQuery(query: string): (_: Event) => void {
return (e: Event) => {
e.preventDefault();
- globals.dispatch(executeQuery('0', 'command', query));
+ globals.dispatch(Actions.executeQuery({
+ engineId: '0',
+ queryId: 'command',
+ query,
+ }));
};
}
@@ -148,7 +146,7 @@
function openTraceUrl(url: string): (e: Event) => void {
return e => {
e.preventDefault();
- globals.dispatch(openTraceFromUrl(url));
+ globals.dispatch(Actions.openTraceFromUrl({url}));
};
}
@@ -157,22 +155,25 @@
throw new Error('Not an input element');
}
if (!e.target.files) return;
- globals.dispatch(openTraceFromFile(e.target.files[0]));
+ globals.dispatch(Actions.openTraceFromFile({file: e.target.files[0]}));
}
function navigateHome(e: Event) {
e.preventDefault();
- globals.dispatch(navigate('/'));
+ globals.dispatch(Actions.navigate({route: '/'}));
}
function navigateRecord(e: Event) {
e.preventDefault();
- globals.dispatch(navigate('/record'));
+ globals.dispatch(Actions.navigate({route: '/record'}));
}
function dispatchCreatePermalink(e: Event) {
e.preventDefault();
- globals.dispatch(createPermalink());
+ // TODO(hjd): Should requestId not be set to nextId++ in the controller?
+ globals.dispatch(Actions.createPermalink({
+ requestId: new Date().toISOString(),
+ }));
}
export class Sidebar implements m.ClassComponent {
diff --git a/ui/src/frontend/topbar.ts b/ui/src/frontend/topbar.ts
index 03b142a..acb4d80 100644
--- a/ui/src/frontend/topbar.ts
+++ b/ui/src/frontend/topbar.ts
@@ -14,7 +14,7 @@
import * as m from 'mithril';
-import {deleteQuery, executeQuery} from '../common/actions';
+import {Actions} from '../common/actions';
import {QueryResponse} from '../common/queries';
import {EngineConfig} from '../common/state';
@@ -28,7 +28,7 @@
function clearOmniboxResults() {
globals.queryResults.delete(QUERY_ID);
- globals.dispatch(deleteQuery(QUERY_ID));
+ globals.dispatch(Actions.deleteQuery({queryId: QUERY_ID}));
}
function onKeyDown(e: Event) {
@@ -81,10 +81,12 @@
if (mode === 'search') {
const name = txt.value.replace(/'/g, '\\\'').replace(/[*]/g, '%');
const query = `select str from strings where str like '%${name}%' limit 10`;
- globals.dispatch(executeQuery('0', QUERY_ID, query));
+ globals.dispatch(
+ Actions.executeQuery({engineId: '0', queryId: QUERY_ID, query}));
}
if (mode === 'command' && key === 'Enter') {
- globals.dispatch(executeQuery('0', 'command', txt.value));
+ globals.dispatch(Actions.executeQuery(
+ {engineId: '0', queryId: 'command', query: txt.value}));
}
}
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index a12024d..9386985 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -14,8 +14,7 @@
import * as m from 'mithril';
-import {moveTrack, toggleTrackPinned} from '../common/actions';
-import {Action} from '../common/actions';
+import {Actions, DeferredAction} from '../common/actions';
import {TrackState} from '../common/state';
import {globals} from './globals';
@@ -41,15 +40,17 @@
'.track-shell',
m('h1', attrs.trackState.name),
m(TrackButton, {
- action: moveTrack(attrs.trackState.id, 'up'),
+ action: Actions.moveTrack(
+ {trackId: attrs.trackState.id, direction: 'up'}),
i: 'arrow_upward_alt',
}),
m(TrackButton, {
- action: moveTrack(attrs.trackState.id, 'down'),
+ action: Actions.moveTrack(
+ {trackId: attrs.trackState.id, direction: 'down'}),
i: 'arrow_downward_alt',
}),
m(TrackButton, {
- action: toggleTrackPinned(attrs.trackState.id),
+ action: Actions.toggleTrackPinned({trackId: attrs.trackState.id}),
i: isPinned(attrs.trackState.id) ? 'star' : 'star_border',
}));
}
@@ -87,7 +88,7 @@
}
interface TrackButtonAttrs {
- action: Action;
+ action: DeferredAction;
i: string;
}
class TrackButton implements m.ClassComponent<TrackButtonAttrs> {
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index e801992..d89bd5d 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -17,20 +17,22 @@
import {QueryResponse} from '../common/queries';
import {TimeSpan} from '../common/time';
+import {copyToClipboard} from './clipboard';
import {FlameGraphPanel} from './flame_graph_panel';
import {globals} from './globals';
import {HeaderPanel} from './header_panel';
import {OverviewTimelinePanel} from './overview_timeline_panel';
import {createPage} from './pages';
import {PanAndZoomHandler} from './pan_and_zoom_handler';
-import {PanelContainer} from './panel_container';
+import {Panel} from './panel';
+import {AnyAttrsVnode, PanelContainer} from './panel_container';
import {TimeAxisPanel} from './time_axis_panel';
import {TRACK_SHELL_WIDTH} from './track_panel';
import {TrackPanel} from './track_panel';
const MAX_ZOOM_SPAN_SEC = 1e-4; // 0.1 ms.
-class QueryTable implements m.ClassComponent {
+class QueryTable extends Panel {
view() {
const resp = globals.queryResults.get('command') as QueryResponse;
if (resp === undefined) {
@@ -53,12 +55,33 @@
return m(
'div',
m('header.overview',
- `Query result - ${Math.round(resp.durationMs)} ms`,
- m('span.code', resp.query)),
+ m('span',
+ `Query result - ${Math.round(resp.durationMs)} ms`,
+ m('span.code', resp.query)),
+ resp.error ? null :
+ m('button',
+ {
+ onclick: () => {
+ const lines: string[][] = [];
+ lines.push(resp.columns);
+ for (const row of resp.rows) {
+ const line = [];
+ for (const col of resp.columns) {
+ line.push(row[col].toString());
+ }
+ lines.push(line);
+ }
+ copyToClipboard(
+ lines.map(line => line.join('\t')).join('\n'));
+ },
+ },
+ 'Copy as .tsv')),
resp.error ?
m('.query-error', `SQL error: ${resp.error}`) :
m('table.query-table', m('thead', header), m('tbody', rows)));
}
+
+ renderCanvas() {}
}
/**
@@ -130,7 +153,8 @@
}
view() {
- const scrollingPanels = globals.state.scrollingTracks.length > 0 ?
+ const scrollingPanels: AnyAttrsVnode[] =
+ globals.state.scrollingTracks.length > 0 ?
[
m(HeaderPanel, {title: 'Tracks', key: 'tracksheader'}),
...globals.state.scrollingTracks.map(
@@ -138,10 +162,10 @@
m(FlameGraphPanel, {key: 'flamegraph'}),
] :
[];
+ scrollingPanels.unshift(m(QueryTable));
+
return m(
'.page',
- m(QueryTable),
- // TODO: Pan and zoom logic should be in its own mithril component.
m('.pan-and-zoom-content',
m('.pinned-panel-container', m(PanelContainer, {
doesScroll: false,
diff --git a/ui/src/tracks/chrome_slices/frontend.ts b/ui/src/tracks/chrome_slices/frontend.ts
index 6f64494..0072a75 100644
--- a/ui/src/tracks/chrome_slices/frontend.ts
+++ b/ui/src/tracks/chrome_slices/frontend.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {requestTrackData} from '../../common/actions';
+import {Actions} from '../../common/actions';
import {TrackState} from '../../common/state';
import {checkerboardExcept} from '../../frontend/checkerboard';
import {globals} from '../../frontend/globals';
@@ -58,8 +58,12 @@
const reqEnd = visibleWindowTime.end + visibleWindowTime.duration;
const reqRes = getCurResolution();
this.reqPending = false;
- globals.dispatch(
- requestTrackData(this.trackState.id, reqStart, reqEnd, reqRes));
+ globals.dispatch(Actions.reqTrackData({
+ trackId: this.trackState.id,
+ start: reqStart,
+ end: reqEnd,
+ resolution: reqRes
+ }));
}
renderCanvas(ctx: CanvasRenderingContext2D): void {
diff --git a/ui/src/tracks/cpu_slices/frontend.ts b/ui/src/tracks/cpu_slices/frontend.ts
index ec01b08..e6d0e6c 100644
--- a/ui/src/tracks/cpu_slices/frontend.ts
+++ b/ui/src/tracks/cpu_slices/frontend.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import {assertTrue} from '../../base/logging';
-import {requestTrackData} from '../../common/actions';
+import {Actions} from '../../common/actions';
import {TrackState} from '../../common/state';
import {checkerboardExcept} from '../../frontend/checkerboard';
import {globals} from '../../frontend/globals';
@@ -78,8 +78,12 @@
const reqEnd = visibleWindowTime.end + visibleWindowTime.duration;
const reqRes = getCurResolution();
this.reqPending = false;
- globals.dispatch(
- requestTrackData(this.trackState.id, reqStart, reqEnd, reqRes));
+ globals.dispatch(Actions.reqTrackData({
+ trackId: this.trackState.id,
+ start: reqStart,
+ end: reqEnd,
+ resolution: reqRes
+ }));
}
renderCanvas(ctx: CanvasRenderingContext2D): void {