Add base::Regex (#5291)

- create base::regex a wrapper around std::regex (to be extended)
- Migrate all direct std::regex users to use base::Regex.
diff --git a/Android.bp b/Android.bp
index d4e4cea..cf5c255 100644
--- a/Android.bp
+++ b/Android.bp
@@ -96,6 +96,7 @@
         ":perfetto_protos_perfetto_trace_track_event_zero_gen",
         ":perfetto_protos_perfetto_trace_translation_zero_gen",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -239,6 +240,7 @@
         ":perfetto_include_perfetto_public_abi_base",
         ":perfetto_include_perfetto_public_base",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         ":perfetto_src_profiling_memory_malloc_interceptor_bionic_hooks",
         ":perfetto_src_profiling_memory_wrap_allocators",
     ],
@@ -273,6 +275,7 @@
         ":perfetto_include_perfetto_public_abi_base",
         ":perfetto_include_perfetto_public_base",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_profiling_memory_client",
         ":perfetto_src_profiling_memory_client_api",
@@ -395,6 +398,7 @@
         ":perfetto_protos_perfetto_trace_track_event_zero_gen",
         ":perfetto_protos_perfetto_trace_translation_zero_gen",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -530,6 +534,7 @@
         ":perfetto_include_perfetto_public_abi_base",
         ":perfetto_include_perfetto_public_base",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         "src/ipc/protoc_plugin/ipc_plugin.cc",
     ],
     static_libs: [
@@ -636,6 +641,7 @@
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -921,6 +927,7 @@
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -1136,6 +1143,7 @@
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -1486,6 +1494,7 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -1868,6 +1877,7 @@
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_test_support",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
@@ -2236,6 +2246,7 @@
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_test_support",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
@@ -2877,6 +2888,7 @@
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_test_support",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
@@ -13572,6 +13584,14 @@
     ],
 }
 
+// GN: //src/base:regex
+filegroup {
+    name: "perfetto_src_base_regex",
+    srcs: [
+        "src/base/regex/regex.cc",
+    ],
+}
+
 // GN: //src/base:test_support
 filegroup {
     name: "perfetto_src_base_test_support",
@@ -13622,6 +13642,7 @@
         "src/base/no_destructor_unittest.cc",
         "src/base/paged_memory_unittest.cc",
         "src/base/periodic_task_unittest.cc",
+        "src/base/regex/regex_unittest.cc",
         "src/base/rt_mutex_unittest.cc",
         "src/base/scoped_file_unittest.cc",
         "src/base/scoped_mmap_unittest.cc",
@@ -14385,6 +14406,7 @@
         ":perfetto_protos_perfetto_common_zero_gen",
         ":perfetto_protos_perfetto_protovm_zero_gen",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_version",
         ":perfetto_src_protozero_descriptor_diff_lib",
         ":perfetto_src_protozero_protozero",
@@ -14533,6 +14555,7 @@
         ":perfetto_include_perfetto_public_abi_base",
         ":perfetto_include_perfetto_public_base",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         "src/protozero/protoc_plugin/cppgen_plugin.cc",
     ],
     static_libs: [
@@ -17119,6 +17142,7 @@
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_http_http",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_kernel_utils_kernel_wakelock_errors",
@@ -19504,6 +19528,7 @@
         ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_http_http",
         ":perfetto_src_base_http_unittests",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_test_support",
         ":perfetto_src_base_threading_threading",
         ":perfetto_src_base_threading_unittests",
@@ -20152,6 +20177,7 @@
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_test_support",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
@@ -20404,6 +20430,7 @@
         ":perfetto_protos_perfetto_trace_android_winscope_common_zero_gen",
         ":perfetto_protos_perfetto_trace_android_winscope_extensions_zero_gen",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         ":perfetto_src_protozero_protozero",
     ],
     host_supported: true,
@@ -20437,6 +20464,7 @@
         ":perfetto_include_perfetto_public_abi_base",
         ":perfetto_include_perfetto_public_base",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         "src/protozero/protoc_plugin/protozero_plugin.cc",
     ],
     static_libs: [
@@ -20524,6 +20552,7 @@
         ":perfetto_protos_third_party_simpleperf_zero_gen",
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
+        ":perfetto_src_base_regex",
         ":perfetto_src_kernel_utils_kernel_wakelock_errors",
         ":perfetto_src_kernel_utils_syscall_table",
         ":perfetto_src_protovm_protovm",
@@ -20916,6 +20945,7 @@
         ":perfetto_protos_perfetto_trace_track_event_zero_gen",
         ":perfetto_protos_perfetto_trace_translation_zero_gen",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         ":perfetto_src_protozero_protozero",
         ":perfetto_src_trace_processor_util_blob",
         ":perfetto_src_trace_processor_util_clock",
@@ -21057,6 +21087,7 @@
         ":perfetto_protos_third_party_simpleperf_zero_gen",
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_version",
         ":perfetto_src_kernel_utils_kernel_wakelock_errors",
         ":perfetto_src_kernel_utils_syscall_table",
@@ -21396,6 +21427,7 @@
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -21625,6 +21657,7 @@
         ":perfetto_protos_perfetto_trace_translation_zero_gen",
         ":perfetto_src_base_base",
         ":perfetto_src_base_clock_snapshots",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -21732,6 +21765,7 @@
         ":perfetto_protos_perfetto_common_zero_gen",
         ":perfetto_protos_perfetto_protovm_zero_gen",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_version",
         ":perfetto_src_protozero_multifile_error_collector",
         ":perfetto_src_protozero_protozero",
@@ -21846,6 +21880,7 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_regex",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
diff --git a/BUILD b/BUILD
index 445e335..42930c5 100644
--- a/BUILD
+++ b/BUILD
@@ -1165,6 +1165,7 @@
         "include/perfetto/ext/base/periodic_task.h",
         "include/perfetto/ext/base/pipe.h",
         "include/perfetto/ext/base/platform.h",
+        "include/perfetto/ext/base/regex.h",
         "include/perfetto/ext/base/rt_mutex.h",
         "include/perfetto/ext/base/scoped_file.h",
         "include/perfetto/ext/base/scoped_mmap.h",
@@ -1637,6 +1638,7 @@
 perfetto_cc_library(
     name = "src_base_base",
     srcs = [
+        ":src_base_regex",
         "src/base/android_utils.cc",
         "src/base/base64.cc",
         "src/base/crash_keys.cc",
@@ -1708,6 +1710,15 @@
     linkstatic = True,
 )
 
+# GN target: //src/base:regex
+perfetto_filegroup(
+    name = "src_base_regex",
+    srcs = [
+        "src/base/regex/regex.cc",
+        "src/base/regex/regex_std.h",
+    ],
+)
+
 # GN target: //src/base:unix_socket
 perfetto_cc_library(
     name = "src_base_unix_socket",
diff --git a/include/perfetto/ext/base/BUILD.gn b/include/perfetto/ext/base/BUILD.gn
index c9c49aa..7dd1648 100644
--- a/include/perfetto/ext/base/BUILD.gn
+++ b/include/perfetto/ext/base/BUILD.gn
@@ -47,6 +47,7 @@
     "periodic_task.h",
     "pipe.h",
     "platform.h",
+    "regex.h",
     "rt_mutex.h",
     "scoped_file.h",
     "scoped_mmap.h",
diff --git a/include/perfetto/ext/base/regex.h b/include/perfetto/ext/base/regex.h
new file mode 100644
index 0000000..dad9bd0
--- /dev/null
+++ b/include/perfetto/ext/base/regex.h
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2026 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 INCLUDE_PERFETTO_EXT_BASE_REGEX_H_
+#define INCLUDE_PERFETTO_EXT_BASE_REGEX_H_
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/ext/base/status_or.h"
+
+namespace perfetto {
+namespace base {
+
+// Forward declaration for the backend implementation (defined in
+// src/base/regex/). The active backend is selected at compile time.
+class RegexStd;
+using RegexImpl = RegexStd;
+
+// A regular expression abstraction that hides the underlying engine.
+//
+// Construct via the two static factories:
+//   - Create()        – fallible; returns StatusOr<Regex>.
+//   - CreateOrCheck() – infallible; crashes on invalid pattern.
+//                       Use for compile-time constant patterns only.
+//
+// Regex is movable but NOT copyable. Use CopyableRegex when copy semantics
+// are needed (e.g. for storage in copyable containers).
+//
+// Patterns use ECMAScript syntax, supporting Perl-style shorthands (\d, \w,
+// \s).
+//
+// Operations:
+//   GlobalReplace()          – global substitution; returns a new string.
+//   FullMatch()              – anchored, no groups.
+//   FullMatchWithGroups()    – anchored, with capture groups.
+//   PartialMatch()           – unanchored, no groups.
+//   PartialMatchWithGroups() – unanchored, with capture groups.
+//   PartialMatchAll()        – iterator over all non-overlapping matches.
+class Regex {
+ public:
+  // Controls whether matching is case-sensitive or case-insensitive.
+  enum class CaseSensitivity : uint8_t {
+    kSensitive,
+    kInsensitive,
+  };
+
+  Regex(Regex&&) noexcept;
+  Regex& operator=(Regex&&) noexcept;
+  ~Regex();
+
+  // Regex is not copyable. Use CopyableRegex for copy semantics.
+  Regex(const Regex&) = delete;
+  Regex& operator=(const Regex&) = delete;
+
+  // Compiles |pattern| into a Regex.
+  // Returns an error if the pattern syntax is invalid.
+  static StatusOr<Regex> Create(
+      std::string_view pattern,
+      CaseSensitivity cs = CaseSensitivity::kSensitive);
+
+  // Like Create(), but crashes (PERFETTO_CHECK) on an invalid pattern.
+  // Use only for patterns that are known at compile time.
+  static Regex CreateOrCheck(std::string_view pattern,
+                             CaseSensitivity cs = CaseSensitivity::kSensitive);
+
+  // Replaces every occurrence of the regex in |s| with |replacement| and
+  // returns the resulting string.  |s| is unchanged.
+  std::string GlobalReplace(std::string_view s,
+                            std::string_view replacement) const;
+
+  // Returns true if the regex matches the entirety of |s|.
+  bool FullMatch(std::string_view s) const;
+
+  // Like FullMatch(), but also fills |out| with captured groups on success.
+  //   out[0]    – the full match.
+  //   out[1..N] – the parenthesised sub-groups.
+  // Returns false (and clears |out|) when there is no match.
+  //
+  // The string_views in |out| point into |s|; they are invalidated if |s| is
+  // modified or destroyed.
+  bool FullMatchWithGroups(std::string_view s,
+                           std::vector<std::string_view>& out) const;
+
+  // Returns true if the regex matches any substring of |s|.
+  bool PartialMatch(std::string_view s) const;
+
+  // Like PartialMatch(), but also fills |out| with captured groups on success.
+  //   out[0]    – the full match.
+  //   out[1..N] – the parenthesised sub-groups.
+  // Returns false (and clears |out|) when there is no match.
+  //
+  // The string_views in |out| point into |s|; they are invalidated if |s| is
+  // modified or destroyed.
+  bool PartialMatchWithGroups(std::string_view s,
+                              std::vector<std::string_view>& out) const;
+
+  // Iterator over all non-overlapping matches in a string.
+  class PartialMatchIterator {
+   public:
+    // Returns the next match, or std::nullopt when no more matches remain.
+    std::optional<std::string_view> Next();
+
+    // Returns the next match with capture groups. Groups are filled into
+    // |groups|. Returns std::nullopt when no more matches remain.
+    std::optional<std::string_view> NextWithGroups(
+        std::vector<std::string_view>& groups);
+
+   private:
+    friend class Regex;
+    PartialMatchIterator(const Regex* re, std::string_view input);
+    const Regex* re_;
+    std::string_view input_;
+    size_t offset_ = 0;
+    std::vector<std::string_view> groups_;  // Cached for Next().
+  };
+
+  // Returns an iterator that yields all non-overlapping matches in |s|.
+  PartialMatchIterator PartialMatchAll(std::string_view s) const;
+
+  // Accessors for pattern and case sensitivity (used by CopyableRegex).
+  const std::string& pattern() const { return pattern_; }
+  CaseSensitivity case_sensitivity() const { return cs_; }
+
+ private:
+  Regex() = default;
+  std::unique_ptr<RegexImpl> impl_;
+  std::string pattern_;
+  CaseSensitivity cs_ = CaseSensitivity::kSensitive;
+};
+
+// A copyable wrapper around Regex. Copying re-compiles the pattern from the
+// stored string, so prefer moving when the source is no longer needed.
+//
+// Use this class when Regex needs to be stored in a copyable container
+// (e.g. std::optional in a copyable struct).
+class CopyableRegex {
+ public:
+  explicit CopyableRegex(Regex regex);
+
+  CopyableRegex(const CopyableRegex& other);
+  CopyableRegex& operator=(const CopyableRegex& other);
+  CopyableRegex(CopyableRegex&&) noexcept = default;
+  CopyableRegex& operator=(CopyableRegex&&) noexcept = default;
+  ~CopyableRegex() = default;
+
+  std::string GlobalReplace(std::string_view s,
+                            std::string_view replacement) const {
+    return regex_.GlobalReplace(s, replacement);
+  }
+  bool FullMatch(std::string_view s) const { return regex_.FullMatch(s); }
+  bool FullMatchWithGroups(std::string_view s,
+                           std::vector<std::string_view>& out) const {
+    return regex_.FullMatchWithGroups(s, out);
+  }
+  bool PartialMatch(std::string_view s) const { return regex_.PartialMatch(s); }
+  bool PartialMatchWithGroups(std::string_view s,
+                              std::vector<std::string_view>& out) const {
+    return regex_.PartialMatchWithGroups(s, out);
+  }
+  Regex::PartialMatchIterator PartialMatchAll(std::string_view s) const {
+    return regex_.PartialMatchAll(s);
+  }
+
+ private:
+  Regex regex_;
+};
+
+}  // namespace base
+}  // namespace perfetto
+
+#endif  // INCLUDE_PERFETTO_EXT_BASE_REGEX_H_
diff --git a/src/base/BUILD.gn b/src/base/BUILD.gn
index 3ced755..6081cc0 100644
--- a/src/base/BUILD.gn
+++ b/src/base/BUILD.gn
@@ -25,6 +25,7 @@
 
 perfetto_component("base") {
   deps = [
+    ":regex",
     "../../gn:base_platform",
     "../../gn:default_deps",
   ]
@@ -90,6 +91,18 @@
   }
 }
 
+source_set("regex") {
+  sources = [
+    "regex/regex.cc",
+    "regex/regex_std.h",
+  ]
+  deps = [ "../../gn:default_deps" ]
+  public_deps = [
+    "../../include/perfetto/base",
+    "../../include/perfetto/ext/base",
+  ]
+}
+
 perfetto_component("clock_snapshots") {
   deps = [ "../../gn:default_deps" ]
   public_deps = [
@@ -198,6 +211,7 @@
   testonly = true
   deps = [
     ":base",
+    ":regex",
     ":test_support",
     "../../gn:default_deps",
     "../../gn:gtest_and_gmock",
@@ -226,6 +240,7 @@
     "no_destructor_unittest.cc",
     "paged_memory_unittest.cc",
     "periodic_task_unittest.cc",
+    "regex/regex_unittest.cc",
     "rt_mutex_unittest.cc",
     "scoped_file_unittest.cc",
     "scoped_mmap_unittest.cc",
@@ -307,11 +322,13 @@
     deps = [
       ":base",
       ":flat_hash_map_benchmark",
+      ":regex",
       "../../gn:benchmark",
       "../../gn:default_deps",
     ]
     sources = [
       "flat_set_benchmark.cc",
+      "regex/regex_benchmark.cc",
       "rt_mutex_benchmark.cc",
       "task_runner_benchmark.cc",
     ]
diff --git a/src/base/regex/regex.cc b/src/base/regex/regex.cc
new file mode 100644
index 0000000..ad9de0a
--- /dev/null
+++ b/src/base/regex/regex.cc
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2026 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.
+ */
+
+#include "perfetto/ext/base/regex.h"
+
+#include <optional>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/base/logging.h"
+#include "src/base/regex/regex_std.h"
+
+namespace perfetto {
+namespace base {
+
+Regex::Regex(Regex&&) noexcept = default;
+Regex& Regex::operator=(Regex&&) noexcept = default;
+Regex::~Regex() = default;
+
+StatusOr<Regex> Regex::Create(std::string_view pattern,
+                              Regex::CaseSensitivity cs) {
+  bool insensitive = (cs == CaseSensitivity::kInsensitive);
+  auto impl_or = RegexImpl::Create(pattern, insensitive);
+  if (!impl_or.ok()) {
+    return base::ErrStatus("%s", impl_or.status().message().c_str());
+  }
+  Regex regex;
+  regex.pattern_ = std::string(pattern);
+  regex.cs_ = cs;
+  regex.impl_ = std::make_unique<RegexImpl>(std::move(*impl_or));
+  return std::move(regex);
+}
+
+Regex Regex::CreateOrCheck(std::string_view pattern,
+                           Regex::CaseSensitivity cs) {
+  auto re_or = Create(pattern, cs);
+  PERFETTO_CHECK(re_or.ok());
+  return std::move(*re_or);
+}
+
+std::string Regex::GlobalReplace(std::string_view s,
+                                 std::string_view replacement) const {
+  PERFETTO_DCHECK(impl_);
+  return impl_->GlobalReplace(s, replacement);
+}
+
+bool Regex::FullMatch(std::string_view s) const {
+  PERFETTO_DCHECK(impl_);
+  return impl_->FullMatch(s);
+}
+
+bool Regex::FullMatchWithGroups(std::string_view s,
+                                std::vector<std::string_view>& out) const {
+  PERFETTO_DCHECK(impl_);
+  return impl_->FullMatchWithGroups(s, out);
+}
+
+bool Regex::PartialMatch(std::string_view s) const {
+  PERFETTO_DCHECK(impl_);
+  return impl_->PartialMatch(s);
+}
+
+bool Regex::PartialMatchWithGroups(std::string_view s,
+                                   std::vector<std::string_view>& out) const {
+  PERFETTO_DCHECK(impl_);
+  return impl_->PartialMatchWithGroups(s, out);
+}
+
+// --- PartialMatchIterator ---
+
+Regex::PartialMatchIterator::PartialMatchIterator(const Regex* re,
+                                                  std::string_view input)
+    : re_(re), input_(input) {}
+
+std::optional<std::string_view> Regex::PartialMatchIterator::Next() {
+  return NextWithGroups(groups_);
+}
+
+std::optional<std::string_view> Regex::PartialMatchIterator::NextWithGroups(
+    std::vector<std::string_view>& groups) {
+  if (offset_ > input_.size())
+    return std::nullopt;
+  PERFETTO_DCHECK(re_->impl_);
+  if (!re_->impl_->SearchWithOffset(input_, offset_, groups))
+    return std::nullopt;
+  if (groups.empty())
+    return std::nullopt;
+  std::string_view match = groups[0];
+  size_t match_end =
+      static_cast<size_t>(match.data() - input_.data()) + match.size();
+  // Advance past the match. If match is empty, advance by one to avoid
+  // infinite loops.
+  offset_ = match.size() == 0 ? match_end + 1 : match_end;
+  return match;
+}
+
+Regex::PartialMatchIterator Regex::PartialMatchAll(std::string_view s) const {
+  return PartialMatchIterator(this, s);
+}
+
+// --- CopyableRegex ---
+
+CopyableRegex::CopyableRegex(Regex regex) : regex_(std::move(regex)) {}
+
+CopyableRegex::CopyableRegex(const CopyableRegex& other)
+    : regex_(Regex::CreateOrCheck(other.regex_.pattern(),
+                                  other.regex_.case_sensitivity())) {}
+
+CopyableRegex& CopyableRegex::operator=(const CopyableRegex& other) {
+  if (this != &other) {
+    this->~CopyableRegex();
+    new (this) CopyableRegex(other);
+  }
+  return *this;
+}
+
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/regex/regex_benchmark.cc b/src/base/regex/regex_benchmark.cc
new file mode 100644
index 0000000..f3bdded
--- /dev/null
+++ b/src/base/regex/regex_benchmark.cc
@@ -0,0 +1,106 @@
+// Copyright (C) 2026 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.
+
+#include <benchmark/benchmark.h>
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/ext/base/regex.h"
+#include "src/base/regex/regex_std.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+// --- Backend-parameterized benchmarks ---
+
+template <typename Backend>
+static void BM_Regex_PartialMatch_Simple(benchmark::State& state) {
+  auto re = Backend::Create("abc", false);
+  PERFETTO_CHECK(re.ok());
+  std::string_view input = "xxabcyy";
+  for (auto _ : state) {
+    benchmark::DoNotOptimize(re->PartialMatch(input));
+  }
+}
+
+template <typename Backend>
+static void BM_Regex_FullMatch_Simple(benchmark::State& state) {
+  auto re = Backend::Create("abc", false);
+  PERFETTO_CHECK(re.ok());
+  std::string_view input = "abc";
+  for (auto _ : state) {
+    benchmark::DoNotOptimize(re->FullMatch(input));
+  }
+}
+
+template <typename Backend>
+static void BM_Regex_PartialMatchWithGroups(benchmark::State& state) {
+  auto re = Backend::Create("([a-z]+)-([0-9]+)", false);
+  PERFETTO_CHECK(re.ok());
+  std::string_view input = "hello-12345";
+  std::vector<std::string_view> groups;
+  for (auto _ : state) {
+    re->PartialMatchWithGroups(input, groups);
+    benchmark::DoNotOptimize(groups.data());
+  }
+}
+
+template <typename Backend>
+static void BM_Regex_GlobalReplace(benchmark::State& state) {
+  auto re = Backend::Create("[0-9]+", false);
+  PERFETTO_CHECK(re.ok());
+  std::string_view input = "abc123def456ghi789";
+  for (auto _ : state) {
+    auto result = re->GlobalReplace(input, "X");
+    benchmark::DoNotOptimize(result.data());
+  }
+}
+
+template <typename Backend>
+static void BM_Regex_PartialMatch_LongInput(benchmark::State& state) {
+  auto re = Backend::Create(R"(\d{4}-\d{2}-\d{2})", false);
+  PERFETTO_CHECK(re.ok());
+  // 1000-char string with a date near the end.
+  std::string input(990, 'x');
+  input += "2026-03-29";
+  for (auto _ : state) {
+    benchmark::DoNotOptimize(re->PartialMatch(input));
+  }
+}
+
+template <typename Backend>
+static void BM_Regex_Compile(benchmark::State& state) {
+  for (auto _ : state) {
+    auto re = Backend::Create(R"(([a-zA-Z]+)(\d+)\.(\w+))", false);
+    benchmark::DoNotOptimize(re.ok());
+  }
+}
+
+// Register benchmarks for std::regex backend.
+BENCHMARK_TEMPLATE(BM_Regex_PartialMatch_Simple, RegexStd);
+BENCHMARK_TEMPLATE(BM_Regex_FullMatch_Simple, RegexStd);
+BENCHMARK_TEMPLATE(BM_Regex_PartialMatchWithGroups, RegexStd);
+BENCHMARK_TEMPLATE(BM_Regex_GlobalReplace, RegexStd);
+BENCHMARK_TEMPLATE(BM_Regex_PartialMatch_LongInput, RegexStd);
+BENCHMARK_TEMPLATE(BM_Regex_Compile, RegexStd);
+
+// When building with RE2 or PCRE2, register benchmarks for those backends too.
+// See src/base/regex/regex_re2.h and regex_pcre2.h.
+
+}  // namespace
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/regex/regex_std.h b/src/base/regex/regex_std.h
new file mode 100644
index 0000000..62e7800
--- /dev/null
+++ b/src/base/regex/regex_std.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2026 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_BASE_REGEX_REGEX_STD_H_
+#define SRC_BASE_REGEX_REGEX_STD_H_
+
+#include <regex>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/ext/base/status_or.h"
+
+namespace perfetto {
+namespace base {
+
+// std::regex backend for Regex. Uses ECMAScript syntax for Perl-style
+// shorthands (\d, \w, \s).
+class RegexStd {
+ public:
+  RegexStd(RegexStd&&) noexcept = default;
+  RegexStd& operator=(RegexStd&&) noexcept = default;
+  ~RegexStd() = default;
+
+  RegexStd(const RegexStd&) = delete;
+  RegexStd& operator=(const RegexStd&) = delete;
+
+  static StatusOr<RegexStd> Create(std::string_view pattern,
+                                   bool case_insensitive) {
+    RegexStd result;
+    auto flags = std::regex::ECMAScript;
+    if (case_insensitive) {
+      flags |= std::regex::icase;
+    }
+    result.re_ = std::regex(std::string(pattern), flags);
+    return std::move(result);
+  }
+
+  std::string GlobalReplace(std::string_view s,
+                            std::string_view replacement) const {
+    return std::regex_replace(std::string(s), re_, std::string(replacement));
+  }
+
+  bool FullMatch(std::string_view s) const {
+    return std::regex_match(s.data(), s.data() + s.size(), re_);
+  }
+
+  bool FullMatchWithGroups(std::string_view s,
+                           std::vector<std::string_view>& out) const {
+    out.clear();
+    if (!std::regex_match(s.data(), s.data() + s.size(), match_data_, re_))
+      return false;
+    FillGroups(s.data(), out);
+    return true;
+  }
+
+  bool PartialMatch(std::string_view s) const {
+    return std::regex_search(s.data(), s.data() + s.size(), re_);
+  }
+
+  bool PartialMatchWithGroups(std::string_view s,
+                              std::vector<std::string_view>& out) const {
+    out.clear();
+    if (!std::regex_search(s.data(), s.data() + s.size(), match_data_, re_))
+      return false;
+    FillGroups(s.data(), out);
+    return true;
+  }
+
+  bool SearchWithOffset(std::string_view s,
+                        size_t start_offset,
+                        std::vector<std::string_view>& out) const {
+    out.clear();
+    const char* begin = s.data() + start_offset;
+    const char* end = s.data() + s.size();
+    if (begin > end)
+      return false;
+    if (!std::regex_search(begin, end, match_data_, re_))
+      return false;
+    FillGroups(begin, out);
+    return true;
+  }
+
+ private:
+  RegexStd() = default;
+
+  void FillGroups(const char* base, std::vector<std::string_view>& out) const {
+    for (size_t i = 0; i < match_data_.size(); ++i) {
+      if (match_data_[i].matched) {
+        out.emplace_back(base + match_data_.position(i),
+                         static_cast<size_t>(match_data_.length(i)));
+      } else {
+        out.emplace_back();
+      }
+    }
+  }
+
+  std::regex re_;
+  mutable std::cmatch match_data_;
+};
+
+}  // namespace base
+}  // namespace perfetto
+
+#endif  // SRC_BASE_REGEX_REGEX_STD_H_
diff --git a/src/base/regex/regex_unittest.cc b/src/base/regex/regex_unittest.cc
new file mode 100644
index 0000000..d4b3c0d
--- /dev/null
+++ b/src/base/regex/regex_unittest.cc
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2026 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.
+ */
+
+#include "perfetto/ext/base/regex.h"
+
+#include <optional>
+#include <string_view>
+#include <vector>
+
+#include "src/base/regex/regex_std.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+template <typename T>
+struct BackendTraits;
+
+template <>
+struct BackendTraits<RegexStd> {
+  static const char* name() { return "RegexStd"; }
+};
+
+template <typename Backend>
+class RegexBackendTest : public ::testing::Test {
+ protected:
+  StatusOr<Backend> Create(std::string_view pattern,
+                           bool case_insensitive = false) {
+    return Backend::Create(pattern, case_insensitive);
+  }
+};
+
+using BackendTypes = ::testing::Types<RegexStd>;
+TYPED_TEST_SUITE(RegexBackendTest, BackendTypes, /* trailing ',' for GCC*/);
+
+TYPED_TEST(RegexBackendTest, PartialMatch) {
+  auto re_or = this->Create("abc");
+  ASSERT_TRUE(re_or.ok());
+  auto& re = re_or.value();
+  EXPECT_TRUE(re.PartialMatch("abc"));
+  EXPECT_TRUE(re.PartialMatch("xabcy"));
+  EXPECT_FALSE(re.PartialMatch("abd"));
+}
+
+TYPED_TEST(RegexBackendTest, FullMatch) {
+  auto re_or = this->Create("abc");
+  ASSERT_TRUE(re_or.ok());
+  auto& re = re_or.value();
+  EXPECT_TRUE(re.FullMatch("abc"));
+  EXPECT_FALSE(re.FullMatch("xabcy"));
+  EXPECT_FALSE(re.FullMatch("abcd"));
+}
+
+TYPED_TEST(RegexBackendTest, PartialMatchWithGroups) {
+  auto re_or = this->Create("([a-z]+)-([0-9]+)");
+  ASSERT_TRUE(re_or.ok());
+  auto& re = re_or.value();
+  std::vector<std::string_view> matches;
+  EXPECT_TRUE(re.PartialMatchWithGroups("foo-123", matches));
+  ASSERT_EQ(matches.size(), 3u);
+  EXPECT_EQ(matches[0], "foo-123");
+  EXPECT_EQ(matches[1], "foo");
+  EXPECT_EQ(matches[2], "123");
+}
+
+TYPED_TEST(RegexBackendTest, PartialMatchWithGroupsNoMatch) {
+  auto re_or = this->Create("([a-z]+)-([0-9]+)");
+  ASSERT_TRUE(re_or.ok());
+  std::vector<std::string_view> matches;
+  EXPECT_FALSE(re_or->PartialMatchWithGroups("foo-bar", matches));
+  EXPECT_TRUE(matches.empty());
+}
+
+TYPED_TEST(RegexBackendTest, PartialMatchWithGroupsOptionalGroup) {
+  auto re_or = this->Create("a(b)?c");
+  ASSERT_TRUE(re_or.ok());
+  std::vector<std::string_view> matches;
+  EXPECT_TRUE(re_or->PartialMatchWithGroups("ac", matches));
+  ASSERT_EQ(matches.size(), 2u);
+  EXPECT_EQ(matches[0], "ac");
+  EXPECT_TRUE(matches[1].empty());
+}
+
+TYPED_TEST(RegexBackendTest, FullMatchWithGroups) {
+  auto re_or = this->Create("([a-z]+)-([0-9]+)");
+  ASSERT_TRUE(re_or.ok());
+  std::vector<std::string_view> matches;
+  EXPECT_TRUE(re_or->FullMatchWithGroups("foo-123", matches));
+  ASSERT_EQ(matches.size(), 3u);
+  EXPECT_EQ(matches[0], "foo-123");
+  EXPECT_EQ(matches[1], "foo");
+  EXPECT_EQ(matches[2], "123");
+  EXPECT_FALSE(re_or->FullMatchWithGroups("xfoo-123x", matches));
+  EXPECT_TRUE(matches.empty());
+}
+
+TYPED_TEST(RegexBackendTest, GlobalReplace) {
+  auto re_or = this->Create("a");
+  ASSERT_TRUE(re_or.ok());
+  EXPECT_EQ(re_or->GlobalReplace("aba", "x"), "xbx");
+  EXPECT_EQ(re_or->GlobalReplace("bbb", "x"), "bbb");
+  EXPECT_EQ(re_or->GlobalReplace("aba", ""), "b");
+}
+
+TYPED_TEST(RegexBackendTest, PerlShorthands) {
+  {
+    auto re_or = this->Create(R"(\d+)");
+    ASSERT_TRUE(re_or.ok());
+    EXPECT_TRUE(re_or->PartialMatch("abc123"));
+    EXPECT_FALSE(re_or->PartialMatch("abcxyz"));
+    EXPECT_TRUE(re_or->FullMatch("42"));
+  }
+  {
+    auto re_or = this->Create(R"(\w+)");
+    ASSERT_TRUE(re_or.ok());
+    EXPECT_TRUE(re_or->FullMatch("hello_world123"));
+    EXPECT_FALSE(re_or->FullMatch("hello world"));
+  }
+  {
+    auto re_or = this->Create(R"(\s+)");
+    ASSERT_TRUE(re_or.ok());
+    EXPECT_TRUE(re_or->PartialMatch("hello world"));
+    EXPECT_FALSE(re_or->PartialMatch("helloworld"));
+  }
+}
+
+TYPED_TEST(RegexBackendTest, CaseInsensitive) {
+  auto re_or = this->Create("abc", /*case_insensitive=*/true);
+  ASSERT_TRUE(re_or.ok());
+  EXPECT_TRUE(re_or->PartialMatch("ABC"));
+  EXPECT_TRUE(re_or->PartialMatch("aBc"));
+  EXPECT_TRUE(re_or->FullMatch("ABC"));
+  EXPECT_FALSE(re_or->PartialMatch("abd"));
+}
+
+TYPED_TEST(RegexBackendTest, CaretMatchesOnlyStringStart) {
+  auto re_or = this->Create("^abc");
+  ASSERT_TRUE(re_or.ok());
+  EXPECT_TRUE(re_or->PartialMatch("abc"));
+  EXPECT_TRUE(re_or->PartialMatch("abcdef"));
+  // ^ should NOT match after a newline (no MULTILINE).
+  EXPECT_FALSE(re_or->PartialMatch("xyz\nabc"));
+}
+
+TYPED_TEST(RegexBackendTest, SearchWithOffset) {
+  auto re_or = this->Create("([0-9]+)");
+  ASSERT_TRUE(re_or.ok());
+  std::vector<std::string_view> matches;
+  std::string_view input = "abc123def456";
+  // From offset 0 — should find "123".
+  EXPECT_TRUE(re_or->SearchWithOffset(input, 0, matches));
+  ASSERT_GE(matches.size(), 1u);
+  EXPECT_EQ(matches[0], "123");
+  // From offset 6 (past "123") — should find "456".
+  EXPECT_TRUE(re_or->SearchWithOffset(input, 6, matches));
+  ASSERT_GE(matches.size(), 1u);
+  EXPECT_EQ(matches[0], "456");
+  // From offset 12 (end) — no match.
+  EXPECT_FALSE(re_or->SearchWithOffset(input, 12, matches));
+}
+
+// ============================================================================
+// Tests for the Regex wrapper class (uses the active backend via RegexImpl).
+// ============================================================================
+
+TEST(RegexTest, PartialMatch) {
+  auto re = Regex::CreateOrCheck("abc");
+  EXPECT_TRUE(re.PartialMatch("abc"));
+  EXPECT_TRUE(re.PartialMatch("xabcy"));
+  EXPECT_FALSE(re.PartialMatch("abd"));
+}
+
+TEST(RegexTest, FullMatch) {
+  auto re = Regex::CreateOrCheck("abc");
+  EXPECT_TRUE(re.FullMatch("abc"));
+  EXPECT_FALSE(re.FullMatch("xabcy"));
+}
+
+TEST(RegexTest, Move) {
+  Regex re1 = Regex::CreateOrCheck("abc");
+  EXPECT_TRUE(re1.PartialMatch("abc"));
+  Regex re2 = std::move(re1);
+  EXPECT_TRUE(re2.PartialMatch("abc"));
+}
+
+TEST(RegexTest, MoveAssignment) {
+  Regex re1 = Regex::CreateOrCheck("abc");
+  Regex re2 = Regex::CreateOrCheck("xyz");
+  re2 = std::move(re1);
+  EXPECT_TRUE(re2.PartialMatch("abc"));
+  EXPECT_FALSE(re2.PartialMatch("xyz"));
+}
+
+TEST(RegexTest, EmptyString) {
+  auto re = Regex::CreateOrCheck("abc");
+  EXPECT_FALSE(re.PartialMatch(""));
+  EXPECT_FALSE(re.FullMatch(""));
+  std::vector<std::string_view> matches;
+  EXPECT_FALSE(re.PartialMatchWithGroups("", matches));
+}
+
+TEST(RegexTest, Alternation) {
+  auto re = Regex::CreateOrCheck("cat|dog");
+  EXPECT_TRUE(re.PartialMatch("I have a cat"));
+  EXPECT_TRUE(re.PartialMatch("I have a dog"));
+  EXPECT_FALSE(re.PartialMatch("I have a bird"));
+}
+
+TEST(RegexTest, PartialMatchAll) {
+  auto re = Regex::CreateOrCheck("([0-9]+)");
+  auto it = re.PartialMatchAll("foo123bar456baz789");
+  std::vector<std::string_view> groups;
+
+  auto m1 = it.NextWithGroups(groups);
+  ASSERT_TRUE(m1.has_value());
+  EXPECT_EQ(*m1, "123");
+  ASSERT_EQ(groups.size(), 2u);
+  EXPECT_EQ(groups[1], "123");
+
+  auto m2 = it.Next();
+  ASSERT_TRUE(m2.has_value());
+  EXPECT_EQ(*m2, "456");
+
+  auto m3 = it.Next();
+  ASSERT_TRUE(m3.has_value());
+  EXPECT_EQ(*m3, "789");
+
+  EXPECT_FALSE(it.Next().has_value());
+}
+
+TEST(RegexTest, PartialMatchAllNoMatch) {
+  auto re = Regex::CreateOrCheck("[0-9]+");
+  auto it = re.PartialMatchAll("hello");
+  EXPECT_FALSE(it.Next().has_value());
+}
+
+// --- CopyableRegex tests ---
+
+TEST(CopyableRegexTest, CopyConstruct) {
+  CopyableRegex re1(Regex::CreateOrCheck("abc"));
+  CopyableRegex re2(re1);
+  EXPECT_TRUE(re2.PartialMatch("abc"));
+  EXPECT_FALSE(re2.PartialMatch("xyz"));
+  EXPECT_TRUE(re1.PartialMatch("abc"));
+}
+
+TEST(CopyableRegexTest, CopyAssignment) {
+  CopyableRegex re1(Regex::CreateOrCheck("abc"));
+  CopyableRegex re2(Regex::CreateOrCheck("xyz"));
+  re2 = re1;
+  EXPECT_TRUE(re2.PartialMatch("abc"));
+  EXPECT_FALSE(re2.PartialMatch("xyz"));
+  EXPECT_TRUE(re1.PartialMatch("abc"));
+}
+
+TEST(CopyableRegexTest, CaseInsensitiveCopy) {
+  CopyableRegex re1(
+      Regex::CreateOrCheck("abc", Regex::CaseSensitivity::kInsensitive));
+  CopyableRegex re2(re1);
+  EXPECT_TRUE(re2.PartialMatch("ABC"));
+  EXPECT_FALSE(re2.PartialMatch("abd"));
+}
+
+TEST(CopyableRegexTest, MoveConstruct) {
+  CopyableRegex re1(Regex::CreateOrCheck("abc"));
+  CopyableRegex re2(std::move(re1));
+  EXPECT_TRUE(re2.PartialMatch("abc"));
+}
+
+TEST(CopyableRegexTest, FullMatchWithGroups) {
+  CopyableRegex re(Regex::CreateOrCheck("([a-z]+)-([0-9]+)"));
+  std::vector<std::string_view> matches;
+  EXPECT_TRUE(re.FullMatchWithGroups("foo-123", matches));
+  ASSERT_EQ(matches.size(), 3u);
+  EXPECT_EQ(matches[1], "foo");
+  EXPECT_EQ(matches[2], "123");
+}
+
+}  // namespace
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/protozero/filtering/filter_util_unittest.cc b/src/protozero/filtering/filter_util_unittest.cc
index 038c473..23ed4a0 100644
--- a/src/protozero/filtering/filter_util_unittest.cc
+++ b/src/protozero/filtering/filter_util_unittest.cc
@@ -18,12 +18,12 @@
 
 #include <cstdint>
 #include <optional>
-#include <regex>
 #include <string>
 #include <vector>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/regex.h"
 #include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/base/temp_file.h"
 #include "src/protozero/filtering/filter_bytecode_generator.h"
@@ -57,8 +57,10 @@
   std::string output;
   PERFETTO_CHECK(perfetto::base::ReadFile(tmp_path, &output));
   // Make the output a bit more compact.
-  output = std::regex_replace(output, std::regex(" +"), " ");
-  return std::regex_replace(output, std::regex(" +\\n"), "\n");
+  auto re = perfetto::base::Regex::CreateOrCheck(" +");
+  output = re.GlobalReplace(output, " ");
+  re = perfetto::base::Regex::CreateOrCheck(" +\\n");
+  return re.GlobalReplace(output, "\n");
 }
 
 TEST(SchemaParserTest, SchemaToBytecode_Simple) {
diff --git a/src/protozero/filtering/string_filter.cc b/src/protozero/filtering/string_filter.cc
index 2c425df..1bafa8a 100644
--- a/src/protozero/filtering/string_filter.cc
+++ b/src/protozero/filtering/string_filter.cc
@@ -19,20 +19,16 @@
 #include <algorithm>
 #include <cstdint>
 #include <cstring>
-#include <regex>
 #include <string>
 #include <string_view>
 #include <utility>
 #include <vector>
 
-#include "perfetto/base/logging.h"
 #include "perfetto/public/compiler.h"
 
 namespace protozero {
 namespace {
 
-using Matches = std::match_results<char*>;
-
 constexpr std::string_view kRedacted = "P60REDACTED";
 constexpr char kRedactedDash = '-';
 
@@ -88,21 +84,25 @@
   return memcmp(ptr + 1, starts_with.data() + 1, len - 1) == 0;
 }
 
-void RedactMatches(const Matches& matches) {
+void RedactMatches(const std::vector<std::string_view>& matches) {
   // Go through every group in the matches.
   for (size_t i = 1; i < matches.size(); ++i) {
     const auto& match = matches[i];
-    PERFETTO_CHECK(match.second >= match.first);
+    // Skip unmatched optional groups: empty string_view may have nullptr
+    // data(), and passing nullptr to memcpy/memset is UB even with size 0.
+    if (match.empty())
+      continue;
 
     // Overwrite the match with characters from |kRedacted|. If match is
     // smaller, we will not use all of |kRedacted| but that's fine (i.e. we
     // will overwrite with a truncated |kRedacted|).
-    auto match_len = static_cast<size_t>(match.second - match.first);
+    size_t match_len = match.size();
     size_t redacted_len = std::min(match_len, kRedacted.size());
-    memcpy(match.first, kRedacted.data(), redacted_len);
+    memcpy(const_cast<char*>(match.data()), kRedacted.data(), redacted_len);
 
     // Overwrite any characters after |kRedacted| with |kRedactedDash|.
-    memset(match.first + redacted_len, kRedactedDash, match_len - redacted_len);
+    memset(const_cast<char*>(match.data()) + redacted_len, kRedactedDash,
+           match_len - redacted_len);
   }
 }
 
@@ -113,12 +113,10 @@
                            std::string atrace_payload_starts_with,
                            std::string name,
                            SemanticTypeMask semantic_type_mask) {
-  Rule new_rule{
-      policy,
-      std::regex(pattern_str.begin(), pattern_str.end(),
-                 std::regex::ECMAScript | std::regex_constants::optimize),
-      std::move(atrace_payload_starts_with), std::move(name),
-      semantic_type_mask};
+  perfetto::base::CopyableRegex re(
+      perfetto::base::Regex::CreateOrCheck(std::string(pattern_str)));
+  Rule new_rule{policy, std::move(re), std::move(atrace_payload_starts_with),
+                std::move(name), semantic_type_mask};
   // If name is non-empty, look for existing rule with same name and replace.
   if (!new_rule.name.empty()) {
     for (Rule& existing : rules_) {
@@ -134,26 +132,26 @@
 bool StringFilter::MaybeFilterInternal(char* ptr,
                                        size_t len,
                                        uint32_t semantic_type) const {
-  std::match_results<char*> matches;
   bool atrace_find_tried = false;
   const char* atrace_payload_ptr = nullptr;
+  std::string_view input(ptr, len);
+
   for (const Rule& rule : rules_) {
     if (!rule.semantic_type_mask.IsSet(semantic_type)) {
       continue;
     }
     switch (rule.policy) {
-      case Policy::kMatchRedactGroups:
       case Policy::kMatchBreak:
+        if (PERFETTO_UNLIKELY(rule.pattern->FullMatch(input)))
+          return false;
+        break;
+      case Policy::kMatchRedactGroups:
         if (PERFETTO_UNLIKELY(
-                std::regex_match(ptr, ptr + len, matches, rule.pattern))) {
-          if (rule.policy == Policy::kMatchBreak) {
-            return false;
-          }
-          RedactMatches(matches);
+                rule.pattern->FullMatchWithGroups(input, matches_))) {
+          RedactMatches(matches_);
           return true;
         }
         break;
-      case Policy::kAtraceMatchRedactGroups:
       case Policy::kAtraceMatchBreak:
         atrace_payload_ptr = atrace_find_tried
                                  ? atrace_payload_ptr
@@ -162,11 +160,21 @@
         if (atrace_payload_ptr &&
             StartsWith(atrace_payload_ptr, ptr + len,
                        rule.atrace_payload_starts_with) &&
-            std::regex_match(ptr, ptr + len, matches, rule.pattern)) {
-          if (rule.policy == Policy::kAtraceMatchBreak) {
-            return false;
-          }
-          RedactMatches(matches);
+            rule.pattern->FullMatch(input)) {
+          return false;
+        }
+        break;
+      case Policy::kAtraceMatchRedactGroups:
+        atrace_payload_ptr = atrace_find_tried
+                                 ? atrace_payload_ptr
+                                 : FindAtracePayloadPtr(ptr, ptr + len);
+        atrace_find_tried = true;
+        if (atrace_payload_ptr &&
+            StartsWith(atrace_payload_ptr, ptr + len,
+                       rule.atrace_payload_starts_with) &&
+            PERFETTO_UNLIKELY(
+                rule.pattern->FullMatchWithGroups(input, matches_))) {
+          RedactMatches(matches_);
           return true;
         }
         break;
@@ -177,11 +185,11 @@
         atrace_find_tried = true;
         if (atrace_payload_ptr && StartsWith(atrace_payload_ptr, ptr + len,
                                              rule.atrace_payload_starts_with)) {
-          auto beg = std::regex_iterator<char*>(ptr, ptr + len, rule.pattern);
-          auto end = std::regex_iterator<char*>();
-          bool has_any_matches = beg != end;
-          for (auto it = std::move(beg); it != end; ++it) {
-            RedactMatches(*it);
+          bool has_any_matches = false;
+          for (auto iter = rule.pattern->PartialMatchAll(input);
+               iter.NextWithGroups(matches_);) {
+            RedactMatches(matches_);
+            has_any_matches = true;
           }
           if (has_any_matches) {
             return true;
diff --git a/src/protozero/filtering/string_filter.h b/src/protozero/filtering/string_filter.h
index 76a4823..4026dfd 100644
--- a/src/protozero/filtering/string_filter.h
+++ b/src/protozero/filtering/string_filter.h
@@ -20,12 +20,13 @@
 #include <array>
 #include <cstddef>
 #include <cstdint>
-#include <regex>
+#include <optional>
 #include <string>
 #include <string_view>
 #include <vector>
 
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/regex.h"
 #include "perfetto/public/compiler.h"
 
 namespace protozero {
@@ -122,7 +123,7 @@
  private:
   struct Rule {
     Policy policy;
-    std::regex pattern;
+    std::optional<perfetto::base::CopyableRegex> pattern;
     std::string atrace_payload_starts_with;
     std::string name;
     SemanticTypeMask semantic_type_mask = SemanticTypeMask::Unspecified();
@@ -132,6 +133,10 @@
 
   // All rules, in the order they were added.
   std::vector<Rule> rules_;
+
+  // Reusable buffer for regex match groups, avoiding heap allocations on each
+  // filter call.
+  mutable std::vector<std::string_view> matches_;
 };
 
 }  // namespace protozero
diff --git a/src/tools/ftrace_proto_gen/proto_gen_utils.cc b/src/tools/ftrace_proto_gen/proto_gen_utils.cc
index d650cd0..e598ef7 100644
--- a/src/tools/ftrace_proto_gen/proto_gen_utils.cc
+++ b/src/tools/ftrace_proto_gen/proto_gen_utils.cc
@@ -17,10 +17,10 @@
 #include "src/tools/ftrace_proto_gen/proto_gen_utils.h"
 
 #include <algorithm>
-#include <regex>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/regex.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/subprocess.h"
 
@@ -203,7 +203,8 @@
 
 ProtoType InferProtoType(const FtraceEvent::Field& field) {
   // Fixed length strings: "char foo[16]"
-  if (std::regex_match(field.type_and_name, std::regex(R"(char \w+\[\d+\])")))
+  auto char_array_re = base::Regex::CreateOrCheck(R"(char \w+\[\d+\])");
+  if (char_array_re.FullMatch(field.type_and_name))
     return ProtoType::String();
 
   // String pointers: "__data_loc char[] foo" (as in
diff --git a/src/trace_processor/metrics/metrics.cc b/src/trace_processor/metrics/metrics.cc
index 4587a0e..563638b 100644
--- a/src/trace_processor/metrics/metrics.cc
+++ b/src/trace_processor/metrics/metrics.cc
@@ -25,7 +25,6 @@
 #include <iterator>
 #include <memory>
 #include <optional>
-#include <regex>
 #include <string>
 #include <unordered_map>
 #include <utility>
@@ -33,6 +32,7 @@
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
+#include "perfetto/ext/base/regex.h"
 #include "perfetto/ext/base/status_macros.h"
 #include "perfetto/ext/base/status_or.h"
 #include "perfetto/ext/base/string_view.h"
@@ -475,24 +475,25 @@
     const std::string& raw_text,
     const std::unordered_map<std::string, std::string>& substitutions,
     std::string* out) {
-  std::regex re(R"(\{\{\s*(\w*)\s*\}\})", std::regex_constants::ECMAScript);
+  auto re = base::Regex::CreateOrCheck(R"(\{\{\s*(\w*)\s*\}\})");
 
-  auto it = std::sregex_iterator(raw_text.begin(), raw_text.end(), re);
-  auto regex_end = std::sregex_iterator();
-  auto start = raw_text.begin();
-  for (; it != regex_end; ++it) {
-    out->insert(out->end(), start, raw_text.begin() + it->position(0));
+  std::string_view input(raw_text);
+  size_t last_end = 0;
+  std::vector<std::string_view> groups;
+  for (auto iter = re.PartialMatchAll(input);
+       iter.NextWithGroups(groups).has_value();) {
+    // groups[0] is the full match, groups[1] is the capture group.
+    size_t match_pos = static_cast<size_t>(groups[0].data() - input.data());
+    out->append(input.substr(last_end, match_pos - last_end));
 
-    auto value_it = substitutions.find(it->str(1));
+    auto value_it = substitutions.find(std::string(groups[1]));
     if (value_it == substitutions.end()) {
       return 1;
     }
-
-    const auto& value = value_it->second;
-    std::copy(value.begin(), value.end(), std::back_inserter(*out));
-    start = raw_text.begin() + it->position(0) + it->length(0);
+    out->append(value_it->second);
+    last_end = match_pos + groups[0].size();
   }
-  out->insert(out->end(), start, raw_text.end());
+  out->append(input.substr(last_end));
   return 0;
 }
 
diff --git a/src/traced/probes/packages_list/packages_list_data_source.cc b/src/traced/probes/packages_list/packages_list_data_source.cc
index 5a37876..ff48b7b 100644
--- a/src/traced/probes/packages_list/packages_list_data_source.cc
+++ b/src/traced/probes/packages_list/packages_list_data_source.cc
@@ -21,7 +21,6 @@
 #include <cstdio>
 #include <functional>
 #include <memory>
-#include <regex>
 #include <set>
 #include <string>
 #include <unordered_map>
@@ -31,6 +30,7 @@
 #include "perfetto/base/logging.h"
 #include "perfetto/base/task_runner.h"
 #include "perfetto/base/time.h"
+#include "perfetto/ext/base/regex.h"
 #include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/tracing/core/basic_types.h"
 #include "perfetto/ext/tracing/core/trace_writer.h"
@@ -66,10 +66,10 @@
     const std::set<std::string>& package_name_filter,
     const std::vector<std::string>& package_name_regex_filter) {
   // Pre-compile regex patterns.
-  std::vector<std::regex> compiled_regexes;
+  std::vector<base::Regex> compiled_regexes;
   compiled_regexes.reserve(package_name_regex_filter.size());
   for (const auto& pattern : package_name_regex_filter) {
-    compiled_regexes.emplace_back(pattern);
+    compiled_regexes.emplace_back(base::Regex::CreateOrCheck(pattern));
   }
 
   bool has_filter = !package_name_filter.empty() || !compiled_regexes.empty();
@@ -91,7 +91,7 @@
       continue;
     }
     for (const auto& re : compiled_regexes) {
-      if (std::regex_match(pkg_struct.name, re)) {
+      if (re.FullMatch(pkg_struct.name)) {
         packages.insert({pkg_struct.uid, pkg_struct});
         break;
       }
diff --git a/src/tracing/service/tracing_service_impl.cc b/src/tracing/service/tracing_service_impl.cc
index b2fd3c3..f18225e 100644
--- a/src/tracing/service/tracing_service_impl.cc
+++ b/src/tracing/service/tracing_service_impl.cc
@@ -30,7 +30,6 @@
 #include <map>
 #include <memory>
 #include <optional>
-#include <regex>
 #include <set>
 #include <string>
 #include <tuple>
@@ -73,6 +72,7 @@
 #include "perfetto/ext/base/fnv_hash.h"
 #include "perfetto/ext/base/metatrace.h"
 #include "perfetto/ext/base/periodic_task.h"
+#include "perfetto/ext/base/regex.h"
 #include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/base/scoped_sched_boost.h"
 #include "perfetto/ext/base/string_utils.h"  // IWYU pragma: keep
@@ -264,15 +264,15 @@
 bool NameMatchesFilter(const std::string& name,
                        const std::vector<std::string>& name_filter,
                        const std::vector<std::string>& name_regex_filter) {
-  bool filter_matches = std::find(name_filter.begin(), name_filter.end(),
-                                  name) != name_filter.end();
-  bool filter_regex_matches =
-      std::find_if(name_regex_filter.begin(), name_regex_filter.end(),
-                   [&](const std::string& regex) {
-                     return std::regex_match(
-                         name, std::regex(regex, std::regex::extended));
-                   }) != name_regex_filter.end();
-  return filter_matches || filter_regex_matches;
+  if (std::find(name_filter.begin(), name_filter.end(), name) !=
+      name_filter.end()) {
+    return true;
+  }
+  return std::any_of(
+      name_regex_filter.begin(), name_regex_filter.end(),
+      [&](const std::string& pattern) {
+        return base::Regex::CreateOrCheck(pattern).FullMatch(name);
+      });
 }
 
 // Used when TraceConfig.write_into_file == true and output_path is not empty.
@@ -1895,11 +1895,11 @@
       // If this trigger requires a certain producer to have sent it
       // (non-empty producer_name()) ensure the producer who sent this trigger
       // matches.
-      if (!iter->producer_name_regex().empty() &&
-          !std::regex_match(
-              producer->name_,
-              std::regex(iter->producer_name_regex(), std::regex::extended))) {
-        continue;
+      if (!iter->producer_name_regex().empty()) {
+        auto re = base::Regex::CreateOrCheck(iter->producer_name_regex());
+        if (!re.FullMatch(producer->name_)) {
+          continue;
+        }
       }
 
       // Use a random number between 0 and 1 to check if we should allow this
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index ffc1c8b..b2a99f7 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -22,12 +22,12 @@
 #include <functional>
 #include <list>
 #include <mutex>
-#include <regex>
 #include <string_view>
 #include <thread>
 #include <unordered_set>
 #include <vector>
 #include "perfetto/ext/base/flags.h"
+#include "perfetto/ext/base/regex.h"
 
 // We also want to test legacy trace events.
 #define PERFETTO_ENABLE_LEGACY_TRACE_EVENTS 1
@@ -6133,7 +6133,8 @@
     // Ignore timestamps and process/thread ids.
     std::string s(line.data() + 28);
     // Filter out durations.
-    s = std::regex_replace(s, std::regex(" [+][0-9]*ms"), "");
+    auto re = perfetto::base::Regex::CreateOrCheck(" [+][0-9]*ms");
+    s = re.GlobalReplace(s, "");
     lines.push_back(std::move(s));
   }
   fclose(f);
diff --git a/test/stress_test/stress_test.cc b/test/stress_test/stress_test.cc
index 0fbbfe5..3dafa4e 100644
--- a/test/stress_test/stress_test.cc
+++ b/test/stress_test/stress_test.cc
@@ -19,8 +19,8 @@
 #include <chrono>
 #include <list>
 #include <map>
+#include <optional>
 #include <random>
-#include <regex>
 #include <string>
 #include <thread>
 #include <vector>
@@ -30,6 +30,7 @@
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/ctrl_c_handler.h"
 #include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/regex.h"
 #include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/base/subprocess.h"
 #include "perfetto/ext/base/temp_file.h"
@@ -439,16 +440,17 @@
 
 void StressTestMain(int argc, char** argv) {
   TestHarness th;
-  std::regex filter;
-  bool has_filter = false;
+  std::optional<base::Regex> filter;
 
   bool verbose = false;
   for (int i = 1; i < argc; ++i) {
     if (!strcmp(argv[i], "-v")) {
       verbose = true;
     } else {
-      filter = std::regex(argv[i], std::regex::ECMAScript | std::regex::icase);
-      has_filter = true;
+      auto re = base::Regex::Create(std::string(argv[i]),
+                                    base::Regex::CaseSensitivity::kInsensitive);
+      PERFETTO_CHECK(re.ok());
+      filter = std::move(*re);
     }
   }
 
@@ -459,8 +461,7 @@
        ++i) {
     const auto& cfg_blob = kStressTestConfigs[i];
     StressTestConfig cfg;
-    std::cmatch ignored;
-    if (has_filter && !std::regex_search(cfg_blob.name, ignored, filter)) {
+    if (filter.has_value() && !filter->PartialMatch(cfg_blob.name)) {
       continue;
     }
     PERFETTO_CHECK(cfg.ParseFromArray(cfg_blob.data, cfg_blob.size));