Merge "Fix flags checks in V4L2Tracker"
diff --git a/BUILD b/BUILD
index 7f3dc0f..bf4ded4 100644
--- a/BUILD
+++ b/BUILD
@@ -1838,6 +1838,7 @@
         "src/trace_processor/trace_processor_storage_impl.h",
         "src/trace_processor/trace_sorter.cc",
         "src/trace_processor/trace_sorter.h",
+        "src/trace_processor/trace_sorter_internal.h",
         "src/trace_processor/trace_sorter_queue.h",
         "src/trace_processor/virtual_destructors.cc",
     ],
diff --git a/CHANGELOG b/CHANGELOG
index bdfce0f..b735102 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,15 +2,34 @@
   Tracing service and probes:
     *
   Trace Processor:
-    * Scheduling: fixed parsing of "R+" (preempted) and "I" (idle kernel
-      thread) end states in |sched.end_state| for traces collected on Linux
-      kernels v4.14 and above. Previously, preemption was not recognised, and
-      idle was reported as "x" (task dead). See commit c60a630cfe0.
+    *
   UI:
     *
   SDK:
     *
 
+
+v30.0 - 2022-10-06:
+  Trace Processor:
+    * Fixed parsing of "R+" (preempted) and "I" (idle kernel thread) end states
+      of sched_switch events, collected on Linux kernels v4.14 and above.
+      Previously, preemption was not recognised, and idle was reported as
+      "x" (task dead). See commit c60a630cfe0.
+    * Add support for parsing sys_write syscalls.
+    * Remove the thread_slice table: all columns have moved to the slice table
+      and thread_slice exists as a view for backwards compatibility. This view
+      will also be removed in the future
+    * Add Base64 encode SQL function.
+    * Add support for importing function graph ftrace events.
+    * Add support for importing V4L2 ftrace events.
+    * Add support for importing virtio-video ftrace events.
+  UI:
+    * Fix downloading profiles from flamegraphs.
+    * Enable Pivot table support by default.
+  SDK:
+    * Add support for disallowing concurrent tracing sessions.
+
+
 v29.0 - 2022-09-06:
   Tracing service and probes:
     * Add support for only tracing selected syscalls. By selecting only syscalls
diff --git a/meson.build b/meson.build
index 0601514..5295c52 100644
--- a/meson.build
+++ b/meson.build
@@ -18,8 +18,8 @@
 
 project(
     'perfetto',
-    ['c','cpp'],
-    default_options: ['c_std=c99', 'cpp_std=c++11']
+    ['cpp'],
+    default_options: ['cpp_std=c++11']
 )
 
 fs = import('fs')
@@ -28,18 +28,32 @@
     error('sdk dir not found, please checkout a release tag, e.g. v14.0')
 endif
 
-dep_threads = dependency('threads')
+cpp = meson.get_compiler('cpp')
+
+deps_perfetto = [dependency('threads')]
+
+if host_machine.system() == 'android'
+  deps_perfetto += cpp.find_library('log')
+endif
 
 lib_perfetto = static_library(
     'perfetto',
     sources: 'sdk/perfetto.cc',
-    dependencies: dep_threads,
+    dependencies: deps_perfetto,
     install: true,
 )
 
 inc_perfetto = include_directories('sdk')
 
+dir_perfetto_trace = join_paths(meson.current_source_dir(),
+                                'protos/perfetto/trace')
+
+install_data(dir_perfetto_trace / 'perfetto_trace.proto')
+
 dep_perfetto = declare_dependency(
     link_with: lib_perfetto,
     include_directories: inc_perfetto,
+    variables: {
+        'pkgdatadir': dir_perfetto_trace,
+    }
 )
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 3a9e151..6975781 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -164,6 +164,7 @@
     "trace_processor_storage_impl.h",
     "trace_sorter.cc",
     "trace_sorter.h",
+    "trace_sorter_internal.h",
     "trace_sorter_queue.h",
     "virtual_destructors.cc",
   ]
diff --git a/src/trace_processor/metrics/sql/android/android_startup.sql b/src/trace_processor/metrics/sql/android/android_startup.sql
index d534274..6204ced 100644
--- a/src/trace_processor/metrics/sql/android/android_startup.sql
+++ b/src/trace_processor/metrics/sql/android/android_startup.sql
@@ -399,6 +399,10 @@
           'broadcastReceiveReg*'
         ) > 10
 
+        UNION ALL
+        SELECT 'No baseline or cloud profiles'
+        Where MISSING_BASELINE_PROFILE_FOR_LAUNCH(launches.id, launches.package)
+
       )
     )
   ) as startup
diff --git a/src/trace_processor/metrics/sql/android/startup/slice_functions.sql b/src/trace_processor/metrics/sql/android/startup/slice_functions.sql
index 509e3bb..bf8048c 100644
--- a/src/trace_processor/metrics/sql/android/startup/slice_functions.sql
+++ b/src/trace_processor/metrics/sql/android/startup/slice_functions.sql
@@ -151,3 +151,28 @@
           )
   '
 );
+
+-- Given a launch id and package name, returns if baseline or cloud profile is missing.
+SELECT CREATE_FUNCTION(
+  'MISSING_BASELINE_PROFILE_FOR_LAUNCH(launch_id LONG, pkg_name STRING)',
+  'BOOL',
+  '
+    SELECT (COUNT(slice_name) > 0)
+    FROM (
+      SELECT *
+      FROM SLICES_FOR_LAUNCH_AND_SLICE_NAME(
+        $launch_id,
+        "location=* status=* filter=* reason=*"
+      )
+      ORDER BY slice_name
+    )
+    WHERE
+      -- when location is the package odex file and the reason is "install" or "install-dm",
+      -- if the compilation filter is not "speed-profile", baseline/cloud profile is missing.
+      SUBSTR(STR_SPLIT(slice_name, " status=", 0), LENGTH("location=") + 1)
+        LIKE ("%" || $pkg_name || "%odex")
+      AND (STR_SPLIT(slice_name, " reason=", 1) = "install"
+        OR STR_SPLIT(slice_name, " reason=", 1) = "install-dm")
+      AND STR_SPLIT(STR_SPLIT(slice_name, " filter=", 1), " reason=", 0) != "speed-profile"
+  '
+);
diff --git a/src/trace_processor/parser_types.h b/src/trace_processor/parser_types.h
index b47b3af..848e024 100644
--- a/src/trace_processor/parser_types.h
+++ b/src/trace_processor/parser_types.h
@@ -56,6 +56,9 @@
                  RefPtr<PacketSequenceStateGeneration> generation)
       : TracePacketData{std::move(pv), std::move(generation)} {}
 
+  explicit TrackEventData(TracePacketData tpd)
+      : TracePacketData(std::move(tpd)) {}
+
   static constexpr size_t kMaxNumExtraCounters = 8;
 
   base::Optional<int64_t> thread_timestamp;
diff --git a/src/trace_processor/trace_sorter.h b/src/trace_processor/trace_sorter.h
index 13bff30..4f00832 100644
--- a/src/trace_processor/trace_sorter.h
+++ b/src/trace_processor/trace_sorter.h
@@ -211,7 +211,7 @@
   int64_t max_timestamp() const { return global_max_ts_; }
 
  private:
-  // Stores offset and type of metadat.
+  // Stores offset and type of metadata.
   struct Descriptor {
    public:
     static constexpr uint8_t kTypeBits = 4;
diff --git a/src/trace_processor/trace_sorter_internal.h b/src/trace_processor/trace_sorter_internal.h
new file mode 100644
index 0000000..9412fca
--- /dev/null
+++ b/src/trace_processor/trace_sorter_internal.h
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2022 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 SRC_TRACE_PROCESSOR_TRACE_SORTER_INTERNAL_H_
+#define SRC_TRACE_PROCESSOR_TRACE_SORTER_INTERNAL_H_
+
+#include <deque>
+#include "perfetto/base/logging.h"
+#include "src/trace_processor/parser_types.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace trace_sorter_internal {
+
+// Moves object to the specified pointer and returns the pointer to the space
+// behind it.
+template <typename T>
+char* AppendUnchecked(char* ptr, T value) {
+  PERFETTO_DCHECK(reinterpret_cast<uintptr_t>(ptr) % alignof(T) == 0);
+  new (ptr) T(std::move(value));
+  return ptr + sizeof(T);
+}
+
+// Evicts object the the specified pointer, which now points to the space behind
+// it.
+template <typename T>
+T EvictUnchecked(char** ptr) {
+  PERFETTO_DCHECK(reinterpret_cast<uintptr_t>(*ptr) % alignof(T) == 0);
+  T* type_ptr = reinterpret_cast<T*>(*ptr);
+  T out(std::move(*type_ptr));
+  type_ptr->~T();
+  *ptr += sizeof(T);
+  return out;
+}
+
+// Stores details of TrackEventData: presence of attributes and the
+// lenght of the array.
+struct TrackEventDataDescriptor {
+ public:
+  static constexpr uint64_t kBitsForCounterValues = 4;
+  static constexpr uint64_t kThreadTimestampMask =
+      1 << (kBitsForCounterValues + 1);
+  static constexpr uint64_t kThreadInstructionCountMask =
+      1 << kBitsForCounterValues;
+
+  TrackEventDataDescriptor(bool has_thread_timestamp,
+                           bool has_thread_instruction_count,
+                           uint64_t number_of_counter_values)
+      : packed_value_(GetPacketValue(has_thread_timestamp,
+                                     has_thread_instruction_count,
+                                     number_of_counter_values)) {
+    PERFETTO_DCHECK(number_of_counter_values <=
+                    TrackEventData::kMaxNumExtraCounters);
+  }
+
+  explicit TrackEventDataDescriptor(const TrackEventData& ted)
+      : TrackEventDataDescriptor(ted.thread_timestamp.has_value(),
+                                 ted.thread_instruction_count.has_value(),
+                                 CountNumberOfCounterValues(ted)) {
+    static_assert(
+        TrackEventData::kMaxNumExtraCounters < (1 << kBitsForCounterValues),
+        "kMaxNumExtraCounters can't be compressed properly");
+  }
+
+  static uint64_t CountNumberOfCounterValues(const TrackEventData& ted) {
+    uint32_t num = 0;
+    for (; num < TrackEventData::kMaxNumExtraCounters; ++num) {
+      if (std::equal_to<double>()(ted.extra_counter_values[num], 0)) {
+        break;
+      }
+    }
+    return num;
+  }
+
+  static uint64_t GetPacketValue(bool has_thread_timestamp,
+                                 bool has_thread_instruction_count,
+                                 uint64_t number_of_counter_values) {
+    return (static_cast<uint64_t>(has_thread_timestamp)
+            << (kBitsForCounterValues + 1)) |
+           (static_cast<uint64_t>(has_thread_instruction_count)
+            << kBitsForCounterValues) |
+           number_of_counter_values;
+  }
+
+  bool HasThreadTimestamp() const {
+    return static_cast<bool>(packed_value_ & kThreadTimestampMask);
+  }
+
+  bool HasThreadInstructionCount() const {
+    return static_cast<bool>(packed_value_ & kThreadInstructionCountMask);
+  }
+
+  uint64_t NumberOfCounterValues() const {
+    return static_cast<uint64_t>(
+        packed_value_ & static_cast<uint64_t>(~(3 << kBitsForCounterValues)));
+  }
+
+  uint64_t AppendedSize() const {
+    return sizeof(TracePacketData) +
+           8l * (/*counter_value*/ 1 + HasThreadTimestamp() +
+                 HasThreadInstructionCount() + NumberOfCounterValues());
+  }
+
+ private:
+  // uint8_t would be enough to hold all of the required data, but we need 8
+  // bytes type for alignment.
+  uint64_t packed_value_ = 0;
+};
+
+// Adds and removes object of the type from queue memory. Can be overriden
+// for more specific functionality related to a type. All child classes
+// should implement the same interface.
+template <typename T>
+class TypedMemoryAccessor {
+ public:
+  static char* Append(char* ptr, T value) {
+    return AppendUnchecked(ptr, std::move(value));
+  }
+  static T Evict(char* ptr) { return EvictUnchecked<T>(&ptr); }
+  static uint64_t AppendSize(const T&) {
+    return static_cast<uint64_t>(sizeof(T));
+  }
+};
+
+// Responsibe for accessing memory in the queue related to TrackEventData.
+// Appends the struct more efficiently by compressing and decompressing some
+// of TrackEventData attributes.
+template <>
+class TypedMemoryAccessor<TrackEventData> {
+ public:
+  static char* Append(char* ptr, TrackEventData ted) {
+    auto ted_desc = TrackEventDataDescriptor(ted);
+    ptr = AppendUnchecked(ptr, ted_desc);
+    ptr = AppendUnchecked(ptr, TracePacketData{std::move(ted.packet),
+                                               std::move(ted.sequence_state)});
+    ptr = AppendUnchecked(ptr, ted.counter_value);
+    if (ted_desc.HasThreadTimestamp()) {
+      ptr = AppendUnchecked(ptr, ted.thread_timestamp.value());
+    }
+    if (ted_desc.HasThreadInstructionCount()) {
+      ptr = AppendUnchecked(ptr, ted.thread_instruction_count.value());
+    }
+    for (uint32_t i = 0; i < ted_desc.NumberOfCounterValues(); i++) {
+      ptr = AppendUnchecked(ptr, ted.extra_counter_values[i]);
+    }
+    return ptr;
+  }
+
+  static TrackEventData Evict(char* ptr) {
+    auto ted_desc = EvictUnchecked<TrackEventDataDescriptor>(&ptr);
+    TrackEventData ted(EvictUnchecked<TracePacketData>(&ptr));
+    ted.counter_value = EvictUnchecked<double>(&ptr);
+    if (ted_desc.HasThreadTimestamp()) {
+      ted.thread_timestamp = EvictUnchecked<int64_t>(&ptr);
+    }
+    if (ted_desc.HasThreadInstructionCount()) {
+      ted.thread_instruction_count = EvictUnchecked<int64_t>(&ptr);
+    }
+    for (uint32_t i = 0; i < ted_desc.NumberOfCounterValues(); i++) {
+      ted.extra_counter_values[i] = EvictUnchecked<double>(&ptr);
+    }
+    return ted;
+  }
+
+  static uint64_t AppendSize(const TrackEventData& value) {
+    return static_cast<uint64_t>(sizeof(TrackEventDataDescriptor)) +
+           TrackEventDataDescriptor(value).AppendedSize();
+  }
+};
+
+}  // namespace trace_sorter_internal
+}  // namespace trace_processor
+}  // namespace perfetto
+#endif  // SRC_TRACE_PROCESSOR_TRACE_SORTER_INTERNAL_H_
diff --git a/src/trace_processor/trace_sorter_queue.h b/src/trace_processor/trace_sorter_queue.h
index a5646d1..a1edf28 100644
--- a/src/trace_processor/trace_sorter_queue.h
+++ b/src/trace_processor/trace_sorter_queue.h
@@ -17,9 +17,11 @@
 #ifndef SRC_TRACE_PROCESSOR_TRACE_SORTER_QUEUE_H_
 #define SRC_TRACE_PROCESSOR_TRACE_SORTER_QUEUE_H_
 
+#include <cstddef>
 #include <deque>
+#include "perfetto/base/logging.h"
 #include "perfetto/ext/base/utils.h"
-#include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/trace_sorter_internal.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -54,12 +56,12 @@
   uint32_t Append(T value) {
     PERFETTO_DCHECK(!mem_blocks_.empty());
 
-    if (PERFETTO_UNLIKELY(!mem_blocks_.back().HasSpace<T>())) {
+    uint64_t size = Block::AppendSize<T>(value);
+    if (PERFETTO_UNLIKELY(!mem_blocks_.back().HasSpace(size))) {
       mem_blocks_.emplace_back(Block(block_size_));
     }
-
     auto& back_block = mem_blocks_.back();
-    PERFETTO_DCHECK(back_block.HasSpace<T>());
+    PERFETTO_DCHECK(back_block.HasSpace(size));
     return GlobalMemOffsetFromLastBlockOffset(
         back_block.Append(std::move(value)));
   }
@@ -100,36 +102,27 @@
           storage_(
               base::AlignedAllocTyped<uint64_t>(size_ / sizeof(uint64_t))) {}
 
-    template <typename T>
-    bool HasSpace() const {
-#if PERFETTO_DCHECK_IS_ON()
-      return sizeof(T) + sizeof(uint64_t) <= size_ - offset_;
-#else
-      return sizeof(T) <= size_ - offset_;
-#endif
-    }
+    bool HasSpace(uint64_t size) const { return size <= size_ - offset_; }
 
     template <typename T>
     uint32_t Append(T value) {
       static_assert(alignof(T) <= 8,
                     "Class must have at most 8 byte alignment");
-
       PERFETTO_DCHECK(offset_ % 8 == 0);
-      PERFETTO_DCHECK(HasSpace<T>());
+      PERFETTO_DCHECK(HasSpace(AppendSize(value)));
 
-      uint32_t cur_offset = offset_;
       char* storage_begin_ptr = reinterpret_cast<char*>(storage_.get());
-      char* ptr = storage_begin_ptr + cur_offset;
+      char* ptr = storage_begin_ptr + offset_;
+
 #if PERFETTO_DCHECK_IS_ON()
-      uint64_t* size_ptr = reinterpret_cast<uint64_t*>(ptr);
-      *size_ptr = sizeof(T);
-      ptr += sizeof(uint64_t);
+      ptr = AppendUnchecked(ptr, TypedMemoryAccessor<T>::AppendSize(value));
 #endif
-      new (ptr) T(std::move(value));
+      ptr = TypedMemoryAccessor<T>::Append(ptr, std::move(value));
       num_elements_++;
-      ptr += sizeof(T);
-      offset_ = static_cast<uint32_t>(
-          base::AlignUp<8>(static_cast<uint32_t>(ptr - storage_begin_ptr)));
+
+      auto cur_offset = offset_;
+      offset_ = static_cast<uint32_t>(base::AlignUp<8>(static_cast<uint32_t>(
+          ptr - reinterpret_cast<char*>(storage_.get()))));
       return cur_offset;
     }
 
@@ -139,16 +132,26 @@
       PERFETTO_DCHECK(offset % 8 == 0);
 
       char* ptr = reinterpret_cast<char*>(storage_.get()) + offset;
+      uint64_t size = 0;
 #if PERFETTO_DCHECK_IS_ON()
-      uint64_t size = *reinterpret_cast<uint64_t*>(ptr);
-      PERFETTO_DCHECK(size == sizeof(T));
-      ptr += sizeof(uint64_t);
+      size = EvictUnchecked<uint64_t>(&ptr);
 #endif
-      T* type_ptr = reinterpret_cast<T*>(ptr);
-      T out(std::move(*type_ptr));
-      type_ptr->~T();
+      T value = TypedMemoryAccessor<T>::Evict(ptr);
+      PERFETTO_DCHECK(size == TypedMemoryAccessor<T>::AppendSize(value));
       num_elements_evicted_++;
-      return out;
+      return value;
+    }
+
+    template <typename T>
+    static uint64_t AppendSize(const T& value) {
+#if PERFETTO_DCHECK_IS_ON()
+      // On debug runs for each append of T we also append the sizeof(T) to the
+      // queue for sanity check, which we later evict and compare with object
+      // size. This value needs to be added to general size of an object.
+      return sizeof(uint64_t) + TypedMemoryAccessor<T>::AppendSize(value);
+#else
+      return TypedMemoryAccessor<T>::AppendSize(value);
+#endif
     }
 
     uint32_t offset() const { return offset_; }
diff --git a/test/trace_processor/startup/android_startup_breakdown.out b/test/trace_processor/startup/android_startup_breakdown.out
index bff5a1f..1ebdfb5 100644
--- a/test/trace_processor/startup/android_startup_breakdown.out
+++ b/test/trace_processor/startup/android_startup_breakdown.out
@@ -82,8 +82,8 @@
     optimization_status {
       odex_status: "up-to-date"
       compilation_filter: "speed"
-      compilation_reason: "prebuilt"
-      location: "/system/framework/oat/arm/com.android.location.provider.odex"
+      compilation_reason: "install-dm"
+      location: "/system/framework/oat/arm/com.google.android.calendar.odex"
     }
     optimization_status {
       odex_status: "io-error-no-oat"
@@ -107,6 +107,7 @@
     slow_start_reason: "Main Thread - Time spent in Runnable state"
     slow_start_reason: "Time spent in bindApplication"
     slow_start_reason: "Time spent in ResourcesManager#getResources"
+    slow_start_reason: "No baseline or cloud profiles"
     startup_type: "cold"
   }
 }
diff --git a/test/trace_processor/startup/android_startup_breakdown.py b/test/trace_processor/startup/android_startup_breakdown.py
index 852ba95..5a5b4a4 100644
--- a/test/trace_processor/startup/android_startup_breakdown.py
+++ b/test/trace_processor/startup/android_startup_breakdown.py
@@ -91,8 +91,8 @@
     ts=to_s(204),
     tid=3,
     pid=3,
-    buf='location=/system/framework/oat/arm/com.android.location.provider' \
-        '.odex status=up-to-date filter=speed reason=prebuilt')
+    buf='location=/system/framework/oat/arm/com.google.android.calendar' \
+        '.odex status=up-to-date filter=speed reason=install-dm')
 trace.add_atrace_end(ts=to_s(205), tid=3, pid=3)
 
 trace.add_atrace_async_end(
diff --git a/test/trace_processor/startup/android_startup_breakdown_slow.out b/test/trace_processor/startup/android_startup_breakdown_slow.out
index a9d0dac..8be42b3 100644
--- a/test/trace_processor/startup/android_startup_breakdown_slow.out
+++ b/test/trace_processor/startup/android_startup_breakdown_slow.out
@@ -81,8 +81,8 @@
     }
     optimization_status {
       odex_status: "up-to-date"
-      compilation_filter: "speed"
-      compilation_reason: "prebuilt"
+      compilation_filter: "speed-profile"
+      compilation_reason: "install"
       location: "/system/framework/oat/arm/com.android.location.provider.odex"
     }
     optimization_status {
diff --git a/test/trace_processor/startup/android_startup_breakdown_slow.py b/test/trace_processor/startup/android_startup_breakdown_slow.py
index 7bf9191..06422a2 100644
--- a/test/trace_processor/startup/android_startup_breakdown_slow.py
+++ b/test/trace_processor/startup/android_startup_breakdown_slow.py
@@ -92,7 +92,7 @@
     tid=3,
     pid=3,
     buf='location=/system/framework/oat/arm/com.android.location.provider' \
-        '.odex status=up-to-date filter=speed reason=prebuilt')
+        '.odex status=up-to-date filter=speed-profile reason=install')
 trace.add_atrace_end(ts=to_s(205), tid=3, pid=3)
 
 trace.add_atrace_async_end(
diff --git a/tools/diff_test_trace_processor.py b/tools/diff_test_trace_processor.py
index aa1454f..1552b2e 100755
--- a/tools/diff_test_trace_processor.py
+++ b/tools/diff_test_trace_processor.py
@@ -291,7 +291,7 @@
     else:
       result_str += write_cmdlines()
 
-    result_str += f"{red_str}[     FAIL ]{end_color_str} {test_name} "
+    result_str += f"{red_str}[  FAILED  ]{end_color_str} {test_name} "
     result_str += f"{os.path.basename(trace_path)}\n"
 
     if args.rebase:
@@ -467,66 +467,67 @@
 
   sys.stderr.write(
       f"[==========] {len(tests)} tests ran. ({test_time_ms} ms total)\n")
-  if test_failures:
+  sys.stderr.write(
+      f"{green(args.no_colors)}[  PASSED  ]{end_color(args.no_colors)} "
+      f"{len(tests) - len(test_failures)} tests.\n")
+  if len(test_failures) > 0:
     sys.stderr.write(
-        f"{red(args.no_colors)}[  PASSED  ]{end_color(args.no_colors)} "
-        f"{len(tests) - len(test_failures)} tests.\n")
-  else:
-    sys.stderr.write(
-        f"{green(args.no_colors)}[  PASSED  ]{end_color(args.no_colors)} "
-        f"{len(tests)} tests.\n")
+        f"{red(args.no_colors)}[  FAILED  ]{end_color(args.no_colors)} "
+        f"{len(test_failures)} tests.\n")
+    for failure in test_failures:
+      sys.stderr.write(
+          f"{red(args.no_colors)}[  FAILED  ]{end_color(args.no_colors)} "
+          f"{failure}\n")
 
   if args.rebase:
     sys.stderr.write('\n')
     sys.stderr.write(f"{rebased} tests rebased.\n")
 
-  if len(test_failures) == 0:
-    if args.perf_file:
-      test_dir = os.path.join(ROOT_DIR, 'test')
-      trace_processor_dir = os.path.join(test_dir, 'trace_processor')
-
-      metrics = []
-      sorted_data = sorted(
-          perf_data,
-          key=lambda x: (x.test_type, x.trace_path, x.query_path_or_metric))
-      for perf_args in sorted_data:
-        trace_short_path = os.path.relpath(perf_args.trace_path, test_dir)
-
-        query_short_path_or_metric = perf_args.query_path_or_metric
-        if perf_args.test_type == 'queries':
-          query_short_path_or_metric = os.path.relpath(
-              perf_args.query_path_or_metric, trace_processor_dir)
-
-        metrics.append({
-            'metric': 'tp_perf_test_ingest_time',
-            'value': float(perf_args.ingest_time_ns) / 1.0e9,
-            'unit': 's',
-            'tags': {
-                'test_name': f"{trace_short_path}-{query_short_path_or_metric}",
-                'test_type': perf_args.test_type,
-            },
-            'labels': {},
-        })
-        metrics.append({
-            'metric': 'perf_test_real_time',
-            'value': float(perf_args.real_time_ns) / 1.0e9,
-            'unit': 's',
-            'tags': {
-                'test_name': f"{trace_short_path}-{query_short_path_or_metric}",
-                'test_type': perf_args.test_type,
-            },
-            'labels': {},
-        })
-
-      output_data = {'metrics': metrics}
-      with open(args.perf_file, 'w+') as perf_file:
-        perf_file.write(json.dumps(output_data, indent=2))
-    return 0
-  else:
-    for failure in test_failures:
-      sys.stderr.write(f"[  FAILED  ] {failure}\n")
+  if len(test_failures) > 0:
     return 1
 
+  if args.perf_file:
+    test_dir = os.path.join(ROOT_DIR, 'test')
+    trace_processor_dir = os.path.join(test_dir, 'trace_processor')
+
+    metrics = []
+    sorted_data = sorted(
+        perf_data,
+        key=lambda x: (x.test_type, x.trace_path, x.query_path_or_metric))
+    for perf_args in sorted_data:
+      trace_short_path = os.path.relpath(perf_args.trace_path, test_dir)
+
+      query_short_path_or_metric = perf_args.query_path_or_metric
+      if perf_args.test_type == 'queries':
+        query_short_path_or_metric = os.path.relpath(
+            perf_args.query_path_or_metric, trace_processor_dir)
+
+      metrics.append({
+          'metric': 'tp_perf_test_ingest_time',
+          'value': float(perf_args.ingest_time_ns) / 1.0e9,
+          'unit': 's',
+          'tags': {
+              'test_name': f"{trace_short_path}-{query_short_path_or_metric}",
+              'test_type': perf_args.test_type,
+          },
+          'labels': {},
+      })
+      metrics.append({
+          'metric': 'perf_test_real_time',
+          'value': float(perf_args.real_time_ns) / 1.0e9,
+          'unit': 's',
+          'tags': {
+              'test_name': f"{trace_short_path}-{query_short_path_or_metric}",
+              'test_type': perf_args.test_type,
+          },
+          'labels': {},
+      })
+
+    output_data = {'metrics': metrics}
+    with open(args.perf_file, 'w+') as perf_file:
+      perf_file.write(json.dumps(output_data, indent=2))
+  return 0
+
 
 if __name__ == '__main__':
   sys.exit(main())
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 059dcb7..31b19db 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -311,6 +311,8 @@
     width: 100%;
     text-align: right;
     line-height: 1;
+    display: block; // Required in order for inherited white-space property not
+                    // to screw up vertical rendering of the popup menu items.
 
     &:hover {
       background: #c7d0db;
@@ -797,6 +799,13 @@
   td.menu {
     text-align: left;
   }
+
+  td {
+    // In context menu icon to go on a separate line.
+    // In regular pivot table cells, avoids wrapping the icon spacer to go on
+    // a separate line.
+    white-space: pre;
+  }
 }
 
 .name-completion {
diff --git a/ui/src/common/recordingV2/adb_connection_impl.ts b/ui/src/common/recordingV2/adb_connection_impl.ts
index 98a10b1..9fae926 100644
--- a/ui/src/common/recordingV2/adb_connection_impl.ts
+++ b/ui/src/common/recordingV2/adb_connection_impl.ts
@@ -42,7 +42,7 @@
 
     // We wait for the stream to be closed by the device, which happens
     // after the shell command is successfully received.
-    adbStream.addOnStreamClose(() => {
+    adbStream.addOnStreamCloseCallback(() => {
       onStreamingEnded.resolve();
     });
     return onStreamingEnded;
@@ -55,10 +55,10 @@
     const commandOutput = new ArrayBufferBuilder();
     const onStreamingEnded = defer<string>();
 
-    adbStream.addOnStreamData((data: Uint8Array) => {
+    adbStream.addOnStreamDataCallback((data: Uint8Array) => {
       commandOutput.append(data);
     });
-    adbStream.addOnStreamClose(() => {
+    adbStream.addOnStreamCloseCallback(() => {
       onStreamingEnded.resolve(
           textDecoder.decode(commandOutput.toArrayBuffer()));
     });
diff --git a/ui/src/common/recordingV2/adb_connection_over_websocket.ts b/ui/src/common/recordingV2/adb_connection_over_websocket.ts
index 529103b..7287b65 100644
--- a/ui/src/common/recordingV2/adb_connection_over_websocket.ts
+++ b/ui/src/common/recordingV2/adb_connection_over_websocket.ts
@@ -114,11 +114,11 @@
     this.websocket.onclose = this.onClose.bind(this);
   }
 
-  addOnStreamData(onStreamData: OnStreamDataCallback) {
+  addOnStreamDataCallback(onStreamData: OnStreamDataCallback) {
     this.onStreamDataCallbacks.push(onStreamData);
   }
 
-  addOnStreamClose(onStreamClose: OnStreamCloseCallback) {
+  addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback) {
     this.onStreamCloseCallbacks.push(onStreamClose);
   }
 
diff --git a/ui/src/common/recordingV2/adb_connection_over_webusb.ts b/ui/src/common/recordingV2/adb_connection_over_webusb.ts
index 2a121ae..42686bb 100644
--- a/ui/src/common/recordingV2/adb_connection_over_webusb.ts
+++ b/ui/src/common/recordingV2/adb_connection_over_webusb.ts
@@ -465,11 +465,11 @@
     this._isConnected = true;
   }
 
-  addOnStreamData(onStreamData: OnStreamDataCallback): void {
+  addOnStreamDataCallback(onStreamData: OnStreamDataCallback): void {
     this.onStreamDataCallbacks.push(onStreamData);
   }
 
-  addOnStreamClose(onStreamClose: OnStreamCloseCallback): void {
+  addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback): void {
     this.onStreamCloseCallbacks.push(onStreamClose);
   }
 
diff --git a/ui/src/common/recordingV2/adb_file_handler.ts b/ui/src/common/recordingV2/adb_file_handler.ts
index 17d4c2d..d659adb 100644
--- a/ui/src/common/recordingV2/adb_file_handler.ts
+++ b/ui/src/common/recordingV2/adb_file_handler.ts
@@ -51,9 +51,9 @@
     this.isPushOngoing = true;
     const transferFinished = defer<void>();
 
-    this.byteStream.addOnStreamData(
+    this.byteStream.addOnStreamDataCallback(
         (data) => this.onStreamData(data, transferFinished));
-    this.byteStream.addOnStreamClose(() => this.isPushOngoing = false);
+    this.byteStream.addOnStreamCloseCallback(() => this.isPushOngoing = false);
 
     const sendMessage = new ArrayBufferBuilder();
     // 'SEND' is the API method used to send a file to device.
diff --git a/ui/src/common/recordingV2/host_os_byte_stream.ts b/ui/src/common/recordingV2/host_os_byte_stream.ts
new file mode 100644
index 0000000..dd46ba8
--- /dev/null
+++ b/ui/src/common/recordingV2/host_os_byte_stream.ts
@@ -0,0 +1,84 @@
+// Copyright (C) 2022 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 {defer} from '../../base/deferred';
+
+import {
+  ByteStream,
+  OnStreamCloseCallback,
+  OnStreamDataCallback,
+} from './recording_interfaces_v2';
+
+// A HostOsByteStream instantiates a websocket connection to the host OS.
+// It exposes an API to write commands to this websocket and read its output.
+export class HostOsByteStream implements ByteStream {
+  // handshakeSignal will be resolved with the stream when the websocket
+  // connection becomes open.
+  private handshakeSignal = defer<HostOsByteStream>();
+  private _isConnected: boolean = false;
+  private websocket: WebSocket;
+  private onStreamDataCallbacks: OnStreamDataCallback[] = [];
+  private onStreamCloseCallbacks: OnStreamCloseCallback[] = [];
+
+  private constructor(websocketUrl: string) {
+    this.websocket = new WebSocket(websocketUrl);
+    this.websocket.onmessage = this.onMessage.bind(this);
+    this.websocket.onopen = this.onOpen.bind(this);
+  }
+
+  addOnStreamDataCallback(onStreamData: OnStreamDataCallback): void {
+    this.onStreamDataCallbacks.push(onStreamData);
+  }
+
+  addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback): void {
+    this.onStreamCloseCallbacks.push(onStreamClose);
+  }
+
+  close(): void {
+    this.websocket.close();
+    for (const onStreamClose of this.onStreamCloseCallbacks) {
+      onStreamClose();
+    }
+    this.onStreamDataCallbacks = [];
+    this.onStreamCloseCallbacks = [];
+  }
+
+  async closeAndWaitForTeardown(): Promise<void> {
+    this.close();
+  }
+
+  isConnected(): boolean {
+    return this._isConnected;
+  }
+
+  write(msg: string|Uint8Array): void {
+    this.websocket.send(msg);
+  }
+
+  private async onMessage(evt: MessageEvent) {
+    for (const onStreamData of this.onStreamDataCallbacks) {
+      const arrayBufferResponse = await evt.data.arrayBuffer();
+      onStreamData(new Uint8Array(arrayBufferResponse));
+    }
+  }
+
+  private onOpen() {
+    this._isConnected = true;
+    this.handshakeSignal.resolve(this);
+  }
+
+  static create(websocketUrl: string): Promise<HostOsByteStream> {
+    return (new HostOsByteStream(websocketUrl)).handshakeSignal;
+  }
+}
diff --git a/ui/src/common/recordingV2/recording_interfaces_v2.ts b/ui/src/common/recordingV2/recording_interfaces_v2.ts
index 6929463..974d277 100644
--- a/ui/src/common/recordingV2/recording_interfaces_v2.ts
+++ b/ui/src/common/recordingV2/recording_interfaces_v2.ts
@@ -78,13 +78,13 @@
   targetType: 'CHROME'|'CHROME_OS';
 }
 
-export interface LinuxTargetInfo extends TargetInfoBase {
-  targetType: 'LINUX';
+export interface HostOsTargetInfo extends TargetInfoBase {
+  targetType: 'LINUX'|'MACOS';
 }
 
 // Holds information about a target. It's used by the UI and the logic which
 // generates a config.
-export type TargetInfo = AndroidTargetInfo|ChromeTargetInfo|LinuxTargetInfo;
+export type TargetInfo = AndroidTargetInfo|ChromeTargetInfo|HostOsTargetInfo;
 
 // RecordingTargetV2 is subclassed by Android devices and the Chrome browser/OS.
 // It creates tracing sessions which are used by the UI. For Android, it manages
@@ -166,8 +166,8 @@
 export interface ByteStream {
   // The caller can add callbacks, to be executed when the stream receives new
   // data or when it finished closing itself.
-  addOnStreamData(onStreamData: OnStreamDataCallback): void;
-  addOnStreamClose(onStreamClose: OnStreamCloseCallback): void;
+  addOnStreamDataCallback(onStreamData: OnStreamDataCallback): void;
+  addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback): void;
 
   isConnected(): boolean;
   write(data: string|Uint8Array): void;
diff --git a/ui/src/common/recordingV2/recording_page_controller.ts b/ui/src/common/recordingV2/recording_page_controller.ts
index c2e0367..8c7d856 100644
--- a/ui/src/common/recordingV2/recording_page_controller.ts
+++ b/ui/src/common/recordingV2/recording_page_controller.ts
@@ -17,6 +17,7 @@
 import {autosaveConfigStore} from '../../frontend/record_config';
 import {
   DEFAULT_ADB_WEBSOCKET_URL,
+  DEFAULT_TRACED_WEBSOCKET_URL,
 } from '../../frontend/recording/recording_ui_utils';
 import {
   couldNotClaimInterface,
@@ -45,6 +46,10 @@
 import {
   ANDROID_WEBUSB_TARGET_FACTORY,
 } from './target_factories/android_webusb_target_factory';
+import {
+  HOST_OS_TARGET_FACTORY,
+  HostOsTargetFactory,
+} from './target_factories/host_os_target_factory';
 import {targetFactoryRegistry} from './target_factory_registry';
 
 // The recording page can be in any of these states. It can transition between
@@ -453,6 +458,13 @@
           AndroidWebsocketTargetFactory;
       websocketTargetFactory.tryEstablishWebsocket(DEFAULT_ADB_WEBSOCKET_URL);
     }
+    if (targetFactoryRegistry.has(HOST_OS_TARGET_FACTORY)) {
+      const websocketTargetFactory =
+          targetFactoryRegistry.get(HOST_OS_TARGET_FACTORY) as
+          HostOsTargetFactory;
+      websocketTargetFactory.tryEstablishWebsocket(
+          DEFAULT_TRACED_WEBSOCKET_URL);
+    }
   }
 
   shouldShowTargetSelection(): boolean {
diff --git a/ui/src/common/recordingV2/recording_utils.ts b/ui/src/common/recordingV2/recording_utils.ts
index eed891e..72d2761 100644
--- a/ui/src/common/recordingV2/recording_utils.ts
+++ b/ui/src/common/recordingV2/recording_utils.ts
@@ -29,6 +29,28 @@
   return hdr + cmd;
 }
 
+// Sample user agent for Chrome on Mac OS:
+// 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
+// (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
+export function isMacOs(userAgent: string) {
+  return userAgent.toLowerCase().includes(' mac os ');
+}
+
+// Sample user agent for Chrome on Linux:
+// Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)
+// Chrome/105.0.0.0 Safari/537.36
+export function isLinux(userAgent: string) {
+  return userAgent.toLowerCase().includes(' linux ');
+}
+
+// Sample user agent for Chrome on Chrome OS:
+// "Mozilla/5.0 (X11; CrOS x86_64 14816.99.0) AppleWebKit/537.36
+// (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36"
+// This condition is wider, in the unlikely possibility of different casing,
+export function isCrOS(userAgent: string) {
+  return userAgent.toLowerCase().includes(' cros ');
+}
+
 // End Websocket //////////////////////////////////////////////////////////
 
 // Begin Adb //////////////////////////////////////////////////////////////
diff --git a/ui/src/common/recordingV2/target_factories/chrome_target_factory.ts b/ui/src/common/recordingV2/target_factories/chrome_target_factory.ts
index 8ac448d..1650934 100644
--- a/ui/src/common/recordingV2/target_factories/chrome_target_factory.ts
+++ b/ui/src/common/recordingV2/target_factories/chrome_target_factory.ts
@@ -18,20 +18,16 @@
   RecordingTargetV2,
   TargetFactory,
 } from '../recording_interfaces_v2';
-import {EXTENSION_ID, EXTENSION_NOT_INSTALLED} from '../recording_utils';
+import {
+  EXTENSION_ID,
+  EXTENSION_NOT_INSTALLED,
+  isCrOS,
+} from '../recording_utils';
 import {targetFactoryRegistry} from '../target_factory_registry';
 import {ChromeTarget} from '../targets/chrome_target';
 
 export const CHROME_TARGET_FACTORY = 'ChromeTargetFactory';
 
-// Sample user agent for Chrome on Chrome OS:
-// "Mozilla/5.0 (X11; CrOS x86_64 14816.99.0) AppleWebKit/537.36
-// (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36"
-// This condition is wider, in the unlikely possibility of different casing,
-export function isCrOS(userAgent: string) {
-  return userAgent.toLowerCase().includes(' cros ');
-}
-
 export class ChromeTargetFactory implements TargetFactory {
   readonly kind = CHROME_TARGET_FACTORY;
   // We only check the connection once at the beginning to:
diff --git a/ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts b/ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts
index 478a3f4..c12365d 100644
--- a/ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts
+++ b/ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts
@@ -12,17 +12,29 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {isCrOS} from './chrome_target_factory';
+import {isCrOS, isLinux, isMacOs} from '../recording_utils';
 
 test('parse Chrome on Chrome OS user agent', () => {
   const userAgent = 'Mozilla/5.0 (X11; CrOS x86_64 14816.99.0) ' +
       'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 ' +
       'Safari/537.36';
   expect(isCrOS(userAgent)).toBe(true);
+  expect(isMacOs(userAgent)).toBe(false);
+  expect(isLinux(userAgent)).toBe(false);
 });
 
 test('parse Chrome on Mac user agent', () => {
   const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
       'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36';
   expect(isCrOS(userAgent)).toBe(false);
+  expect(isMacOs(userAgent)).toBe(true);
+  expect(isLinux(userAgent)).toBe(false);
+});
+
+test('parse Chrome on Linux user agent', () => {
+  const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
+      '(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36';
+  expect(isCrOS(userAgent)).toBe(false);
+  expect(isMacOs(userAgent)).toBe(false);
+  expect(isLinux(userAgent)).toBe(true);
 });
diff --git a/ui/src/common/recordingV2/target_factories/host_os_target_factory.ts b/ui/src/common/recordingV2/target_factories/host_os_target_factory.ts
new file mode 100644
index 0000000..d7342c0
--- /dev/null
+++ b/ui/src/common/recordingV2/target_factories/host_os_target_factory.ts
@@ -0,0 +1,81 @@
+// Copyright (C) 2022 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 {RecordingError} from '../recording_error_handling';
+import {
+  OnTargetChangeCallback,
+  RecordingTargetV2,
+  TargetFactory,
+} from '../recording_interfaces_v2';
+import {isLinux, isMacOs} from '../recording_utils';
+import {targetFactoryRegistry} from '../target_factory_registry';
+import {HostOsTarget} from '../targets/host_os_target';
+
+export const HOST_OS_TARGET_FACTORY = 'HostOsTargetFactory';
+
+export class HostOsTargetFactory implements TargetFactory {
+  readonly kind = HOST_OS_TARGET_FACTORY;
+  private target?: HostOsTarget;
+  private onTargetChange: OnTargetChangeCallback = () => {};
+
+  connectNewTarget(): Promise<RecordingTargetV2> {
+    throw new RecordingError(
+        'Can not create a new Host OS target.' +
+        'The Host OS target is created at factory initialisation.');
+  }
+
+  getName(): string {
+    return 'HostOs';
+  }
+
+  listRecordingProblems(): string[] {
+    return [];
+  }
+
+  listTargets(): RecordingTargetV2[] {
+    if (this.target) {
+      return [this.target];
+    }
+    return [];
+  }
+
+  tryEstablishWebsocket(websocketUrl: string) {
+    if (this.target) {
+      if (this.target.getUrl() === websocketUrl) {
+        return;
+      } else {
+        this.target.disconnect();
+      }
+    }
+    this.target = new HostOsTarget(
+        websocketUrl, this.maybeClearTarget.bind(this), this.onTargetChange);
+    this.onTargetChange();
+  }
+
+  maybeClearTarget(target: HostOsTarget): void {
+    if (this.target === target) {
+      this.target = undefined;
+      this.onTargetChange();
+    }
+  }
+
+  setOnTargetChange(onTargetChange: OnTargetChangeCallback): void {
+    this.onTargetChange = onTargetChange;
+  }
+}
+
+// We instantiate the host target factory only on Mac and Linux.
+if (isMacOs(navigator.userAgent) || isLinux(navigator.userAgent)) {
+  targetFactoryRegistry.register(new HostOsTargetFactory());
+}
diff --git a/ui/src/common/recordingV2/target_factories/index.ts b/ui/src/common/recordingV2/target_factories/index.ts
index 3386d5b..3c1e3af 100644
--- a/ui/src/common/recordingV2/target_factories/index.ts
+++ b/ui/src/common/recordingV2/target_factories/index.ts
@@ -15,4 +15,5 @@
 import './android_webusb_target_factory';
 import './android_websocket_target_factory';
 import './chrome_target_factory';
+import './host_os_target_factory';
 import './virtual_target_factory';
diff --git a/ui/src/common/recordingV2/targets/android_websocket_target.ts b/ui/src/common/recordingV2/targets/android_websocket_target.ts
index ed085d9..ab4f130 100644
--- a/ui/src/common/recordingV2/targets/android_websocket_target.ts
+++ b/ui/src/common/recordingV2/targets/android_websocket_target.ts
@@ -33,7 +33,7 @@
       targetType: 'ANDROID',
       // 'androidApiLevel' will be populated after ADB authorization.
       androidApiLevel: this.androidApiLevel,
-      dataSources: [],
+      dataSources: this.dataSources || [],
       name: this.serialNumber + ' WebSocket',
     };
   }
diff --git a/ui/src/common/recordingV2/targets/host_os_target.ts b/ui/src/common/recordingV2/targets/host_os_target.ts
new file mode 100644
index 0000000..112c984
--- /dev/null
+++ b/ui/src/common/recordingV2/targets/host_os_target.ts
@@ -0,0 +1,133 @@
+// Copyright (C) 2022 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 {HostOsByteStream} from '../host_os_byte_stream';
+import {RecordingError} from '../recording_error_handling';
+import {
+  DataSource,
+  HostOsTargetInfo,
+  OnDisconnectCallback,
+  OnTargetChangeCallback,
+  RecordingTargetV2,
+  TracingSession,
+  TracingSessionListener,
+} from '../recording_interfaces_v2';
+import {
+  isLinux,
+  isMacOs,
+  WEBSOCKET_CLOSED_ABNORMALLY_CODE,
+} from '../recording_utils';
+import {TracedTracingSession} from '../traced_tracing_session';
+
+export class HostOsTarget implements RecordingTargetV2 {
+  private readonly targetType: 'LINUX'|'MACOS';
+  private readonly name: string;
+  private websocket: WebSocket;
+  private streams = new Set<HostOsByteStream>();
+  private dataSources?: DataSource[];
+  private onDisconnect: OnDisconnectCallback = (_) => {};
+
+  constructor(
+      websocketUrl: string,
+      private maybeClearTarget: (target: HostOsTarget) => void,
+      private onTargetChange: OnTargetChangeCallback) {
+    if (isMacOs(navigator.userAgent)) {
+      this.name = 'MacOS';
+      this.targetType = 'MACOS';
+    } else if (isLinux(navigator.userAgent)) {
+      this.name = 'Linux';
+      this.targetType = 'LINUX';
+    } else {
+      throw new RecordingError(
+          'Host OS target created on an unsupported operating system.');
+    }
+
+    this.websocket = new WebSocket(websocketUrl);
+    this.websocket.onclose = this.onClose.bind(this);
+  }
+
+  getInfo(): HostOsTargetInfo {
+    return {
+      targetType: this.targetType,
+      name: this.name,
+      dataSources: this.dataSources || [],
+    };
+  }
+
+  canCreateTracingSession(): boolean {
+    return true;
+  }
+
+  async createTracingSession(tracingSessionListener: TracingSessionListener):
+      Promise<TracingSession> {
+    this.onDisconnect = tracingSessionListener.onDisconnect;
+
+    const osStream = await HostOsByteStream.create(this.getUrl());
+    this.streams.add(osStream);
+    const tracingSession =
+        new TracedTracingSession(osStream, tracingSessionListener);
+    await tracingSession.initConnection();
+
+    if (!this.dataSources) {
+      this.dataSources = await tracingSession.queryServiceState();
+      this.onTargetChange();
+    }
+    return tracingSession;
+  }
+
+  // Starts a tracing session in order to fetch data sources from the
+  // device. Then, it cancels the session.
+  async fetchTargetInfo(tracingSessionListener: TracingSessionListener):
+      Promise<void> {
+    const tracingSession =
+        await this.createTracingSession(tracingSessionListener);
+    tracingSession.cancel();
+  }
+
+  async disconnect(): Promise<void> {
+    if (this.websocket.readyState === this.websocket.OPEN) {
+      this.websocket.close();
+      // We remove the 'onclose' callback so the 'disconnect' method doesn't get
+      // executed twice.
+      this.websocket.onclose = null;
+    }
+    for (const stream of this.streams) {
+      stream.close();
+    }
+    // We remove the existing target from the factory if present.
+    this.maybeClearTarget(this);
+    // We run the onDisconnect callback in case this target is used for tracing.
+    this.onDisconnect();
+  }
+
+  // We can connect to the Host OS without taking the connection away from
+  // another process.
+  async canConnectWithoutContention(): Promise<boolean> {
+    return true;
+  }
+
+  getUrl() {
+    return this.websocket.url;
+  }
+
+  private onClose(ev: CloseEvent): void {
+    if (ev.code === WEBSOCKET_CLOSED_ABNORMALLY_CODE) {
+      console.info(
+          `It's safe to ignore the 'WebSocket connection to ${
+              this.getUrl()} error above, if present. It occurs when ` +
+          'checking the connection to the local Websocket server.');
+    }
+    this.disconnect();
+  }
+}
diff --git a/ui/src/common/recordingV2/traced_tracing_session.ts b/ui/src/common/recordingV2/traced_tracing_session.ts
index f898c8f..c8a1d10 100644
--- a/ui/src/common/recordingV2/traced_tracing_session.ts
+++ b/ui/src/common/recordingV2/traced_tracing_session.ts
@@ -110,8 +110,9 @@
   constructor(
       private byteStream: ByteStream,
       private tracingSessionListener: TracingSessionListener) {
-    this.byteStream.addOnStreamData((data) => this.handleReceivedData(data));
-    this.byteStream.addOnStreamClose(() => this.clearState());
+    this.byteStream.addOnStreamDataCallback(
+        (data) => this.handleReceivedData(data));
+    this.byteStream.addOnStreamCloseCallback(() => this.clearState());
   }
 
   queryServiceState(): Promise<DataSource[]> {
diff --git a/ui/src/frontend/recording/recording_ui_utils.ts b/ui/src/frontend/recording/recording_ui_utils.ts
index 7c91a4a..97a5e24 100644
--- a/ui/src/frontend/recording/recording_ui_utils.ts
+++ b/ui/src/frontend/recording/recording_ui_utils.ts
@@ -25,6 +25,7 @@
 
 export const FORCE_RESET_MESSAGE = 'Force reset the USB interface';
 export const DEFAULT_ADB_WEBSOCKET_URL = 'ws://127.0.0.1:8037/adb';
+export const DEFAULT_TRACED_WEBSOCKET_URL = 'ws://127.0.0.1:8037/traced';
 
 export function getWebsocketTargetFactory(): AndroidWebsocketTargetFactory {
   return targetFactoryRegistry.get(ANDROID_WEBSOCKET_TARGET_FACTORY) as
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index ff64c2f..e1be6a7 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -628,7 +628,11 @@
     fileName = url.split('/').slice(-1)[0];
   } else if (src.type === 'ARRAY_BUFFER') {
     const blob = new Blob([src.buffer], {type: 'application/octet-stream'});
-    if (src.fileName) {
+    const inputFileName =
+        window.prompt('Please enter a name for your file or leave blank');
+    if (inputFileName) {
+      fileName = `${inputFileName}.perfetto_trace.gz`;
+    } else if (src.fileName) {
       fileName = src.fileName;
     }
     url = URL.createObjectURL(blob);