Merge "Chrome: Change string table indices to unsigned"
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/file_utils.h b/include/perfetto/base/file_utils.h
index e23add1..3126b7c 100644
--- a/include/perfetto/base/file_utils.h
+++ b/include/perfetto/base/file_utils.h
@@ -19,12 +19,27 @@
 
 #include <string>
 
+#include "perfetto/base/build_config.h"
+
 namespace perfetto {
 namespace base {
 
 bool ReadFileDescriptor(int fd, std::string* out);
 bool ReadFile(const std::string& path, std::string* out);
 
+#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+
+// 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);
+
+#endif
+
 }  // namespace base
 }  // namespace perfetto
 
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/file_utils.cc b/src/base/file_utils.cc
index 6487dbd..89440b1 100644
--- a/src/base/file_utils.cc
+++ b/src/base/file_utils.cc
@@ -20,6 +20,9 @@
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/scoped_file.h"
+#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+#include <unistd.h>
+#endif
 
 namespace perfetto {
 namespace base {
@@ -60,5 +63,23 @@
   return ReadFileDescriptor(*fd, out);
 }
 
+#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+
+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);
+}
+
+#endif
+
 }  // 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 887b9ec..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"
@@ -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/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/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 d2fb134..8c8f20e 100644
--- a/src/perfetto_cmd/perfetto_cmd.cc
+++ b/src/perfetto_cmd/perfetto_cmd.cc
@@ -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 af7ef7f..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"
@@ -184,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 8a180a9..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"
@@ -71,7 +72,7 @@
 void WriteGarbageToFile(const std::string& path) {
   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/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 08a1deb..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"
@@ -76,7 +77,7 @@
   auto fd = base::OpenFile(path, O_WRONLY);
   if (!fd)
     return;
-  perfetto::base::ignore_result(write(*fd, str, strlen(str)));
+  base::ignore_result(base::WriteAll(*fd, str, strlen(str)));
 }
 
 void ClearFile(const char* path) {
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/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/ui/src/common/actions.ts b/ui/src/common/actions.ts
index fb108c0..5ae4598 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -15,122 +15,17 @@
 import {DraftObject} from 'immer';
 
 import {defaultTraceTime, State, Status, TraceTime} from './state';
-import {TimeSpan} from './time';
-
-export interface Action { type: string; }
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function openTraceFromUrl(url: string) {
-  return Actions.openTraceFromUrl({
-    url,
-  });
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function openTraceFromFile(file: File) {
-  return Actions.openTraceFromFile({
-    file,
-  });
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function addTrack(
-    engineId: string, trackKind: string, name: string, config: {}) {
-  return Actions.addTrack({
-    engineId,
-    kind: trackKind,
-    name,
-    config,
-  });
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function requestTrackData(
-    trackId: string, start: number, end: number, resolution: number) {
-  return Actions.reqTrackData({trackId, start, end, resolution});
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function clearTrackDataRequest(trackId: string) {
-  return Actions.clearTrackDataReq({trackId});
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function deleteQuery(queryId: string) {
-  return Actions.deleteQuery({
-    queryId,
-  });
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function navigate(route: string) {
-  return Actions.navigate({
-    route,
-  });
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function moveTrack(trackId: string, direction: 'up'|'down') {
-  return Actions.moveTrack({
-    trackId,
-    direction,
-  });
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function toggleTrackPinned(trackId: string) {
-  return Actions.toggleTrackPinned({
-    trackId,
-  });
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function setEngineReady(engineId: string, ready = true) {
-  return Actions.setEngineReady({engineId, ready});
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function createPermalink() {
-  return Actions.createPermalink({requestId: new Date().toISOString()});
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function setPermalink(requestId: string, hash: string) {
-  return Actions.setPermalink({requestId, hash});
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function loadPermalink(hash: string) {
-  return Actions.loadPermalink({requestId: new Date().toISOString(), hash});
-}
-
-// TODO(hjd): Temporary until the reducer/action refactoring is done.
-export function setState(newState: State) {
-  return Actions.setState({newState});
-}
-
-export function setTraceTime(ts: TimeSpan) {
-  return Actions.setTraceTime({
-    startSec: ts.start,
-    endSec: ts.end,
-    lastUpdate: Date.now() / 1000,
-  });
-}
-
-export function setVisibleTraceTime(ts: TimeSpan) {
-  return Actions.setVisibleTraceTime({
-    startSec: ts.start,
-    endSec: ts.end,
-    lastUpdate: Date.now() / 1000,
-  });
-}
-
-export function updateStatus(msg: string) {
-  return Actions.updateStatus({msg, timestamp: Date.now() / 1000});
-}
 
 type StateDraft = DraftObject<State>;
 
+
+function clearTraceState(state: StateDraft) {
+  state.traceTime = defaultTraceTime;
+  state.visibleTraceTime = defaultTraceTime;
+  state.pinnedTracks = [];
+  state.scrollingTracks = [];
+}
+
 export const StateActions = {
 
   navigate(state: StateDraft, args: {route: string}): void {
@@ -138,12 +33,8 @@
   },
 
   openTraceFromFile(state: StateDraft, args: {file: File}): void {
-    state.traceTime = defaultTraceTime;
-    state.visibleTraceTime = defaultTraceTime;
+    clearTraceState(state);
     const id = `${state.nextId++}`;
-    // Reset displayed tracks.
-    state.pinnedTracks = [];
-    state.scrollingTracks = [];
     state.engines[id] = {
       id,
       ready: false,
@@ -153,12 +44,8 @@
   },
 
   openTraceFromUrl(state: StateDraft, args: {url: string}): void {
-    state.traceTime = defaultTraceTime;
-    state.visibleTraceTime = defaultTraceTime;
+    clearTraceState(state);
     const id = `${state.nextId++}`;
-    // Reset displayed tracks.
-    state.pinnedTracks = [];
-    state.scrollingTracks = [];
     state.engines[id] = {
       id,
       ready: false,
@@ -292,7 +179,15 @@
   },
 };
 
-// A DeferredAction is a bundle of Args and a method name.
+
+// 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;
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/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 08b205f..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 {
-  addTrack,
+  Actions,
   DeferredAction,
-  navigate,
-  setEngineReady,
-  setTraceTime,
-  setVisibleTraceTime,
-  updateStatus
 } 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: 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/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 15be904..9329053 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -57,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;
@@ -91,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();
     };
 
@@ -113,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(
@@ -134,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')) :
@@ -148,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;
@@ -174,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;
@@ -190,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/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 073814c..543c78c 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -14,13 +14,7 @@
 
 import * as m from 'mithril';
 
-import {
-  Actions,
-  createPermalink,
-  navigate,
-  openTraceFromFile,
-  openTraceFromUrl
-} from '../common/actions';
+import {Actions} from '../common/actions';
 
 import {globals} from './globals';
 
@@ -152,7 +146,7 @@
 function openTraceUrl(url: string): (e: Event) => void {
   return e => {
     e.preventDefault();
-    globals.dispatch(openTraceFromUrl(url));
+    globals.dispatch(Actions.openTraceFromUrl({url}));
   };
 }
 
@@ -161,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 6e1acdb..acb4d80 100644
--- a/ui/src/frontend/topbar.ts
+++ b/ui/src/frontend/topbar.ts
@@ -14,7 +14,7 @@
 
 import * as m from 'mithril';
 
-import {Actions, deleteQuery} 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) {
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/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 {