Merge "Trace-Redaction - Redact sched_switch events" into main
diff --git a/Android.bp b/Android.bp
index 79d28ce..d4a968e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -12818,6 +12818,7 @@
         "src/trace_redaction/process_thread_timeline.cc",
         "src/trace_redaction/proto_util.cc",
         "src/trace_redaction/prune_package_list.cc",
+        "src/trace_redaction/redact_sched_switch.cc",
         "src/trace_redaction/scrub_ftrace_events.cc",
         "src/trace_redaction/scrub_process_trees.cc",
         "src/trace_redaction/scrub_task_rename.cc",
@@ -12836,6 +12837,7 @@
         "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/scrub_ftrace_events_unittest.cc",
         "src/trace_redaction/scrub_task_rename_unittest.cc",
         "src/trace_redaction/scrub_trace_packet_unittest.cc",
diff --git a/src/trace_redaction/BUILD.gn b/src/trace_redaction/BUILD.gn
index 3630898..ed04178 100644
--- a/src/trace_redaction/BUILD.gn
+++ b/src/trace_redaction/BUILD.gn
@@ -42,6 +42,8 @@
     "proto_util.h",
     "prune_package_list.cc",
     "prune_package_list.h",
+    "redact_sched_switch.cc",
+    "redact_sched_switch.h",
     "scrub_ftrace_events.cc",
     "scrub_ftrace_events.h",
     "scrub_process_trees.cc",
@@ -72,6 +74,7 @@
 source_set("integrationtests") {
   testonly = true
   sources = [
+    "redact_sched_switch_integrationtest.cc",
     "scrub_ftrace_events_integrationtest.cc",
     "scrub_process_trees_integrationtest.cc",
     "scrub_task_rename_integrationtest.cc",
@@ -98,6 +101,7 @@
     "process_thread_timeline_unittest.cc",
     "proto_util_unittest.cc",
     "prune_package_list_unittest.cc",
+    "redact_sched_switch_unittest.cc",
     "scrub_ftrace_events_unittest.cc",
     "scrub_task_rename_unittest.cc",
     "scrub_trace_packet_unittest.cc",
diff --git a/src/trace_redaction/main.cc b/src/trace_redaction/main.cc
index e8cea13..0b9aafe 100644
--- a/src/trace_redaction/main.cc
+++ b/src/trace_redaction/main.cc
@@ -21,6 +21,7 @@
 #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/scrub_ftrace_events.h"
 #include "src/trace_redaction/scrub_process_trees.h"
 #include "src/trace_redaction/scrub_task_rename.h"
@@ -50,6 +51,7 @@
   redactor.transformers()->emplace_back(new ScrubFtraceEvents());
   redactor.transformers()->emplace_back(new ScrubProcessTrees());
   redactor.transformers()->emplace_back(new ScrubTaskRename());
+  redactor.transformers()->emplace_back(new RedactSchedSwitch());
 
   Context context;
   context.package_name = package_name;
diff --git a/src/trace_redaction/redact_sched_switch.cc b/src/trace_redaction/redact_sched_switch.cc
new file mode 100644
index 0000000..b70ef7a
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch.cc
@@ -0,0 +1,190 @@
+/*
+ * 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_switch.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 switch trace events in an ftrace event bundle:
+//
+//  event {
+//    timestamp: 6702093744772646
+//    pid: 0
+//    sched_switch {
+//      prev_comm: "swapper/0"
+//      prev_pid: 0
+//      prev_prio: 120
+//      prev_state: 0
+//      next_comm: "writer"
+//      next_pid: 23020
+//      next_prio: 96
+//    }
+//  }
+//
+// In the above message, it should be noted that "event.pid" will always be
+// equal to "event.sched_switch.prev_pid".
+//
+// "ftrace_event_bundle_message" is the ftrace event bundle (contains a
+// collection of ftrace event messages) because data in a sched_switch message
+// is needed in order to know if the event should be added to the bundle.
+void RedactSwitchEvent(
+    const Context& context,
+    protos::pbzero::FtraceEvent::Decoder event,
+    protos::pbzero::FtraceEventBundle* ftrace_event_bundle_message) {
+  PERFETTO_DCHECK(context.timeline);
+  PERFETTO_DCHECK(context.package_uid.has_value());
+  PERFETTO_DCHECK(event.has_sched_switch());
+  PERFETTO_DCHECK(ftrace_event_bundle_message);
+
+  // If there is no timestamp in the event, it is not possible to query the
+  // timeline. This is too risky to keep.
+  if (!event.has_timestamp()) {
+    return;
+  }
+
+  protos::pbzero::SchedSwitchFtraceEvent::Decoder sched_switch(
+      event.sched_switch());
+
+  // There must be a prev pid and a next pid. Otherwise, the event is invalid.
+  // Dropping the event is the safest option.
+  if (!sched_switch.has_prev_pid() || !sched_switch.has_next_pid()) {
+    return;
+  }
+
+  auto uid = context.package_uid.value();
+
+  auto prev_slice =
+      context.timeline->Search(event.timestamp(), sched_switch.prev_pid());
+  auto next_slice =
+      context.timeline->Search(event.timestamp(), sched_switch.next_pid());
+
+  // Build a new event, clearing the comm values when needed.
+  auto* event_message = ftrace_event_bundle_message->add_event();
+
+  // Reset to scan event fields.
+  event.Reset();
+
+  for (auto event_field = event.ReadField(); event_field.valid();
+       event_field = event.ReadField()) {
+    // This primitive only needs to affect sched switch events.
+    if (event_field.id() !=
+        protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber) {
+      proto_util::AppendField(event_field, event_message);
+      continue;
+    }
+
+    // Reset to scan sched_switch fields.
+    sched_switch.Reset();
+
+    auto switch_message = event_message->set_sched_switch();
+
+    for (auto switch_field = sched_switch.ReadField(); switch_field.valid();
+         switch_field = sched_switch.ReadField()) {
+      switch (switch_field.id()) {
+        case protos::pbzero::SchedSwitchFtraceEvent::kPrevCommFieldNumber:
+          if (prev_slice.uid == uid) {
+            proto_util::AppendField(switch_field, switch_message);
+          }
+          break;
+
+        case protos::pbzero::SchedSwitchFtraceEvent::kNextCommFieldNumber: {
+          if (next_slice.uid == uid) {
+            proto_util::AppendField(switch_field, switch_message);
+          }
+          break;
+        }
+
+        default:
+          proto_util::AppendField(switch_field, switch_message);
+          break;
+      }
+    }
+  }
+}
+
+}  // namespace
+
+base::Status RedactSchedSwitch::Transform(const Context& context,
+                                          std::string* packet) const {
+  if (packet == nullptr || packet->empty()) {
+    return base::ErrStatus("RedactSchedSwitch: null or empty packet.");
+  }
+
+  if (!context.package_uid.has_value()) {
+    return base::ErrStatus("RedactSchedSwitch: missing packet uid.");
+  }
+
+  if (!context.timeline) {
+    return base::ErrStatus("RedactSchedSwitch: 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;
+
+  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(packet_field.as_bytes());
+
+    auto* bundle_message = packet_message->set_ftrace_events();
+
+    for (auto field = bundle.ReadField(); field.valid();
+         field = bundle.ReadField()) {
+      if (field.id() != protos::pbzero::FtraceEventBundle::kEventFieldNumber) {
+        proto_util::AppendField(field, bundle_message);
+        continue;
+      }
+
+      protos::pbzero::FtraceEvent::Decoder ftrace_event(field.as_bytes());
+
+      if (ftrace_event.has_sched_switch()) {
+        RedactSwitchEvent(context, std::move(ftrace_event), bundle_message);
+      } else {
+        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_switch.h b/src/trace_redaction/redact_sched_switch.h
new file mode 100644
index 0000000..e33527d
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch.h
@@ -0,0 +1,41 @@
+/*
+ * 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_SWITCH_H_
+#define SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
+
+#include <string>
+
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+//  Assumptions:
+//    1. This is a hot path (a lot of ftrace packets)
+//    2. Allocations are slower than CPU cycles.
+//
+// Redact sched switch is called "redact" and not "prune" or "scrub" because it
+// is not removing sched switch events, but rather removing information from
+// within the event.
+class RedactSchedSwitch 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_SWITCH_H_
diff --git a/src/trace_redaction/redact_sched_switch_integrationtest.cc b/src/trace_redaction/redact_sched_switch_integrationtest.cc
new file mode 100644
index 0000000..7cb516b
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch_integrationtest.cc
@@ -0,0 +1,237 @@
+/*
+ * 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 <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_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/ftrace/sched.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+#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 {
+ protected:
+  void SetUp() override {
+    redactor_.collectors()->emplace_back(new FindPackageUid());
+    redactor_.collectors()->emplace_back(new BuildTimeline());
+    redactor_.builders()->emplace_back(new OptimizeTimeline());
+    redactor_.transformers()->emplace_back(new RedactSchedSwitch());
+
+    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_);
+  }
+
+ 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
+// >>>   FROM package_list
+// >>>   WHERE package_name='com.Unity.com.unity.multiplayer.samples.coop'
+//
+//     +-------+
+//     |  uid  |
+//     +-------+
+//     | 10252 |
+//     +-------+
+//
+// >>> SELECT uid, upid, name
+// >>>   FROM process
+// >>>   WHERE uid=10252
+//
+//     +-------+------+----------------------------------------------+
+//     |  uid  | upid | name                                         |
+//     +-------+------+----------------------------------------------+
+//     | 10252 | 843  | com.Unity.com.unity.multiplayer.samples.coop |
+//     +-------+------+----------------------------------------------+
+//
+// >>> SELECT tid, name
+// >>>   FROM thread
+// >>>   WHERE upid=843 AND name IS NOT NULL
+//
+//     +------+-----------------+
+//     | tid  | name            |
+//     +------+-----------------+
+//     | 7120 | Binder:7105_2   |
+//     | 7127 | UnityMain       |
+//     | 7142 | Job.worker 0    |
+//     | 7143 | Job.worker 1    |
+//     | 7144 | Job.worker 2    |
+//     | 7145 | Job.worker 3    |
+//     | 7146 | Job.worker 4    |
+//     | 7147 | Job.worker 5    |
+//     | 7148 | Job.worker 6    |
+//     | 7150 | Background Job. |
+//     | 7151 | Background Job. |
+//     | 7167 | UnityGfxDeviceW |
+//     | 7172 | AudioTrack      |
+//     | 7174 | FMOD stream thr |
+//     | 7180 | Binder:7105_3   |
+//     | 7184 | UnityChoreograp |
+//     | 7945 | Filter0         |
+//     | 7946 | Filter1         |
+//     | 7947 | Thread-7        |
+//     | 7948 | FMOD mixer thre |
+//     | 7950 | UnityGfxDeviceW |
+//     | 7969 | UnityGfxDeviceW |
+//     +------+-----------------+
+
+TEST_F(RedactSchedSwitchIntegrationTest, ClearsNonTargetSwitchComms) {
+  auto result = Redact();
+  ASSERT_OK(result) << result.c_message();
+
+  auto original = LoadOriginal();
+  ASSERT_OK(original) << original.status().c_message();
+
+  auto redacted = LoadRedacted();
+  ASSERT_OK(redacted) << redacted.status().c_message();
+
+  base::FlatHashMap<int32_t, std::string> expected_names;
+  expected_names.Insert(7120, "Binder:7105_2");
+  expected_names.Insert(7127, "UnityMain");
+  expected_names.Insert(7142, "Job.worker 0");
+  expected_names.Insert(7143, "Job.worker 1");
+  expected_names.Insert(7144, "Job.worker 2");
+  expected_names.Insert(7145, "Job.worker 3");
+  expected_names.Insert(7146, "Job.worker 4");
+  expected_names.Insert(7147, "Job.worker 5");
+  expected_names.Insert(7148, "Job.worker 6");
+  expected_names.Insert(7150, "Background Job.");
+  expected_names.Insert(7151, "Background Job.");
+  expected_names.Insert(7167, "UnityGfxDeviceW");
+  expected_names.Insert(7172, "AudioTrack");
+  expected_names.Insert(7174, "FMOD stream thr");
+  expected_names.Insert(7180, "Binder:7105_3");
+  expected_names.Insert(7184, "UnityChoreograp");
+  expected_names.Insert(7945, "Filter0");
+  expected_names.Insert(7946, "Filter1");
+  expected_names.Insert(7947, "Thread-7");
+  expected_names.Insert(7948, "FMOD mixer thre");
+  expected_names.Insert(7950, "UnityGfxDeviceW");
+  expected_names.Insert(7969, "UnityGfxDeviceW");
+
+  auto redacted_trace_data = LoadRedacted();
+  ASSERT_OK(redacted_trace_data) << redacted.status().c_message();
+
+  protos::pbzero::Trace::Decoder decoder(redacted_trace_data.value());
+
+  for (auto packet = decoder.packet(); packet; ++packet) {
+    protos::pbzero::TracePacket::Decoder packet_decoder(*packet);
+
+    if (!packet_decoder.has_ftrace_events()) {
+      continue;
+    }
+
+    protos::pbzero::FtraceEventBundle::Decoder ftrace_events_decoder(
+        packet_decoder.ftrace_events());
+
+    for (auto event = ftrace_events_decoder.event(); event; ++event) {
+      protos::pbzero::FtraceEvent::Decoder event_decoder(*event);
+
+      if (!event_decoder.has_sched_switch()) {
+        continue;
+      }
+
+      protos::pbzero::SchedSwitchFtraceEvent::Decoder sched_decoder(
+          event_decoder.sched_switch());
+
+      ASSERT_TRUE(sched_decoder.has_next_pid());
+      ASSERT_TRUE(sched_decoder.has_prev_pid());
+
+      auto next_pid = sched_decoder.next_pid();
+      auto prev_pid = sched_decoder.prev_pid();
+
+      // If the pid is expected, make sure it has the right now. If it is not
+      // expected, it should be missing.
+      const auto* next_comm = expected_names.Find(next_pid);
+      const auto* prev_comm = expected_names.Find(prev_pid);
+
+      if (next_comm) {
+        EXPECT_TRUE(sched_decoder.has_next_comm());
+        EXPECT_EQ(sched_decoder.next_comm().ToStdString(), *next_comm);
+      } else {
+        EXPECT_FALSE(sched_decoder.has_next_comm());
+      }
+
+      if (prev_comm) {
+        EXPECT_TRUE(sched_decoder.has_prev_comm());
+        EXPECT_EQ(sched_decoder.prev_comm().ToStdString(), *prev_comm);
+      } else {
+        EXPECT_FALSE(sched_decoder.has_prev_comm());
+      }
+    }
+  }
+}
+
+}  // namespace
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_switch_unittest.cc b/src/trace_redaction/redact_sched_switch_unittest.cc
new file mode 100644
index 0000000..5f74078
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch_unittest.cc
@@ -0,0 +1,422 @@
+/*
+ * 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_switch.h"
+#include "protos/perfetto/trace/ftrace/power.gen.h"
+#include "protos/perfetto/trace/ftrace/sched.gen.h"
+#include "protos/perfetto/trace/trace.gen.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/trace_packet.gen.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+constexpr uint64_t kUidA = 1;
+constexpr uint64_t kUidB = 2;
+constexpr uint64_t kUidC = 3;
+
+constexpr int32_t kNoParent = 10;
+constexpr int32_t kPidA = 11;
+constexpr int32_t kPidB = 12;
+constexpr int32_t kPidC = 13;
+
+constexpr std::string_view kCommA = "comm-a";
+constexpr std::string_view kCommB = "comm-b";
+constexpr std::string_view kCommC = "comm-c";
+
+constexpr uint64_t kTimeA = 100;
+constexpr uint64_t kTimeB = 200;
+constexpr uint64_t kTimeC = 300;
+
+}  // namespace
+
+// Tests which nested messages and fields are removed.
+class RedactSchedSwitchTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    context_.timeline = std::make_unique<ProcessThreadTimeline>();
+
+    // Three concurrent processes. No parent. All in different packages.
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(0, kPidA, kNoParent, kUidA));
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(0, kPidB, kNoParent, kUidB));
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(0, kPidC, kNoParent, kUidC));
+
+    context_.timeline->Sort();
+  }
+
+  void BeginBundle() { ftrace_bundle_ = trace_packet_.mutable_ftrace_events(); }
+
+  void AddSwitch(uint64_t ts,
+                 int32_t prev_pid,
+                 std::string_view prev_comm,
+                 int32_t next_pid,
+                 std::string_view next_comm) {
+    ASSERT_NE(ftrace_bundle_, nullptr);
+
+    auto* event = ftrace_bundle_->add_event();
+    event->set_timestamp(ts);
+
+    auto* sched_switch = event->mutable_sched_switch();
+    sched_switch->set_prev_pid(prev_pid);
+    sched_switch->set_prev_comm(std::string(prev_comm));
+    sched_switch->set_next_pid(next_pid);
+    sched_switch->set_next_comm(std::string(next_comm));
+  }
+
+  base::StatusOr<protos::gen::TracePacket> Transform() {
+    auto packet = trace_packet_.SerializeAsString();
+    auto result = transform_.Transform(context_, &packet);
+
+    if (!result.ok()) {
+      return result;
+    }
+
+    protos::gen::TracePacket redacted_packet;
+    redacted_packet.ParseFromString(packet);
+
+    return redacted_packet;
+  }
+
+  Context context_;
+
+  const RedactSchedSwitch& transform() const { return transform_; }
+
+ private:
+  protos::gen::TracePacket trace_packet_;
+  protos::gen::FtraceEventBundle* ftrace_bundle_;
+
+  RedactSchedSwitch transform_;
+};
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForNullPacket) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kUidA;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  ASSERT_FALSE(transform().Transform(context, nullptr).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForEmptyPacket) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kUidA;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  std::string packet_str = "";
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForNoTimeline) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kUidA;
+
+  protos::gen::TracePacket packet;
+  std::string packet_str = packet.SerializeAsString();
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForNoPackage) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  protos::gen::TracePacket packet;
+  std::string packet_str = packet.SerializeAsString();
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, BundleWithNonEventChild) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context.package_uid = 0;
+
+  // packet {
+  //   ftrace_events {
+  //     cpu: 0
+  //     event {
+  //       timestamp: 6702093744772646
+  //       pid: 0
+  //       sched_switch {
+  //         prev_comm: "swapper/0"
+  //         prev_pid: 0
+  //         prev_prio: 120
+  //         prev_state: 0
+  //         next_comm: "writer"
+  //         next_pid: 23020
+  //         next_prio: 96
+  //       }
+  //     }
+  //   }
+  // }
+
+  protos::gen::TracePacket packet;
+  auto* events = packet.mutable_ftrace_events();
+  events->set_cpu(0);
+
+  auto* event = events->add_event();
+  event->set_timestamp(kPidA);
+  event->set_pid(kPidA);
+
+  auto* sched_switch = event->mutable_sched_switch();
+  sched_switch->set_prev_comm("swapper/0");
+  sched_switch->set_prev_pid(kPidA);
+  sched_switch->set_prev_prio(120);
+  sched_switch->set_prev_state(0);
+
+  sched_switch->set_next_comm("writer");
+  sched_switch->set_next_pid(kPidB);
+  sched_switch->set_next_prio(96);
+
+  std::string packet_str = packet.SerializeAsString();
+
+  ASSERT_TRUE(transform().Transform(context, &packet_str).ok());
+
+  protos::gen::TracePacket redacted;
+  redacted.ParseFromString(packet_str);
+
+  // Make sure values alongside the "event" value (e.g. "cpu") are retained.
+  ASSERT_TRUE(redacted.has_ftrace_events());
+  ASSERT_TRUE(redacted.ftrace_events().has_cpu());
+}
+
+// There are more than sched_switch events in the ftrace_events message.
+// Beyond supporting simple fields along side the event (e.g. cpu), not all
+// events will contain sched_switch events. Make sure that all every message is
+// retained while redacting the sched_switch.
+TEST_F(RedactSchedSwitchTest, KeepsNonSwitchEvents) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = 2;
+
+  // Keep the previous PID and remove the next PID.
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context.timeline->Append(ProcessThreadTimeline::Event::Open(0, 0, 1, 2));
+  context.timeline->Sort();
+
+  // packet {
+  //   ftrace_events {
+  //     cpu: 0
+  //     event {
+  //       timestamp: 6702093744766292
+  //       pid: 0
+  //       cpu_idle {
+  //         state: 4294967295
+  //         cpu_id: 0
+  //       }
+  //     }
+  //     event {
+  //       timestamp: 6702093744772646
+  //       pid: 0
+  //       sched_switch {
+  //         prev_comm: "swapper/0"
+  //         prev_pid: 0
+  //         prev_prio: 120
+  //         prev_state: 0
+  //         next_comm: "writer"
+  //         next_pid: 23020
+  //         next_prio: 96
+  //       }
+  //     }
+  //     event {
+  //       timestamp: 6702093744803376
+  //       pid: 23020
+  //       sched_waking {
+  //         comm: "FastMixer"
+  //         pid: 1619
+  //         prio: 96
+  //         success: 1
+  //         target_cpu: 1
+  //       }
+  //     }
+  //   }
+  // }
+
+  protos::gen::TracePacket source_packet;
+  source_packet.mutable_ftrace_events()->set_cpu(0);
+
+  // cpu_idle
+  do {
+    auto* event = source_packet.mutable_ftrace_events()->add_event();
+    event->set_timestamp(6702093744766292);
+    event->set_pid(0);
+
+    auto* cpu_idle = event->mutable_cpu_idle();
+    cpu_idle->set_state(4294967295);
+    cpu_idle->set_cpu_id(0);
+  } while (false);
+
+  // sched_switch
+  do {
+    auto* event = source_packet.mutable_ftrace_events()->add_event();
+    event->set_timestamp(6702093744772646);
+    event->set_pid(0);
+
+    auto* sched_switch = event->mutable_sched_switch();
+    sched_switch->set_prev_comm("swapper/0");
+    sched_switch->set_prev_pid(0);
+    sched_switch->set_prev_prio(120);
+    sched_switch->set_prev_state(0);
+    sched_switch->set_next_comm("writer");
+    sched_switch->set_next_pid(23020);
+    sched_switch->set_next_prio(96);
+  } while (false);
+
+  // sched_waking
+  do {
+    auto* event = source_packet.mutable_ftrace_events()->add_event();
+    event->set_timestamp(6702093744803376);
+    event->set_pid(23020);
+
+    auto* sched_waking = event->mutable_sched_waking();
+    sched_waking->set_comm("FastMixer");
+    sched_waking->set_pid(1619);
+    sched_waking->set_prio(96);
+    sched_waking->set_success(1);
+    sched_waking->set_target_cpu(1);
+  } while (false);
+
+  auto packet_str = source_packet.SerializeAsString();
+
+  ASSERT_TRUE(transform().Transform(context, &packet_str).ok());
+
+  protos::gen::TracePacket packet;
+  source_packet.ParseFromString(packet_str);
+
+  // Make sure values alongside the "event" value (e.g. "cpu") are retained.
+  ASSERT_TRUE(source_packet.has_ftrace_events());
+
+  auto& ftrace_packets = source_packet.ftrace_events();
+
+  ASSERT_TRUE(ftrace_packets.has_cpu());
+  ASSERT_EQ(ftrace_packets.cpu(), 0u);
+
+  // Assumes order is retained.
+  ASSERT_EQ(ftrace_packets.event_size(), 3);
+  ASSERT_TRUE(ftrace_packets.event().at(0).has_cpu_idle());
+  ASSERT_TRUE(ftrace_packets.event().at(1).has_sched_switch());
+  ASSERT_TRUE(ftrace_packets.event().at(2).has_sched_waking());
+
+  // The sched switch event's next comm should be cleared.
+  const auto& sched_switch = ftrace_packets.event().at(1).sched_switch();
+
+  ASSERT_TRUE(sched_switch.has_prev_comm());
+  ASSERT_EQ(sched_switch.prev_comm(), "swapper/0");
+
+  ASSERT_FALSE(sched_switch.has_next_comm());
+}
+
+class CommTestParams {
+ public:
+  CommTestParams(size_t event_index,
+                 int32_t prev_pid,
+                 std::optional<std::string_view> prev_comm,
+                 int32_t next_pid,
+                 std::optional<std::string_view> next_comm)
+      : event_index_(event_index),
+        prev_pid_(prev_pid),
+        prev_comm_(prev_comm),
+        next_pid_(next_pid),
+        next_comm_(next_comm) {}
+
+  size_t event_index() const { return event_index_; }
+
+  int32_t prev_pid() const { return prev_pid_; }
+
+  std::optional<std::string> prev_comm() const { return prev_comm_; }
+
+  int32_t next_pid() const { return next_pid_; }
+
+  std::optional<std::string> next_comm() const { return next_comm_; }
+
+ private:
+  size_t event_index_;
+
+  int32_t prev_pid_;
+  std::optional<std::string> prev_comm_;
+
+  int32_t next_pid_;
+  std::optional<std::string> next_comm_;
+};
+
+class RedactSchedSwitchTestRemoveComm
+    : public RedactSchedSwitchTest,
+      public testing::WithParamInterface<CommTestParams> {};
+
+TEST_P(RedactSchedSwitchTestRemoveComm, AllEvents) {
+  auto params = GetParam();
+
+  context_.package_uid = kUidA;
+
+  BeginBundle();
+
+  // Cycle through all the processes: Pid A -> Pid B -> Pid C -> Pid A
+  AddSwitch(kTimeA, kPidA, kCommA, kPidB, kCommB);
+  AddSwitch(kTimeB, kPidB, kCommB, kPidC, kCommC);
+  AddSwitch(kTimeC, kPidC, kCommC, kPidA, kCommA);
+
+  auto packet = Transform();
+
+  ASSERT_TRUE(packet->has_ftrace_events());
+
+  auto& ftrace_events = packet->ftrace_events().event();
+
+  ASSERT_EQ(ftrace_events.size(), 3u);
+
+  auto event_index = params.event_index();
+
+  ASSERT_TRUE(ftrace_events[event_index].has_sched_switch());
+
+  auto& sched_switch = ftrace_events[event_index].sched_switch();
+
+  ASSERT_EQ(sched_switch.prev_pid(), params.prev_pid());
+  ASSERT_EQ(sched_switch.next_pid(), params.next_pid());
+
+  ASSERT_EQ(sched_switch.has_prev_comm(), params.prev_comm().has_value());
+  ASSERT_EQ(sched_switch.has_next_comm(), params.next_comm().has_value());
+
+  if (sched_switch.has_prev_comm()) {
+    ASSERT_EQ(sched_switch.prev_comm(), params.prev_comm());
+  }
+
+  if (sched_switch.has_next_comm()) {
+    ASSERT_EQ(sched_switch.next_comm(), params.next_comm());
+  }
+}
+
+// Cycle through all the processes: Pid A -> Pid B -> Pid C -> Pid A
+//
+// Only kPidA is attached to kUidA, so it should be the only one with a comm
+// value.
+INSTANTIATE_TEST_SUITE_P(
+    EveryPid,
+    RedactSchedSwitchTestRemoveComm,
+    testing::Values(CommTestParams(0, kPidA, kCommA, kPidB, std::nullopt),
+                    CommTestParams(1, kPidB, std::nullopt, kPidC, std::nullopt),
+                    CommTestParams(2, kPidC, std::nullopt, kPidA, kCommA)));
+
+}  // namespace perfetto::trace_redaction