Merge "tp: rewrite span join to remove dependency on SqliteTable" into main
diff --git a/Android.bp b/Android.bp
index dce7dbc..0c8ab42 100644
--- a/Android.bp
+++ b/Android.bp
@@ -12850,6 +12850,8 @@
     name: "perfetto_src_trace_redaction_trace_redaction",
     srcs: [
         "src/trace_redaction/build_timeline.cc",
+        "src/trace_redaction/filter_ftrace_using_allowlist.cc",
+        "src/trace_redaction/filter_sched_waking_events.cc",
         "src/trace_redaction/find_package_uid.cc",
         "src/trace_redaction/optimize_timeline.cc",
         "src/trace_redaction/populate_allow_lists.cc",
@@ -12857,7 +12859,6 @@
         "src/trace_redaction/proto_util.cc",
         "src/trace_redaction/prune_package_list.cc",
         "src/trace_redaction/redact_sched_switch.cc",
-        "src/trace_redaction/redact_sched_waking.cc",
         "src/trace_redaction/scrub_ftrace_events.cc",
         "src/trace_redaction/scrub_process_trees.cc",
         "src/trace_redaction/scrub_task_rename.cc",
@@ -12872,13 +12873,13 @@
     name: "perfetto_src_trace_redaction_unittests",
     srcs: [
         "src/trace_redaction/build_timeline_unittest.cc",
+        "src/trace_redaction/filter_ftrace_using_allowlist_unittest.cc",
+        "src/trace_redaction/filter_sched_waking_events_unittest.cc",
         "src/trace_redaction/find_package_uid_unittest.cc",
         "src/trace_redaction/process_thread_timeline_unittest.cc",
         "src/trace_redaction/proto_util_unittest.cc",
         "src/trace_redaction/prune_package_list_unittest.cc",
         "src/trace_redaction/redact_sched_switch_unittest.cc",
-        "src/trace_redaction/redact_sched_waking_unittest.cc",
-        "src/trace_redaction/scrub_ftrace_events_unittest.cc",
         "src/trace_redaction/scrub_task_rename_unittest.cc",
         "src/trace_redaction/scrub_trace_packet_unittest.cc",
     ],
diff --git a/infra/ci/frontend/static/script.js b/infra/ci/frontend/static/script.js
index a2e9dc6..24591d4 100644
--- a/infra/ci/frontend/static/script.js
+++ b/infra/ci/frontend/static/script.js
@@ -231,7 +231,7 @@
     return lastUpdateMins + ' mins ago';
   if (lastUpdateMins < 60 * 24)
     return Math.ceil(lastUpdateMins / 60) + ' hours ago';
-  return lastUpdate.toLocaleDateString();
+  return lastUpdate.toISOString().substr(0, 10);
 }
 
 function renderCLRow(cl) {
diff --git a/src/trace_redaction/BUILD.gn b/src/trace_redaction/BUILD.gn
index 9f67428..f908e96 100644
--- a/src/trace_redaction/BUILD.gn
+++ b/src/trace_redaction/BUILD.gn
@@ -30,6 +30,10 @@
   sources = [
     "build_timeline.cc",
     "build_timeline.h",
+    "filter_ftrace_using_allowlist.cc",
+    "filter_ftrace_using_allowlist.h",
+    "filter_sched_waking_events.cc",
+    "filter_sched_waking_events.h",
     "find_package_uid.cc",
     "find_package_uid.h",
     "optimize_timeline.cc",
@@ -44,8 +48,6 @@
     "prune_package_list.h",
     "redact_sched_switch.cc",
     "redact_sched_switch.h",
-    "redact_sched_waking.cc",
-    "redact_sched_waking.h",
     "scrub_ftrace_events.cc",
     "scrub_ftrace_events.h",
     "scrub_process_trees.cc",
@@ -76,11 +78,14 @@
 source_set("integrationtests") {
   testonly = true
   sources = [
+    "filter_ftrace_using_allowlist_integrationtest.cc",
+    "filter_sched_waking_events_integrationtest.cc",
     "redact_sched_switch_integrationtest.cc",
-    "redact_sched_waking_integrationtest.cc",
     "scrub_ftrace_events_integrationtest.cc",
     "scrub_process_trees_integrationtest.cc",
     "scrub_task_rename_integrationtest.cc",
+    "trace_redaction_integration_fixture.cc",
+    "trace_redaction_integration_fixture.h",
     "trace_redactor_integrationtest.cc",
   ]
   deps = [
@@ -100,13 +105,13 @@
   testonly = true
   sources = [
     "build_timeline_unittest.cc",
+    "filter_ftrace_using_allowlist_unittest.cc",
+    "filter_sched_waking_events_unittest.cc",
     "find_package_uid_unittest.cc",
     "process_thread_timeline_unittest.cc",
     "proto_util_unittest.cc",
     "prune_package_list_unittest.cc",
     "redact_sched_switch_unittest.cc",
-    "redact_sched_waking_unittest.cc",
-    "scrub_ftrace_events_unittest.cc",
     "scrub_task_rename_unittest.cc",
     "scrub_trace_packet_unittest.cc",
   ]
diff --git a/src/trace_redaction/filter_ftrace_using_allowlist.cc b/src/trace_redaction/filter_ftrace_using_allowlist.cc
new file mode 100644
index 0000000..f32ddeb
--- /dev/null
+++ b/src/trace_redaction/filter_ftrace_using_allowlist.cc
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 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 "src/trace_redaction/filter_ftrace_using_allowlist.h"
+
+#include "perfetto/base/status.h"
+#include "perfetto/protozero/field.h"
+#include "perfetto/protozero/proto_decoder.h"
+
+namespace perfetto::trace_redaction {
+
+base::Status FilterFtraceUsingAllowlist::VerifyContext(
+    const Context& context) const {
+  if (context.ftrace_packet_allow_list.empty()) {
+    return base::ErrStatus(
+        "FilterFtraceUsingAllowlist: missing ftrace allowlist.");
+  }
+
+  return base::OkStatus();
+}
+
+bool FilterFtraceUsingAllowlist::KeepEvent(const Context& context,
+                                           protozero::ConstBytes bytes) const {
+  PERFETTO_DCHECK(!context.ftrace_packet_allow_list.empty());
+
+  protozero::ProtoDecoder event(bytes);
+
+  for (auto field = event.ReadField(); field.valid();
+       field = event.ReadField()) {
+    if (context.ftrace_packet_allow_list.count(field.id()) != 0) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/filter_ftrace_using_allowlist.h b/src/trace_redaction/filter_ftrace_using_allowlist.h
new file mode 100644
index 0000000..661da68
--- /dev/null
+++ b/src/trace_redaction/filter_ftrace_using_allowlist.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 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_REDACTION_FILTER_FTRACE_USING_ALLOWLIST_H_
+#define SRC_TRACE_REDACTION_FILTER_FTRACE_USING_ALLOWLIST_H_
+
+#include "perfetto/protozero/field.h"
+#include "src/trace_redaction/scrub_ftrace_events.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+// event {
+//   timestamp: 6702094168934980
+//   pid: 7127
+//   sched_waking {               <-- event type
+//     comm: "Job.worker 1"
+//     pid: 7143
+//     prio: 120
+//     success: 1
+//     target_cpu: 7
+//   }
+// }
+//
+// Check if the event type appears in the ftrace allow-list. If it doesn't
+// appear there, then mark the event as "don't keep".
+class FilterFtraceUsingAllowlist : public FtraceEventFilter {
+ public:
+  base::Status VerifyContext(const Context& context) const override;
+  bool KeepEvent(const Context& context,
+                 protozero::ConstBytes bytes) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_FILTER_FTRACE_USING_ALLOWLIST_H_
diff --git a/src/trace_redaction/filter_ftrace_using_allowlist_integrationtest.cc b/src/trace_redaction/filter_ftrace_using_allowlist_integrationtest.cc
new file mode 100644
index 0000000..c0abf3f
--- /dev/null
+++ b/src/trace_redaction/filter_ftrace_using_allowlist_integrationtest.cc
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2024 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 <cstdint>
+#include <string>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "src/base/test/status_matchers.h"
+#include "src/base/test/tmp_dir_tree.h"
+#include "src/base/test/utils.h"
+#include "src/trace_redaction/filter_ftrace_using_allowlist.h"
+#include "src/trace_redaction/populate_allow_lists.h"
+#include "src/trace_redaction/scrub_ftrace_events.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redactor.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+
+class FilterFtraceUsingAllowlistTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    redactor_.emplace_build<PopulateAllowlists>();
+
+    auto* scrub_ftrace_events =
+        redactor_.emplace_transform<ScrubFtraceEvents>();
+    scrub_ftrace_events->emplace_back<FilterFtraceUsingAllowlist>();
+
+    src_trace_ =
+        base::GetTestDataPath("test/data/trace-redaction-general.pftrace");
+
+    dest_trace_ = tmp_dir_.AbsolutePath("dst.pftrace");
+  }
+
+  base::Status Redact() {
+    auto status = redactor_.Redact(src_trace_, dest_trace_, &context_);
+
+    // If redaction failed, the redactor should not have written the file to
+    // disk.
+    if (status.ok()) {
+      tmp_dir_.TrackFile("dst.pftrace");
+    }
+
+    return status;
+  }
+
+  base::StatusOr<std::string> LoadOriginal() const {
+    return ReadRawTrace(src_trace_);
+  }
+
+  base::StatusOr<std::string> LoadRedacted() const {
+    return ReadRawTrace(dest_trace_);
+  }
+
+  // Parse the given buffer and gather field ids from across all events. This
+  // will also include fields like timestamp.
+  base::FlatSet<uint32_t> ParseEvents(std::string trace_buffer) {
+    base::FlatSet<uint32_t> event_ids;
+
+    protos::pbzero::Trace::Decoder trace_decoder(trace_buffer);
+
+    for (auto packet = trace_decoder.packet(); packet; ++packet) {
+      protos::pbzero::TracePacket::Decoder packet_decoder(*packet);
+
+      if (!packet_decoder.has_ftrace_events()) {
+        continue;
+      }
+
+      protos::pbzero::FtraceEventBundle::Decoder bundle_decoder(
+          packet_decoder.ftrace_events());
+
+      for (auto event = bundle_decoder.event(); event; ++event) {
+        protozero::ProtoDecoder event_decoder(*event);
+
+        for (auto field = event_decoder.ReadField(); field.valid();
+             field = event_decoder.ReadField()) {
+          event_ids.insert(field.id());
+        }
+      }
+    }
+
+    return event_ids;
+  }
+
+ private:
+  base::StatusOr<std::string> ReadRawTrace(const std::string& path) const {
+    std::string redacted_buffer;
+
+    if (base::ReadFile(path, &redacted_buffer)) {
+      return redacted_buffer;
+    }
+
+    return base::ErrStatus("Failed to read %s", path.c_str());
+  }
+
+  Context context_;
+  TraceRedactor redactor_;
+
+  base::TmpDirTree tmp_dir_;
+
+  std::string src_trace_;
+  std::string dest_trace_;
+};
+
+// This is not a test of FilterFtraceUsingAllowlist, but instead verifies of the
+// sample trace used in the test.
+TEST_F(FilterFtraceUsingAllowlistTest, TraceHasAllEvents) {
+  auto trace = LoadOriginal();
+  ASSERT_OK(trace) << trace->c_str();
+
+  auto events = ParseEvents(std::move(trace.value()));
+  ASSERT_EQ(events.size(), 14u);
+
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kCpuFrequencyFieldNumber));
+  ASSERT_TRUE(events.count(protos::pbzero::FtraceEvent::kCpuIdleFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kOomScoreAdjUpdateFieldNumber));
+  ASSERT_TRUE(events.count(protos::pbzero::FtraceEvent::kPidFieldNumber));
+  ASSERT_TRUE(events.count(protos::pbzero::FtraceEvent::kPrintFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kSchedProcessExitFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kSchedProcessFreeFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kSchedWakeupFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kSchedWakeupNewFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kSchedWakingFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kTaskNewtaskFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kTaskRenameFieldNumber));
+  ASSERT_TRUE(events.count(protos::pbzero::FtraceEvent::kTimestampFieldNumber));
+}
+
+TEST_F(FilterFtraceUsingAllowlistTest, RetainsAllowedEvents) {
+  auto redacted = Redact();
+  ASSERT_OK(redacted) << redacted.c_message();
+
+  auto trace = LoadRedacted();
+  ASSERT_OK(trace) << trace.status().c_message();
+
+  auto events = ParseEvents(std::move(trace.value()));
+
+  // These are not events, they are fields that exist alongside the event.
+  ASSERT_TRUE(events.count(protos::pbzero::FtraceEvent::kPidFieldNumber));
+  ASSERT_TRUE(events.count(protos::pbzero::FtraceEvent::kTimestampFieldNumber));
+
+  // These are events.
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kCpuFrequencyFieldNumber));
+  ASSERT_TRUE(events.count(protos::pbzero::FtraceEvent::kCpuIdleFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kSchedProcessFreeFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kSchedWakingFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kTaskNewtaskFieldNumber));
+  ASSERT_TRUE(
+      events.count(protos::pbzero::FtraceEvent::kTaskRenameFieldNumber));
+}
+
+TEST_F(FilterFtraceUsingAllowlistTest, RemovesNotAllowedEvents) {
+  auto redacted = Redact();
+  ASSERT_OK(redacted) << redacted.c_message();
+
+  auto trace = LoadRedacted();
+  ASSERT_OK(trace) << trace.status().c_message();
+
+  auto events = ParseEvents(std::move(trace.value()));
+
+  // These are events.
+  ASSERT_FALSE(
+      events.count(protos::pbzero::FtraceEvent::kOomScoreAdjUpdateFieldNumber));
+  ASSERT_FALSE(events.count(protos::pbzero::FtraceEvent::kPrintFieldNumber));
+  ASSERT_FALSE(
+      events.count(protos::pbzero::FtraceEvent::kSchedProcessExitFieldNumber));
+  ASSERT_FALSE(
+      events.count(protos::pbzero::FtraceEvent::kSchedWakeupFieldNumber));
+  ASSERT_FALSE(
+      events.count(protos::pbzero::FtraceEvent::kSchedWakeupNewFieldNumber));
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/scrub_ftrace_events_unittest.cc b/src/trace_redaction/filter_ftrace_using_allowlist_unittest.cc
similarity index 86%
rename from src/trace_redaction/scrub_ftrace_events_unittest.cc
rename to src/trace_redaction/filter_ftrace_using_allowlist_unittest.cc
index 5b5012f..6f6672a 100644
--- a/src/trace_redaction/scrub_ftrace_events_unittest.cc
+++ b/src/trace_redaction/filter_ftrace_using_allowlist_unittest.cc
@@ -15,6 +15,7 @@
  */
 
 #include "src/trace_redaction/scrub_ftrace_events.h"
+#include "src/trace_redaction/filter_ftrace_using_allowlist.h"
 #include "protos/perfetto/trace/ftrace/power.gen.h"
 #include "src/base/test/status_matchers.h"
 #include "test/gtest_and_gmock.h"
@@ -30,12 +31,12 @@
 namespace perfetto::trace_redaction {
 
 // Tests which nested messages and fields are removed.
-class ScrubFtraceEventsTest : public testing::Test {
- public:
-  ScrubFtraceEventsTest() = default;
-  ~ScrubFtraceEventsTest() override = default;
-
+class FilterFtraceUsingAllowlistTest : public testing::Test {
  protected:
+  void SetUp() override {
+    transform_.emplace_back<FilterFtraceUsingAllowlist>();
+  }
+
   // task_rename should be in the allow-list.
   static void AddTaskRename(protos::gen::FtraceEventBundle* bundle,
                             int32_t pid,
@@ -56,19 +57,20 @@
     e->mutable_clock_set_rate()->set_name(name);
     e->mutable_clock_set_rate()->set_state(state);
   }
+
+  ScrubFtraceEvents transform_;
 };
 
-TEST_F(ScrubFtraceEventsTest, ReturnErrorForNullPacket) {
+TEST_F(FilterFtraceUsingAllowlistTest, ReturnErrorForNullPacket) {
   // Have something in the allow-list to avoid that error.
   Context context;
   context.ftrace_packet_allow_list = {
       protos::pbzero::FtraceEvent::kTaskRenameFieldNumber};
 
-  ScrubFtraceEvents scrub;
-  ASSERT_FALSE(scrub.Transform(context, nullptr).ok());
+  ASSERT_FALSE(transform_.Transform(context, nullptr).ok());
 }
 
-TEST_F(ScrubFtraceEventsTest, ReturnErrorForEmptyPacket) {
+TEST_F(FilterFtraceUsingAllowlistTest, ReturnErrorForEmptyPacket) {
   // Have something in the allow-list to avoid that error.
   Context context;
   context.ftrace_packet_allow_list = {
@@ -76,22 +78,20 @@
 
   std::string packet_str = "";
 
-  ScrubFtraceEvents scrub;
-  ASSERT_FALSE(scrub.Transform(context, &packet_str).ok());
+  ASSERT_FALSE(transform_.Transform(context, &packet_str).ok());
 }
 
-TEST_F(ScrubFtraceEventsTest, ReturnErrorForEmptyAllowList) {
+TEST_F(FilterFtraceUsingAllowlistTest, ReturnErrorForEmptyAllowList) {
   // The context will have no allow-list entries. ScrubFtraceEvents should fail.
   Context context;
 
   protos::gen::TracePacket packet;
   std::string packet_str = packet.SerializeAsString();
 
-  ScrubFtraceEvents scrub;
-  ASSERT_FALSE(scrub.Transform(context, &packet_str).ok());
+  ASSERT_FALSE(transform_.Transform(context, &packet_str).ok());
 }
 
-TEST_F(ScrubFtraceEventsTest, IgnorePacketWithNoFtraceEvents) {
+TEST_F(FilterFtraceUsingAllowlistTest, IgnorePacketWithNoFtraceEvents) {
   protos::gen::TracePacket trace_packet;
   auto* tree = trace_packet.mutable_process_tree();
 
@@ -112,8 +112,8 @@
   context.ftrace_packet_allow_list = {
       protos::pbzero::FtraceEvent::kTaskRenameFieldNumber};
 
-  ScrubFtraceEvents transform;
-  ASSERT_OK(transform.Transform(context, &packet));
+  auto transform_status = transform_.Transform(context, &packet);
+  ASSERT_OK(transform_status) << transform_status.c_message();
 
   // The packet doesn't have any ftrace events. It should not be affected by
   // this transform.
@@ -122,7 +122,7 @@
 
 // There are some values in a ftrace event that sits behind the ftrace bundle.
 // These values should be retained.
-TEST_F(ScrubFtraceEventsTest, KeepsFtraceBundleSiblingValues) {
+TEST_F(FilterFtraceUsingAllowlistTest, KeepsFtraceBundleSiblingValues) {
   protos::gen::TracePacket trace_packet;
   auto* ftrace_events = trace_packet.mutable_ftrace_events();
 
@@ -137,8 +137,7 @@
   context.ftrace_packet_allow_list = {
       protos::pbzero::FtraceEvent::kTaskRenameFieldNumber};
 
-  ScrubFtraceEvents transform;
-  ASSERT_OK(transform.Transform(context, &packet));
+  ASSERT_OK(transform_.Transform(context, &packet));
 
   protos::gen::TracePacket gen_packet;
   gen_packet.ParseFromString(packet);
@@ -157,7 +156,7 @@
   ASSERT_TRUE(gen_events.event().front().has_task_rename());
 }
 
-TEST_F(ScrubFtraceEventsTest, KeepsAllowedEvents) {
+TEST_F(FilterFtraceUsingAllowlistTest, KeepsAllowedEvents) {
   Context context;
   context.ftrace_packet_allow_list = {
       protos::pbzero::FtraceEvent::kTaskRenameFieldNumber,
@@ -171,8 +170,7 @@
   auto before_str = before.SerializeAsString();
   auto after_str = before_str;
 
-  ScrubFtraceEvents transform;
-  ASSERT_OK(transform.Transform(context, &after_str));
+  ASSERT_OK(transform_.Transform(context, &after_str));
 
   protos::gen::TracePacket after;
   after.ParseFromString(after_str);
@@ -203,7 +201,7 @@
 }
 
 // Only the specific non-allowed events should be removed from the event list.
-TEST_F(ScrubFtraceEventsTest, OnlyDropsNotAllowedEvents) {
+TEST_F(FilterFtraceUsingAllowlistTest, OnlyDropsNotAllowedEvents) {
   // AddTaskRename >> Keep
   // AddClockSetRate >> Drop
   protos::gen::TracePacket original_packet;
@@ -221,8 +219,7 @@
   context.ftrace_packet_allow_list = {
       protos::pbzero::FtraceEvent::kTaskRenameFieldNumber};
 
-  ScrubFtraceEvents transform;
-  ASSERT_OK(transform.Transform(context, &packet));
+  ASSERT_OK(transform_.Transform(context, &packet));
 
   protos::gen::TracePacket modified_packet;
   ASSERT_TRUE(modified_packet.ParseFromString(packet));
diff --git a/src/trace_redaction/filter_sched_waking_events.cc b/src/trace_redaction/filter_sched_waking_events.cc
new file mode 100644
index 0000000..aded8fc
--- /dev/null
+++ b/src/trace_redaction/filter_sched_waking_events.cc
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 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 "src/trace_redaction/filter_sched_waking_events.h"
+
+#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+
+namespace perfetto::trace_redaction {
+
+base::Status FilterSchedWakingEvents::VerifyContext(
+    const Context& context) const {
+  if (!context.package_uid.has_value()) {
+    return base::ErrStatus("FilterSchedWakingEvents: missing packet uid.");
+  }
+
+  if (!context.timeline) {
+    return base::ErrStatus("FilterSchedWakingEvents: missing timeline.");
+  }
+
+  return base::OkStatus();
+}
+
+bool FilterSchedWakingEvents::KeepEvent(const Context& context,
+                                        protozero::ConstBytes bytes) const {
+  PERFETTO_DCHECK(context.package_uid.has_value());
+  PERFETTO_DCHECK(context.timeline);
+
+  protozero::ProtoDecoder event_decoder(bytes);
+
+  auto sched_waking = event_decoder.FindField(
+      protos::pbzero::FtraceEvent::kSchedWakingFieldNumber);
+
+  if (!sched_waking.valid()) {
+    return true;  // Keep
+  }
+
+  auto timestamp = event_decoder.FindField(
+      protos::pbzero::FtraceEvent::kTimestampFieldNumber);
+
+  if (!timestamp.valid()) {
+    return false;  // Remove
+  }
+
+  auto outer_pid =
+      event_decoder.FindField(protos::pbzero::FtraceEvent::kPidFieldNumber);
+
+  if (!outer_pid.valid()) {
+    return false;  // Remove
+  }
+
+  auto outer_slice = context.timeline->Search(
+      timestamp.as_uint64(), static_cast<int32_t>(outer_pid.as_uint32()));
+
+  if (outer_slice.uid != context.package_uid.value()) {
+    return false;  // Remove
+  }
+
+  protozero::ProtoDecoder waking_decoder(sched_waking.as_bytes());
+
+  auto inner_pid = waking_decoder.FindField(
+      protos::pbzero::SchedWakingFtraceEvent::kPidFieldNumber);
+
+  if (!inner_pid.valid()) {
+    return false;  // Remove
+  }
+
+  auto inner_slice =
+      context.timeline->Search(timestamp.as_uint64(), inner_pid.as_int32());
+  return inner_slice.uid == context.package_uid.value();
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/filter_sched_waking_events.h b/src/trace_redaction/filter_sched_waking_events.h
new file mode 100644
index 0000000..5d33ed6
--- /dev/null
+++ b/src/trace_redaction/filter_sched_waking_events.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 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_REDACTION_FILTER_SCHED_WAKING_EVENTS_H_
+#define SRC_TRACE_REDACTION_FILTER_SCHED_WAKING_EVENTS_H_
+
+#include "perfetto/protozero/field.h"
+#include "src/trace_redaction/scrub_ftrace_events.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+// Redact sched waking trace events in a ftrace event bundle:
+//
+//  event {
+//    timestamp: 6702093787823849
+//    pid: 814                      <-- waker
+//    sched_waking {
+//      comm: "surfaceflinger"
+//      pid: 756                    <-- target
+//      prio: 97
+//      success: 1
+//      target_cpu: 2
+//    }
+//  }
+//
+// The three values needed are:
+//
+//  1. event.pid
+//  2. event.timestamp
+//  3. event.sched_waking.pid
+//
+// The two checks that are executed are:
+//
+//  1. package(event.pid).at(event.timestamp).is(target)
+//  2. package(event.sched_waking.pid).at(event.timestamp).is(target)
+//
+// Both must be true in order to keep an event.
+class FilterSchedWakingEvents : public FtraceEventFilter {
+ public:
+  base::Status VerifyContext(const Context& context) const override;
+  bool KeepEvent(const Context& context,
+                 protozero::ConstBytes bytes) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_FILTER_SCHED_WAKING_EVENTS_H_
diff --git a/src/trace_redaction/redact_sched_waking_integrationtest.cc b/src/trace_redaction/filter_sched_waking_events_integrationtest.cc
similarity index 81%
rename from src/trace_redaction/redact_sched_waking_integrationtest.cc
rename to src/trace_redaction/filter_sched_waking_events_integrationtest.cc
index b83f7e9..08bc991 100644
--- a/src/trace_redaction/redact_sched_waking_integrationtest.cc
+++ b/src/trace_redaction/filter_sched_waking_events_integrationtest.cc
@@ -26,10 +26,12 @@
 #include "src/base/test/tmp_dir_tree.h"
 #include "src/base/test/utils.h"
 #include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/filter_sched_waking_events.h"
 #include "src/trace_redaction/find_package_uid.h"
 #include "src/trace_redaction/optimize_timeline.h"
-#include "src/trace_redaction/redact_sched_waking.h"
+#include "src/trace_redaction/scrub_ftrace_events.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redaction_integration_fixture.h"
 #include "src/trace_redaction/trace_redactor.h"
 #include "test/gtest_and_gmock.h"
 
@@ -42,57 +44,24 @@
 namespace perfetto::trace_redaction {
 namespace {
 
-constexpr std::string_view kTracePath =
-    "test/data/trace-redaction-general.pftrace";
 constexpr std::string_view kPackageName =
     "com.Unity.com.unity.multiplayer.samples.coop";
 
-class RedactSchedWakingIntegrationTest : public testing::Test {
+class RedactSchedWakingIntegrationTest
+    : public testing::Test,
+      protected TraceRedactionIntegrationFixure {
  protected:
   void SetUp() override {
-    redactor_.emplace_collect<FindPackageUid>();
-    redactor_.emplace_collect<BuildTimeline>();
-    redactor_.emplace_build<OptimizeTimeline>();
-    redactor_.emplace_transform<RedactSchedWaking>();
+    trace_redactor()->emplace_collect<FindPackageUid>();
+    trace_redactor()->emplace_collect<BuildTimeline>();
+    trace_redactor()->emplace_build<OptimizeTimeline>();
 
-    context_.package_name = kPackageName;
+    auto* ftrace_filter =
+        trace_redactor()->emplace_transform<ScrubFtraceEvents>();
+    ftrace_filter->emplace_back<FilterSchedWakingEvents>();
 
-    src_trace_ = base::GetTestDataPath(std::string(kTracePath));
-
-    dest_trace_ = tmp_dir_.AbsolutePath("dst.pftrace");
-    tmp_dir_.TrackFile("dst.pftrace");
+    context()->package_name = kPackageName;
   }
-
-  base::Status Redact() {
-    return redactor_.Redact(src_trace_, dest_trace_, &context_);
-  }
-
-  base::StatusOr<std::string> LoadOriginal() const {
-    return ReadRawTrace(src_trace_);
-  }
-
-  base::StatusOr<std::string> LoadRedacted() const {
-    return ReadRawTrace(dest_trace_);
-  }
-
- private:
-  base::StatusOr<std::string> ReadRawTrace(const std::string& path) const {
-    std::string redacted_buffer;
-
-    if (base::ReadFile(path, &redacted_buffer)) {
-      return redacted_buffer;
-    }
-
-    return base::ErrStatus("Failed to read %s", path.c_str());
-  }
-
-  Context context_;
-  TraceRedactor redactor_;
-
-  base::TmpDirTree tmp_dir_;
-
-  std::string src_trace_;
-  std::string dest_trace_;
 };
 
 // >>> SELECT uid
diff --git a/src/trace_redaction/redact_sched_waking_unittest.cc b/src/trace_redaction/filter_sched_waking_events_unittest.cc
similarity index 91%
rename from src/trace_redaction/redact_sched_waking_unittest.cc
rename to src/trace_redaction/filter_sched_waking_events_unittest.cc
index a196b09..891a86a 100644
--- a/src/trace_redaction/redact_sched_waking_unittest.cc
+++ b/src/trace_redaction/filter_sched_waking_events_unittest.cc
@@ -14,22 +14,24 @@
  * limitations under the License.
  */
 
-#include "src/trace_redaction/redact_sched_waking.h"
-#include "test/gtest_and_gmock.h"
-
-#include "protos/perfetto/trace/ftrace/ftrace_event.gen.h"
 #include "protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event.gen.h"
 #include "protos/perfetto/trace/ftrace/sched.gen.h"
-#include "protos/perfetto/trace/trace.gen.h"
 #include "protos/perfetto/trace/trace_packet.gen.h"
+#include "protos/perfetto/trace/trace.gen.h"
+#include "src/trace_redaction/filter_sched_waking_events.h"
+#include "src/trace_redaction/scrub_ftrace_events.h"
+#include "test/gtest_and_gmock.h"
 
 namespace perfetto::trace_redaction {
 namespace {
 constexpr int32_t kPackageUid = 1;
 }  // namespace
 
-class RedactSchedWakingTest : public testing::Test {
+class FilterSchedWakingEventsTest : public testing::Test {
  protected:
+  void SetUp() override { transform_.emplace_back<FilterSchedWakingEvents>(); }
+
   void BeginBundle() { ftrace_bundle_ = trace_packet_.mutable_ftrace_events(); }
 
   void AddWaking(uint64_t ts, int32_t pid, std::string_view comm) {
@@ -43,7 +45,7 @@
     sched_waking->set_comm(std::string(comm));
   }
 
-  const RedactSchedWaking& transform() const { return transform_; }
+  const ScrubFtraceEvents& transform() const { return transform_; }
 
   // event {
   //   timestamp: 6702093757720043
@@ -105,10 +107,10 @@
   protos::gen::TracePacket trace_packet_;
   protos::gen::FtraceEventBundle* ftrace_bundle_;
 
-  RedactSchedWaking transform_;
+  ScrubFtraceEvents transform_;
 };
 
-TEST_F(RedactSchedWakingTest, ReturnsErrorForNullPacket) {
+TEST_F(FilterSchedWakingEventsTest, ReturnsErrorForNullPacket) {
   // Don't use context_. These tests will use invalid contexts.
   Context context;
   context.package_uid = kPackageUid;
@@ -117,7 +119,7 @@
   ASSERT_FALSE(transform().Transform(context, nullptr).ok());
 }
 
-TEST_F(RedactSchedWakingTest, ReturnsErrorForEmptyPacket) {
+TEST_F(FilterSchedWakingEventsTest, ReturnsErrorForEmptyPacket) {
   // Don't use context_. These tests will use invalid contexts.
   Context context;
   context.package_uid = kPackageUid;
@@ -128,7 +130,7 @@
   ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
 }
 
-TEST_F(RedactSchedWakingTest, ReturnsErrorForNoTimeline) {
+TEST_F(FilterSchedWakingEventsTest, ReturnsErrorForNoTimeline) {
   // Don't use context_. These tests will use invalid contexts.
   Context context;
   context.package_uid = kPackageUid;
@@ -139,7 +141,7 @@
   ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
 }
 
-TEST_F(RedactSchedWakingTest, ReturnsErrorForMissingPackage) {
+TEST_F(FilterSchedWakingEventsTest, ReturnsErrorForMissingPackage) {
   // Don't use context_. These tests will use invalid contexts.
   Context context;
   context.timeline = std::make_unique<ProcessThreadTimeline>();
@@ -182,7 +184,7 @@
 // KeepsWakingWhenBothPidsConnectToPackage for more information on how). Because
 // this transform only affects waking events, the sched switch event should be
 // retain.
-TEST_F(RedactSchedWakingTest, RetainsNonWakingEvents) {
+TEST_F(FilterSchedWakingEventsTest, RetainsNonWakingEvents) {
   std::string packet_str;
 
   {
@@ -255,7 +257,7 @@
 //
 // Because the sched waking event pid's appears in the timeline and is connected
 // to the target package (kPackageUid), the waking even should remain.
-TEST_F(RedactSchedWakingTest, KeepsWakingWhenBothPidsConnectToPackage) {
+TEST_F(FilterSchedWakingEventsTest, KeepsWhenBothPidsConnectToPackage) {
   std::string packet_str;
 
   {
@@ -322,10 +324,10 @@
 //   }
 // }
 //
-// Because the only one of the sched waking events pid's appears in the
+// Because only one of the sched waking events pid's appears in the
 // timeline and is connected to the target package (kPackageUid), the waking
-// even should remain.
-TEST_F(RedactSchedWakingTest, DropsWakingWhenOnlyWakerPidsConnectToPackage) {
+// even should be removed.
+TEST_F(FilterSchedWakingEventsTest, DropWhenOnlyWakerConnectsToPackage) {
   std::string packet_str;
 
   {
@@ -385,7 +387,7 @@
 // Because the only one of the sched waking events pid's appears in the
 // timeline and is connected to the target package (kPackageUid), the waking
 // even should remain.
-TEST_F(RedactSchedWakingTest, DropsWakingWhenOnlyTargetPidsConnectToPackage) {
+TEST_F(FilterSchedWakingEventsTest, DropWhenOnlyTargetConnectsToPackage) {
   std::string packet_str;
 
   {
diff --git a/src/trace_redaction/main.cc b/src/trace_redaction/main.cc
index ea728bc..c0061b6 100644
--- a/src/trace_redaction/main.cc
+++ b/src/trace_redaction/main.cc
@@ -17,12 +17,13 @@
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/filter_ftrace_using_allowlist.h"
+#include "src/trace_redaction/filter_sched_waking_events.h"
 #include "src/trace_redaction/find_package_uid.h"
 #include "src/trace_redaction/optimize_timeline.h"
 #include "src/trace_redaction/populate_allow_lists.h"
 #include "src/trace_redaction/prune_package_list.h"
 #include "src/trace_redaction/redact_sched_switch.h"
-#include "src/trace_redaction/redact_sched_waking.h"
 #include "src/trace_redaction/scrub_ftrace_events.h"
 #include "src/trace_redaction/scrub_process_trees.h"
 #include "src/trace_redaction/scrub_task_rename.h"
@@ -49,11 +50,16 @@
   // Add all transforms.
   redactor.emplace_transform<PrunePackageList>();
   redactor.emplace_transform<ScrubTracePacket>();
-  redactor.emplace_transform<ScrubFtraceEvents>();
+
+  // Scrub ftrace events before other ftrace events in order to reduce the
+  // number of events they need to iterate over.
+  auto scrub_ftrace_events = redactor.emplace_transform<ScrubFtraceEvents>();
+  scrub_ftrace_events->emplace_back<FilterFtraceUsingAllowlist>();
+  scrub_ftrace_events->emplace_back<FilterSchedWakingEvents>();
+
   redactor.emplace_transform<ScrubProcessTrees>();
   redactor.emplace_transform<ScrubTaskRename>();
   redactor.emplace_transform<RedactSchedSwitch>();
-  redactor.emplace_transform<RedactSchedWaking>();
 
   Context context;
   context.package_name = package_name;
diff --git a/src/trace_redaction/populate_allow_lists.cc b/src/trace_redaction/populate_allow_lists.cc
index fbfe279..2ba81b6 100644
--- a/src/trace_redaction/populate_allow_lists.cc
+++ b/src/trace_redaction/populate_allow_lists.cc
@@ -29,6 +29,15 @@
     return base::ErrStatus("Trace packet allow-list should be empty.");
   }
 
+  // TRACE PACKET NOTES
+  //
+  //    protos::pbzero::TracePacket::kAndroidSystemPropertyFieldNumber
+  //
+  //      AndroidSystemProperty exposes a key-value pair structure with no
+  //      constraints around keys or values, making fine-grain redaction
+  //      difficult. Because this packet's value has no measurable, the safest
+  //      option to drop the whole packet.
+
   context->trace_packet_allow_list = {
       protos::pbzero::TracePacket::kProcessTreeFieldNumber,
       protos::pbzero::TracePacket::kProcessStatsFieldNumber,
@@ -42,7 +51,6 @@
       protos::pbzero::TracePacket::kServiceEventFieldNumber,
       protos::pbzero::TracePacket::kInitialDisplayStateFieldNumber,
       protos::pbzero::TracePacket::kFrameTimelineEventFieldNumber,
-      protos::pbzero::TracePacket::kAndroidSystemPropertyFieldNumber,
       protos::pbzero::TracePacket::kSynchronizationMarkerFieldNumber,
       protos::pbzero::TracePacket::kFtraceEventsFieldNumber,
 
@@ -75,11 +83,12 @@
   // without additional redaction. This list should be configured in a build
   // primitive so that they can be optionally included.
   //
+  // protos::pbzero::FtraceEvent::kPrintFieldNumber,
+  //
   // TODO: Some fields will create new packets (e.g. binder calls may create
   // new spans. This is currently not supported (generated packets still
   // need to be redacted).
   //
-  // protos::pbzero::FtraceEvent::kPrintFieldNumber,
   // protos::pbzero::FtraceEvent::kBinderTransactionFieldNumber,
   // protos::pbzero::FtraceEvent::kBinderTransactionReceivedFieldNumber,
   // protos::pbzero::FtraceEvent::kBinderSetPriorityFieldNumber,
diff --git a/src/trace_redaction/redact_sched_switch_integrationtest.cc b/src/trace_redaction/redact_sched_switch_integrationtest.cc
index 26dd400..9480977 100644
--- a/src/trace_redaction/redact_sched_switch_integrationtest.cc
+++ b/src/trace_redaction/redact_sched_switch_integrationtest.cc
@@ -16,20 +16,16 @@
 
 #include <cstdint>
 #include <string>
-#include <string_view>
-#include <vector>
 
 #include "perfetto/base/status.h"
-#include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "src/base/test/status_matchers.h"
-#include "src/base/test/tmp_dir_tree.h"
-#include "src/base/test/utils.h"
 #include "src/trace_redaction/build_timeline.h"
 #include "src/trace_redaction/find_package_uid.h"
 #include "src/trace_redaction/optimize_timeline.h"
 #include "src/trace_redaction/redact_sched_switch.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redaction_integration_fixture.h"
 #include "src/trace_redaction/trace_redactor.h"
 #include "test/gtest_and_gmock.h"
 
@@ -40,59 +36,19 @@
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
 namespace perfetto::trace_redaction {
-namespace {
 
-constexpr std::string_view kTracePath =
-    "test/data/trace-redaction-general.pftrace";
-constexpr std::string_view kPackageName =
-    "com.Unity.com.unity.multiplayer.samples.coop";
-
-class RedactSchedSwitchIntegrationTest : public testing::Test {
+class RedactSchedSwitchIntegrationTest
+    : public testing::Test,
+      protected TraceRedactionIntegrationFixure {
  protected:
   void SetUp() override {
-    redactor_.emplace_collect<FindPackageUid>();
-    redactor_.emplace_collect<BuildTimeline>();
-    redactor_.emplace_build<OptimizeTimeline>();
-    redactor_.emplace_transform<RedactSchedSwitch>();
+    trace_redactor()->emplace_collect<FindPackageUid>();
+    trace_redactor()->emplace_collect<BuildTimeline>();
+    trace_redactor()->emplace_build<OptimizeTimeline>();
+    trace_redactor()->emplace_transform<RedactSchedSwitch>();
 
-    context_.package_name = kPackageName;
-
-    src_trace_ = base::GetTestDataPath(std::string(kTracePath));
-
-    dest_trace_ = tmp_dir_.AbsolutePath("dst.pftrace");
-    tmp_dir_.TrackFile("dst.pftrace");
+    context()->package_name = "com.Unity.com.unity.multiplayer.samples.coop";
   }
-
-  base::Status Redact() {
-    return redactor_.Redact(src_trace_, dest_trace_, &context_);
-  }
-
-  base::StatusOr<std::string> LoadOriginal() const {
-    return ReadRawTrace(src_trace_);
-  }
-
-  base::StatusOr<std::string> LoadRedacted() const {
-    return ReadRawTrace(dest_trace_);
-  }
-
- private:
-  base::StatusOr<std::string> ReadRawTrace(const std::string& path) const {
-    std::string redacted_buffer;
-
-    if (base::ReadFile(path, &redacted_buffer)) {
-      return redacted_buffer;
-    }
-
-    return base::ErrStatus("Failed to read %s", path.c_str());
-  }
-
-  Context context_;
-  TraceRedactor redactor_;
-
-  base::TmpDirTree tmp_dir_;
-
-  std::string src_trace_;
-  std::string dest_trace_;
 };
 
 // >>> SELECT uid
@@ -233,5 +189,4 @@
   }
 }
 
-}  // namespace
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_waking.cc b/src/trace_redaction/redact_sched_waking.cc
deleted file mode 100644
index ce5c26d..0000000
--- a/src/trace_redaction/redact_sched_waking.cc
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (C) 2024 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 "src/trace_redaction/redact_sched_waking.h"
-
-#include <string>
-
-#include "perfetto/protozero/scattered_heap_buffer.h"
-#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
-#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
-#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
-#include "protos/perfetto/trace/trace_packet.pbzero.h"
-#include "src/trace_redaction/proto_util.h"
-
-namespace perfetto::trace_redaction {
-
-namespace {
-
-// Redact sched waking trace events in a ftrace event bundle:
-//
-//  event {
-//    timestamp: 6702093787823849
-//    pid: 814
-//    sched_waking {
-//      comm: "surfaceflinger"
-//      pid: 756
-//      prio: 97
-//      success: 1
-//      target_cpu: 2
-//    }
-//  }
-//
-// The three values needed are:
-//
-//  1. event.pid
-//  2. event.timestamp
-//  3. event.sched_waking.pid
-//
-// The two checks that are executed are:
-//
-//  1. package(event.pid).at(event.timestamp).is(target)
-//  2. package(event.sched_waking.pid).at(event.timestamp).is(target)
-//
-// Both must be true in order to keep an event.
-bool KeepEvent(const Context& context, protozero::Field bundle_field) {
-  PERFETTO_DCHECK(context.timeline);
-  PERFETTO_DCHECK(context.package_uid.has_value());
-
-  PERFETTO_DCHECK(bundle_field.valid());
-  PERFETTO_DCHECK(bundle_field.id() ==
-                  protos::pbzero::FtraceEventBundle::kEventFieldNumber);
-
-  protozero::ProtoDecoder event_decoder(bundle_field.as_bytes());
-
-  auto sched_waking = event_decoder.FindField(
-      protos::pbzero::FtraceEvent::kSchedWakingFieldNumber);
-
-  if (!sched_waking.valid()) {
-    return true;
-  }
-
-  auto timestamp = event_decoder.FindField(
-      protos::pbzero::FtraceEvent::kTimestampFieldNumber);
-
-  if (!timestamp.valid()) {
-    return false;
-  }
-
-  auto outer_pid =
-      event_decoder.FindField(protos::pbzero::FtraceEvent::kPidFieldNumber);
-
-  if (!outer_pid.valid()) {
-    return false;
-  }
-
-  auto outer_slice = context.timeline->Search(
-      timestamp.as_uint64(), static_cast<int32_t>(outer_pid.as_uint32()));
-
-  if (outer_slice.uid != context.package_uid.value()) {
-    return false;
-  }
-
-  protozero::ProtoDecoder waking_decoder(sched_waking.as_bytes());
-
-  auto inner_pid = waking_decoder.FindField(
-      protos::pbzero::SchedWakingFtraceEvent::kPidFieldNumber);
-
-  if (!inner_pid.valid()) {
-    return false;
-  }
-
-  auto inner_slice =
-      context.timeline->Search(timestamp.as_uint64(), inner_pid.as_int32());
-  return inner_slice.uid == context.package_uid.value();
-}
-
-}  // namespace
-
-base::Status RedactSchedWaking::Transform(const Context& context,
-                                          std::string* packet) const {
-  if (packet == nullptr || packet->empty()) {
-    return base::ErrStatus("RedactSchedWaking: null or empty packet.");
-  }
-
-  if (!context.package_uid.has_value()) {
-    return base::ErrStatus("RedactSchedWaking: missing packet uid.");
-  }
-
-  if (!context.timeline) {
-    return base::ErrStatus("RedactSchedWaking: missing timeline.");
-  }
-
-  protozero::ProtoDecoder packet_decoder(*packet);
-
-  auto trace_event_bundle = packet_decoder.FindField(
-      protos::pbzero::TracePacket::kFtraceEventsFieldNumber);
-
-  if (!trace_event_bundle.valid()) {
-    return base::OkStatus();
-  }
-
-  protozero::HeapBuffered<protos::pbzero::TracePacket> packet_message;
-  packet_message.Reset();
-
-  for (auto packet_field = packet_decoder.ReadField(); packet_field.valid();
-       packet_field = packet_decoder.ReadField()) {
-    if (packet_field.id() !=
-        protos::pbzero::TracePacket::kFtraceEventsFieldNumber) {
-      proto_util::AppendField(packet_field, packet_message.get());
-      continue;
-    }
-
-    protozero::ProtoDecoder bundle_decoder(packet_field.as_bytes());
-
-    auto* bundle_message = packet_message->set_ftrace_events();
-
-    for (auto field = bundle_decoder.ReadField(); field.valid();
-         field = bundle_decoder.ReadField()) {
-      if (field.id() != protos::pbzero::FtraceEventBundle::kEventFieldNumber ||
-          KeepEvent(context, field)) {
-        proto_util::AppendField(field, bundle_message);
-      }
-    }
-  }
-
-  *packet = packet_message.SerializeAsString();
-
-  return base::OkStatus();
-}
-
-}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_waking.h b/src/trace_redaction/redact_sched_waking.h
deleted file mode 100644
index adb1ec8..0000000
--- a/src/trace_redaction/redact_sched_waking.h
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2024 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_REDACTION_REDACT_SCHED_WAKING_H_
-#define SRC_TRACE_REDACTION_REDACT_SCHED_WAKING_H_
-
-#include <string>
-
-#include "src/trace_redaction/trace_redaction_framework.h"
-
-namespace perfetto::trace_redaction {
-
-class RedactSchedWaking final : public TransformPrimitive {
- public:
-  base::Status Transform(const Context& context,
-                         std::string* packet) const override;
-};
-
-}  // namespace perfetto::trace_redaction
-
-#endif  // SRC_TRACE_REDACTION_REDACT_SCHED_WAKING_H_
diff --git a/src/trace_redaction/scrub_ftrace_events.cc b/src/trace_redaction/scrub_ftrace_events.cc
index b41fc6e..3260c71 100644
--- a/src/trace_redaction/scrub_ftrace_events.cc
+++ b/src/trace_redaction/scrub_ftrace_events.cc
@@ -25,67 +25,15 @@
 #include "protos/perfetto/trace/trace.pbzero.h"
 
 namespace perfetto::trace_redaction {
-namespace {
 
-constexpr auto kFtraceEventsFieldNumber =
-    protos::pbzero::TracePacket::kFtraceEventsFieldNumber;
-
-constexpr auto kEventFieldNumber =
-    protos::pbzero::FtraceEventBundle::kEventFieldNumber;
-
-enum class Redact : uint8_t {
-  // Some resources in the target need to be redacted.
-  kSomething = 0,
-
-  // No resources in the target need to be redacted.
-  kNothing = 1,
-};
-
-// Return kSomething if an event will change after redaction . If a packet
-// will not change, then the packet should skip redaction and be appended
-// to the output.
-//
-// Event packets have few packets (e.g. timestamp, pid, the event payload).
-// because of this, it is relatively cheap to test a packet.
-//
-//  event {
-//    timestamp: 6702095044306682
-//    pid: 0
-//    sched_switch {
-//      prev_comm: "swapper/2"
-//      prev_pid: 0
-//      prev_prio: 120
-//      prev_state: 0
-//      next_comm: "surfaceflinger"
-//      next_pid: 819
-//      next_prio: 120
-//    }
-//  }
-Redact ProbeEvent(const Context& context, const protozero::Field& event) {
-  if (event.id() != kEventFieldNumber) {
-    PERFETTO_FATAL("Invalid proto field. Expected kEventFieldNumber.");
-  }
-
-  protozero::ProtoDecoder decoder(event.data(), event.size());
-
-  for (auto field = decoder.ReadField(); field.valid();
-       field = decoder.ReadField()) {
-    if (context.ftrace_packet_allow_list.count(field.id()) != 0) {
-      return Redact::kNothing;
-    }
-  }
-
-  return Redact::kSomething;
-}
-
-}  // namespace
+FtraceEventFilter::~FtraceEventFilter() = default;
 
 //  packet {
 //    ftrace_events {
 //      event {                   <-- This is where we test the allow-list
 //        timestamp: 6702095044299807
 //        pid: 0
-//        cpu_idle {              <-- This is the event data (allow-list)
+//        cpu_idle {              <-- This is the event type
 //          state: 4294967295
 //          cpu_id: 2
 //        }
@@ -95,58 +43,63 @@
 base::Status ScrubFtraceEvents::Transform(const Context& context,
                                           std::string* packet) const {
   if (packet == nullptr || packet->empty()) {
-    return base::ErrStatus("Cannot scrub null or empty trace packet.");
+    return base::ErrStatus("FilterPrintEvents: null or empty packet.");
   }
 
-  if (context.ftrace_packet_allow_list.empty()) {
-    return base::ErrStatus("Cannot scrub ftrace packets, missing allow-list.");
+  for (const auto& filter : filters_) {
+    auto status = filter->VerifyContext(context);
+
+    if (!status.ok()) {
+      return status;
+    }
   }
 
-  // If the packet has no ftrace events, skip it, leaving it unmodified.
-  protos::pbzero::TracePacket::Decoder query(*packet);
-  if (!query.has_ftrace_events()) {
+  protozero::ProtoDecoder packet_decoder(*packet);
+
+  if (!packet_decoder
+           .FindField(protos::pbzero::TracePacket::kFtraceEventsFieldNumber)
+           .valid()) {
     return base::OkStatus();
   }
 
-  protozero::HeapBuffered<protos::pbzero::TracePacket> packet_msg;
+  protozero::HeapBuffered<protos::pbzero::TracePacket> packet_message;
 
-  // packet.foreach_child.foreach( ... )
-  protozero::ProtoDecoder d_packet(*packet);
-  for (auto packet_child_it = d_packet.ReadField(); packet_child_it.valid();
-       packet_child_it = d_packet.ReadField()) {
-    // packet.child_not<ftrace_events>( ).do ( ... )
-    if (packet_child_it.id() != kFtraceEventsFieldNumber) {
-      proto_util::AppendField(packet_child_it, packet_msg.get());
+  for (auto field = packet_decoder.ReadField(); field.valid();
+       field = packet_decoder.ReadField()) {
+    if (field.id() != protos::pbzero::TracePacket::kFtraceEventsFieldNumber) {
+      proto_util::AppendField(field, packet_message.get());
       continue;
     }
 
-    // To clarify, "ftrace_events" is the field name and "FtraceEventBundle" is
-    // the field type. The terms are often used interchangeably.
-    auto* ftrace_events_msg = packet_msg->set_ftrace_events();
+    auto* bundle_message = packet_message->set_ftrace_events();
 
-    // packet.child<ftrace_events>( ).foreach_child( ... )
-    protozero::ProtoDecoder ftrace_events(packet_child_it.as_bytes());
-    for (auto ftrace_events_it = ftrace_events.ReadField();
-         ftrace_events_it.valid();
-         ftrace_events_it = ftrace_events.ReadField()) {
-      // packet.child<ftrace_events>( ).child_not<event>( ).do ( ... )
-      if (ftrace_events_it.id() != kEventFieldNumber) {
-        proto_util::AppendField(ftrace_events_it, ftrace_events_msg);
-        continue;
+    protozero::ProtoDecoder bundle(field.as_bytes());
+
+    for (auto event_it = bundle.ReadField(); event_it.valid();
+         event_it = bundle.ReadField()) {
+      if (event_it.id() !=
+              protos::pbzero::FtraceEventBundle::kEventFieldNumber ||
+          KeepEvent(context, event_it.as_bytes())) {
+        proto_util::AppendField(event_it, bundle_message);
       }
-
-      // packet.child<ftrace_events>( ).child_is<event>( ).do ( ... )
-      if (ProbeEvent(context, ftrace_events_it) == Redact::kNothing) {
-        proto_util::AppendField(ftrace_events_it, ftrace_events_msg);
-        continue;
-      }
-
-      // Dropping packet = "is event" and "is redacted"
     }
   }
 
-  packet->assign(packet_msg.SerializeAsString());
+  packet->assign(packet_message.SerializeAsString());
+
   return base::OkStatus();
 }
 
+// Logical AND of all filters.
+bool ScrubFtraceEvents::KeepEvent(const Context& context,
+                                  protozero::ConstBytes bytes) const {
+  for (const auto& filter : filters_) {
+    if (!filter->KeepEvent(context, bytes)) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/scrub_ftrace_events.h b/src/trace_redaction/scrub_ftrace_events.h
index 77e6835..35c12bf 100644
--- a/src/trace_redaction/scrub_ftrace_events.h
+++ b/src/trace_redaction/scrub_ftrace_events.h
@@ -19,10 +19,22 @@
 
 #include <string>
 
+#include "perfetto/protozero/field.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
 namespace perfetto::trace_redaction {
 
+class FtraceEventFilter {
+ public:
+  virtual ~FtraceEventFilter();
+
+  // Checks if the context contains all neccessary parameters.
+  virtual base::Status VerifyContext(const Context& context) const = 0;
+
+  virtual bool KeepEvent(const Context& context,
+                         protozero::ConstBytes bytes) const = 0;
+};
+
 //  Assumptions:
 //    1. This is a hot path (a lot of ftrace packets)
 //    2. Allocations are slower than CPU cycles.
@@ -46,11 +58,21 @@
 //        Block C is different, there is a block D that falls within block C.
 //        Block D contains sensitive information and should be dropped. When C
 //        is probed, it will come back saying that C needs additional redaction.
-
-class ScrubFtraceEvents final : public TransformPrimitive {
+class ScrubFtraceEvents : public TransformPrimitive {
  public:
   base::Status Transform(const Context& context,
                          std::string* packet) const override;
+
+  // Add a new filter. T must extend FtraceEventFilter.
+  template <typename T>
+  void emplace_back() {
+    filters_.push_back(std::make_unique<T>());
+  }
+
+ private:
+  bool KeepEvent(const Context& context, protozero::ConstBytes bytes) const;
+
+  std::vector<std::unique_ptr<FtraceEventFilter>> filters_;
 };
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/scrub_ftrace_events_integrationtest.cc b/src/trace_redaction/scrub_ftrace_events_integrationtest.cc
index 7584b0f..ec026d1 100644
--- a/src/trace_redaction/scrub_ftrace_events_integrationtest.cc
+++ b/src/trace_redaction/scrub_ftrace_events_integrationtest.cc
@@ -22,6 +22,7 @@
 #include "perfetto/ext/base/file_utils.h"
 #include "src/base/test/status_matchers.h"
 #include "src/base/test/utils.h"
+#include "src/trace_redaction/filter_ftrace_using_allowlist.h"
 #include "src/trace_redaction/scrub_ftrace_events.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 #include "test/gtest_and_gmock.h"
diff --git a/src/trace_redaction/scrub_process_trees_integrationtest.cc b/src/trace_redaction/scrub_process_trees_integrationtest.cc
index 61a7bcd..7d13721 100644
--- a/src/trace_redaction/scrub_process_trees_integrationtest.cc
+++ b/src/trace_redaction/scrub_process_trees_integrationtest.cc
@@ -30,6 +30,7 @@
 #include "src/trace_redaction/optimize_timeline.h"
 #include "src/trace_redaction/scrub_process_trees.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redaction_integration_fixture.h"
 #include "src/trace_redaction/trace_redactor.h"
 #include "test/gtest_and_gmock.h"
 
@@ -37,14 +38,14 @@
 
 namespace {
 
-constexpr std::string_view kTracePath =
-    "test/data/trace-redaction-general.pftrace";
 constexpr std::string_view kProcessName =
     "com.Unity.com.unity.multiplayer.samples.coop";
 
 }  // namespace
 
-class ScrubProcessTreesIntegrationTest : public testing::Test {
+class ScrubProcessTreesIntegrationTest
+    : public testing::Test,
+      protected TraceRedactionIntegrationFixure {
  protected:
   void SetUp() override {
     // ScrubProcessTrees depends on:
@@ -58,30 +59,13 @@
     // BuildTimeline depends on.... nothing
     // FindPackageUid depends on... nothing
 
-    redactor_.emplace_collect<FindPackageUid>();
-    redactor_.emplace_collect<BuildTimeline>();
-    redactor_.emplace_build<OptimizeTimeline>();
-    redactor_.emplace_transform<ScrubProcessTrees>();
+    trace_redactor()->emplace_collect<FindPackageUid>();
+    trace_redactor()->emplace_collect<BuildTimeline>();
+    trace_redactor()->emplace_build<OptimizeTimeline>();
+    trace_redactor()->emplace_transform<ScrubProcessTrees>();
 
     // In this case, the process and package have the same name.
-    context_.package_name = kProcessName;
-
-    src_trace_ = base::GetTestDataPath(std::string(kTracePath));
-
-    dest_trace_ = tmp_dir_.AbsolutePath("dst.pftrace");
-    tmp_dir_.TrackFile("dst.pftrace");
-  }
-
-  base::Status Redact() {
-    return redactor_.Redact(src_trace_, dest_trace_, &context_);
-  }
-
-  base::StatusOr<std::string> LoadOriginal() const {
-    return ReadRawTrace(src_trace_);
-  }
-
-  base::StatusOr<std::string> LoadRedacted() const {
-    return ReadRawTrace(dest_trace_);
+    context()->package_name = kProcessName;
   }
 
   std::vector<std::string> CollectProcessNames(
@@ -109,25 +93,6 @@
 
     return names;
   }
-
- private:
-  base::StatusOr<std::string> ReadRawTrace(const std::string& path) const {
-    std::string redacted_buffer;
-
-    if (base::ReadFile(path, &redacted_buffer)) {
-      return redacted_buffer;
-    }
-
-    return base::ErrStatus("Failed to read %s", path.c_str());
-  }
-
-  Context context_;
-  TraceRedactor redactor_;
-
-  base::TmpDirTree tmp_dir_;
-
-  std::string src_trace_;
-  std::string dest_trace_;
 };
 
 TEST_F(ScrubProcessTreesIntegrationTest, RemovesProcessNamesFromProcessTrees) {
diff --git a/src/trace_redaction/scrub_task_rename_integrationtest.cc b/src/trace_redaction/scrub_task_rename_integrationtest.cc
index dee77e6..82903c0 100644
--- a/src/trace_redaction/scrub_task_rename_integrationtest.cc
+++ b/src/trace_redaction/scrub_task_rename_integrationtest.cc
@@ -29,6 +29,7 @@
 #include "src/trace_redaction/optimize_timeline.h"
 #include "src/trace_redaction/scrub_task_rename.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redaction_integration_fixture.h"
 #include "src/trace_redaction/trace_redactor.h"
 #include "test/gtest_and_gmock.h"
 
@@ -40,44 +41,29 @@
 namespace perfetto::trace_redaction {
 
 namespace {
-using FtraceEvent = protos::pbzero::FtraceEvent;
 
-constexpr std::string_view kTracePath =
-    "test/data/trace-redaction-general.pftrace";
+using FtraceEvent = protos::pbzero::FtraceEvent;
 
 // Set the package name to "just some package name". If a specific package name
 // is needed, the test it should overwrite this value.
 constexpr std::string_view kPackageName =
     "com.Unity.com.unity.multiplayer.samples.coop";
 
-class RenameEventsTraceRedactorIntegrationTest : public testing::Test {
+}  // namespace
+
+class RenameEventsTraceRedactorIntegrationTest
+    : public testing::Test,
+      protected TraceRedactionIntegrationFixure {
  protected:
   void SetUp() override {
     // In order for ScrubTaskRename to work, it needs the timeline. All
     // registered primitives are there to generate the timeline.
-    redactor_.emplace_collect<FindPackageUid>();
-    redactor_.emplace_collect<BuildTimeline>();
-    redactor_.emplace_build<OptimizeTimeline>();
-    redactor_.emplace_transform<ScrubTaskRename>();
+    trace_redactor()->emplace_collect<FindPackageUid>();
+    trace_redactor()->emplace_collect<BuildTimeline>();
+    trace_redactor()->emplace_build<OptimizeTimeline>();
+    trace_redactor()->emplace_transform<ScrubTaskRename>();
 
-    context_.package_name = kPackageName;
-
-    src_trace_ = base::GetTestDataPath(std::string(kTracePath));
-
-    dest_trace_ = tmp_dir_.AbsolutePath("dst.pftrace");
-    tmp_dir_.TrackFile("dst.pftrace");
-  }
-
-  base::Status Redact() {
-    return redactor_.Redact(src_trace_, dest_trace_, &context_);
-  }
-
-  base::StatusOr<std::string> LoadOriginal() const {
-    return ReadRawTrace(src_trace_);
-  }
-
-  base::StatusOr<std::string> LoadRedacted() const {
-    return ReadRawTrace(dest_trace_);
+    context()->package_name = kPackageName;
   }
 
   std::vector<uint32_t> GetAllRenamedPids(
@@ -105,25 +91,6 @@
 
     return renamed_pids;
   }
-
- private:
-  base::StatusOr<std::string> ReadRawTrace(const std::string& path) const {
-    std::string redacted_buffer;
-
-    if (base::ReadFile(path, &redacted_buffer)) {
-      return redacted_buffer;
-    }
-
-    return base::ErrStatus("Failed to read %s", path.c_str());
-  }
-
-  Context context_;
-  TraceRedactor redactor_;
-
-  base::TmpDirTree tmp_dir_;
-
-  std::string src_trace_;
-  std::string dest_trace_;
 };
 
 TEST_F(RenameEventsTraceRedactorIntegrationTest, RemovesUnwantedRenameTasks) {
@@ -153,5 +120,4 @@
   ASSERT_TRUE(redacted_rename_pids.empty());
 }
 
-}  // namespace
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/trace_redaction_integration_fixture.cc b/src/trace_redaction/trace_redaction_integration_fixture.cc
new file mode 100644
index 0000000..85555f3
--- /dev/null
+++ b/src/trace_redaction/trace_redaction_integration_fixture.cc
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 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 "src/trace_redaction/trace_redaction_integration_fixture.h"
+
+#include "perfetto/ext/base/file_utils.h"
+#include "src/base/test/utils.h"
+
+namespace perfetto::trace_redaction {
+
+TraceRedactionIntegrationFixure::TraceRedactionIntegrationFixure() {
+  src_trace_ =
+      base::GetTestDataPath("test/data/trace-redaction-general.pftrace");
+  dest_trace_ = tmp_dir_.AbsolutePath("dst.pftrace");
+}
+
+base::Status TraceRedactionIntegrationFixure::Redact() {
+  auto status = trace_redactor_.Redact(src_trace_, dest_trace_, &context_);
+
+  if (status.ok()) {
+    tmp_dir_.TrackFile("dst.pftrace");
+  }
+
+  return status;
+}
+
+base::StatusOr<std::string> TraceRedactionIntegrationFixure::LoadOriginal()
+    const {
+  return ReadRawTrace(src_trace_);
+}
+
+base::StatusOr<std::string> TraceRedactionIntegrationFixure::LoadRedacted()
+    const {
+  return ReadRawTrace(dest_trace_);
+}
+
+base::StatusOr<std::string> TraceRedactionIntegrationFixure::ReadRawTrace(
+    const std::string& path) const {
+  std::string redacted_buffer;
+
+  if (base::ReadFile(path, &redacted_buffer)) {
+    return redacted_buffer;
+  }
+
+  return base::ErrStatus("Failed to read %s", path.c_str());
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/trace_redaction_integration_fixture.h b/src/trace_redaction/trace_redaction_integration_fixture.h
new file mode 100644
index 0000000..55060f9
--- /dev/null
+++ b/src/trace_redaction/trace_redaction_integration_fixture.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 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_REDACTION_TRACE_REDACTION_INTEGRATION_FIXTURE_H_
+#define SRC_TRACE_REDACTION_TRACE_REDACTION_INTEGRATION_FIXTURE_H_
+
+#include <string>
+
+#include "perfetto/ext/base/status_or.h"
+#include "src/base/test/tmp_dir_tree.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redactor.h"
+
+namespace perfetto::trace_redaction {
+
+class TraceRedactionIntegrationFixure {
+ protected:
+  TraceRedactionIntegrationFixure();
+
+  // Redact the source file and write it to the destination file. The contents
+  // of each file can be read using LoadOriginal() and LoadRedacted().
+  base::Status Redact();
+
+  base::StatusOr<std::string> LoadOriginal() const;
+
+  base::StatusOr<std::string> LoadRedacted() const;
+
+  Context* context() { return &context_; }
+
+  TraceRedactor* trace_redactor() { return &trace_redactor_; }
+
+ private:
+  base::StatusOr<std::string> ReadRawTrace(const std::string& path) const;
+
+  Context context_;
+
+  TraceRedactor trace_redactor_;
+
+  base::TmpDirTree tmp_dir_;
+
+  std::string src_trace_;
+
+  // Path to the redacted trace. This will only be valid after Redact()
+  // completely. If redaction was successful, this file will be tracked by
+  // tmp_dir_.
+  std::string dest_trace_;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_TRACE_REDACTION_INTEGRATION_FIXTURE_H_
diff --git a/src/trace_redaction/trace_redactor_integrationtest.cc b/src/trace_redaction/trace_redactor_integrationtest.cc
index db3e23f..a62359f 100644
--- a/src/trace_redaction/trace_redactor_integrationtest.cc
+++ b/src/trace_redaction/trace_redactor_integrationtest.cc
@@ -25,10 +25,7 @@
 #include "src/base/test/tmp_dir_tree.h"
 #include "src/base/test/utils.h"
 #include "src/trace_redaction/find_package_uid.h"
-#include "src/trace_redaction/populate_allow_lists.h"
 #include "src/trace_redaction/prune_package_list.h"
-#include "src/trace_redaction/scrub_ftrace_events.h"
-#include "src/trace_redaction/scrub_trace_packet.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 #include "src/trace_redaction/trace_redactor.h"
 #include "test/gtest_and_gmock.h"
@@ -197,170 +194,5 @@
   ASSERT_EQ(package_names[7], "com.google.android.networkstack.tethering");
 }
 
-// Broadphase redactions are meant to remove large pieces of information (e.g.
-// whole packets). They need to be tested on their own because finer-grain
-// redactions can make it harder to verify the results. For example, broadphase
-// redactions should not remove rename events. There is a finer grain redaction
-// that takes care of those events. However, it the "scrub rename events"
-// redaction removes all the rename events, it could look like the broadphase
-// primitive did it.
-class BroadphaseTraceRedactorIntegrationTest
-    : public TraceRedactorIntegrationTest {
- protected:
-  void SetUp() override {
-    TraceRedactorIntegrationTest::SetUp();
-
-    redactor_.emplace_collect<FindPackageUid>();
-    redactor_.emplace_build<PopulateAllowlists>();
-    redactor_.emplace_transform<ScrubTracePacket>();
-    redactor_.emplace_transform<ScrubFtraceEvents>();
-  }
-
-  static base::StatusOr<protozero::ConstBytes> FindFirstFtraceEvents(
-      const protos::pbzero::Trace::Decoder& trace) {
-    for (auto packet_it = trace.packet(); packet_it; ++packet_it) {
-      protos::pbzero::TracePacket::Decoder packet(*packet_it);
-
-      if (packet.has_ftrace_events()) {
-        return packet.ftrace_events();
-      }
-    }
-
-    return base::ErrStatus("Failed to find ftrace events");
-  }
-
-  // NOTE - this will include fields like "timestamp" and "pid".
-  static void GetEventFields(const protos::pbzero::Trace::Decoder& trace,
-                             base::FlatSet<uint32_t>* set) {
-    for (auto packet_it = trace.packet(); packet_it; ++packet_it) {
-      protos::pbzero::TracePacket::Decoder packet(*packet_it);
-
-      if (!packet.has_ftrace_events()) {
-        continue;
-      }
-
-      protos::pbzero::FtraceEventBundle::Decoder bundle(packet.ftrace_events());
-
-      if (!bundle.has_event()) {
-        continue;
-      }
-
-      for (auto events_it = bundle.event(); events_it; ++events_it) {
-        protozero::ProtoDecoder event(*events_it);
-
-        for (auto event_it = event.ReadField(); event_it.valid();
-             event_it = event.ReadField()) {
-          set->insert(event_it.id());
-        }
-      }
-    }
-  }
-};
-
-// Makes sure all not-allowed ftrace event is removed from a trace.
-TEST_F(BroadphaseTraceRedactorIntegrationTest, RemovesFtraceEvents) {
-  auto pre_redaction_file = src_trace();
-  auto post_redaction_file = tmp_dir_.AbsolutePath("dst.pftrace");
-
-  // We know that there are two oom score updates in the test trace. These
-  // events are not in the allowlist and should be dropped.
-  auto pre_redaction_buffer = ReadRawTrace(pre_redaction_file);
-  ASSERT_OK(pre_redaction_buffer) << pre_redaction_buffer.status().message();
-  protos::pbzero::Trace::Decoder pre_redaction_trace(*pre_redaction_buffer);
-
-  base::FlatSet<uint32_t> pre_redaction_event_types;
-  GetEventFields(pre_redaction_trace, &pre_redaction_event_types);
-  ASSERT_GT(pre_redaction_event_types.count(
-                FtraceEvent::kOomScoreAdjUpdateFieldNumber),
-            0u);
-
-  auto result =
-      redactor_.Redact(pre_redaction_file, post_redaction_file, &context_);
-  tmp_dir_.TrackFile("dst.pftrace");
-  ASSERT_OK(result) << result.message();
-
-  auto post_redaction_buffer = ReadRawTrace(post_redaction_file);
-  ASSERT_OK(post_redaction_buffer) << post_redaction_buffer.status().message();
-  protos::pbzero::Trace::Decoder post_redaction_trace(*post_redaction_buffer);
-
-  base::FlatSet<uint32_t> post_redaction_event_types;
-  GetEventFields(post_redaction_trace, &post_redaction_event_types);
-  ASSERT_EQ(post_redaction_event_types.count(
-                FtraceEvent::kOomScoreAdjUpdateFieldNumber),
-            0u);
-}
-
-// When a event is dropped from ftrace_events, only that event should be droped,
-// the other events in the ftrace_events should be retained.
-TEST_F(BroadphaseTraceRedactorIntegrationTest,
-       RetainsFtraceEventsWhenRemovingFtraceEvent) {
-  auto pre_redaction_file = src_trace();
-  auto post_redaction_file = tmp_dir_.AbsolutePath("dst.pftrace");
-
-  auto pre_redaction_buffer = ReadRawTrace(pre_redaction_file);
-  ASSERT_OK(pre_redaction_buffer) << pre_redaction_buffer.status().message();
-
-  protos::pbzero::Trace::Decoder pre_redaction_trace(*pre_redaction_buffer);
-
-  auto pre_redaction_first_events = FindFirstFtraceEvents(pre_redaction_trace);
-  ASSERT_OK(pre_redaction_first_events)
-      << pre_redaction_first_events.status().message();
-
-  auto result =
-      redactor_.Redact(pre_redaction_file, post_redaction_file, &context_);
-  tmp_dir_.TrackFile("dst.pftrace");
-  ASSERT_OK(result) << result.message();
-
-  auto post_redaction_buffer = ReadRawTrace(post_redaction_file);
-  ASSERT_OK(post_redaction_buffer) << post_redaction_buffer.status().message();
-
-  protos::pbzero::Trace::Decoder post_redaction_trace(*post_redaction_buffer);
-
-  auto post_redaction_ftrace_events =
-      FindFirstFtraceEvents(post_redaction_trace);
-  ASSERT_OK(post_redaction_ftrace_events)
-      << post_redaction_ftrace_events.status().message();
-
-  base::FlatSet<uint32_t> events_before;
-  GetEventFields(pre_redaction_trace, &events_before);
-  ASSERT_EQ(events_before.size(), 14u);
-  ASSERT_TRUE(events_before.count(FtraceEvent::kTimestampFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kPidFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kPrintFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kSchedSwitchFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kCpuFrequencyFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kCpuIdleFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kSchedWakeupFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kSchedWakingFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kSchedWakeupNewFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kTaskNewtaskFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kTaskRenameFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kSchedProcessExitFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kSchedProcessFreeFieldNumber));
-  ASSERT_TRUE(events_before.count(FtraceEvent::kOomScoreAdjUpdateFieldNumber));
-
-  base::FlatSet<uint32_t> events_after;
-  GetEventFields(post_redaction_trace, &events_after);
-  ASSERT_EQ(events_after.size(), 9u);
-
-  // Retained.
-  ASSERT_TRUE(events_after.count(FtraceEvent::kTimestampFieldNumber));
-  ASSERT_TRUE(events_after.count(FtraceEvent::kPidFieldNumber));
-  ASSERT_TRUE(events_after.count(FtraceEvent::kSchedSwitchFieldNumber));
-  ASSERT_TRUE(events_after.count(FtraceEvent::kCpuFrequencyFieldNumber));
-  ASSERT_TRUE(events_after.count(FtraceEvent::kCpuIdleFieldNumber));
-  ASSERT_TRUE(events_after.count(FtraceEvent::kSchedWakingFieldNumber));
-  ASSERT_TRUE(events_after.count(FtraceEvent::kTaskNewtaskFieldNumber));
-  ASSERT_TRUE(events_after.count(FtraceEvent::kTaskRenameFieldNumber));
-  ASSERT_TRUE(events_after.count(FtraceEvent::kSchedProcessFreeFieldNumber));
-
-  // Dropped.
-  ASSERT_FALSE(events_after.count(FtraceEvent::kPrintFieldNumber));
-  ASSERT_FALSE(events_after.count(FtraceEvent::kSchedWakeupFieldNumber));
-  ASSERT_FALSE(events_after.count(FtraceEvent::kSchedWakeupNewFieldNumber));
-  ASSERT_FALSE(events_after.count(FtraceEvent::kSchedProcessExitFieldNumber));
-  ASSERT_FALSE(events_after.count(FtraceEvent::kOomScoreAdjUpdateFieldNumber));
-}
-
 }  // namespace
 }  // namespace perfetto::trace_redaction
diff --git a/tools/delete_permalink b/tools/delete_permalink
new file mode 100755
index 0000000..9963b03
--- /dev/null
+++ b/tools/delete_permalink
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+
+"""A tool to delete permalinks and traces behind them.
+
+To be used by the Perfetto team, who has write access to the GCS bucket.
+"""
+
+import json
+import logging
+import re
+import subprocess
+import sys
+
+from typing import List
+
+GCS_BUCKET = 'perfetto-ui-data'
+GCS_HTTP = 'https://storage.googleapis.com/%s/' % GCS_BUCKET
+
+
+def delete_gcs_obj(url: str, gcs_delete_list: List[str]):
+  if not url.startswith(GCS_HTTP):
+    logging.error('The URL %s should start with %s', url, GCS_HTTP)
+    return
+  gs_url = 'gs://%s/%s' % (GCS_BUCKET, url[len(GCS_HTTP):])
+  gcs_delete_list.append(gs_url)
+
+
+def delete_permalink_uuid(uuid: str, gcs_delete_list: List[str]):
+  state_url = GCS_HTTP + uuid
+  delete_gcs_obj(state_url, gcs_delete_list)
+  state_json = subprocess.check_output(['curl', '-Ls', state_url])
+  state = json.loads(state_json)
+  trace_url = state['engine']['source']['url']
+  delete_gcs_obj(trace_url, gcs_delete_list)
+
+
+def main():
+  gcs_delete_list = []
+  if sys.stdin.isatty():
+    logging.warn('This tool expects a list of uuids or https://ui.perfetto.dev/#!#?s=deadbeef')
+
+  for line in sys.stdin.readlines():
+    line = line.strip()
+    m = re.match(r'.*?\b([a-f0-9]{64})', line)
+    if not m:
+      logging.error('Could not find a 64 hex UUID from %s', line)
+      continue
+    uuid = m.group(1)
+    delete_permalink_uuid(uuid, gcs_delete_list)
+
+  if len(gcs_delete_list) == 0:
+    logging.info('No object to delete, quitting without taking any action')
+    return 0
+
+  print('Removing the following objects:')
+  for gs_uri in gcs_delete_list:
+    print('  ', gs_uri)
+  subprocess.check_call(['gsutil', '-m', 'rm', '-f', '-a'] + gcs_delete_list)
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 80e9ca7..dfb93d7 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -504,7 +504,10 @@
     return this._plugins.get(pluginId);
   }
 
-  async onTraceLoad(engine: Engine): Promise<void> {
+  async onTraceLoad(
+    engine: Engine,
+    beforeEach?: (id: string) => void,
+  ): Promise<void> {
     this.engine = engine;
     const plugins = Array.from(this._plugins.entries());
     // Awaiting all plugins in parallel will skew timing data as later plugins
@@ -513,6 +516,7 @@
     // most plugins use the same engine, which can only process one query at a
     // time.
     for (const [id, pluginDetails] of plugins) {
+      beforeEach?.(id);
       await doPluginTraceLoad(pluginDetails, engine, id);
     }
   }
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index e5df631..de8fde3 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -528,8 +528,9 @@
 
     await defineMaxLayoutDepthSqlFunction(engine);
 
-    this.updateStatus('Loading plugins');
-    await pluginManager.onTraceLoad(engine);
+    await pluginManager.onTraceLoad(engine, (id) => {
+      this.updateStatus(`Running plugin: ${id}`);
+    });
 
     {
       // When we reload from a permalink don't create extra tracks: