Merge "Hash scope into id for legacy trace events"
diff --git a/Android.bp b/Android.bp
index b54baa5..5bf2da2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -3743,6 +3743,7 @@
         "protos/perfetto/metrics/chrome/scroll_jank.proto",
         "protos/perfetto/metrics/chrome/test_chrome_metric.proto",
         "protos/perfetto/metrics/chrome/touch_jank.proto",
+        "protos/perfetto/metrics/chrome/user_event_hashes.proto",
         "protos/perfetto/metrics/custom_options.proto",
         "protos/perfetto/metrics/metrics.proto",
     ],
@@ -8392,6 +8393,7 @@
         "src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_processes.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_thread_slice.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_user_event_hashes.sql",
         "src/trace_processor/metrics/sql/chrome/cpu_time_by_category.sql",
         "src/trace_processor/metrics/sql/chrome/cpu_time_by_rail_mode.sql",
         "src/trace_processor/metrics/sql/chrome/estimated_power_by_category.sql",
diff --git a/BUILD b/BUILD
index eba87b7..8870bbd 100644
--- a/BUILD
+++ b/BUILD
@@ -1117,6 +1117,7 @@
         "src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_processes.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_thread_slice.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_user_event_hashes.sql",
         "src/trace_processor/metrics/sql/chrome/cpu_time_by_category.sql",
         "src/trace_processor/metrics/sql/chrome/cpu_time_by_rail_mode.sql",
         "src/trace_processor/metrics/sql/chrome/estimated_power_by_category.sql",
@@ -2751,6 +2752,7 @@
         "protos/perfetto/metrics/chrome/scroll_jank.proto",
         "protos/perfetto/metrics/chrome/test_chrome_metric.proto",
         "protos/perfetto/metrics/chrome/touch_jank.proto",
+        "protos/perfetto/metrics/chrome/user_event_hashes.proto",
     ],
     visibility = [
         PERFETTO_CONFIG.proto_library_visibility,
diff --git a/buildtools/BUILD.gn b/buildtools/BUILD.gn
index a508a74..6c8106c 100644
--- a/buildtools/BUILD.gn
+++ b/buildtools/BUILD.gn
@@ -1162,7 +1162,7 @@
 # writing, the only c++14 specific code is behind an #ifndef NDEBUG, so we
 # could keep building as c++11 in non-debug builds, but we always use c++14 for
 # consistency.
-static_library("llvm_demangle") {
+source_set("llvm_demangle") {
   visibility = _buildtools_visibility
   configs -= [
     "//gn/standalone:extra_warnings",
diff --git a/protos/perfetto/metrics/chrome/BUILD.gn b/protos/perfetto/metrics/chrome/BUILD.gn
index a170ace..ad53980 100644
--- a/protos/perfetto/metrics/chrome/BUILD.gn
+++ b/protos/perfetto/metrics/chrome/BUILD.gn
@@ -32,6 +32,7 @@
     "scroll_jank.proto",
     "test_chrome_metric.proto",
     "touch_jank.proto",
+    "user_event_hashes.proto",
   ]
 }
 
diff --git a/protos/perfetto/metrics/chrome/all_chrome_metrics.proto b/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
index 03c2bb2..5513b52 100644
--- a/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
+++ b/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
@@ -29,6 +29,7 @@
 import "protos/perfetto/metrics/chrome/scroll_jank.proto";
 import "protos/perfetto/metrics/chrome/test_chrome_metric.proto";
 import "protos/perfetto/metrics/chrome/touch_jank.proto";
+import "protos/perfetto/metrics/chrome/user_event_hashes.proto";
 
 // TODO(lalitm): rename metrics below to include a "chrome_" prefix.
 extend TraceMetrics {
@@ -42,4 +43,5 @@
   optional ChromeDroppedFrames chrome_dropped_frames = 1008;
   optional ChromeLongLatency chrome_long_latency = 1009;
   optional ChromeHistogramHashes chrome_histogram_hashes = 1010;
+  optional ChromeUserEventHashes chrome_user_event_hashes = 1011;
 }
diff --git a/protos/perfetto/metrics/chrome/user_event_hashes.proto b/protos/perfetto/metrics/chrome/user_event_hashes.proto
new file mode 100644
index 0000000..e50e6bd
--- /dev/null
+++ b/protos/perfetto/metrics/chrome/user_event_hashes.proto
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+// The list of Chrome user event hashes in trace track events.
+// Use cases include translating user event hashes to action
+// names by getting this list, and prepending a translation table to the trace.
+message ChromeUserEventHashes {
+  repeated int64 action_hash = 1;
+}
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 0961c10..ec49f71 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -54,7 +54,18 @@
   ]
 }
 
-static_library("demangle") {
+# In Bazel builds the ":demangle" target (below) should be a static_library so
+# it gets mapped to an actual target (rather than being squashed as a filegroup)
+# and can be replaced in Google internal builds via perfetto_cfg.bzl.
+# Unfortunately, however, static_library targets seem to break Wasm builds on
+# Mac. For this reason we just make it a source_set for all other build types.
+if (is_perfetto_build_generator) {
+  _demangle_target_type = "static_library"
+} else {
+  _demangle_target_type = "source_set"
+}
+
+target(_demangle_target_type, "demangle") {
   sources = [ "demangle.cc" ]
   deps = [
     "../../gn:default_deps",
diff --git a/src/trace_processor/export_json.cc b/src/trace_processor/export_json.cc
index 3bfc0fb..e05680e 100644
--- a/src/trace_processor/export_json.cc
+++ b/src/trace_processor/export_json.cc
@@ -709,7 +709,7 @@
     const auto& thread_table = storage_->thread_table();
     for (UniqueTid utid = 0; utid < thread_table.row_count(); ++utid) {
       auto opt_name = thread_table.name()[utid];
-      if (!opt_name.is_null()) {
+      if (opt_name.has_value()) {
         const char* thread_name = GetNonNullString(storage_, opt_name);
         auto pid_and_tid = UtidToPidAndTid(utid);
         writer_.WriteMetadataEvent("thread_name", "name", thread_name,
@@ -723,7 +723,7 @@
     const auto& process_table = storage_->process_table();
     for (UniquePid upid = 0; upid < process_table.row_count(); ++upid) {
       auto opt_name = process_table.name()[upid];
-      if (!opt_name.is_null()) {
+      if (opt_name.has_value()) {
         const char* process_name = GetNonNullString(storage_, opt_name);
         writer_.WriteMetadataEvent("process_name", "name", process_name,
                                    UpidToPid(upid), /*tid=*/0);
diff --git a/src/trace_processor/importers/common/args_translation_table.cc b/src/trace_processor/importers/common/args_translation_table.cc
index 216305d..72ced8b 100644
--- a/src/trace_processor/importers/common/args_translation_table.cc
+++ b/src/trace_processor/importers/common/args_translation_table.cc
@@ -22,16 +22,27 @@
 constexpr char ArgsTranslationTable::kChromeHistogramHashKey[];
 constexpr char ArgsTranslationTable::kChromeHistogramNameKey[];
 
+constexpr char ArgsTranslationTable::kChromeUserEventHashKey[];
+constexpr char ArgsTranslationTable::kChromeUserEventActionKey[];
+
 ArgsTranslationTable::ArgsTranslationTable(TraceStorage* storage)
     : storage_(storage),
+      interned_chrome_histogram_hash_key_(
+          storage->InternString(kChromeHistogramHashKey)),
       interned_chrome_histogram_name_key_(
-          storage->InternString(kChromeHistogramNameKey)) {}
+          storage->InternString(kChromeHistogramNameKey)),
+      interned_chrome_user_event_hash_key_(
+          storage->InternString(kChromeUserEventHashKey)),
+      interned_chrome_user_event_action_key_(
+          storage->InternString(kChromeUserEventActionKey)) {}
 
 bool ArgsTranslationTable::TranslateUnsignedIntegerArg(
     const Key& key,
     uint64_t value,
     ArgsTracker::BoundInserter& inserter) {
   if (key.key == kChromeHistogramHashKey) {
+    inserter.AddArg(interned_chrome_histogram_hash_key_,
+                    Variadic::UnsignedInteger(value));
     const base::Optional<base::StringView> translated_value =
         TranslateChromeHistogramHash(value);
     if (translated_value) {
@@ -39,17 +50,24 @@
           interned_chrome_histogram_name_key_,
           Variadic::String(storage_->InternString(*translated_value)));
     }
+    return true;
+  }
+  if (key.key == kChromeUserEventHashKey) {
+    inserter.AddArg(interned_chrome_user_event_hash_key_,
+                    Variadic::UnsignedInteger(value));
+    const base::Optional<base::StringView> translated_value =
+        TranslateChromeUserEventHash(value);
+    if (translated_value) {
+      inserter.AddArg(
+          interned_chrome_user_event_action_key_,
+          Variadic::String(storage_->InternString(*translated_value)));
+    }
+    return true;
   }
   return false;
 }
 
 base::Optional<base::StringView>
-ArgsTranslationTable::TranslateChromeHistogramHashForTesting(
-    uint64_t hash) const {
-  return TranslateChromeHistogramHash(hash);
-}
-
-base::Optional<base::StringView>
 ArgsTranslationTable::TranslateChromeHistogramHash(uint64_t hash) const {
   auto* value = chrome_histogram_hash_to_name_.Find(hash);
   if (!value) {
@@ -58,10 +76,13 @@
   return base::StringView(*value);
 }
 
-void ArgsTranslationTable::AddChromeHistogramTranslationRule(
-    uint64_t hash,
-    base::StringView name) {
-  chrome_histogram_hash_to_name_.Insert(hash, name.ToStdString());
+base::Optional<base::StringView>
+ArgsTranslationTable::TranslateChromeUserEventHash(uint64_t hash) const {
+  auto* value = chrome_user_event_hash_to_action_.Find(hash);
+  if (!value) {
+    return base::nullopt;
+  }
+  return base::StringView(*value);
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/args_translation_table.h b/src/trace_processor/importers/common/args_translation_table.h
index aac2bb6..a27656f 100644
--- a/src/trace_processor/importers/common/args_translation_table.h
+++ b/src/trace_processor/importers/common/args_translation_table.h
@@ -44,10 +44,22 @@
                                    uint64_t value,
                                    ArgsTracker::BoundInserter& inserter);
 
-  void AddChromeHistogramTranslationRule(uint64_t hash, base::StringView name);
+  void AddChromeHistogramTranslationRule(uint64_t hash, base::StringView name) {
+    chrome_histogram_hash_to_name_.Insert(hash, name.ToStdString());
+  }
+  void AddChromeUserEventTranslationRule(uint64_t hash,
+                                         base::StringView action) {
+    chrome_user_event_hash_to_action_.Insert(hash, action.ToStdString());
+  }
 
   base::Optional<base::StringView> TranslateChromeHistogramHashForTesting(
-      uint64_t hash) const;
+      uint64_t hash) const {
+    return TranslateChromeHistogramHash(hash);
+  }
+  base::Optional<base::StringView> TranslateChromeUserEventHashForTesting(
+      uint64_t hash) const {
+    return TranslateChromeUserEventHash(hash);
+  }
 
  private:
   static constexpr char kChromeHistogramHashKey[] =
@@ -55,12 +67,23 @@
   static constexpr char kChromeHistogramNameKey[] =
       "chrome_histogram_sample.name";
 
+  static constexpr char kChromeUserEventHashKey[] =
+      "chrome_user_event.action_hash";
+  static constexpr char kChromeUserEventActionKey[] =
+      "chrome_user_event.action";
+
   TraceStorage* storage_;
+  StringId interned_chrome_histogram_hash_key_;
   StringId interned_chrome_histogram_name_key_;
+  StringId interned_chrome_user_event_hash_key_;
+  StringId interned_chrome_user_event_action_key_;
   base::FlatHashMap<uint64_t, std::string> chrome_histogram_hash_to_name_;
+  base::FlatHashMap<uint64_t, std::string> chrome_user_event_hash_to_action_;
 
   base::Optional<base::StringView> TranslateChromeHistogramHash(
       uint64_t hash) const;
+  base::Optional<base::StringView> TranslateChromeUserEventHash(
+      uint64_t hash) const;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/args_translation_table_unittest.cc b/src/trace_processor/importers/common/args_translation_table_unittest.cc
index 08bc458..5e5ab4b 100644
--- a/src/trace_processor/importers/common/args_translation_table_unittest.cc
+++ b/src/trace_processor/importers/common/args_translation_table_unittest.cc
@@ -25,9 +25,10 @@
   TraceStorage storage;
   ArgsTranslationTable table(&storage);
   EXPECT_EQ(table.TranslateChromeHistogramHashForTesting(1), base::nullopt);
+  EXPECT_EQ(table.TranslateChromeUserEventHashForTesting(1), base::nullopt);
 }
 
-TEST(ArgsTranslationTable, TranslatesHashes) {
+TEST(ArgsTranslationTable, TranslatesHistogramHashes) {
   TraceStorage storage;
   ArgsTranslationTable table(&storage);
   table.AddChromeHistogramTranslationRule(1, "hash1");
@@ -39,6 +40,18 @@
   EXPECT_EQ(table.TranslateChromeHistogramHashForTesting(2), base::nullopt);
 }
 
+TEST(ArgsTranslationTable, TranslatesUserEventHashes) {
+  TraceStorage storage;
+  ArgsTranslationTable table(&storage);
+  table.AddChromeUserEventTranslationRule(1, "action1");
+  table.AddChromeUserEventTranslationRule(10, "action2");
+  EXPECT_EQ(table.TranslateChromeUserEventHashForTesting(1),
+            base::Optional<base::StringView>("action1"));
+  EXPECT_EQ(table.TranslateChromeUserEventHashForTesting(10),
+            base::Optional<base::StringView>("action2"));
+  EXPECT_EQ(table.TranslateChromeUserEventHashForTesting(2), base::nullopt);
+}
+
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/common/process_tracker.cc b/src/trace_processor/importers/common/process_tracker.cc
index 93a5890..2ef798d 100644
--- a/src/trace_processor/importers/common/process_tracker.cc
+++ b/src/trace_processor/importers/common/process_tracker.cc
@@ -285,7 +285,7 @@
   auto* process_table = context_->storage->mutable_process_table();
   auto* thread_table = context_->storage->mutable_thread_table();
 
-  PERFETTO_DCHECK(process_table->name()[upid].is_null());
+  PERFETTO_DCHECK(!process_table->name()[upid].has_value());
   PERFETTO_DCHECK(!process_table->start_ts()[upid].has_value());
 
   if (timestamp) {
@@ -339,7 +339,7 @@
 void ProcessTracker::SetProcessNameIfUnset(UniquePid upid,
                                            StringId process_name_id) {
   auto* process_table = context_->storage->mutable_process_table();
-  if (process_table->name()[upid].is_null())
+  if (!process_table->name()[upid].has_value())
     process_table->mutable_name()->Set(upid, process_name_id);
 }
 
diff --git a/src/trace_processor/importers/common/process_tracker_unittest.cc b/src/trace_processor/importers/common/process_tracker_unittest.cc
index 7fc9055..d037136 100644
--- a/src/trace_processor/importers/common/process_tracker_unittest.cc
+++ b/src/trace_processor/importers/common/process_tracker_unittest.cc
@@ -86,8 +86,8 @@
 TEST_F(ProcessTrackerTest, AddProcessEntry_CorrectName) {
   context.process_tracker->SetProcessMetadata(1, base::nullopt, "test",
                                               base::StringView());
-  auto name =
-      context.storage->GetString(context.storage->process_table().name()[1]);
+  auto name = context.storage->process_table().name().GetString(1);
+
   ASSERT_EQ(name, "test");
 }
 
diff --git a/src/trace_processor/importers/ftrace/binder_tracker.cc b/src/trace_processor/importers/ftrace/binder_tracker.cc
index fd229f1..5e95579 100644
--- a/src/trace_processor/importers/ftrace/binder_tracker.cc
+++ b/src/trace_processor/importers/ftrace/binder_tracker.cc
@@ -113,12 +113,13 @@
     if (is_reply) {
       UniqueTid utid = context_->process_tracker->GetOrCreateThread(
           static_cast<uint32_t>(dest_tid));
-      StringId dest_thread_name =
-          context_->storage->thread_table().name()[utid];
+      auto dest_thread_name = context_->storage->thread_table().name()[utid];
       auto dest_args_inserter = [this, dest_tid, &dest_thread_name](
                                     ArgsTracker::BoundInserter* inserter) {
         inserter->AddArg(dest_thread_, Variadic::Integer(dest_tid));
-        inserter->AddArg(dest_name_, Variadic::String(dest_thread_name));
+        if (dest_thread_name.has_value()) {
+          inserter->AddArg(dest_name_, Variadic::String(*dest_thread_name));
+        }
       };
       context_->slice_tracker->AddArgs(track_id, binder_category_id_, reply_id_,
                                        dest_args_inserter);
@@ -179,9 +180,10 @@
       auto args_inserter = [this, utid,
                             pid](ArgsTracker::BoundInserter* inserter) {
         inserter->AddArg(dest_thread_, Variadic::UnsignedInteger(pid));
-        inserter->AddArg(
-            dest_name_,
-            Variadic::String(context_->storage->thread_table().name()[utid]));
+        auto dest_thread_name = context_->storage->thread_table().name()[utid];
+        if (dest_thread_name.has_value()) {
+          inserter->AddArg(dest_name_, Variadic::String(*dest_thread_name));
+        }
       };
       context_->slice_tracker->AddArgs(*transaction.send_track_id,
                                        binder_category_id_,
diff --git a/src/trace_processor/importers/ftrace/sched_event_tracker.cc b/src/trace_processor/importers/ftrace/sched_event_tracker.cc
index 00fc93f..66a78b3 100644
--- a/src/trace_processor/importers/ftrace/sched_event_tracker.cc
+++ b/src/trace_processor/importers/ftrace/sched_event_tracker.cc
@@ -175,7 +175,9 @@
 
   // Do a fresh task name lookup in case it was updated by a task_rename while
   // scheduled.
-  StringId prev_comm_id = context_->storage->thread_table().name()[prev_utid];
+  StringId prev_comm_id =
+      context_->storage->thread_table().name()[prev_utid].value_or(
+          kNullStringId);
 
   auto new_slice_idx = AddRawEventAndStartSlice(
       cpu, ts, prev_utid, prev_pid, prev_comm_id, prev_prio, prev_state,
diff --git a/src/trace_processor/importers/ftrace/sched_event_tracker_unittest.cc b/src/trace_processor/importers/ftrace/sched_event_tracker_unittest.cc
index 15f6eca..802990f 100644
--- a/src/trace_processor/importers/ftrace/sched_event_tracker_unittest.cc
+++ b/src/trace_processor/importers/ftrace/sched_event_tracker_unittest.cc
@@ -69,8 +69,7 @@
   ASSERT_EQ(timestamps[0], timestamp);
   ASSERT_EQ(context.storage->thread_table().start_ts()[1], base::nullopt);
 
-  auto name =
-      context.storage->GetString(context.storage->thread_table().name()[1]);
+  auto name = context.storage->thread_table().name().GetString(1);
   ASSERT_STREQ(name.c_str(), kCommProc1);
   ASSERT_EQ(context.storage->sched_slice_table().utid()[0], 1u);
   ASSERT_EQ(context.storage->sched_slice_table().dur()[0], 1);
diff --git a/src/trace_processor/importers/proto/translation_table_module.cc b/src/trace_processor/importers/proto/translation_table_module.cc
index cf523d9..24d4ebe 100644
--- a/src/trace_processor/importers/proto/translation_table_module.cc
+++ b/src/trace_processor/importers/proto/translation_table_module.cc
@@ -42,6 +42,8 @@
       protos::pbzero::TranslationTable::Decoder(decoder.translation_table());
   if (translation_table.has_chrome_histogram()) {
     ParseChromeHistogramRules(translation_table.chrome_histogram());
+  } else if (translation_table.has_chrome_user_event()) {
+    ParseChromeUserEventRules(translation_table.chrome_user_event());
   }
 }
 
@@ -57,5 +59,17 @@
   }
 }
 
+void TranslationTableModule::ParseChromeUserEventRules(
+    protozero::ConstBytes bytes) {
+  const auto chrome_user_event =
+      protos::pbzero::ChromeUserEventTranslationTable::Decoder(bytes);
+  for (auto it = chrome_user_event.action_hash_to_name(); it; ++it) {
+    protos::pbzero::ChromeUserEventTranslationTable::ActionHashToNameEntry::
+        Decoder entry(*it);
+    context_->args_translation_table->AddChromeUserEventTranslationRule(
+        entry.key(), entry.value());
+  }
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/proto/translation_table_module.h b/src/trace_processor/importers/proto/translation_table_module.h
index c6c61a6..bc96f00 100644
--- a/src/trace_processor/importers/proto/translation_table_module.h
+++ b/src/trace_processor/importers/proto/translation_table_module.h
@@ -39,6 +39,7 @@
 
  private:
   void ParseChromeHistogramRules(protozero::ConstBytes bytes);
+  void ParseChromeUserEventRules(protozero::ConstBytes bytes);
 
   TraceProcessorContext* context_;
 };
diff --git a/src/trace_processor/metrics/sql/BUILD.gn b/src/trace_processor/metrics/sql/BUILD.gn
index 27facfb..5c07ca1 100644
--- a/src/trace_processor/metrics/sql/BUILD.gn
+++ b/src/trace_processor/metrics/sql/BUILD.gn
@@ -87,6 +87,7 @@
   "chrome/chrome_histogram_hashes.sql",
   "chrome/chrome_processes.sql",
   "chrome/chrome_thread_slice.sql",
+  "chrome/chrome_user_event_hashes.sql",
   "chrome/cpu_time_by_category.sql",
   "chrome/cpu_time_by_rail_mode.sql",
   "chrome/estimated_power_by_category.sql",
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql b/src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql
index ba48b5d..ef5d918 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql
@@ -21,6 +21,6 @@
     SELECT RepeatedField(int_value)
     FROM args
     WHERE key = 'chrome_histogram_sample.name_hash'
-    ORDER BY arg_set_id
+    ORDER BY int_value
   )
 );
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_user_event_hashes.sql b/src/trace_processor/metrics/sql/chrome/chrome_user_event_hashes.sql
new file mode 100644
index 0000000..005e2ad
--- /dev/null
+++ b/src/trace_processor/metrics/sql/chrome/chrome_user_event_hashes.sql
@@ -0,0 +1,26 @@
+--
+-- Copyright 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
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+DROP VIEW IF EXISTS chrome_user_event_hashes_output;
+
+CREATE VIEW chrome_user_event_hashes_output AS
+SELECT ChromeUserEventHashes(
+  'action_hash', (
+    SELECT RepeatedField(int_value)
+    FROM args
+    WHERE key = 'chrome_user_event.action_hash'
+    ORDER BY int_value
+  )
+);
diff --git a/src/trace_processor/sqlite/sqlite_raw_table.cc b/src/trace_processor/sqlite/sqlite_raw_table.cc
index e579fda..a42d83b 100644
--- a/src/trace_processor/sqlite/sqlite_raw_table.cc
+++ b/src/trace_processor/sqlite/sqlite_raw_table.cc
@@ -619,7 +619,7 @@
   if (opt_upid.has_value()) {
     tgid = storage_->process_table().pid()[*opt_upid];
   }
-  auto name = storage_->GetString(storage_->thread_table().name()[utid]);
+  auto name = storage_->thread_table().name().GetString(utid);
 
   FtraceTime ftrace_time(ts);
   if (tid == 0) {
diff --git a/src/trace_processor/tables/metadata_tables.h b/src/trace_processor/tables/metadata_tables.h
index 2fad555..22431b9 100644
--- a/src/trace_processor/tables/metadata_tables.h
+++ b/src/trace_processor/tables/metadata_tables.h
@@ -88,7 +88,7 @@
   NAME(ThreadTable, "internal_thread")                \
   PERFETTO_TP_ROOT_TABLE(PARENT, C)                   \
   C(uint32_t, tid)                                    \
-  C(StringPool::Id, name)                             \
+  C(base::Optional<StringPool::Id>, name)             \
   C(base::Optional<int64_t>, start_ts)                \
   C(base::Optional<int64_t>, end_ts)                  \
   C(base::Optional<uint32_t>, upid)                   \
@@ -129,7 +129,7 @@
   NAME(ProcessTable, "internal_process")               \
   PERFETTO_TP_ROOT_TABLE(PARENT, C)                    \
   C(uint32_t, pid)                                     \
-  C(StringPool::Id, name)                              \
+  C(base::Optional<StringPool::Id>, name)              \
   C(base::Optional<int64_t>, start_ts)                 \
   C(base::Optional<int64_t>, end_ts)                   \
   C(base::Optional<uint32_t>, parent_upid)             \
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index e0e8707..dda24f1 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -128,6 +128,22 @@
   }
 };
 
+static void WriteFile(const std::string& file_name,
+                      const char* content,
+                      size_t len) {
+  std::ofstream output;
+  output.open(file_name.c_str(), std::ios::out | std::ios::binary);
+  output.write(content, static_cast<std::streamsize>(len));
+  output.close();
+}
+
+// Unused in merged code, but very handy for debugging when trace generated in
+// a test needs to be exported, to understand it further with other tools.
+__attribute__((unused)) static void WriteFile(const std::string& file_name,
+                                              const std::vector<char>& data) {
+  return WriteFile(file_name, data.data(), data.size());
+}
+
 // Represents an opaque (from Perfetto's point of view) thread identifier (e.g.,
 // base::PlatformThreadId in Chromium).
 struct MyThreadId {
diff --git a/test/trace_processor/chrome/chrome_user_event_hashes.out b/test/trace_processor/chrome/chrome_user_event_hashes.out
new file mode 100644
index 0000000..69ca70f
--- /dev/null
+++ b/test/trace_processor/chrome/chrome_user_event_hashes.out
@@ -0,0 +1,5 @@
+[perfetto.protos.chrome_user_event_hashes]: {
+  action_hash: 10
+  action_hash: 20
+}
+
diff --git a/test/trace_processor/chrome/chrome_user_event_hashes.textproto b/test/trace_processor/chrome/chrome_user_event_hashes.textproto
new file mode 100644
index 0000000..41b21b9
--- /dev/null
+++ b/test/trace_processor/chrome/chrome_user_event_hashes.textproto
@@ -0,0 +1,27 @@
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  incremental_state_cleared: true
+  track_event {
+    categories: "cat1"
+    type: 3
+    name_iid: 1
+    chrome_user_event {
+      action_hash: 10
+    }
+  }
+}
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  incremental_state_cleared: true
+  track_event {
+    categories: "cat2"
+    type: 3
+    name_iid: 2
+    chrome_user_event {
+      action_hash: 20
+    }
+  }
+}
+
diff --git a/test/trace_processor/chrome/index b/test/trace_processor/chrome/index
index 1f727f4..9bdeb89 100644
--- a/test/trace_processor/chrome/index
+++ b/test/trace_processor/chrome/index
@@ -57,3 +57,6 @@
 
 # Chrome histogram hashes
 chrome_histogram_hashes.textproto chrome_histogram_hashes chrome_histogram_hashes.out
+
+# Chrome user events
+chrome_user_event_hashes.textproto chrome_user_event_hashes chrome_user_event_hashes.out
diff --git a/test/trace_processor/translation/chrome_histogram.sql b/test/trace_processor/translation/chrome_args.sql
similarity index 100%
rename from test/trace_processor/translation/chrome_histogram.sql
rename to test/trace_processor/translation/chrome_args.sql
diff --git a/test/trace_processor/translation/chrome_user_event.out b/test/trace_processor/translation/chrome_user_event.out
new file mode 100644
index 0000000..b092f7a
--- /dev/null
+++ b/test/trace_processor/translation/chrome_user_event.out
@@ -0,0 +1,9 @@
+"flat_key","key","int_value","string_value"
+"is_root_in_scope","is_root_in_scope",1,"[NULL]"
+"source","source","[NULL]","descriptor"
+"source_id","source_id",0,"[NULL]"
+"chrome_user_event.action","chrome_user_event.action","[NULL]","action1"
+"chrome_user_event.action_hash","chrome_user_event.action_hash",10,"[NULL]"
+"chrome_user_event.action","chrome_user_event.action","[NULL]","action2"
+"chrome_user_event.action_hash","chrome_user_event.action_hash",20,"[NULL]"
+"chrome_user_event.action_hash","chrome_user_event.action_hash",30,"[NULL]"
diff --git a/test/trace_processor/translation/chrome_user_event.textproto b/test/trace_processor/translation/chrome_user_event.textproto
new file mode 100644
index 0000000..9dd30c1
--- /dev/null
+++ b/test/trace_processor/translation/chrome_user_event.textproto
@@ -0,0 +1,52 @@
+# Chrome histogram hashes translation rules
+packet {
+  translation_table {
+    chrome_user_event {
+      action_hash_to_name { key: 10 value: "action1" }
+      action_hash_to_name { key: 20 value: "action2" }
+    }
+  }
+}
+# Known action hash, should be translated to a name
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  incremental_state_cleared: true
+  track_event {
+    categories: "cat1"
+    type: 3
+    name_iid: 1
+    chrome_user_event {
+      action_hash: 10
+    }
+  }
+}
+# Another known hash, should be translated to a name
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  incremental_state_cleared: true
+  track_event {
+    categories: "cat2"
+    type: 3
+    name_iid: 2
+    chrome_user_event {
+      action_hash: 20
+    }
+  }
+}
+# Unknown hash, should not be translated to any name
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  incremental_state_cleared: true
+  track_event {
+    categories: "cat3"
+    type: 3
+    name_iid: 3
+    chrome_user_event {
+      action_hash: 30
+    }
+  }
+}
+
diff --git a/test/trace_processor/translation/index b/test/trace_processor/translation/index
index 77fd3d4..fc07479 100644
--- a/test/trace_processor/translation/index
+++ b/test/trace_processor/translation/index
@@ -1 +1,2 @@
-chrome_histogram.textproto chrome_histogram.sql chrome_histogram.out
+chrome_histogram.textproto chrome_args.sql chrome_histogram.out
+chrome_user_event.textproto chrome_args.sql chrome_user_event.out
diff --git a/ui/src/base/generic_set.ts b/ui/src/base/generic_set.ts
new file mode 100644
index 0000000..d2b73cf
--- /dev/null
+++ b/ui/src/base/generic_set.ts
@@ -0,0 +1,30 @@
+// ES6 Set does not allow to reasonably store compound objects; this class
+// rectifies the problem by implementing generic set on top of Map and an
+// injective function from objects of generic type to strings.
+export class GenericSet<T> {
+  interner: (t: T) => string;
+
+  // Passed function should be injective (as in never having the same output for
+  // two different inputs).
+  constructor(interner: (t: T) => string) {
+    this.interner = interner;
+  }
+
+  backingMap = new Map<string, T>();
+
+  has(column: T): boolean {
+    return this.backingMap.has(this.interner(column));
+  }
+
+  add(column: T) {
+    this.backingMap.set(this.interner(column), column);
+  }
+
+  delete(column: T) {
+    this.backingMap.delete(this.interner(column));
+  }
+
+  values(): Iterable<T> {
+    return this.backingMap.values();
+  }
+}
diff --git a/ui/src/frontend/pivot_table_redux.ts b/ui/src/frontend/pivot_table_redux.ts
index ea2b7d3..a8f3e8b 100644
--- a/ui/src/frontend/pivot_table_redux.ts
+++ b/ui/src/frontend/pivot_table_redux.ts
@@ -1,5 +1,6 @@
 import * as m from 'mithril';
 
+import {GenericSet} from '../base/generic_set';
 import {sqliteString} from '../base/string_utils';
 import {Actions} from '../common/actions';
 import {ColumnType} from '../common/query_result';
@@ -15,7 +16,7 @@
 import {
   aggregationIndex,
   areaFilter,
-  ColumnSet,
+  createColumnSet,
   generateQuery,
   QueryGeneratorError,
   sliceAggregationColumns,
@@ -26,7 +27,7 @@
 } from './pivot_table_redux_query_generator';
 
 interface ColumnSetCheckboxAttrs {
-  set: ColumnSet;
+  set: GenericSet<TableColumn>;
   setKey: TableColumn;
 }
 
@@ -74,8 +75,8 @@
 }
 
 export class PivotTableRedux extends Panel<PivotTableReduxAttrs> {
-  selectedPivotsMap = new ColumnSet();
-  selectedAggregations = new ColumnSet();
+  selectedPivotsMap = createColumnSet();
+  selectedAggregations = createColumnSet();
   constrainToArea = true;
   editMode = true;
 
diff --git a/ui/src/frontend/pivot_table_redux_query_generator.ts b/ui/src/frontend/pivot_table_redux_query_generator.ts
index 8e9586b..8c201fc 100644
--- a/ui/src/frontend/pivot_table_redux_query_generator.ts
+++ b/ui/src/frontend/pivot_table_redux_query_generator.ts
@@ -1,3 +1,4 @@
+import {GenericSet} from '../base/generic_set';
 import {Area, PivotTableReduxQuery} from '../common/state';
 import {toNs} from '../common/time';
 import {
@@ -48,35 +49,8 @@
 // Pair of table name and column name.
 export type TableColumn = [string, string];
 
-// ES6 Set does not allow to reasonably store compound objects; this class
-// rectifies the problem by providing a domain-specific set of pairs of strings.
-export class ColumnSet {
-  // Should've been Set<TableColumn>, but JavaScript Set does not support custom
-  // hashing/equality predicates, so TableColumn is keyed by a string generated
-  // by columnKey method.
-  backingMap = new Map<string, TableColumn>();
-
-  private static columnKey(column: TableColumn): string {
-    // None of table and column names used in Perfetto tables contain periods,
-    // so this function should not lead to collisions.
-    return `${column[0]}.${column[1]}`;
-  }
-
-  has(column: TableColumn): boolean {
-    return this.backingMap.has(ColumnSet.columnKey(column));
-  }
-
-  add(column: TableColumn) {
-    this.backingMap.set(ColumnSet.columnKey(column), column);
-  }
-
-  delete(column: TableColumn) {
-    this.backingMap.delete(ColumnSet.columnKey(column));
-  }
-
-  values(): Iterable<[string, string]> {
-    return this.backingMap.values();
-  }
+export function createColumnSet(): GenericSet<TableColumn> {
+  return new GenericSet((column: TableColumn) => `${column[0]}.${column[1]}`);
 }
 
 // Exception thrown by query generator in case incoming parameters are not
@@ -124,7 +98,8 @@
   `;
 }
 
-function computeSliceTableAggregations(selectedAggregations: ColumnSet):
+function computeSliceTableAggregations(
+    selectedAggregations: GenericSet<TableColumn>):
     {tableName: string, flatAggregations: string[]} {
   let hasThreadSliceColumn = false;
   const allColumns = [];
@@ -154,8 +129,8 @@
 }
 
 export function generateQuery(
-    selectedPivots: ColumnSet,
-    selectedAggregations: ColumnSet,
+    selectedPivots: GenericSet<TableColumn>,
+    selectedAggregations: GenericSet<TableColumn>,
     area: Area,
     constrainToArea: boolean): PivotTableReduxQuery {
   const sliceTableAggregations =