Merge "trace_processor: Parse process and trace packets"
diff --git a/include/perfetto/base/BUILD.gn b/include/perfetto/base/BUILD.gn
index 41f075d..908baf4 100644
--- a/include/perfetto/base/BUILD.gn
+++ b/include/perfetto/base/BUILD.gn
@@ -24,6 +24,7 @@
     "string_splitter.h",
     "string_utils.h",
     "task_runner.h",
+    "temp_file.h",
     "thread_checker.h",
     "time.h",
     "unix_task_runner.h",
diff --git a/src/base/watchdog_posix.cc b/src/base/watchdog_posix.cc
index 9864e7b..5f8755f 100644
--- a/src/base/watchdog_posix.cc
+++ b/src/base/watchdog_posix.cc
@@ -247,7 +247,7 @@
   }
 }
 
-Watchdog::Timer::Timer(Timer&& other) {
+Watchdog::Timer::Timer(Timer&& other) noexcept {
   timerid_ = other.timerid_;
   other.timerid_ = nullptr;
 }
diff --git a/src/trace_processor/sched_slice_table.cc b/src/trace_processor/sched_slice_table.cc
index 3a5af21..6d90d0b 100644
--- a/src/trace_processor/sched_slice_table.cc
+++ b/src/trace_processor/sched_slice_table.cc
@@ -17,6 +17,9 @@
 #include "src/trace_processor/sched_slice_table.h"
 
 #include <string.h>
+#include <algorithm>
+#include <bitset>
+#include <numeric>
 
 #include "perfetto/base/logging.h"
 
@@ -51,6 +54,109 @@
 inline SchedSliceTable::Cursor* AsCursor(sqlite3_vtab_cursor* cursor) {
   return reinterpret_cast<SchedSliceTable::Cursor*>(cursor);
 }
+
+template <size_t N = TraceStorage::kMaxCpus>
+bool PopulateFilterBitmap(int op,
+                          sqlite3_value* value,
+                          std::bitset<N>* filter) {
+  bool constraint_implemented = true;
+  int64_t int_value = sqlite3_value_int64(value);
+  if (IsOpGe(op) || IsOpGt(op)) {
+    // If the operator is gt, then add one to the upper bound.
+    int_value = IsOpGt(op) ? int_value + 1 : int_value;
+
+    // Set to false all values less than |int_value|.
+    size_t ub = static_cast<size_t>(std::max(0l, int_value));
+    ub = std::min(ub, filter->size());
+    for (size_t i = 0; i < ub; i++) {
+      filter->set(i, false);
+    }
+  } else if (IsOpLe(op) || IsOpLt(op)) {
+    // If the operator is lt, then minus one to the lower bound.
+    int_value = IsOpLt(op) ? int_value - 1 : int_value;
+
+    // Set to false all values greater than |int_value|.
+    size_t lb = static_cast<size_t>(std::max(0l, int_value));
+    lb = std::min(lb, filter->size());
+    for (size_t i = lb; i < filter->size(); i++) {
+      filter->set(i, false);
+    }
+  } else if (IsOpEq(op)) {
+    if (int_value >= 0 && static_cast<size_t>(int_value) < filter->size()) {
+      // If the value is in bounds, set all bits to false and restore the value
+      // of the bit at the specified index.
+      bool existing = filter->test(static_cast<size_t>(int_value));
+      filter->reset();
+      filter->set(static_cast<size_t>(int_value), existing);
+    } else {
+      // If the index is out of bounds, nothing should match.
+      filter->reset();
+    }
+  } else {
+    constraint_implemented = false;
+  }
+  return constraint_implemented;
+}
+
+template <class T>
+inline int Compare(T first, T second, bool desc) {
+  if (first < second) {
+    return desc ? 1 : -1;
+  } else if (first > second) {
+    return desc ? -1 : 1;
+  }
+  return 0;
+}
+
+// Compares the slice at index |f| in |f_slices| for CPU |f_cpu| with the
+// slice at index |s| in |s_slices| for CPU |s_cpu| on |column| in either
+// ascending or descending mode depending on |desc|
+// Returns -1 if the first slice is before the second in the ordering, 1 if
+// the first slice is after the second and 0 if they are equal.
+inline int CompareValuesForColumn(uint32_t f_cpu,
+                                  const TraceStorage::SlicesPerCpu& f_slices,
+                                  size_t f,
+                                  uint32_t s_cpu,
+                                  const TraceStorage::SlicesPerCpu& s_slices,
+                                  size_t s,
+                                  SchedSliceTable::Column column,
+                                  bool desc) {
+  switch (column) {
+    case SchedSliceTable::Column::kTimestamp:
+      return Compare(f_slices.start_ns()[f], s_slices.start_ns()[s], desc);
+    case SchedSliceTable::Column::kDuration:
+      return Compare(f_slices.durations()[f], s_slices.durations()[s], desc);
+    case SchedSliceTable::Column::kCpu:
+      return Compare(f_cpu, s_cpu, desc);
+  }
+}
+
+// Creates a vector of indices into the given |slices| sorted by the ordering
+// criteria given by |order_by|.
+std::vector<uint32_t> CreateSortedIndexVector(
+    uint32_t cpu,
+    const TraceStorage::SlicesPerCpu& slices,
+    const std::vector<SchedSliceTable::OrderBy>& order_by) {
+  PERFETTO_CHECK(slices.slice_count() <= std::numeric_limits<uint32_t>::max());
+
+  std::vector<uint32_t> indices;
+  indices.resize(slices.slice_count());
+  std::iota(indices.begin(), indices.end(), 0u);
+  auto callback = [cpu, &order_by, &slices](uint32_t f, uint32_t s) {
+    for (const auto& ob : order_by) {
+      int value = CompareValuesForColumn(cpu, slices, f, cpu, slices, s,
+                                         ob.column, ob.desc);
+      if (value < 0)
+        return true;
+      else if (value > 0)
+        return false;
+    }
+    return false;
+  };
+  std::sort(indices.begin(), indices.end(), callback);
+  return indices;
+}
+
 }  // namespace
 
 SchedSliceTable::SchedSliceTable(const TraceStorage* storage)
@@ -113,29 +219,29 @@
 // Called at least once but possibly many times before filtering things and is
 // the best time to keep track of constriants.
 int SchedSliceTable::BestIndex(sqlite3_index_info* idx) {
-  bool external_ordering_required = false;
-  for (int i = 0; i < idx->nOrderBy; i++) {
-    if (idx->aOrderBy[i].iColumn != Column::kTimestamp ||
-        idx->aOrderBy[i].desc) {
-      // TODO(lalitm): support ordering by other fields.
-      external_ordering_required = true;
-      break;
-    }
-  }
-  idx->orderByConsumed = !external_ordering_required;
-
   indexes_.emplace_back();
-  idx->idxNum = static_cast<int>(indexes_.size() - 1);
-  std::vector<Constraint>* constraints = &indexes_.back();
+  IndexInfo* index = &indexes_.back();
+  for (int i = 0; i < idx->nOrderBy; i++) {
+    index->order_by.emplace_back();
+
+    OrderBy* order = &index->order_by.back();
+    order->column = static_cast<Column>(idx->aOrderBy[i].iColumn);
+    order->desc = idx->aOrderBy[i].desc;
+  }
+  idx->orderByConsumed = true;
+
   for (int i = 0; i < idx->nConstraint; i++) {
     const auto& cs = idx->aConstraint[i];
     if (!cs.usable)
       continue;
-    constraints->emplace_back(cs);
+    index->constraints.emplace_back(cs);
 
     // argvIndex is 1-based so use the current size of the vector.
-    idx->aConstraintUsage[i].argvIndex = static_cast<int>(constraints->size());
+    int argv_index = static_cast<int>(index->constraints.size());
+    idx->aConstraintUsage[i].argvIndex = argv_index;
   }
+  idx->idxNum = static_cast<int>(indexes_.size() - 1);
+
   return SQLITE_OK;
 }
 
@@ -154,34 +260,31 @@
   // Reset the filter state.
   filter_state_ = {};
 
-  const auto& constraints = table_->indexes_[static_cast<size_t>(idxNum)];
-  PERFETTO_CHECK(constraints.size() == static_cast<size_t>(argc));
-  for (size_t i = 0; i < constraints.size(); i++) {
-    const auto& cs = constraints[i];
+  std::bitset<TraceStorage::kMaxCpus> cpu_filter;
+  const auto& index = table_->indexes_[static_cast<size_t>(idxNum)];
+  PERFETTO_CHECK(index.constraints.size() == static_cast<size_t>(argc));
+  for (size_t i = 0; i < index.constraints.size(); i++) {
+    const auto& cs = index.constraints[i];
     switch (cs.iColumn) {
-      case Column::kTimestamp:
-        filter_state_.timestamp_constraints.Initialize(cs, argv[i]);
-        break;
       case Column::kCpu:
-        filter_state_.cpu_constraints.Initialize(cs, argv[i]);
+        PopulateFilterBitmap(cs.op, argv[i], &cpu_filter);
         break;
     }
   }
 
+  // Update the filter state with the order by info.
+  *filter_state_.order_by() = std::move(index.order_by);
+
   // First setup CPU filtering because the trace storage is indexed by CPU.
   for (uint32_t cpu = 0; cpu < TraceStorage::kMaxCpus; cpu++) {
-    const auto& slices = storage_->SlicesForCpu(cpu);
-
-    // Start by setting index out of bounds if filtering below doesn't
-    // yield any results.
-    PerCpuState* state = &filter_state_.per_cpu_state[cpu];
-    state->index = slices.slice_count();
-
-    if (!filter_state_.cpu_constraints.Matches(cpu))
+    if (!cpu_filter.test(cpu))
       continue;
 
-    // Filter on other constraints now.
-    FindNextSliceForCpu(cpu, 0ul /* start_index */);
+    PerCpuState* state = filter_state_.StateForCpu(cpu);
+
+    // Create a sorted index vector based on the order by requirements.
+    *state->sorted_row_ids() = CreateSortedIndexVector(
+        cpu, storage_->SlicesForCpu(cpu), *filter_state_.order_by());
   }
 
   // Set the cpu index to be the first item to look at.
@@ -192,37 +295,40 @@
 }
 
 int SchedSliceTable::Cursor::Next() {
-  uint32_t cpu = static_cast<uint32_t>(filter_state_.next_slice_cpu);
-  FindNextSliceForCpu(cpu, filter_state_.per_cpu_state[cpu].index + 1);
+  uint32_t cpu = filter_state_.next_cpu();
+  auto* state = filter_state_.StateForCpu(cpu);
+
+  // TODO(lalitm): maybe one day we may want to filter more efficiently. If so
+  // update this method with filter logic.
+  state->set_next_row_id_index(state->next_row_id_index() + 1);
+
   FindNextSliceAmongCpus();
   return SQLITE_OK;
 }
 
 int SchedSliceTable::Cursor::Eof() {
-  return filter_state_.next_slice_cpu >= filter_state_.per_cpu_state.size();
+  return !filter_state_.IsNextCpuValid();
 }
 
 int SchedSliceTable::Cursor::Column(sqlite3_context* context, int N) {
-  if (filter_state_.next_slice_cpu >= filter_state_.per_cpu_state.size()) {
+  if (!filter_state_.IsNextCpuValid())
     return SQLITE_ERROR;
-  }
-  uint32_t cpu_index = static_cast<uint32_t>(filter_state_.next_slice_cpu);
-  const auto& state = filter_state_.per_cpu_state[cpu_index];
-  const auto& slices = storage_->SlicesForCpu(cpu_index);
+
+  uint32_t cpu = filter_state_.next_cpu();
+  size_t row = filter_state_.StateForCpu(cpu)->next_row_id();
+  const auto& slices = storage_->SlicesForCpu(cpu);
   switch (N) {
     case Column::kTimestamp: {
-      auto timestamp =
-          static_cast<sqlite3_int64>(slices.start_ns()[state.index]);
+      auto timestamp = static_cast<sqlite3_int64>(slices.start_ns()[row]);
       sqlite3_result_int64(context, timestamp);
       break;
     }
     case Column::kCpu: {
-      sqlite3_result_int(context, static_cast<int>(cpu_index));
+      sqlite3_result_int(context, static_cast<int>(cpu));
       break;
     }
     case Column::kDuration: {
-      auto duration =
-          static_cast<sqlite3_int64>(slices.durations()[state.index]);
+      auto duration = static_cast<sqlite3_int64>(slices.durations()[row]);
       sqlite3_result_int64(context, duration);
       break;
     }
@@ -234,68 +340,40 @@
   return SQLITE_ERROR;
 }
 
-void SchedSliceTable::Cursor::FindNextSliceForCpu(uint32_t cpu,
-                                                  size_t start_index) {
-  auto* state = &filter_state_.per_cpu_state[cpu];
-  const auto& slices = storage_->SlicesForCpu(cpu);
-
-  // Store the position we should start filtering from before setting
-  // the index out of bounds in case the loop doesn't match anything.
-  state->index = slices.slice_count();
-
-  for (size_t i = start_index; i < slices.slice_count(); i++) {
-    if (filter_state_.timestamp_constraints.Matches(slices.start_ns()[i])) {
-      state->index = i;
-      break;
-    }
-  }
-}
-
 void SchedSliceTable::Cursor::FindNextSliceAmongCpus() {
-  filter_state_.next_slice_cpu = filter_state_.per_cpu_state.size();
+  filter_state_.InvalidateNextCpu();
 
-  uint64_t min_timestamp = std::numeric_limits<uint64_t>::max();
-  for (uint32_t i = 0; i < filter_state_.per_cpu_state.size(); i++) {
-    const auto& cpu_state = filter_state_.per_cpu_state[i];
-    const auto& slices = storage_->SlicesForCpu(i);
-    if (cpu_state.index >= slices.slice_count())
+  for (uint32_t cpu = 0; cpu < TraceStorage::kMaxCpus; cpu++) {
+    const auto& cpu_state = *filter_state_.StateForCpu(cpu);
+    if (!cpu_state.IsNextRowIdIndexValid())
       continue;
 
-    // TODO(lalitm): handle sorting by things other than timestamp.
-    uint64_t cur_timestamp = slices.start_ns()[cpu_state.index];
-    if (cur_timestamp < min_timestamp) {
-      min_timestamp = cur_timestamp;
-      filter_state_.next_slice_cpu = i;
+    // The first CPU with a valid slice can be set to the next CPU.
+    if (!filter_state_.IsNextCpuValid()) {
+      filter_state_.set_next_cpu(cpu);
+      continue;
+    }
+
+    uint32_t cur_cpu = filter_state_.next_cpu();
+    const auto& cur_slices = storage_->SlicesForCpu(cur_cpu);
+    size_t cur_row = filter_state_.StateForCpu(cur_cpu)->next_row_id();
+
+    const auto& slices = storage_->SlicesForCpu(cpu);
+    size_t row = cpu_state.next_row_id();
+    for (const auto& ob : *filter_state_.order_by()) {
+      int ret = CompareValuesForColumn(cpu, slices, row, cur_cpu, cur_slices,
+                                       cur_row, ob.column, ob.desc);
+      if (ret < 0) {
+        filter_state_.set_next_cpu(cpu);
+        break;
+      } else if (ret > 0) {
+        // If the cpu we are iterating over is not ordered before the current
+        // lowest CPU, then exit the loop.
+        break;
+      }
     }
   }
 }
 
-template <typename T>
-bool SchedSliceTable::Cursor::NumericConstraints<T>::Initialize(
-    const Constraint& cs,
-    sqlite3_value* value) {
-  bool is_integral = std::is_integral<T>::value;
-  PERFETTO_DCHECK(is_integral ? sqlite3_value_type(value) == SQLITE_INTEGER
-                              : sqlite3_value_type(value) == SQLITE_FLOAT);
-  bool constraint_implemented = true;
-  T const_value = static_cast<T>(is_integral ? sqlite3_value_int64(value)
-                                             : sqlite3_value_double(value));
-  if (IsOpGe(cs.op) || IsOpGt(cs.op)) {
-    min_value = const_value;
-    min_equals = IsOpGt(cs.op);
-  } else if (IsOpLe(cs.op) || IsOpLt(cs.op)) {
-    max_value = const_value;
-    max_equals = IsOpLt(cs.op);
-  } else if (IsOpEq(cs.op)) {
-    max_value = const_value;
-    max_equals = true;
-    min_value = const_value;
-    min_equals = true;
-  } else {
-    constraint_implemented = false;
-  }
-  return constraint_implemented;
-}
-
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/sched_slice_table.h b/src/trace_processor/sched_slice_table.h
index 612f911..ecdb252 100644
--- a/src/trace_processor/sched_slice_table.h
+++ b/src/trace_processor/sched_slice_table.h
@@ -29,6 +29,17 @@
  public:
   using Constraint = sqlite3_index_info::sqlite3_index_constraint;
 
+  enum Column { kTimestamp = 0, kCpu = 1, kDuration = 2 };
+  struct OrderBy {
+    Column column = kTimestamp;
+    bool desc = false;
+  };
+
+  struct IndexInfo {
+    std::vector<OrderBy> order_by;
+    std::vector<Constraint> constraints;
+  };
+
   class Cursor {
    public:
     Cursor(SchedSliceTable* table, const TraceStorage* storage);
@@ -41,41 +52,56 @@
     int RowId(sqlite_int64* pRowid);
 
    private:
-    template <class T>
-    class NumericConstraints {
+    class PerCpuState {
      public:
-      bool Initialize(const Constraint& cs, sqlite3_value* value);
-      bool Matches(T value) {
-        if (value < min_value || (value == min_value && !min_equals)) {
-          return false;
-        } else if (value > max_value || (value == max_value && !max_equals)) {
-          return false;
-        }
-        return true;
+      bool IsNextRowIdIndexValid() const {
+        return next_row_id_index_ < sorted_row_ids_.size();
       }
 
+      uint32_t next_row_id_index() const { return next_row_id_index_; }
+
+      void set_next_row_id_index(uint32_t index) {
+        PERFETTO_DCHECK(next_row_id_index_ < sorted_row_ids_.size());
+        next_row_id_index_ = index;
+      }
+
+      std::vector<uint32_t>* sorted_row_ids() { return &sorted_row_ids_; }
+
+      size_t next_row_id() const { return sorted_row_ids_[next_row_id_index_]; }
+
      private:
-      T min_value = std::numeric_limits<T>::min();
-      bool min_equals = true;
-      T max_value = std::numeric_limits<T>::max();
-      bool max_equals = true;
+      // Vector of row ids sorted by the the given order by constraints.
+      std::vector<uint32_t> sorted_row_ids_;
+
+      // An offset into |sorted_row_ids_| indicating the next row to return.
+      uint32_t next_row_id_index_ = 0;
     };
 
-    struct PerCpuState {
-      size_t index = 0;
-    };
+    class FilterState {
+     public:
+      PerCpuState* StateForCpu(uint32_t cpu) { return &per_cpu_state_[cpu]; }
 
-    struct FilterState {
+      void InvalidateNextCpu() { next_cpu_ = per_cpu_state_.size(); }
+
+      bool IsNextCpuValid() const { return next_cpu_ < per_cpu_state_.size(); }
+
+      uint32_t next_cpu() const { return static_cast<uint32_t>(next_cpu_); }
+
+      void set_next_cpu(uint32_t cpu) {
+        PERFETTO_DCHECK(cpu < per_cpu_state_.size());
+        next_cpu_ = cpu;
+      }
+
+      std::vector<OrderBy>* order_by() { return &order_by_; }
+
+     private:
       // One entry for each cpu which is used in filtering.
-      std::array<PerCpuState, TraceStorage::kMaxCpus> per_cpu_state;
-      size_t next_slice_cpu = 0;
+      std::array<PerCpuState, TraceStorage::kMaxCpus> per_cpu_state_;
+      size_t next_cpu_ = 0;
 
-      NumericConstraints<uint64_t> timestamp_constraints;
-      NumericConstraints<uint32_t> cpu_constraints;
+      std::vector<OrderBy> order_by_;
     };
 
-    void FindNextSliceForCpu(uint32_t cpu, size_t start_index);
-
     void FindNextSliceAmongCpus();
 
     sqlite3_vtab_cursor base_;  // Must be first.
@@ -90,18 +116,14 @@
   static sqlite3_module CreateModule();
 
   int BestIndex(sqlite3_index_info* index_info);
-
   int Open(sqlite3_vtab_cursor** ppCursor);
 
  private:
-  enum Column { kTimestamp = 0, kCpu = 1, kDuration = 2 };
-
   sqlite3_vtab base_;  // Must be first.
   const TraceStorage* const storage_;
 
-  // This vector contains one outer entry for each xBestIndex call and one
-  // inner entry for each constraint provided by that xBestIndex call.
-  std::vector<std::vector<Constraint>> indexes_;
+  // One entry for each BestIndex call.
+  std::vector<IndexInfo> indexes_;
 };
 
 }  // namespace trace_processor
diff --git a/src/traced/probes/ftrace/ftrace_procfs.cc b/src/traced/probes/ftrace/ftrace_procfs.cc
index 145d0df..87095cf 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs.cc
@@ -189,6 +189,8 @@
     end = s.find(' ', start);
     if (end == std::string::npos)
       end = s.size();
+    while (end > start && s[end - 1] == '\n')
+      end--;
     if (start == end)
       break;
 
diff --git a/src/traced/probes/ftrace/ftrace_procfs_unittest.cc b/src/traced/probes/ftrace/ftrace_procfs_unittest.cc
index 053a87d..cac3360 100644
--- a/src/traced/probes/ftrace/ftrace_procfs_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs_unittest.cc
@@ -56,8 +56,44 @@
   EXPECT_THAT(ftrace.GetClock(), "global");
 
   EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .WillOnce(Return("local global [boot]"));
+  EXPECT_THAT(ftrace.GetClock(), "boot");
+
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .WillOnce(Return(""));
   EXPECT_THAT(ftrace.AvailableClocks(), IsEmpty());
+
+  // trace_clock text may end in a new line:
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .WillOnce(Return("[local] global boot\n"));
+  EXPECT_THAT(ftrace.AvailableClocks(),
+              UnorderedElementsAre("local", "global", "boot"));
+
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .WillOnce(Return("local global [boot]\n"));
+  EXPECT_THAT(ftrace.AvailableClocks(),
+              UnorderedElementsAre("local", "global", "boot"));
+
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .WillOnce(Return("local global [boot]\n"));
+  EXPECT_THAT(ftrace.GetClock(), "boot");
+
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .WillOnce(Return("\n"));
+  EXPECT_THAT(ftrace.AvailableClocks(), IsEmpty());
+
+  // We should handle many newlines (just in case):
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .WillOnce(Return("local global [boot]\n\n\n"));
+  EXPECT_THAT(ftrace.GetClock(), "boot");
+
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .WillOnce(Return("local global [boot]\n\n"));
+  EXPECT_THAT(ftrace.GetClock(), "boot");
+
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .WillOnce(Return("\n\n\n\n"));
+  EXPECT_THAT(ftrace.AvailableClocks(), IsEmpty());
 }
 
 }  // namespace
diff --git a/src/traced/probes/ftrace/proto_translation_table_unittest.cc b/src/traced/probes/ftrace/proto_translation_table_unittest.cc
index b2b07dd..fe296d0 100644
--- a/src/traced/probes/ftrace/proto_translation_table_unittest.cc
+++ b/src/traced/probes/ftrace/proto_translation_table_unittest.cc
@@ -178,6 +178,7 @@
 print fmt: "some format")"));
   ;
 
+  EXPECT_CALL(ftrace, ReadPageHeaderFormat()).Times(AnyNumber());
   EXPECT_CALL(ftrace, ReadEventFormat(_, _)).Times(AnyNumber());
 
   {
diff --git a/ui/BUILD.gn b/ui/BUILD.gn
index e50fa60..3a98f45 100644
--- a/ui/BUILD.gn
+++ b/ui/BUILD.gn
@@ -66,60 +66,82 @@
 }
 
 # +----------------------------------------------------------------------------+
+# | Template for "sorcery" the source map resolver.                            |
+# +----------------------------------------------------------------------------+
+template("sorcery") {
+  node_bin(target_name) {
+    assert(defined(invoker.input))
+    assert(defined(invoker.output))
+    forward_variables_from(invoker, [ "deps" ])
+    inputs = [
+      invoker.input,
+    ]
+    outputs = [
+      invoker.output,
+      invoker.output + ".map",
+    ]
+    node_cmd = "sorcery"
+    args = [
+      "-i",
+      rebase_path(invoker.input, root_build_dir),
+      "-o",
+      rebase_path(invoker.output, root_build_dir),
+    ]
+  }
+}
+
+# +----------------------------------------------------------------------------+
+# | Template for bundling js                                                   |
+# +----------------------------------------------------------------------------+
+template("bundle") {
+  node_bin(target_name) {
+    assert(defined(invoker.input))
+    assert(defined(invoker.output))
+    forward_variables_from(invoker, [ "deps" ])
+    inputs = [
+      invoker.input,
+      "rollup.config.js",
+    ]
+    outputs = [
+      invoker.output,
+      invoker.output + ".map",
+    ]
+    node_cmd = "rollup"
+    args = [
+      "-c",
+      rebase_path("rollup.config.js", root_build_dir),
+      rebase_path(invoker.input, root_build_dir),
+      "-o",
+      rebase_path(invoker.output, root_build_dir),
+      "-f",
+      "iife",
+      "-m",
+      "--silent",
+    ]
+  }
+}
+
+# +----------------------------------------------------------------------------+
 # | Bundles all *.js files together resolving CommonJS require() deps.         |
 # +----------------------------------------------------------------------------+
 
 # Bundle together all js sources into a bundle.js file, that will ultimately be
 # included by the .html files.
 
-node_bin("main_bundle") {
+bundle("main_bundle") {
   deps = [
     ":transpile_all_ts",
   ]
-  node_cmd = "rollup"
-  inputs = [
-    "$target_out_dir/main.js",
-    "rollup.config.js",
-  ]
-  outputs = [
-    "$target_out_dir/main_bundle.js",
-  ]
-  args = [
-    "-c",
-    rebase_path("rollup.config.js", root_build_dir),
-    rebase_path(inputs[0], root_build_dir),
-    "-o",
-    rebase_path(outputs[0], root_build_dir),
-    "-f",
-    "iife",
-    "-m",
-    "--silent",
-  ]
+  input = "$target_out_dir/main.js"
+  output = "$target_out_dir/main_bundle.js"
 }
 
-node_bin("worker_bundle") {
+bundle("worker_bundle") {
   deps = [
     ":transpile_all_ts",
   ]
-  node_cmd = "rollup"
-  inputs = [
-    "$target_out_dir/worker.js",
-    "rollup.config.js",
-  ]
-  outputs = [
-    "$target_out_dir/worker_bundle.js",
-  ]
-  args = [
-    "-c",
-    rebase_path("rollup.config.js", root_build_dir),
-    rebase_path(inputs[0], root_build_dir),
-    "-o",
-    rebase_path(outputs[0], root_build_dir),
-    "-f",
-    "iife",
-    "-m",
-    "--silent",
-  ]
+  input = "$target_out_dir/worker.js"
+  output = "$target_out_dir/worker_bundle.js"
 }
 
 # +----------------------------------------------------------------------------+
@@ -226,42 +248,20 @@
   ]
 }
 
-node_bin("main_bundle_release") {
+sorcery("main_bundle_release") {
   deps = [
     ":main_bundle",
   ]
-  inputs = [
-    "$target_out_dir/main_bundle.js",
-  ]
-  outputs = [
-    "$ui_dir/main_bundle.js",
-  ]
-  node_cmd = "sorcery"
-  args = [
-    "-i",
-    rebase_path(inputs[0], root_build_dir),
-    "-o",
-    rebase_path(outputs[0], root_build_dir),
-  ]
+  input = "$target_out_dir/main_bundle.js"
+  output = "$ui_dir/main_bundle.js"
 }
 
-node_bin("worker_bundle_release") {
+sorcery("worker_bundle_release") {
   deps = [
     ":worker_bundle",
   ]
-  inputs = [
-    "$target_out_dir/worker_bundle.js",
-  ]
-  outputs = [
-    "$ui_dir/worker_bundle.js",
-  ]
-  node_cmd = "sorcery"
-  args = [
-    "-i",
-    rebase_path(inputs[0], root_build_dir),
-    "-o",
-    rebase_path(outputs[0], root_build_dir),
-  ]
+  input = "$target_out_dir/worker_bundle.js"
+  output = "$ui_dir/worker_bundle.js"
 }
 
 copy("wasm_release") {