Merge "Revert "[metrics] Add thread creation spam stats to android_task_names""
diff --git a/Android.bp b/Android.bp
index 63c30e1..9e9630c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -2103,6 +2103,7 @@
         ":perfetto_src_trace_processor_util_proto_profiler",
         ":perfetto_src_trace_processor_util_proto_to_args_parser",
         ":perfetto_src_trace_processor_util_protozero_to_text",
+        ":perfetto_src_trace_processor_util_regex",
         ":perfetto_src_trace_processor_util_sql_argument",
         ":perfetto_src_trace_processor_util_stack_traces_util",
         ":perfetto_src_trace_processor_util_stdlib",
@@ -4437,6 +4438,7 @@
         "protos/perfetto/metrics/chrome/reported_by_page.proto",
         "protos/perfetto/metrics/chrome/scroll_jank.proto",
         "protos/perfetto/metrics/chrome/scroll_jank_v2.proto",
+        "protos/perfetto/metrics/chrome/scroll_jank_v3.proto",
         "protos/perfetto/metrics/chrome/slice_names.proto",
         "protos/perfetto/metrics/chrome/test_chrome_metric.proto",
         "protos/perfetto/metrics/chrome/touch_jank.proto",
@@ -10264,6 +10266,7 @@
         "src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_caused_by_scheduling.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_v2.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_v3.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_stack_samples_for_task.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_tasks.sql",
@@ -10294,7 +10297,6 @@
         "src/trace_processor/metrics/sql/chrome/scroll_jank_cause_blocking_touch_move.sql",
         "src/trace_processor/metrics/sql/chrome/scroll_jank_cause_get_bitmap.sql",
         "src/trace_processor/metrics/sql/chrome/scroll_jank_cause_queuing_delay.sql",
-        "src/trace_processor/metrics/sql/chrome/scroll_jank_v3.sql",
         "src/trace_processor/metrics/sql/chrome/sufficient_chrome_processes.sql",
         "src/trace_processor/metrics/sql/chrome/test_chrome_metric.sql",
         "src/trace_processor/metrics/sql/chrome/touch_flow_event.sql",
@@ -10841,6 +10843,11 @@
     ],
 }
 
+// GN: //src/trace_processor/util:regex
+filegroup {
+    name: "perfetto_src_trace_processor_util_regex",
+}
+
 // GN: //src/trace_processor/util:sql_argument
 filegroup {
     name: "perfetto_src_trace_processor_util_sql_argument",
@@ -12313,6 +12320,7 @@
         ":perfetto_src_trace_processor_util_proto_profiler",
         ":perfetto_src_trace_processor_util_proto_to_args_parser",
         ":perfetto_src_trace_processor_util_protozero_to_text",
+        ":perfetto_src_trace_processor_util_regex",
         ":perfetto_src_trace_processor_util_sql_argument",
         ":perfetto_src_trace_processor_util_stack_traces_util",
         ":perfetto_src_trace_processor_util_stdlib",
@@ -12997,6 +13005,7 @@
         ":perfetto_src_trace_processor_util_proto_profiler",
         ":perfetto_src_trace_processor_util_proto_to_args_parser",
         ":perfetto_src_trace_processor_util_protozero_to_text",
+        ":perfetto_src_trace_processor_util_regex",
         ":perfetto_src_trace_processor_util_sql_argument",
         ":perfetto_src_trace_processor_util_stack_traces_util",
         ":perfetto_src_trace_processor_util_stdlib",
@@ -13225,6 +13234,7 @@
         ":perfetto_src_trace_processor_util_proto_profiler",
         ":perfetto_src_trace_processor_util_proto_to_args_parser",
         ":perfetto_src_trace_processor_util_protozero_to_text",
+        ":perfetto_src_trace_processor_util_regex",
         ":perfetto_src_trace_processor_util_sql_argument",
         ":perfetto_src_trace_processor_util_stack_traces_util",
         ":perfetto_src_trace_processor_util_stdlib",
diff --git a/BUILD b/BUILD
index a7e0557..73afdf9 100644
--- a/BUILD
+++ b/BUILD
@@ -129,6 +129,7 @@
         ":src_trace_processor_util_proto_profiler",
         ":src_trace_processor_util_proto_to_args_parser",
         ":src_trace_processor_util_protozero_to_text",
+        ":src_trace_processor_util_regex",
         ":src_trace_processor_util_sql_argument",
         ":src_trace_processor_util_stack_traces_util",
         ":src_trace_processor_util_stdlib",
@@ -1927,6 +1928,7 @@
         "src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_caused_by_scheduling.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_v2.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_v3.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_stack_samples_for_task.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_tasks.sql",
@@ -1957,7 +1959,6 @@
         "src/trace_processor/metrics/sql/chrome/scroll_jank_cause_blocking_touch_move.sql",
         "src/trace_processor/metrics/sql/chrome/scroll_jank_cause_get_bitmap.sql",
         "src/trace_processor/metrics/sql/chrome/scroll_jank_cause_queuing_delay.sql",
-        "src/trace_processor/metrics/sql/chrome/scroll_jank_v3.sql",
         "src/trace_processor/metrics/sql/chrome/sufficient_chrome_processes.sql",
         "src/trace_processor/metrics/sql/chrome/test_chrome_metric.sql",
         "src/trace_processor/metrics/sql/chrome/touch_flow_event.sql",
@@ -2509,6 +2510,14 @@
     ],
 )
 
+# GN target: //src/trace_processor/util:regex
+perfetto_filegroup(
+    name = "src_trace_processor_util_regex",
+    srcs = [
+        "src/trace_processor/util/regex.h",
+    ],
+)
+
 # GN target: //src/trace_processor/util:sql_argument
 perfetto_filegroup(
     name = "src_trace_processor_util_sql_argument",
@@ -4070,6 +4079,7 @@
         "protos/perfetto/metrics/chrome/reported_by_page.proto",
         "protos/perfetto/metrics/chrome/scroll_jank.proto",
         "protos/perfetto/metrics/chrome/scroll_jank_v2.proto",
+        "protos/perfetto/metrics/chrome/scroll_jank_v3.proto",
         "protos/perfetto/metrics/chrome/slice_names.proto",
         "protos/perfetto/metrics/chrome/test_chrome_metric.proto",
         "protos/perfetto/metrics/chrome/touch_jank.proto",
@@ -5186,6 +5196,7 @@
         ":src_trace_processor_util_proto_profiler",
         ":src_trace_processor_util_proto_to_args_parser",
         ":src_trace_processor_util_protozero_to_text",
+        ":src_trace_processor_util_regex",
         ":src_trace_processor_util_sql_argument",
         ":src_trace_processor_util_stack_traces_util",
         ":src_trace_processor_util_stdlib",
@@ -5349,6 +5360,7 @@
         ":src_trace_processor_util_proto_profiler",
         ":src_trace_processor_util_proto_to_args_parser",
         ":src_trace_processor_util_protozero_to_text",
+        ":src_trace_processor_util_regex",
         ":src_trace_processor_util_sql_argument",
         ":src_trace_processor_util_stack_traces_util",
         ":src_trace_processor_util_stdlib",
@@ -5567,6 +5579,7 @@
         ":src_trace_processor_util_proto_profiler",
         ":src_trace_processor_util_proto_to_args_parser",
         ":src_trace_processor_util_protozero_to_text",
+        ":src_trace_processor_util_regex",
         ":src_trace_processor_util_sql_argument",
         ":src_trace_processor_util_stack_traces_util",
         ":src_trace_processor_util_stdlib",
diff --git a/gn/BUILD.gn b/gn/BUILD.gn
index 749c2ee..a639b4d 100644
--- a/gn/BUILD.gn
+++ b/gn/BUILD.gn
@@ -56,6 +56,7 @@
   } else {
     perfetto_watchdog = "0"
   }
+
   if (enable_perfetto_tools) {
     perfetto_local_symbolizer =
         "PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_LINUX() || " +
diff --git a/gn/perfetto.gni b/gn/perfetto.gni
index 579fc0f..eebfc41 100644
--- a/gn/perfetto.gni
+++ b/gn/perfetto.gni
@@ -300,8 +300,7 @@
   # service and by the "perfetto" cmdline client) and to decompress traces (by
   # trace_processor).
   enable_perfetto_zlib =
-      (enable_perfetto_trace_processor && !build_with_chromium) ||
-      enable_perfetto_platform_services
+      enable_perfetto_trace_processor || enable_perfetto_platform_services
 
   # Enables function name demangling using sources from llvm. Otherwise
   # trace_processor falls back onto using the c++ runtime demangler, which
diff --git a/protos/perfetto/metrics/chrome/BUILD.gn b/protos/perfetto/metrics/chrome/BUILD.gn
index 1a7ae98..f82d634 100644
--- a/protos/perfetto/metrics/chrome/BUILD.gn
+++ b/protos/perfetto/metrics/chrome/BUILD.gn
@@ -32,6 +32,7 @@
     "reported_by_page.proto",
     "scroll_jank.proto",
     "scroll_jank_v2.proto",
+    "scroll_jank_v3.proto",
     "slice_names.proto",
     "test_chrome_metric.proto",
     "touch_jank.proto",
diff --git a/protos/perfetto/metrics/chrome/all_chrome_metrics.proto b/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
index a82d617..908af94 100644
--- a/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
+++ b/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
@@ -30,6 +30,7 @@
 import "protos/perfetto/metrics/chrome/slice_names.proto";
 import "protos/perfetto/metrics/chrome/scroll_jank.proto";
 import "protos/perfetto/metrics/chrome/scroll_jank_v2.proto";
+import "protos/perfetto/metrics/chrome/scroll_jank_v3.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";
@@ -53,4 +54,5 @@
   optional ChromeUnsymbolizedArgs chrome_unsymbolized_args = 1014;
   optional ChromeArgsClassNames chrome_args_class_names = 1015;
   optional ChromeScrollJankV2 chrome_scroll_jank_v2 = 1016;
+  optional ChromeScrollJankV3 chrome_scroll_jank_v3 = 1017;
 }
diff --git a/protos/perfetto/metrics/chrome/scroll_jank_v3.proto b/protos/perfetto/metrics/chrome/scroll_jank_v3.proto
new file mode 100644
index 0000000..6f677a6
--- /dev/null
+++ b/protos/perfetto/metrics/chrome/scroll_jank_v3.proto
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 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;
+
+import "protos/perfetto/metrics/custom_options.proto";
+
+message ChromeScrollJankV3 {
+  // Total count of unique frames in the trace.
+  optional int64 trace_num_frames = 1 [(unit) = "count_biggerIsBetter"];
+  // Number of frames in the trace that had missed vsyncs.
+  optional int64 trace_num_janky_frames = 2 [(unit) = "count_smallerIsBetter"];
+  // Computed as: `100 * trace_num_janky_frames /
+  // trace_num_frames`.
+  optional double trace_scroll_jank_percentage = 3
+      [(unit) = "n%_smallerIsBetter"];
+  // Vsync interval at the time of recording the trace.
+  optional double vsync_interval_ms = 4 [(unit) = "ms_biggerIsBetter"];
+  // Per-scroll metrics.
+  message Scroll {
+    // Total count of unique frames during this scroll.
+    optional int64 num_frames = 1 [(unit) = "count_biggerIsBetter"];
+    // Number of frames during this scroll that had missed vsyncs.
+    optional int64 num_janky_frames = 2 [(unit) = "count_smallerIsBetter"];
+    // Computed as: `100 * num_janky_frames / num_frames`.
+    optional double scroll_jank_percentage = 3 [(unit) = "n%_smallerIsBetter"];
+    // Maximum `delay_since_last_frame` for janky frames during this scroll
+    // (i.e. maximum `delay_since_last_frame` from the `scroll_jank_causes`
+    // repeated field below).
+    // This is measured in units of vsync interval. For example, a value of 2
+    // means the delay was 2x the vsync interval.
+    optional double max_delay_since_last_frame = 4
+        [(unit) = "unitless_smallerIsBetter"];
+    // The primary cause and sub-cause for scroll jank, one for each jank.
+    // There are exactly `num_janky_frames` items in this field.
+    message ScrollJankCause {
+      // May be empty, if a cause is not available.
+      optional string cause = 1;
+      // May be empty, if a sub-cause is not available.
+      optional string sub_cause = 2;
+      // Number of vsyncs elapsed since the previous frame was presented: will
+      // be > 1.0 since this was a "janky" frame. For example, a value of 2
+      // means the delay was 2x the vsync interval.
+      optional double delay_since_last_frame = 3
+          [(unit) = "unitless_smallerIsBetter"];
+    }
+    repeated ScrollJankCause scroll_jank_causes = 5;
+  }
+  repeated Scroll scrolls = 5;
+}
diff --git a/python/generators/diff_tests/runner.py b/python/generators/diff_tests/runner.py
index 4aa0286..b3c9fe9 100644
--- a/python/generators/diff_tests/runner.py
+++ b/python/generators/diff_tests/runner.py
@@ -149,6 +149,7 @@
   trace_processor_path: str
   trace_descriptor_path: str
   colors: ColorFormatter
+  override_sql_module_paths: List[str]
 
   def __output_to_text_proto(self, actual: str, out: BinaryProto) -> str:
     """Deserializes a binary proto and returns its text representation.
@@ -207,6 +208,8 @@
         tmp_perf_file.name,
         trace_path,
     ]
+    for sql_module_path in self.override_sql_module_paths:
+      cmd += ['--override-sql-module', sql_module_path]
     tp = subprocess.Popen(
         cmd,
         stdout=subprocess.PIPE,
@@ -267,6 +270,8 @@
         tmp_perf_file.name,
         trace_path,
     ]
+    for sql_module_path in self.override_sql_module_paths:
+      cmd += ['--override-sql-module', sql_module_path]
     tp = subprocess.Popen(
         cmd,
         stdout=subprocess.PIPE,
@@ -381,11 +386,9 @@
 
   # Run a TestCase.
   def execute(self, extension_descriptor_paths: List[str],
-              metrics_descriptor: str, keep_input: bool,
+              metrics_descriptor_paths: List[str], keep_input: bool,
               rebase: bool) -> Tuple[str, str, TestResult]:
-    if metrics_descriptor:
-      metrics_descriptor_paths = [metrics_descriptor]
-    else:
+    if not metrics_descriptor_paths:
       out_path = os.path.dirname(self.trace_processor_path)
       metrics_protos_path = os.path.join(out_path, 'gen', 'protos', 'perfetto',
                                          'metrics')
@@ -416,8 +419,9 @@
   test_runners: List[TestCaseRunner]
 
   def __init__(self, name_filter: str, trace_processor_path: str,
-               trace_descriptor: str, no_colors: bool):
-    self.tests = read_all_tests(name_filter, ROOT_DIR)
+               trace_descriptor: str, no_colors: bool,
+               override_sql_module_paths: List[str], test_dir: str):
+    self.tests = read_all_tests(name_filter, test_dir)
     self.trace_processor_path = trace_processor_path
 
     out_path = os.path.dirname(self.trace_processor_path)
@@ -428,26 +432,21 @@
     for test in self.tests:
       self.test_runners.append(
           TestCaseRunner(test, self.trace_processor_path,
-                         self.trace_descriptor_path, color_formatter))
+                         self.trace_descriptor_path, color_formatter,
+                         override_sql_module_paths))
 
-  def run_all_tests(self, metrics_descriptor: str, keep_input: bool,
-                    rebase: bool) -> TestResults:
+  def run_all_tests(self, metrics_descriptor_paths: List[str],
+                    chrome_extensions: str, test_extensions: str,
+                    keep_input: bool, rebase: bool) -> TestResults:
     perf_results = []
     failures = []
     rebased = []
     test_run_start = datetime.datetime.now()
 
-    out_path = os.path.dirname(self.trace_processor_path)
-    chrome_extensions = os.path.join(out_path, 'gen', 'protos', 'third_party',
-                                     'chromium',
-                                     'chrome_track_event.descriptor')
-    test_extensions = os.path.join(out_path, 'gen', 'protos', 'perfetto',
-                                   'trace', 'test_extensions.descriptor')
-
     with concurrent.futures.ProcessPoolExecutor() as e:
       fut = [
           e.submit(test.execute, [chrome_extensions, test_extensions],
-                   metrics_descriptor, keep_input, rebase)
+                   metrics_descriptor_paths, keep_input, rebase)
           for test in self.test_runners
       ]
       for res in concurrent.futures.as_completed(fut):
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 6c27685..221e7b4 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -182,6 +182,7 @@
       "util",
       "util:gzip",
       "util:protozero_to_text",
+      "util:regex",
       "util:stdlib",
       "views",
     ]
@@ -217,7 +218,8 @@
       "util/proto_to_json.cc",
       "util/proto_to_json.h",
     ]
-    if (perfetto_build_standalone && !is_perfetto_build_generator) {
+    if ((perfetto_build_standalone || build_with_chromium) &&
+        !is_perfetto_build_generator) {
       data_deps = [
         # The diff testing framework depends on these descriptors.
         "../../protos/perfetto/metrics:descriptor",
diff --git a/src/trace_processor/db/BUILD.gn b/src/trace_processor/db/BUILD.gn
index 3117602..7a926f9 100644
--- a/src/trace_processor/db/BUILD.gn
+++ b/src/trace_processor/db/BUILD.gn
@@ -40,6 +40,7 @@
     "../../../include/perfetto/trace_processor",
     "../containers",
     "../util:glob",
+    "../util:regex",
     "overlays",
     "storage",
   ]
diff --git a/src/trace_processor/db/column.cc b/src/trace_processor/db/column.cc
index 01c6b5e..fbb6236 100644
--- a/src/trace_processor/db/column.cc
+++ b/src/trace_processor/db/column.cc
@@ -13,12 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 #include "src/trace_processor/db/column.h"
 
+#include "perfetto/base/logging.h"
 #include "src/trace_processor/db/compare.h"
+#include "src/trace_processor/db/storage/utils.h"
 #include "src/trace_processor/db/table.h"
 #include "src/trace_processor/util/glob.h"
+#include "src/trace_processor/util/regex.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -281,6 +283,7 @@
     case FilterOp::kGlob:
       rm->Clear();
       break;
+    case FilterOp::kRegex:
     case FilterOp::kIsNull:
     case FilterOp::kIsNotNull:
       PERFETTO_FATAL("Should be handled above");
@@ -359,6 +362,22 @@
       });
       break;
     }
+    case FilterOp::kRegex: {
+      if constexpr (regex::IsRegexSupported()) {
+        auto regex = regex::Regex::Create(str_value.c_str());
+        if (!regex.status().ok()) {
+          rm->Clear();
+          break;
+        }
+        overlay().FilterInto(rm, [this, &regex](uint32_t idx) {
+          auto v = GetStringPoolStringAtIdx(idx);
+          return v.data() != nullptr && regex->Search(v.c_str());
+        });
+      } else {
+        PERFETTO_FATAL("Regex not supported");
+      }
+      break;
+    }
     case FilterOp::kIsNull:
     case FilterOp::kIsNotNull:
       PERFETTO_FATAL("Should be handled above");
@@ -415,6 +434,7 @@
       });
       break;
     case FilterOp::kGlob:
+    case FilterOp::kRegex:
       rm->Clear();
       break;
     case FilterOp::kIsNull:
diff --git a/src/trace_processor/db/column.h b/src/trace_processor/db/column.h
index 7fa0fcc..8ed49f9 100644
--- a/src/trace_processor/db/column.h
+++ b/src/trace_processor/db/column.h
@@ -388,6 +388,10 @@
     return Constraint{index_in_table_, FilterOp::kGlob, value};
   }
 
+  Constraint regex_value(SqlValue value) const {
+    return Constraint{index_in_table_, FilterOp::kRegex, value};
+  }
+
   // Returns an Order for each Order type for this Column.
   Order ascending() const { return Order{index_in_table_, false}; }
   Order descending() const { return Order{index_in_table_, true}; }
@@ -541,6 +545,7 @@
       case FilterOp::kIsNull:
       case FilterOp::kIsNotNull:
       case FilterOp::kGlob:
+      case FilterOp::kRegex:
         break;
     }
     return false;
diff --git a/src/trace_processor/db/query_executor_benchmark.cc b/src/trace_processor/db/query_executor_benchmark.cc
index 6f325ce..963552f 100644
--- a/src/trace_processor/db/query_executor_benchmark.cc
+++ b/src/trace_processor/db/query_executor_benchmark.cc
@@ -273,6 +273,13 @@
 
 BENCHMARK(BM_QESliceTableNameGlob)->ArgsProduct({{DB::V1, DB::V2}});
 
+static void BM_QESliceTableNameRegex(benchmark::State& state) {
+  SliceTableForBenchmark table(state);
+  BenchmarkSliceTable(state, table, {table.table_.name().regex(".*Pool.*")});
+}
+
+BENCHMARK(BM_QESliceTableNameRegex)->ArgsProduct({{DB::V1, DB::V2}});
+
 static void BM_QESliceTableSorted(benchmark::State& state) {
   SliceTableForBenchmark table(state);
   BenchmarkSliceTable(state, table, {table.table_.ts().gt(1000)});
diff --git a/src/trace_processor/db/query_executor_unittest.cc b/src/trace_processor/db/query_executor_unittest.cc
index 4841d19..d8b79d7 100644
--- a/src/trace_processor/db/query_executor_unittest.cc
+++ b/src/trace_processor/db/query_executor_unittest.cc
@@ -445,6 +445,64 @@
   ASSERT_EQ(res.Get(0), 2u);
 }
 
+TEST(QueryExecutor, StringBinarySearchRegex) {
+  StringPool pool;
+  std::vector<std::string> strings{"cheese",  "pasta", "pizza",
+                                   "pierogi", "onion", "fries"};
+  std::vector<StringPool::Id> ids;
+  for (const auto& string : strings) {
+    ids.push_back(pool.InternString(base::StringView(string)));
+  }
+  ids.insert(ids.begin() + 3, StringPool::Id::Null());
+  StringStorage storage(&pool, ids.data(), 7);
+
+  // Final vec {"cheese", "pasta", "NULL", "pierogi", "fries"}.
+  BitVector selector_bv{1, 1, 0, 1, 1, 0, 1};
+  SelectorOverlay selector_overlay(&selector_bv);
+
+  // Create the column.
+  OverlaysVec overlays_vec;
+  overlays_vec.emplace_back(&selector_overlay);
+  SimpleColumn col{overlays_vec, &storage};
+
+  // Filter.
+  Constraint c{0, FilterOp::kRegex, SqlValue::String("p.*")};
+  QueryExecutor exec({col}, 5);
+  RowMap res = exec.Filter({c});
+
+  ASSERT_EQ(res.size(), 2u);
+  ASSERT_EQ(res.Get(0), 1u);
+  ASSERT_EQ(res.Get(1), 3u);
+}
+
+TEST(QueryExecutor, StringBinarySearchRegexWithNum) {
+  StringPool pool;
+  std::vector<std::string> strings{"cheese",  "pasta", "pizza",
+                                   "pierogi", "onion", "fries"};
+  std::vector<StringPool::Id> ids;
+  for (const auto& string : strings) {
+    ids.push_back(pool.InternString(base::StringView(string)));
+  }
+  ids.insert(ids.begin() + 3, StringPool::Id::Null());
+  StringStorage storage(&pool, ids.data(), 7);
+
+  // Final vec {"cheese", "pasta", "NULL", "pierogi", "fries"}.
+  BitVector selector_bv{1, 1, 0, 1, 1, 0, 1};
+  SelectorOverlay selector_overlay(&selector_bv);
+
+  // Create the column.
+  OverlaysVec overlays_vec;
+  overlays_vec.emplace_back(&selector_overlay);
+  SimpleColumn col{overlays_vec, &storage};
+
+  // Filter.
+  Constraint c{0, FilterOp::kRegex, SqlValue::Long(4)};
+  QueryExecutor exec({col}, 5);
+  RowMap res = exec.Filter({c});
+
+  ASSERT_EQ(res.size(), 0u);
+}
+
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/db/storage/BUILD.gn b/src/trace_processor/db/storage/BUILD.gn
index 69e26c1..f27c9db 100644
--- a/src/trace_processor/db/storage/BUILD.gn
+++ b/src/trace_processor/db/storage/BUILD.gn
@@ -31,8 +31,10 @@
     "../..:metatrace",
     "../../../../gn:default_deps",
     "../../../../include/perfetto/trace_processor:basic_types",
+    "../../../base",
     "../../containers",
     "../../util:glob",
+    "../../util:regex",
   ]
 }
 
diff --git a/src/trace_processor/db/storage/id_storage.cc b/src/trace_processor/db/storage/id_storage.cc
index 4bec887..0f08647 100644
--- a/src/trace_processor/db/storage/id_storage.cc
+++ b/src/trace_processor/db/storage/id_storage.cc
@@ -93,7 +93,8 @@
   if (op == FilterOp::kIsNotNull)
     return RangeOrBitVector(BitVector(indices_size, true));
 
-  if (op == FilterOp::kIsNull || op == FilterOp::kGlob || sql_val.is_null() ||
+  if (op == FilterOp::kIsNull || op == FilterOp::kGlob ||
+      op == FilterOp::kRegex || sql_val.is_null() ||
       sql_val.AsLong() > std::numeric_limits<uint32_t>::max() ||
       sql_val.AsLong() < std::numeric_limits<uint32_t>::min())
     return RangeOrBitVector(BitVector(indices_size, false));
@@ -120,6 +121,7 @@
       return IndexSearchWithComparator(val, indices, indices_size,
                                        std::greater_equal<uint32_t>());
     case FilterOp::kGlob:
+    case FilterOp::kRegex:
     case FilterOp::kIsNotNull:
     case FilterOp::kIsNull:
       PERFETTO_FATAL("Illegal argument");
@@ -165,6 +167,7 @@
     case FilterOp::kIsNull:
     case FilterOp::kIsNotNull:
     case FilterOp::kGlob:
+    case FilterOp::kRegex:
       return RowMap::Range();
   }
   return RowMap::Range();
diff --git a/src/trace_processor/db/storage/numeric_storage.cc b/src/trace_processor/db/storage/numeric_storage.cc
index 4768848..31f1245 100644
--- a/src/trace_processor/db/storage/numeric_storage.cc
+++ b/src/trace_processor/db/storage/numeric_storage.cc
@@ -91,6 +91,7 @@
     case FilterOp::kLt:
       return FilterOpVariant<T>(std::less<T>());
     case FilterOp::kGlob:
+    case FilterOp::kRegex:
     case FilterOp::kIsNotNull:
     case FilterOp::kIsNull:
       PERFETTO_FATAL("Not a valid operation on numeric type.");
@@ -187,6 +188,7 @@
       return utils::LinearSearchWithComparator(
           typed_val, start, std::greater_equal<T>(), builder);
     case FilterOp::kGlob:
+    case FilterOp::kRegex:
     case FilterOp::kIsNotNull:
     case FilterOp::kIsNull:
       PERFETTO_DFATAL("Illegal argument");
@@ -318,6 +320,7 @@
     case FilterOp::kIsNull:
     case FilterOp::kIsNotNull:
     case FilterOp::kGlob:
+    case FilterOp::kRegex:
       return RowMap::Range();
   }
   return RowMap::Range();
@@ -357,6 +360,7 @@
     case FilterOp::kIsNull:
     case FilterOp::kIsNotNull:
     case FilterOp::kGlob:
+    case FilterOp::kRegex:
       return RowMap::Range();
   }
   return RowMap::Range();
diff --git a/src/trace_processor/db/storage/string_storage.cc b/src/trace_processor/db/storage/string_storage.cc
index 51676d1..2a31652 100644
--- a/src/trace_processor/db/storage/string_storage.cc
+++ b/src/trace_processor/db/storage/string_storage.cc
@@ -15,6 +15,10 @@
  */
 
 #include "src/trace_processor/db/storage/string_storage.h"
+#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/trace_processor/basic_types.h"
 
 #include "perfetto/base/logging.h"
 #include "src/trace_processor/containers/bit_vector.h"
@@ -25,6 +29,7 @@
 #include "src/trace_processor/db/storage/utils.h"
 #include "src/trace_processor/tp_metatrace.h"
 #include "src/trace_processor/util/glob.h"
+#include "src/trace_processor/util/regex.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -90,6 +95,35 @@
   std::vector<uint8_t> matches_;
 };
 
+#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+
+struct Regex {
+  bool operator()(StringPool::Id rhs, regex::Regex& pattern) const {
+    return rhs != StringPool::Id::Null() &&
+           pattern.Search(pool_->Get(rhs).c_str());
+  }
+  const StringPool* pool_;
+};
+
+struct RegexFullStringPool {
+  RegexFullStringPool(StringPool* pool, const regex::Regex& regex)
+      : pool_(pool), matches_(pool->MaxSmallStringId().raw_id()) {
+    PERFETTO_DCHECK(!pool->HasLargeString());
+    for (auto it = pool->CreateIterator(); it; ++it) {
+      auto id = it.StringId();
+      matches_[id.raw_id()] =
+          id != StringPool::Id::Null() && regex.Search(pool_->Get(id).c_str());
+    }
+  }
+  bool operator()(StringPool::Id rhs, StringPool::Id) {
+    return matches_[rhs.raw_id()];
+  }
+  StringPool* pool_;
+  std::vector<uint8_t> matches_;
+};
+
+#endif
+
 struct IsNull {
   bool operator()(StringPool::Id rhs, StringPool::Id) const {
     return rhs == StringPool::Id::Null();
@@ -112,6 +146,11 @@
     return RangeOrBitVector(Range());
   }
 
+  if (sql_val.type != SqlValue::kString &&
+      (op == FilterOp::kGlob || op == FilterOp::kRegex)) {
+    return RangeOrBitVector(Range());
+  }
+
   StringPool::Id val =
       (op == FilterOp::kIsNull || op == FilterOp::kIsNotNull)
           ? StringPool::Id::Null()
@@ -163,17 +202,10 @@
         break;
       }
 
-      // For very big string pools (or small ranges) run a standard glob
-      // function.
-      if (range.size() < string_pool_->size()) {
-        utils::LinearSearchWithComparator(std::move(matcher), start,
-                                          Glob{string_pool_}, builder);
-        break;
-      }
-
-      // Optimised glob function works only if there are no large strings in
-      // string pool.
-      if (string_pool_->HasLargeString()) {
+      // For very big string pools (or small ranges) or pools with large strings
+      // run a standard glob function.
+      if (range.size() < string_pool_->size() ||
+          string_pool_->HasLargeString()) {
         utils::LinearSearchWithComparator(std::move(matcher), start,
                                           Glob{string_pool_}, builder);
         break;
@@ -184,6 +216,31 @@
           GlobFullStringPool{string_pool_, matcher}, builder);
       break;
     }
+    case FilterOp::kRegex:
+      if constexpr (regex::IsRegexSupported()) {
+        base::StatusOr<regex::Regex> regex =
+            regex::Regex::Create(sql_val.AsString());
+        if (!regex.status().ok()) {
+          break;
+        }
+
+        // For very big string pools (or small ranges) or pools with large
+        // strings run a standard regex function.
+        if (range.size() < string_pool_->size() ||
+            string_pool_->HasLargeString()) {
+          utils::LinearSearchWithComparator(std::move(regex.value()), start,
+                                            Regex{string_pool_}, builder);
+          break;
+        }
+
+        utils::LinearSearchWithComparator(
+            StringPool::Id::Null(), start,
+            RegexFullStringPool{string_pool_, regex.value()}, builder);
+        break;
+      } else {
+        PERFETTO_DFATAL("Regex not supported");
+      }
+
     case FilterOp::kIsNull:
       utils::LinearSearchWithComparator(val, start, IsNull(), builder);
       break;
@@ -255,7 +312,15 @@
                                        Glob{string_pool_}, builder);
       break;
     }
-
+    case FilterOp::kRegex:
+      PERFETTO_CHECK(regex::IsRegexSupported());
+      if constexpr (regex::IsRegexSupported()) {
+        base::StatusOr<regex::Regex> regex =
+            regex::Regex::Create(sql_val.AsString());
+        utils::IndexSearchWithComparator(std::move(regex.value()), start,
+                                         indices, Regex{string_pool_}, builder);
+      }
+      break;
     case FilterOp::kIsNull:
       utils::IndexSearchWithComparator(val, start, indices, IsNull(), builder);
       break;
diff --git a/src/trace_processor/db/storage/string_storage_unittest.cc b/src/trace_processor/db/storage/string_storage_unittest.cc
index 86be7b9..ab3d7d8 100644
--- a/src/trace_processor/db/storage/string_storage_unittest.cc
+++ b/src/trace_processor/db/storage/string_storage_unittest.cc
@@ -194,6 +194,42 @@
   ASSERT_EQ(bv.CountSetBits(), 3u);
 }
 
+TEST(StringStorageUnittest, LinearSearchRegex) {
+  std::vector<std::string> strings{"cheese",  "pasta", "pizza",
+                                   "pierogi", "onion", "fries"};
+  std::vector<StringPool::Id> ids;
+  StringPool pool;
+  for (const auto& string : strings) {
+    ids.push_back(pool.InternString(base::StringView(string)));
+  }
+  ids.insert(ids.begin() + 3, StringPool::Id::Null());
+
+  StringStorage storage(&pool, ids.data(), 6);
+  BitVector bv =
+      storage.Search(FilterOp::kRegex, SqlValue::String(".*zz.*"), Range(0, 7))
+          .TakeIfBitVector();
+
+  ASSERT_EQ(bv.CountSetBits(), 1u);
+}
+
+TEST(StringStorageUnittest, LinearSearchRegexMalformed) {
+  std::vector<std::string> strings{"cheese",  "pasta", "pizza",
+                                   "pierogi", "onion", "fries"};
+  std::vector<StringPool::Id> ids;
+  StringPool pool;
+  for (const auto& string : strings) {
+    ids.push_back(pool.InternString(base::StringView(string)));
+  }
+  ids.insert(ids.begin() + 3, StringPool::Id::Null());
+
+  StringStorage storage(&pool, ids.data(), 6);
+  BitVector bv =
+      storage.Search(FilterOp::kRegex, SqlValue::String("*"), Range(0, 7))
+          .TakeIfBitVector();
+
+  ASSERT_EQ(bv.CountSetBits(), 0u);
+}
+
 TEST(StringStorageUnittest, IndexSearchEq) {
   std::vector<std::string> strings{"cheese",  "pasta", "pizza",
                                    "pierogi", "onion", "fries"};
diff --git a/src/trace_processor/db/storage/types.h b/src/trace_processor/db/storage/types.h
index 6b12f3d..ff3fc57 100644
--- a/src/trace_processor/db/storage/types.h
+++ b/src/trace_processor/db/storage/types.h
@@ -57,6 +57,7 @@
   kIsNull,
   kIsNotNull,
   kGlob,
+  kRegex,
 };
 
 // Represents a constraint on a column.
diff --git a/src/trace_processor/db/storage/utils.h b/src/trace_processor/db/storage/utils.h
index f4e2a0a..55e18f8 100644
--- a/src/trace_processor/db/storage/utils.h
+++ b/src/trace_processor/db/storage/utils.h
@@ -82,6 +82,7 @@
     builder.Append(comparator(*(data_ptr + *cur_idx), val));
   }
 }
+
 }  // namespace utils
 
 }  // namespace storage
diff --git a/src/trace_processor/db/typed_column.h b/src/trace_processor/db/typed_column.h
index a08aeed..d0bc19b 100644
--- a/src/trace_processor/db/typed_column.h
+++ b/src/trace_processor/db/typed_column.h
@@ -110,6 +110,9 @@
   Constraint ge(sql_value_type v) const { return ge_value(ToSqlValue(v)); }
   Constraint le(sql_value_type v) const { return le_value(ToSqlValue(v)); }
   Constraint glob(sql_value_type v) const { return glob_value(ToSqlValue(v)); }
+  Constraint regex(sql_value_type v) const {
+    return regex_value(ToSqlValue(v));
+  }
 
   // Implements equality between two items of type |T|.
   static constexpr bool Equals(T a, T b) { return TH::Equals(a, b); }
diff --git a/src/trace_processor/metrics/sql/chrome/BUILD.gn b/src/trace_processor/metrics/sql/chrome/BUILD.gn
index 1f4cff8..0a5fb6c 100644
--- a/src/trace_processor/metrics/sql/chrome/BUILD.gn
+++ b/src/trace_processor/metrics/sql/chrome/BUILD.gn
@@ -37,6 +37,7 @@
     "chrome_scroll_inputs_per_frame.sql",
     "chrome_scroll_jank_caused_by_scheduling.sql",
     "chrome_scroll_jank_v2.sql",
+    "chrome_scroll_jank_v3.sql",
     "chrome_slice_names.sql",
     "chrome_stack_samples_for_task.sql",
     "chrome_tasks.sql",
@@ -67,7 +68,6 @@
     "scroll_jank_cause_blocking_touch_move.sql",
     "scroll_jank_cause_get_bitmap.sql",
     "scroll_jank_cause_queuing_delay.sql",
-    "scroll_jank_v3.sql",
     "sufficient_chrome_processes.sql",
     "test_chrome_metric.sql",
     "touch_flow_event.sql",
diff --git a/src/trace_processor/metrics/sql/chrome/scroll_jank_v3.sql b/src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_v3.sql
similarity index 77%
rename from src/trace_processor/metrics/sql/chrome/scroll_jank_v3.sql
rename to src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_v3.sql
index 72211a3..720ac0d 100644
--- a/src/trace_processor/metrics/sql/chrome/scroll_jank_v3.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_v3.sql
@@ -212,6 +212,7 @@
 -- @column event_latency_id       Event latency id of the presented frame.
 -- @column vsync_interval         Vsync interval at the time of recording the trace.
 -- @column hardware_class         Device brand and model.
+-- @column scroll_id              The scroll corresponding to this frame.
 DROP VIEW IF EXISTS chrome_janky_frames;
 CREATE VIEW chrome_janky_frames AS
 SELECT
@@ -220,7 +221,8 @@
   delay_since_last_frame,
   event_latency_id,
   (SELECT vsync_interval FROM chrome_vsyncs) AS vsync_interval,
-  CHROME_HARDWARE_CLASS() AS hardware_class
+  CHROME_HARDWARE_CLASS() AS hardware_class,
+  scroll_id
 FROM chrome_janky_frame_info_with_delay
 WHERE delay_since_last_frame > (select vsync_interval + vsync_interval / 2 from chrome_vsyncs)
       AND delay_since_last_input < (select vsync_interval + vsync_interval / 2 from chrome_vsyncs);
@@ -243,4 +245,100 @@
  FROM chrome_janky_frames) * 1.0
 / (SELECT
     COUNT()
-  FROM chrome_unique_frame_presentation_ts) * 100 AS delayed_frame_percentage;
\ No newline at end of file
+  FROM chrome_unique_frame_presentation_ts) * 100 AS delayed_frame_percentage;
+
+-- Number of frames and janky frames per scroll.
+DROP VIEW IF EXISTS frames_per_scroll;
+CREATE VIEW frames_per_scroll AS
+WITH
+  frames AS (
+    SELECT scroll_id, COUNT(*) AS num_frames
+    FROM
+      chrome_janky_frame_info_with_delay
+    GROUP BY scroll_id
+  ),
+  janky_frames AS (
+    SELECT scroll_id, COUNT(*) AS num_janky_frames
+    FROM
+      chrome_janky_frames
+    GROUP BY scroll_id
+  )
+SELECT
+  frames.scroll_id AS scroll_id,
+  frames.num_frames AS num_frames,
+  janky_frames.num_janky_frames AS num_janky_frames,
+  100.0 * janky_frames.num_janky_frames / frames.num_frames
+    AS scroll_jank_percentage
+FROM frames
+INNER JOIN janky_frames
+  ON frames.scroll_id = janky_frames.scroll_id;
+
+-- Scroll jank causes per scroll.
+DROP VIEW IF EXISTS causes_per_scroll;
+CREATE VIEW causes_per_scroll AS
+SELECT
+  scroll_id,
+  MAX(1.0 * delay_since_last_frame / vsync_interval)
+    AS max_delay_since_last_frame,
+  -- MAX does not matter, since `vsync_interval` is the computed as the
+  -- same value for a single trace.
+  MAX(vsync_interval) AS vsync_interval,
+  RepeatedField(
+    ChromeScrollJankV3_Scroll_ScrollJankCause(
+      'cause',
+      cause_of_jank,
+      'sub_cause',
+      sub_cause_of_jank,
+      'delay_since_last_frame',
+      1.0 * delay_since_last_frame / vsync_interval))
+    AS scroll_jank_causes
+FROM
+  chrome_janky_frames
+GROUP BY scroll_id;
+
+-- An "intermediate" view for computing `chrome_scroll_jank_v3_output` below.
+DROP VIEW IF EXISTS chrome_scroll_jank_v3_intermediate;
+CREATE VIEW chrome_scroll_jank_v3_intermediate AS
+SELECT
+  -- MAX does not matter for these aggregations, since the values are the
+  -- same across rows.
+  (SELECT COUNT(*) FROM chrome_janky_frame_info_with_delay)
+    AS trace_num_frames,
+  (SELECT COUNT(*) FROM chrome_janky_frames)
+    AS trace_num_janky_frames,
+  causes.vsync_interval,
+  RepeatedField(
+    ChromeScrollJankV3_Scroll(
+      'num_frames',
+      frames.num_frames,
+      'num_janky_frames',
+      frames.num_janky_frames,
+      'scroll_jank_percentage',
+      frames.scroll_jank_percentage,
+      'max_delay_since_last_frame',
+      causes.max_delay_since_last_frame,
+      'scroll_jank_causes',
+      causes.scroll_jank_causes))
+    AS scrolls
+FROM
+  frames_per_scroll AS frames
+INNER JOIN causes_per_scroll AS causes
+  ON frames.scroll_id = causes.scroll_id;
+
+-- For producing a "native" Perfetto UI metric.
+DROP VIEW IF EXISTS chrome_scroll_jank_v3_output;
+CREATE VIEW chrome_scroll_jank_v3_output AS
+SELECT
+  ChromeScrollJankV3(
+    'trace_num_frames',
+    trace_num_frames,
+    'trace_num_janky_frames',
+    trace_num_janky_frames,
+    'trace_scroll_jank_percentage',
+    100.0 * trace_num_janky_frames / trace_num_frames,
+    'vsync_interval_ms',
+    vsync_interval,
+    'scrolls',
+    scrolls)
+FROM
+  chrome_scroll_jank_v3_intermediate;
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc
index 295a385..3ed0449 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc
@@ -202,7 +202,8 @@
   for (; !TokenIsTerminal(token); token = tokenizer_.Next()) {
   }
   uint32_t offset = static_cast<uint32_t>(first.str.data() - sql_.sql().data());
-  uint32_t len = static_cast<uint32_t>(token.str.end() - sql_.sql().data());
+  uint32_t len = static_cast<uint32_t>((token.str.data() + token.str.size()) -
+                                       sql_.sql().data());
 
   statement_ = CreateFunction{std::move(prototype), std::string(ret_token.str),
                               sql_.Substr(offset, len)};
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/functions/BUILD.gn
index fb15c53..99488e2 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/BUILD.gn
@@ -54,6 +54,7 @@
     "../../../../base",
     "../../../containers",
     "../../../db",
+    "../../../db/storage",
     "../../../importers/common",
     "../../../importers/ftrace:ftrace_descriptors",
     "../../../perfetto_sql/intrinsics/table_functions",
@@ -62,6 +63,7 @@
     "../../../types",
     "../../../util",
     "../../../util:profile_builder",
+    "../../../util:regex",
     "../../../util:sql_argument",
     "../../../util:stdlib",
     "../../engine",
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h b/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
index 8154c72..36f1afc 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
@@ -20,14 +20,18 @@
 #include <sqlite3.h>
 #include <unordered_map>
 
+#include "perfetto/base/compiler.h"
 #include "perfetto/ext/base/base64.h"
 #include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/trace_processor/demangle.h"
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
+#include "src/trace_processor/db/storage/utils.h"
 #include "src/trace_processor/export_json.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.h"
 #include "src/trace_processor/sqlite/sqlite_utils.h"
+#include "src/trace_processor/util/regex.h"
 #include "src/trace_processor/util/status_macros.h"
 
 namespace perfetto {
@@ -110,10 +114,10 @@
 };
 
 base::Status Reverse::Run(void*,
-                       size_t argc,
-                       sqlite3_value** argv,
-                       SqlValue& out,
-                       Destructors& destructors) {
+                          size_t argc,
+                          sqlite3_value** argv,
+                          SqlValue& out,
+                          Destructors& destructors) {
   if (argc != 1) {
     return base::ErrStatus("REVERSE: expected one arg but got %zu", argc);
   }
@@ -360,6 +364,30 @@
   }
 };
 
+struct Regex : public SqlFunction {
+  static base::Status Run(void*,
+                          size_t,
+                          sqlite3_value** argv,
+                          SqlValue& out,
+                          Destructors&) {
+    if constexpr (regex::IsRegexSupported()) {
+      const char* pattern_str =
+          reinterpret_cast<const char*>(sqlite3_value_text(argv[0]));
+      const char* text =
+          reinterpret_cast<const char*>(sqlite3_value_text(argv[1]));
+      if (pattern_str && text) {
+        auto regex = regex::Regex::Create(pattern_str);
+        if (!regex.status().ok()) {
+          return regex.status();
+        }
+        out = SqlValue::Long(regex->Search(text));
+      }
+      return base::OkStatus();
+    }
+    PERFETTO_FATAL("Regex not supported");
+  }
+};
+
 }  // namespace trace_processor
 }  // namespace perfetto
 
diff --git a/src/trace_processor/perfetto_sql/prelude/views.sql b/src/trace_processor/perfetto_sql/prelude/views.sql
index 3e5b8af..6962bae 100644
--- a/src/trace_processor/perfetto_sql/prelude/views.sql
+++ b/src/trace_processor/perfetto_sql/prelude/views.sql
@@ -1,45 +1,45 @@
-CREATE VIEW counters AS
+CREATE VIEW counters AS 
 SELECT *
-FROM counter v
-JOIN counter_track t ON v.track_id = t.id
+FROM counter v 
+JOIN counter_track t ON v.track_id = t.id 
 ORDER BY ts;
 
-CREATE VIEW slice AS
+CREATE VIEW slice AS 
 SELECT
-  *,
-  category AS cat,
-  id AS slice_id
+  *, 
+  category AS cat, 
+  id AS slice_id 
 FROM internal_slice;
 
-CREATE VIEW instant AS
-SELECT ts, track_id, name, arg_set_id
-FROM slice
+CREATE VIEW instant AS 
+SELECT ts, track_id, name, arg_set_id 
+FROM slice 
 WHERE dur = 0;
 
-CREATE VIEW sched AS
-SELECT
+CREATE VIEW sched AS 
+SELECT 
   *,
   ts + dur as ts_end
 FROM sched_slice;
 
-CREATE VIEW slices AS
+CREATE VIEW slices AS 
 SELECT * FROM slice;
 
-CREATE VIEW thread AS
-SELECT
+CREATE VIEW thread AS 
+SELECT 
   id as utid,
   *
 FROM internal_thread;
 
-CREATE VIEW process AS
-SELECT
+CREATE VIEW process AS 
+SELECT 
   id as upid,
-  *
+  * 
 FROM internal_process;
 
 -- This should be kept in sync with GlobalArgsTracker::AddArgSet.
-CREATE VIEW args AS
-SELECT
+CREATE VIEW args AS 
+SELECT 
   *,
   CASE value_type
     WHEN 'int' THEN CAST(int_value AS text)
@@ -51,5 +51,5 @@
       CASE WHEN int_value <> 0 THEN 'true'
       ELSE 'false' END)
     WHEN 'json' THEN string_value
-  ELSE 'NULL' END AS display_value
+  ELSE NULL END AS display_value
 FROM internal_args;
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scroll_janks.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scroll_janks.sql
index b12e6a7..fd870f5 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scroll_janks.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scroll_janks.sql
@@ -13,7 +13,7 @@
 -- limitations under the License.
 
 -- TODO(b/286187288): Move this dependency to stdlib.
-SELECT RUN_METRIC('chrome/scroll_jank_v3.sql');
+SELECT RUN_METRIC('chrome/chrome_scroll_jank_v3.sql');
 SELECT IMPORT('common.slices');
 
 -- Selects EventLatency slices that correspond with janks in a scroll. This is
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/args.sql b/src/trace_processor/perfetto_sql/stdlib/common/args.sql
index 90a5ced..4835bf8 100644
--- a/src/trace_processor/perfetto_sql/stdlib/common/args.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/common/args.sql
@@ -1,3 +1,18 @@
+--
+-- Copyright 2023 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.
+--
 -- Copyright 2023 The Android Open Source Project
 --
 -- Licensed under the Apache License, Version 2.0 (the "License");
@@ -27,4 +42,4 @@
 SELECT display_value
 FROM args
 WHERE arg_set_id = $arg_set_id AND key = $key
-');
+');
\ No newline at end of file
diff --git a/src/trace_processor/sqlite/BUILD.gn b/src/trace_processor/sqlite/BUILD.gn
index ddb52f9..b93a55a 100644
--- a/src/trace_processor/sqlite/BUILD.gn
+++ b/src/trace_processor/sqlite/BUILD.gn
@@ -55,6 +55,7 @@
     "../types",
     "../util",
     "../util:profile_builder",
+    "../util:regex",
   ]
 }
 
diff --git a/src/trace_processor/sqlite/db_sqlite_table.cc b/src/trace_processor/sqlite/db_sqlite_table.cc
index dab30d1..9f88cd9 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.cc
+++ b/src/trace_processor/sqlite/db_sqlite_table.cc
@@ -15,6 +15,7 @@
  */
 
 #include "src/trace_processor/sqlite/db_sqlite_table.h"
+#include <optional>
 
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/small_vector.h"
@@ -23,6 +24,7 @@
 #include "src/trace_processor/sqlite/query_cache.h"
 #include "src/trace_processor/sqlite/sqlite_utils.h"
 #include "src/trace_processor/tp_metatrace.h"
+#include "src/trace_processor/util/regex.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -49,6 +51,11 @@
       return FilterOp::kIsNotNull;
     case SQLITE_INDEX_CONSTRAINT_GLOB:
       return FilterOp::kGlob;
+    case SQLITE_INDEX_CONSTRAINT_REGEXP:
+      if constexpr (regex::IsRegexSupported()) {
+        return FilterOp::kRegex;
+      }
+      return std::nullopt;
     case SQLITE_INDEX_CONSTRAINT_LIKE:
     // TODO(lalitm): start supporting these constraints.
     case SQLITE_INDEX_CONSTRAINT_LIMIT:
@@ -452,6 +459,16 @@
       continue;
 
     SqlValue value = SqliteValueToSqlValue(argv[i]);
+    if constexpr (regex::IsRegexSupported()) {
+      if (*opt_op == FilterOp::kRegex) {
+        if (value.type != SqlValue::kString)
+          return base::ErrStatus("Value has to be a string");
+
+        if (auto regex_status = regex::Regex::Create(value.AsString());
+            !regex_status.ok())
+          return regex_status.status();
+      }
+    }
     constraints_[constraints_pos++] = Constraint{col, *opt_op, value};
   }
   constraints_.resize(constraints_pos);
@@ -537,6 +554,9 @@
             case FilterOp::kGlob:
               writer.AppendString("GLOB");
               break;
+            case FilterOp::kRegex:
+              writer.AppendString("REGEXP");
+              break;
           }
           writer.AppendString(" ");
 
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 9bf46d4..b7ae178 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -87,6 +87,7 @@
 #include "src/trace_processor/tp_metatrace.h"
 #include "src/trace_processor/types/variadic.h"
 #include "src/trace_processor/util/protozero_to_text.h"
+#include "src/trace_processor/util/regex.h"
 #include "src/trace_processor/util/sql_modules.h"
 #include "src/trace_processor/util/status_macros.h"
 
@@ -432,6 +433,9 @@
       std::unique_ptr<ToFtrace::Context>(new ToFtrace::Context{
           context_.storage.get(), SystraceSerializer(&context_)}));
 
+  if constexpr (regex::IsRegexSupported()) {
+    RegisterFunction<Regex>(&engine_, "regexp", 2);
+  }
   // Old style function registration.
   // TODO(lalitm): migrate this over to using RegisterFunction once aggregate
   // functions are supported.
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index 2aa130d..ba49625 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -691,7 +691,7 @@
   std::string trace_file_path;
   std::string port_number;
   std::string override_stdlib_path;
-  std::string override_sql_module_path;
+  std::vector<std::string> override_sql_module_paths;
   std::vector<std::string> raw_metric_extensions;
   bool launch_shell = false;
   bool enable_httpd = false;
@@ -766,8 +766,7 @@
                                       module name.
  --override-sql-module MODULE_PATH    Will override trace processor module with
                                       passed contents. The outer directory will
-                                      specify the module name. Only allowed when
-                                      --dev is specified.
+                                      specify the module name.
  --override-stdlib=[path_to_stdlib]   Will override trace_processor/stdlib with
                                       passed contents. The outer directory will
                                       be ignored. Only allowed when --dev is
@@ -963,7 +962,7 @@
     }
 
     if (option == OPT_OVERRIDE_SQL_MODULE) {
-      command_line_options.override_sql_module_path = optarg;
+      command_line_options.override_sql_module_paths.push_back(optarg);
       continue;
     }
 
@@ -1543,14 +1542,14 @@
                              status.c_message());
   }
 
-  if (!options.override_sql_module_path.empty()) {
-    if (!options.dev)
-      return base::ErrStatus("Overriding stdlib modules requires --dev flag");
-
-    auto status = IncludeSqlModule(options.override_sql_module_path, true);
-    if (!status.ok())
-      return base::ErrStatus("Couldn't override stdlib module: %s",
-                             status.c_message());
+  if (!options.override_sql_module_paths.empty()) {
+    for (const auto& override_sql_module_path :
+         options.override_sql_module_paths) {
+      auto status = IncludeSqlModule(override_sql_module_path, true);
+      if (!status.ok())
+        return base::ErrStatus("Couldn't override stdlib module: %s",
+                               status.c_message());
+    }
   }
 
   if (!options.sql_module_path.empty()) {
diff --git a/src/trace_processor/util/BUILD.gn b/src/trace_processor/util/BUILD.gn
index d22329b..7aa64b3 100644
--- a/src/trace_processor/util/BUILD.gn
+++ b/src/trace_processor/util/BUILD.gn
@@ -167,6 +167,14 @@
   ]
 }
 
+source_set("regex") {
+  sources = [ "regex.h" ]
+  deps = [
+    "../../../gn:default_deps",
+    "../../base",
+  ]
+}
+
 source_set("sql_argument") {
   sources = [
     "sql_argument.cc",
diff --git a/src/trace_processor/util/regex.h b/src/trace_processor/util/regex.h
new file mode 100644
index 0000000..d999c48
--- /dev/null
+++ b/src/trace_processor/util/regex.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 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_UTIL_REGEX_H_
+#define SRC_TRACE_PROCESSOR_UTIL_REGEX_H_
+
+#include <optional>
+#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/status_or.h"
+
+#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+#include <regex.h>
+#endif
+
+namespace perfetto {
+namespace trace_processor {
+namespace regex {
+
+constexpr bool IsRegexSupported() {
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+  return false;
+#else
+  return true;
+#endif
+}
+
+#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+
+// Implements regex parsing and regex search based on C library `regex.h`.
+// Doesn't work on Windows.
+class Regex {
+ public:
+  ~Regex() {
+    if (regex_) {
+      regfree(&regex_.value());
+    }
+  }
+  Regex(Regex&) = delete;
+  Regex(Regex&& other) {
+    regex_ = std::move(other.regex_);
+    other.regex_ = std::nullopt;
+  }
+  Regex& operator=(Regex&& other) {
+    this->~Regex();
+    new (this) Regex(std::move(other));
+    return *this;
+  }
+  Regex& operator=(const Regex&) = delete;
+
+  // Parse regex pattern. Returns error if regex pattern is invalid.
+  static base::StatusOr<Regex> Create(const char* pattern) {
+    regex_t regex;
+    if (regcomp(&regex, pattern, 0)) {
+      return base::ErrStatus("Regex pattern '%s' is malformed.", pattern);
+    }
+    return Regex(std::move(regex));
+  }
+
+  // Returns true if string matches the regex.
+  bool Search(const char* s) const {
+    PERFETTO_CHECK(regex_);
+    return regexec(&regex_.value(), s, 0, nullptr, 0) == 0;
+  }
+
+ private:
+  explicit Regex(regex_t regex) : regex_(std::move(regex)) {}
+
+  std::optional<regex_t> regex_;
+};
+
+#endif
+}  // namespace regex
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_UTIL_REGEX_H_
diff --git a/test/trace_processor/diff_tests/chrome/tests_scroll_jank.py b/test/trace_processor/diff_tests/chrome/tests_scroll_jank.py
index 3e21de5..ce2066a 100644
--- a/test/trace_processor/diff_tests/chrome/tests_scroll_jank.py
+++ b/test/trace_processor/diff_tests/chrome/tests_scroll_jank.py
@@ -69,7 +69,7 @@
     return DiffTestBlueprint(
         trace=DataPath('chrome_input_with_frame_view.pftrace'),
         query="""
-        SELECT RUN_METRIC('chrome/scroll_jank_v3.sql');
+        SELECT RUN_METRIC('chrome/chrome_scroll_jank_v3.sql');
 
         SELECT
           cause_of_jank,
@@ -84,7 +84,7 @@
     return DiffTestBlueprint(
         trace=DataPath('chrome_input_with_frame_view.pftrace'),
         query="""
-        SELECT RUN_METRIC('chrome/scroll_jank_v3.sql');
+        SELECT RUN_METRIC('chrome/chrome_scroll_jank_v3.sql');
 
         SELECT
           delayed_frame_percentage
@@ -658,3 +658,39 @@
           }
         }
         """))
+
+  def test_chrome_scroll_jank_v3(self):
+    return DiffTestBlueprint(
+        trace=DataPath('chrome_input_with_frame_view.pftrace'),
+        query=Metric('chrome_scroll_jank_v3'),
+        out=TextProto(r"""
+        [perfetto.protos.chrome_scroll_jank_v3] {
+          trace_num_frames: 291
+          trace_num_janky_frames: 3
+          trace_scroll_jank_percentage: 1.0309278350515463
+          vsync_interval_ms: 16.368
+          scrolls {
+            num_frames: 105
+            num_janky_frames: 2
+            scroll_jank_percentage: 1.9047619047619047
+            max_delay_since_last_frame: 6.126221896383187
+            scroll_jank_causes {
+              delay_since_last_frame: 2.044354838709678
+            }
+            scroll_jank_causes {
+              cause: "RendererCompositorFinishedToBeginImplFrame"
+              delay_since_last_frame: 6.126221896383187
+            }
+          }
+          scrolls {
+            num_frames: 84
+            num_janky_frames: 1
+            scroll_jank_percentage: 1.1904761904761905
+            max_delay_since_last_frame: 2.040811339198436
+            scroll_jank_causes {
+              cause: "RendererCompositorQueueingDelay"
+              delay_since_last_frame: 2.040811339198436
+            }
+          }
+        }
+        """))
diff --git a/tools/diff_test_trace_processor.py b/tools/diff_test_trace_processor.py
index e505258..5316763 100755
--- a/tools/diff_test_trace_processor.py
+++ b/tools/diff_test_trace_processor.py
@@ -38,9 +38,14 @@
   parser = argparse.ArgumentParser()
   parser.add_argument('--test-type', type=str, default='all')
   parser.add_argument('--trace-descriptor', type=str)
-  parser.add_argument('--metrics-descriptor', type=str)
+  parser.add_argument('--metrics-descriptor', nargs='+', type=str)
+  parser.add_argument('--chrome-track-event-descriptor', type=str, default=None)
+  parser.add_argument('--test-extensions', type=str, default=None)
   parser.add_argument('--perf-file', type=str)
   parser.add_argument(
+      '--override-sql-module', type=str, action='append', default=[])
+  parser.add_argument('--test-dir', type=str, default=ROOT_DIR)
+  parser.add_argument(
       '--name-filter',
       default='.*',
       type=str,
@@ -59,11 +64,23 @@
       'trace_processor', type=str, help='location of trace processor binary')
   args = parser.parse_args()
 
+  out_path = os.path.dirname(args.trace_processor)
+  if args.chrome_track_event_descriptor is None:
+    args.chrome_track_event_descriptor = os.path.join(
+        out_path, 'gen', 'protos', 'third_party', 'chromium',
+        'chrome_track_event.descriptor')
+  if args.test_extensions is None:
+    args.test_extensions = os.path.join(out_path, 'gen', 'protos', 'perfetto',
+                                        'trace', 'test_extensions.descriptor')
+
   test_runner = DiffTestsRunner(args.name_filter, args.trace_processor,
-                                args.trace_descriptor, args.no_colors)
+                                args.trace_descriptor, args.no_colors,
+                                args.override_sql_module, args.test_dir)
   sys.stderr.write(f"[==========] Running {len(test_runner.tests)} tests.\n")
 
-  results = test_runner.run_all_tests(args.metrics_descriptor, args.keep_input,
+  results = test_runner.run_all_tests(args.metrics_descriptor,
+                                      args.chrome_track_event_descriptor,
+                                      args.test_extensions, args.keep_input,
                                       args.rebase)
   sys.stderr.write(results.str(args.no_colors, len(test_runner.tests)))
 
diff --git a/ui/release/channels.json b/ui/release/channels.json
index 0dcf5cb..704f94b 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -2,11 +2,11 @@
   "channels": [
     {
       "name": "stable",
-      "rev": "1b8de44522f33d371def110325bf31b847c1f5c4"
+      "rev": "ad24a64421b255cccc7f80dd0532dcf8ff8902a5"
     },
     {
       "name": "canary",
-      "rev": "5f456dbc00731d4dff20ae4e395cf06f96374a2d"
+      "rev": "e6805137e51cde470d227d63ba06f99be1ed8639"
     },
     {
       "name": "autopush",
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 19954e4..9f791b6 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -56,6 +56,7 @@
   NewEngineMode,
   OmniboxState,
   Pagination,
+  PendingDeeplinkState,
   PivotTableResult,
   PrimaryTrackSortKey,
   ProfileType,
@@ -479,8 +480,7 @@
     }
   },
 
-  maybeSetPendingDeeplink(
-      state: StateDraft, args: {ts?: string, dur?: string, tid?: string}) {
+  maybeSetPendingDeeplink(state: StateDraft, args: PendingDeeplinkState) {
     state.pendingDeeplink = args;
   },
 
diff --git a/ui/src/common/commands.ts b/ui/src/common/commands.ts
index 3b9446b..b3973ed 100644
--- a/ui/src/common/commands.ts
+++ b/ui/src/common/commands.ts
@@ -12,11 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-export interface Command {
-  id: string;
-  name: string;
-  callback: (...args: any[]) => void;
-}
+import {Command} from '../public';
 
 export interface CommandSource {
   commands(): Command[];
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index ede9e57..df93cc0 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -12,51 +12,29 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Disposable} from 'src/base/disposable';
 
 import {
   TrackControllerFactory,
   trackControllerRegistry,
 } from '../controller/track_controller';
-import {Store} from '../frontend/store';
 import {TrackCreator} from '../frontend/track';
 import {trackRegistry} from '../frontend/track_registry';
 import {
+  Command,
   EngineProxy,
   PluginContext,
   PluginInfo,
+  Store,
+  TracePlugin,
+  TracePluginFactory,
   TrackInfo,
   TrackProvider,
 } from '../public';
 
-import {Command} from './commands';
 import {Engine} from './engine';
 import {Registry} from './registry';
 import {State} from './state';
 
-// All trace plugins must implement this interface.
-export interface TracePlugin extends Disposable {
-  commands(): Command[];
-}
-
-// This interface defines what a plugin factory should look like.
-// This can be defined in the plugin class definition by defining a constructor
-// and the relevant static methods:
-// E.g.
-// class MyPlugin implements TracePlugin<MyState> {
-//   static migrate(initialState: unknown): MyState {...}
-//   constructor(store: Store<MyState>, engine: EngineProxy) {...}
-//   ... methods from the TracePlugin interface go here ...
-// }
-// ... which can then be passed around by class i.e. MyPlugin
-export interface TracePluginFactory<StateT> {
-  // Function to migrate the persistent state. Called before new().
-  migrate(initialState: unknown): StateT;
-
-  // Instantiate the plugin.
-  new(store: Store<StateT>, engine: EngineProxy): TracePlugin;
-}
-
 interface TracePluginContext {
   plugin: TracePlugin;
   store: Store<unknown>;
@@ -106,7 +84,7 @@
     const TracePluginClass = this.tracePluginFactory;
     if (TracePluginClass) {
       // Make an engine proxy for this plugin.
-      const engineProxy = engine.getProxy(this.pluginId);
+      const engineProxy: EngineProxy = engine.getProxy(this.pluginId);
 
       // Extract the initial state and pass to the plugin factory for migration.
       const initialState = store.state.plugins[this.pluginId];
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 7cdc177..9237dd4 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -108,7 +108,8 @@
 // 31. Convert all timestamps to bigints.
 // 32. Add pendingDeeplink.
 // 33. Add plugins state.
-export const STATE_VERSION = 33;
+// 34. Add additional pendingDeeplink fields (query, pid).
+export const STATE_VERSION = 34;
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
@@ -525,6 +526,8 @@
   ts?: string;
   dur?: string;
   tid?: string;
+  pid?: string;
+  query?: string;
 }
 
 export interface State {
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index f78d656..59cf7b4 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -63,6 +63,7 @@
   publishOverviewData,
   publishThreads,
 } from '../frontend/publish';
+import {runQueryInNewTab} from '../frontend/query_result_tab';
 import {Router} from '../frontend/router';
 
 import {
@@ -531,6 +532,9 @@
     if (pendingDeeplink !== undefined) {
       globals.dispatch(Actions.clearPendingDeeplink({}));
       await this.selectPendingDeeplink(pendingDeeplink);
+      if (pendingDeeplink.query !== undefined) {
+        runQueryInNewTab(pendingDeeplink.query, 'Deeplink Query');
+      }
     }
 
     // If the trace was shared via a permalink, it might already have a
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 3b8f9bd..64980da 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -367,6 +367,8 @@
       ts: route.args.ts,
       tid: route.args.tid,
       dur: route.args.dur,
+      pid: route.args.dur,
+      query: route.args.query,
     }));
 
     if (!globals.embeddedMode) {
diff --git a/ui/src/frontend/router.ts b/ui/src/frontend/router.ts
index 1057d80..54e0144 100644
--- a/ui/src/frontend/router.ts
+++ b/ui/src/frontend/router.ts
@@ -77,6 +77,8 @@
   ts: optStr,
   dur: optStr,
   tid: optStr,
+  pid: optStr,
+  query: optStr,
 });
 type RouteArgs = ValidatedType<typeof routeArgs>;
 
diff --git a/ui/src/frontend/topbar.ts b/ui/src/frontend/topbar.ts
index 11bd3dc..c7336f4 100644
--- a/ui/src/frontend/topbar.ts
+++ b/ui/src/frontend/topbar.ts
@@ -15,9 +15,9 @@
 import m from 'mithril';
 
 import {Actions} from '../common/actions';
-import {Command} from '../common/commands';
 import {raf} from '../core/raf_scheduler';
 import {VERSION} from '../gen/perfetto_version';
+import {Command} from '../public';
 
 import {classNames} from './classnames';
 import {globals} from './globals';
diff --git a/ui/src/plugins/dev.perfetto.ExamplePlugin/index.ts b/ui/src/plugins/dev.perfetto.ExamplePlugin/index.ts
index 1ce82f3..261ad39 100644
--- a/ui/src/plugins/dev.perfetto.ExamplePlugin/index.ts
+++ b/ui/src/plugins/dev.perfetto.ExamplePlugin/index.ts
@@ -12,10 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Command} from '../../common/commands';
-import {TracePlugin} from '../../common/plugins';
-import {Store} from '../../frontend/store';
-import {EngineProxy, PluginContext} from '../../public';
+import {
+  Command,
+  EngineProxy,
+  PluginContext,
+  Store,
+  TracePlugin,
+} from '../../public';
 
 interface ExampleState {
   counter: number;
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index fc10405..63a2657 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -12,9 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {Disposable} from '../base/disposable';
 import {EngineProxy} from '../common/engine';
-import {TracePluginFactory} from '../common/plugins';
 import {TrackControllerFactory} from '../controller/track_controller';
+import {Store} from '../frontend/store';
 import {TrackCreator} from '../frontend/track';
 
 export {EngineProxy} from '../common/engine';
@@ -26,6 +27,39 @@
   STR,
   STR_NULL,
 } from '../common/query_result';
+export {Store} from '../frontend/store';
+
+export interface Command {
+  // A unique id for this command.
+  id: string;
+  // A friendly human name for the command.
+  name: string;
+  // Callback is called when the command is invoked.
+  callback: (...args: any[]) => void;
+}
+
+// All trace plugins must implement this interface.
+export interface TracePlugin extends Disposable {
+  commands(): Command[];
+}
+
+// This interface defines what a plugin factory should look like.
+// This can be defined in the plugin class definition by defining a constructor
+// and the relevant static methods:
+// E.g.
+// class MyPlugin implements TracePlugin<MyState> {
+//   static migrate(initialState: unknown): MyState {...}
+//   constructor(store: Store<MyState>, engine: EngineProxy) {...}
+//   ... methods from the TracePlugin interface go here ...
+// }
+// ... which can then be passed around by class i.e. MyPlugin
+export interface TracePluginFactory<StateT> {
+  // Function to migrate the persistent state. Called before new().
+  migrate(initialState: unknown): StateT;
+
+  // Instantiate the plugin.
+  new(store: Store<StateT>, engine: EngineProxy): TracePlugin;
+}
 
 export interface TrackInfo {
   // The id of this 'type' of track. This id is used to select the