Merge "Improve ftrace_config_muxer_unittest" into main
diff --git a/src/traced/probes/ftrace/atrace_wrapper.cc b/src/traced/probes/ftrace/atrace_wrapper.cc
index 7254325..1a64544 100644
--- a/src/traced/probes/ftrace/atrace_wrapper.cc
+++ b/src/traced/probes/ftrace/atrace_wrapper.cc
@@ -38,9 +38,6 @@
 
 namespace {
 
-RunAtraceFunction g_run_atrace_for_testing = nullptr;
-std::optional<bool> g_is_old_atrace_for_testing{};
-
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
 // Args should include "atrace" for argv[0].
 bool ExecvAtrace(const std::vector<std::string>& args,
@@ -174,25 +171,23 @@
 
 }  // namespace
 
-bool RunAtrace(const std::vector<std::string>& args,
-               std::string* atrace_errors) {
-  if (g_run_atrace_for_testing)
-    return g_run_atrace_for_testing(args, atrace_errors);
+AtraceWrapper::~AtraceWrapper() = default;
+
+AtraceWrapperImpl::~AtraceWrapperImpl() = default;
+
+bool AtraceWrapperImpl::RunAtrace(const std::vector<std::string>& args,
+                                  std::string* atrace_errors) {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
   return ExecvAtrace(args, atrace_errors);
 #else
+  base::ignore_result(args);
+  base::ignore_result(atrace_errors);
   PERFETTO_LOG("Atrace only supported on Android.");
   return false;
 #endif
 }
 
-void SetRunAtraceForTesting(RunAtraceFunction f) {
-  g_run_atrace_for_testing = f;
-}
-
-bool IsOldAtrace() {
-  if (g_is_old_atrace_for_testing.has_value())
-    return *g_is_old_atrace_for_testing;
+bool AtraceWrapperImpl::IsOldAtrace() {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) && \
     !PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
   // Sideloaded case. We could be sideloaded on a modern device or an older one.
@@ -207,12 +202,4 @@
 #endif
 }
 
-void SetIsOldAtraceForTesting(bool value) {
-  g_is_old_atrace_for_testing = value;
-}
-
-void ClearIsOldAtraceForTesting() {
-  g_is_old_atrace_for_testing.reset();
-}
-
 }  // namespace perfetto
diff --git a/src/traced/probes/ftrace/atrace_wrapper.h b/src/traced/probes/ftrace/atrace_wrapper.h
index 29847aa..5c6a148 100644
--- a/src/traced/probes/ftrace/atrace_wrapper.h
+++ b/src/traced/probes/ftrace/atrace_wrapper.h
@@ -18,27 +18,30 @@
 #define SRC_TRACED_PROBES_FTRACE_ATRACE_WRAPPER_H_
 
 #include <string>
-#include <type_traits>
 #include <vector>
 
 namespace perfetto {
 
-using RunAtraceFunction =
-    std::add_pointer<bool(const std::vector<std::string>& /*args*/,
-                          std::string* /*atrace_errors*/)>::type;
+class AtraceWrapper {
+ public:
+  virtual ~AtraceWrapper();
+  // When we are sideloaded on an old version of Android (pre P), we cannot use
+  // atrace --only_userspace because that option doesn't exist. In that case we:
+  // - Just use atrace --async_start/stop, which will cause atrace to also
+  //   poke at ftrace.
+  // - Suppress the checks for "somebody else enabled ftrace unexpectedly".
+  virtual bool IsOldAtrace() = 0;
+  virtual bool RunAtrace(const std::vector<std::string>& args,
+                         std::string* atrace_errors) = 0;
+};
 
-// When we are sideloaded on an old version of Android (pre P), we cannot use
-// atrace --only_userspace because that option doesn't exist. In that case we:
-// - Just use atrace --async_start/stop, which will cause atrace to also
-//   poke at ftrace.
-// - Suppress the checks for "somebody else enabled ftrace unexpectedly".
-bool IsOldAtrace();
-void SetIsOldAtraceForTesting(bool);
-void ClearIsOldAtraceForTesting();
-
-bool RunAtrace(const std::vector<std::string>& args,
-               std::string* atrace_errors);
-void SetRunAtraceForTesting(RunAtraceFunction);
+class AtraceWrapperImpl : public AtraceWrapper {
+ public:
+  ~AtraceWrapperImpl() override;
+  bool IsOldAtrace() override;
+  bool RunAtrace(const std::vector<std::string>& args,
+                 std::string* atrace_errors) override;
+};
 
 }  // namespace perfetto
 
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index 3e2b9ab..e4d8a19 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -579,11 +579,13 @@
 
 FtraceConfigMuxer::FtraceConfigMuxer(
     FtraceProcfs* ftrace,
+    AtraceWrapper* atrace_wrapper,
     ProtoTranslationTable* table,
     SyscallTable syscalls,
     std::map<std::string, std::vector<GroupAndName>> vendor_events,
     bool secondary_instance)
     : ftrace_(ftrace),
+      atrace_wrapper_(atrace_wrapper),
       table_(table),
       syscalls_(std::move(syscalls)),
       current_state_(),
@@ -654,7 +656,7 @@
           "atrace_apps options as they affect global state");
       return false;
     }
-    if (IsOldAtrace() && !ds_configs_.empty()) {
+    if (atrace_wrapper_->IsOldAtrace() && !ds_configs_.empty()) {
       PERFETTO_ELOG(
           "Concurrent atrace sessions are not supported before Android P, "
           "bailing out.");
@@ -1040,7 +1042,6 @@
   }
 }
 
-// static
 bool FtraceConfigMuxer::StartAtrace(const std::vector<std::string>& apps,
                                     const std::vector<std::string>& categories,
                                     std::string* atrace_errors) {
@@ -1049,7 +1050,7 @@
   std::vector<std::string> args;
   args.push_back("atrace");  // argv0 for exec()
   args.push_back("--async_start");
-  if (!IsOldAtrace())
+  if (!atrace_wrapper_->IsOldAtrace())
     args.push_back("--only_userspace");
 
   for (const auto& category : categories)
@@ -1066,7 +1067,7 @@
     args.push_back(arg);
   }
 
-  bool result = RunAtrace(args, atrace_errors);
+  bool result = atrace_wrapper_->RunAtrace(args, atrace_errors);
   PERFETTO_DLOG("...done (%s)", result ? "success" : "fail");
   return result;
 }
@@ -1077,9 +1078,9 @@
   PERFETTO_DLOG("Stop atrace...");
 
   std::vector<std::string> args{"atrace", "--async_stop"};
-  if (!IsOldAtrace())
+  if (!atrace_wrapper_->IsOldAtrace())
     args.push_back("--only_userspace");
-  if (RunAtrace(args, /*atrace_errors=*/nullptr)) {
+  if (atrace_wrapper_->RunAtrace(args, /*atrace_errors=*/nullptr)) {
     current_state_.atrace_categories.clear();
     current_state_.atrace_apps.clear();
     current_state_.atrace_on = false;
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.h b/src/traced/probes/ftrace/ftrace_config_muxer.h
index e11efb0..4c3f10b 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.h
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.h
@@ -22,6 +22,7 @@
 #include <set>
 
 #include "src/kernel_utils/syscall_table.h"
+#include "src/traced/probes/ftrace/atrace_wrapper.h"
 #include "src/traced/probes/ftrace/compact_sched.h"
 #include "src/traced/probes/ftrace/ftrace_config_utils.h"
 #include "src/traced/probes/ftrace/ftrace_print_filter.h"
@@ -106,6 +107,7 @@
   // should outlive this instance.
   FtraceConfigMuxer(
       FtraceProcfs* ftrace,
+      AtraceWrapper* atrace_wrapper,
       ProtoTranslationTable* table,
       SyscallTable syscalls,
       std::map<std::string, std::vector<GroupAndName>> vendor_events,
@@ -174,10 +176,6 @@
       const SyscallTable& syscalls);
 
  private:
-  static bool StartAtrace(const std::vector<std::string>& apps,
-                          const std::vector<std::string>& categories,
-                          std::string* atrace_errors);
-
   struct FtraceState {
     EventFilter ftrace_events;
     std::set<size_t> syscall_filter;  // syscall ids or kAllSyscallsId
@@ -198,6 +196,9 @@
   void SetupBufferSize(const FtraceConfig& request);
   bool UpdateBufferPercent();
   void UpdateAtrace(const FtraceConfig& request, std::string* atrace_errors);
+  bool StartAtrace(const std::vector<std::string>& apps,
+                   const std::vector<std::string>& categories,
+                   std::string* atrace_errors);
   void DisableAtrace();
 
   // This processes the config to get the exact events.
@@ -226,6 +227,7 @@
   bool SetSyscallEventFilter(const EventFilter& extra_syscalls);
 
   FtraceProcfs* ftrace_;
+  AtraceWrapper* atrace_wrapper_;
   ProtoTranslationTable* table_;
   SyscallTable syscalls_;
 
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
index 3ef5635..513d851 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
@@ -93,19 +93,10 @@
               (const, override));
 };
 
-struct MockRunAtrace {
-  MockRunAtrace() {
-    static MockRunAtrace* instance;
-    instance = this;
-    SetRunAtraceForTesting(
-        [](const std::vector<std::string>& args, std::string* atrace_errors) {
-          return instance->RunAtrace(args, atrace_errors);
-        });
-  }
-
-  ~MockRunAtrace() { SetRunAtraceForTesting(nullptr); }
-
+class MockAtraceWrapper : public AtraceWrapper {
+ public:
   MOCK_METHOD(bool, RunAtrace, (const std::vector<std::string>&, std::string*));
+  MOCK_METHOD(bool, IsOldAtrace, ());
 };
 
 class MockProtoTranslationTable : public ProtoTranslationTable {
@@ -131,22 +122,66 @@
               (const, override));
 };
 
+TEST(ComputeCpuBufferSizeInPagesTest, DifferentCases) {
+  constexpr auto test = ComputeCpuBufferSizeInPages;
+  auto KbToPages = [](uint64_t kb) {
+    return kb * 1024 / base::GetSysPageSize();
+  };
+  auto kMaxBufSizePages = KbToPages(64 * 1024);
+  int64_t kNoRamInfo = 0;
+  bool kExactSize = false;
+  bool kLowerBoundSize = true;
+  int64_t kLowRamPages =
+      static_cast<int64_t>(KbToPages(3 * (1ULL << 20) + 512 * (1ULL << 10)));
+  int64_t kHighRamPages =
+      static_cast<int64_t>(KbToPages(7 * (1ULL << 20) + 512 * (1ULL << 10)));
+
+  // No buffer size given: good default.
+  EXPECT_EQ(test(0, kExactSize, kNoRamInfo), KbToPages(2048));
+  // Default depends on device ram size.
+  EXPECT_EQ(test(0, kExactSize, kLowRamPages), KbToPages(2048));
+  EXPECT_EQ(test(0, kExactSize, kHighRamPages), KbToPages(8192));
+
+  // buffer_size_lower_bound lets us choose a higher default than given.
+  // default > requested:
+  EXPECT_EQ(test(4096, kExactSize, kHighRamPages), KbToPages(4096));
+  EXPECT_EQ(test(4096, kLowerBoundSize, kHighRamPages), KbToPages(8192));
+  // requested > default:
+  EXPECT_EQ(test(4096, kExactSize, kLowRamPages), KbToPages(4096));
+  EXPECT_EQ(test(4096, kLowerBoundSize, kLowRamPages), KbToPages(4096));
+  // requested > default:
+  EXPECT_EQ(test(16384, kExactSize, kHighRamPages), KbToPages(16384));
+  EXPECT_EQ(test(16384, kLowerBoundSize, kHighRamPages), KbToPages(16384));
+
+  // Buffer size given way too big: good default.
+  EXPECT_EQ(test(10 * (1ULL << 20), kExactSize, kNoRamInfo), kMaxBufSizePages);
+  EXPECT_EQ(test(512 * 1024, kExactSize, kNoRamInfo), kMaxBufSizePages);
+
+  // Your size ends up with less than 1 page per cpu -> 1 page.
+  EXPECT_EQ(test(3, kExactSize, kNoRamInfo), 1u);
+  // You picked a good size -> your size rounded to nearest page.
+  EXPECT_EQ(test(42, kExactSize, kNoRamInfo), KbToPages(42));
+
+  // Sysconf returning an error is ok.
+  EXPECT_EQ(test(0, kExactSize, -1), KbToPages(2048));
+  EXPECT_EQ(test(4096, kExactSize, -1), KbToPages(4096));
+}
+
+// Base fixture that provides some dependencies but doesn't construct a
+// FtraceConfigMuxer.
 class FtraceConfigMuxerTest : public ::testing::Test {
  protected:
-  void SetUp() override {
-    // Don't probe for older SDK levels, that would relax the atrace-related
-    // checks on older versions of Android (But some tests here test those).
-    // We want the unittests to behave consistently (as if we were on a post P
-    // device) regardless of the Android versions they run on.
-    SetIsOldAtraceForTesting(false);
+  FtraceConfigMuxerTest() {
+    ON_CALL(atrace_wrapper_, RunAtrace).WillByDefault(Return(true));
+    ON_CALL(atrace_wrapper_, IsOldAtrace).WillByDefault(Return(false));
   }
-  void TearDown() override { ClearIsOldAtraceForTesting(); }
+
   std::unique_ptr<MockProtoTranslationTable> GetMockTable() {
     std::vector<Field> common_fields;
     std::vector<Event> events;
     return std::unique_ptr<MockProtoTranslationTable>(
         new MockProtoTranslationTable(
-            &table_procfs_, events, std::move(common_fields),
+            &ftrace_, events, std::move(common_fields),
             ProtoTranslationTable::DefaultPageHeaderSpecForTesting(),
             InvalidCompactSchedEventFormatForTesting()));
   }
@@ -225,956 +260,25 @@
     }
 
     return std::unique_ptr<ProtoTranslationTable>(new ProtoTranslationTable(
-        &table_procfs_, events, std::move(common_fields),
+        &ftrace_, events, std::move(common_fields),
         ProtoTranslationTable::DefaultPageHeaderSpecForTesting(),
         compact_format, PrintkMap()));
   }
 
-  NiceMock<MockFtraceProcfs> table_procfs_;
-  std::unique_ptr<ProtoTranslationTable> table_ = CreateFakeTable();
+  NiceMock<MockFtraceProcfs> ftrace_;
+  NiceMock<MockAtraceWrapper> atrace_wrapper_;
 };
 
-TEST_F(FtraceConfigMuxerTest, ComputeCpuBufferSizeInPages) {
-  constexpr auto test = ComputeCpuBufferSizeInPages;
-  auto KbToPages = [](uint64_t kb) {
-    return kb * 1024 / base::GetSysPageSize();
-  };
-  auto kMaxBufSizePages = KbToPages(64 * 1024);
-  int64_t kNoRamInfo = 0;
-  bool kExactSize = false;
-  bool kLowerBoundSize = true;
-  int64_t kLowRamPages =
-      static_cast<int64_t>(KbToPages(3 * (1ULL << 20) + 512 * (1ULL << 10)));
-  int64_t kHighRamPages =
-      static_cast<int64_t>(KbToPages(7 * (1ULL << 20) + 512 * (1ULL << 10)));
-
-  // No buffer size given: good default.
-  EXPECT_EQ(test(0, kExactSize, kNoRamInfo), KbToPages(2048));
-  // Default depends on device ram size.
-  EXPECT_EQ(test(0, kExactSize, kLowRamPages), KbToPages(2048));
-  EXPECT_EQ(test(0, kExactSize, kHighRamPages), KbToPages(8192));
-
-  // buffer_size_lower_bound lets us choose a higher default than given.
-  // default > requested:
-  EXPECT_EQ(test(4096, kExactSize, kHighRamPages), KbToPages(4096));
-  EXPECT_EQ(test(4096, kLowerBoundSize, kHighRamPages), KbToPages(8192));
-  // requested > default:
-  EXPECT_EQ(test(4096, kExactSize, kLowRamPages), KbToPages(4096));
-  EXPECT_EQ(test(4096, kLowerBoundSize, kLowRamPages), KbToPages(4096));
-  // requested > default:
-  EXPECT_EQ(test(16384, kExactSize, kHighRamPages), KbToPages(16384));
-  EXPECT_EQ(test(16384, kLowerBoundSize, kHighRamPages), KbToPages(16384));
-
-  // Buffer size given way too big: good default.
-  EXPECT_EQ(test(10 * (1ULL << 20), kExactSize, kNoRamInfo), kMaxBufSizePages);
-  EXPECT_EQ(test(512 * 1024, kExactSize, kNoRamInfo), kMaxBufSizePages);
-
-  // Your size ends up with less than 1 page per cpu -> 1 page.
-  EXPECT_EQ(test(3, kExactSize, kNoRamInfo), 1u);
-  // You picked a good size -> your size rounded to nearest page.
-  EXPECT_EQ(test(42, kExactSize, kNoRamInfo), KbToPages(42));
-
-  // Sysconf returning an error is ok.
-  EXPECT_EQ(test(0, kExactSize, -1), KbToPages(2048));
-  EXPECT_EQ(test(4096, kExactSize, -1), KbToPages(4096));
-}
-
-TEST_F(FtraceConfigMuxerTest, GenericSyscallFiltering) {
+TEST_F(FtraceConfigMuxerTest, SecondaryInstanceDoNotSupportAtrace) {
   auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"raw_syscalls/sys_enter"});
-  *config.add_syscall_events() = "sys_open";
-  *config.add_syscall_events() = "sys_read";
-
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-  EXPECT_CALL(ftrace, WriteToFile(_, _)).WillRepeatedly(Return(true));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/raw_syscalls/sys_enter/filter",
-                                  "id == 0 || id == 1"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/raw_syscalls/sys_exit/filter",
-                                  "id == 0 || id == 1"));
-
-  FtraceConfigId id = 37;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const std::set<size_t>& filter = model.GetSyscallFilterForTesting();
-  ASSERT_THAT(filter, UnorderedElementsAre(0, 1));
-}
-
-TEST_F(FtraceConfigMuxerTest, UnknownSyscallFilter) {
-  auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
-
-  FtraceConfig config = CreateFtraceConfig({"raw_syscalls/sys_enter"});
-  config.add_syscall_events("sys_open");
-  config.add_syscall_events("sys_not_a_call");
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-
-  // Unknown syscall is ignored.
-  ASSERT_TRUE(model.SetupConfig(/*id = */ 73, config));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
-}
-
-TEST_F(FtraceConfigMuxerTest, SyscallFilterMuxing) {
-  auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
-
-  FtraceConfig empty_config = CreateFtraceConfig({});
-
-  FtraceConfig syscall_config = empty_config;
-  syscall_config.add_ftrace_events("raw_syscalls/sys_enter");
-
-  FtraceConfig syscall_open_config = syscall_config;
-  syscall_open_config.add_syscall_events("sys_open");
-
-  FtraceConfig syscall_read_config = syscall_config;
-  syscall_read_config.add_syscall_events("sys_read");
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-
-  // Expect no filter for non-syscall config.
-  ASSERT_TRUE(model.SetupConfig(/* id= */ 179239, empty_config));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
-
-  // Expect no filter for syscall config with no specified events.
-  FtraceConfigId syscall_id = 73;
-  ASSERT_TRUE(model.SetupConfig(syscall_id, syscall_config));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
-
-  // Still expect no filter to satisfy this and the above.
-  FtraceConfigId syscall_open_id = 101;
-  ASSERT_TRUE(model.SetupConfig(syscall_open_id, syscall_open_config));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
-
-  // After removing the generic syscall trace, only the one with filter is left.
-  ASSERT_TRUE(model.RemoveConfig(syscall_id));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
-
-  // With sys_read and sys_open traced separately, filter includes both.
-  FtraceConfigId syscall_read_id = 57;
-  ASSERT_TRUE(model.SetupConfig(syscall_read_id, syscall_read_config));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre(0, 1));
-
-  // After removing configs with filters, filter is reset to empty.
-  ASSERT_TRUE(model.RemoveConfig(syscall_open_id));
-  ASSERT_TRUE(model.RemoveConfig(syscall_read_id));
-  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
-}
-
-TEST_F(FtraceConfigMuxerTest, AddGenericEvent) {
-  auto mock_table = GetMockTable();
-  MockFtraceProcfs ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"power/cpu_frequency"});
-
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
-
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/power/cpu_frequency/enable", "1"));
-  EXPECT_CALL(*mock_table, GetEvent(GroupAndName("power", "cpu_frequency")))
-      .Times(AnyNumber());
-
-  static constexpr int kExpectedEventId = 77;
-  Event event_to_return;
-  event_to_return.name = "cpu_frequency";
-  event_to_return.group = "power";
-  event_to_return.ftrace_event_id = kExpectedEventId;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("power", "cpu_frequency")))
-      .WillByDefault(Return(&event_to_return));
-  EXPECT_CALL(*mock_table,
-              GetOrCreateEvent(GroupAndName("power", "cpu_frequency")));
-
-  FtraceConfigId id = 7;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              ElementsAreArray({kExpectedEventId}));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  ASSERT_THAT(central_filter->GetEnabledEvents(),
-              ElementsAreArray({kExpectedEventId}));
-}
-
-TEST_F(FtraceConfigMuxerTest, AddSameNameEvents) {
-  auto mock_table = GetMockTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"group_one/foo", "group_two/foo"});
-
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
-
-  static constexpr int kEventId1 = 1;
-  Event event1;
-  event1.name = "foo";
-  event1.group = "group_one";
-  event1.ftrace_event_id = kEventId1;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_one", "foo")))
-      .WillByDefault(Return(&event1));
-  EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_one", "foo")));
-
-  static constexpr int kEventId2 = 2;
-  Event event2;
-  event2.name = "foo";
-  event2.group = "group_two";
-  event2.ftrace_event_id = kEventId2;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")))
-      .WillByDefault(Return(&event2));
-  EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")));
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-
-  FtraceConfigId id = 5;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              ElementsAreArray({kEventId1, kEventId2}));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  ASSERT_THAT(central_filter->GetEnabledEvents(),
-              ElementsAreArray({kEventId1, kEventId2}));
-}
-
-TEST_F(FtraceConfigMuxerTest, AddAllEvents) {
-  auto mock_table = GetMockTable();
-  MockFtraceProcfs ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"sched/*"});
-
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_new_event/enable", "1"));
-
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
-  std::set<std::string> n = {"sched_switch", "sched_new_event"};
-  ON_CALL(ftrace, GetEventNamesForGroup("events/sched"))
-      .WillByDefault(Return(n));
-  EXPECT_CALL(ftrace, GetEventNamesForGroup("events/sched")).Times(1);
-
-  // Non-generic event.
-  static constexpr int kSchedSwitchEventId = 1;
-  Event sched_switch = {"sched_switch", "sched", {}, 0, 0, 0};
-  sched_switch.ftrace_event_id = kSchedSwitchEventId;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("sched", "sched_switch")))
-      .WillByDefault(Return(&sched_switch));
-  EXPECT_CALL(*mock_table,
-              GetOrCreateEvent(GroupAndName("sched", "sched_switch")))
-      .Times(AnyNumber());
-
-  // Generic event.
-  static constexpr int kGenericEventId = 2;
-  Event event_to_return;
-  event_to_return.name = "sched_new_event";
-  event_to_return.group = "sched";
-  event_to_return.ftrace_event_id = kGenericEventId;
-  ON_CALL(*mock_table,
-          GetOrCreateEvent(GroupAndName("sched", "sched_new_event")))
-      .WillByDefault(Return(&event_to_return));
-  EXPECT_CALL(*mock_table,
-              GetOrCreateEvent(GroupAndName("sched", "sched_new_event")));
-
-  FtraceConfigId id = 13;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-  ASSERT_TRUE(id);
-
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              ElementsAreArray({kSchedSwitchEventId, kGenericEventId}));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  ASSERT_THAT(central_filter->GetEnabledEvents(),
-              ElementsAreArray({kSchedSwitchEventId, kGenericEventId}));
-}
-
-TEST_F(FtraceConfigMuxerTest, TwoWildcardGroups) {
-  auto mock_table = GetMockTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"group_one/*", "group_two/*"});
-
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
-
-  std::set<std::string> event_names = {"foo"};
-  ON_CALL(ftrace, GetEventNamesForGroup("events/group_one"))
-      .WillByDefault(Return(event_names));
-  EXPECT_CALL(ftrace, GetEventNamesForGroup("events/group_one"))
-      .Times(AnyNumber());
-
-  ON_CALL(ftrace, GetEventNamesForGroup("events/group_two"))
-      .WillByDefault(Return(event_names));
-  EXPECT_CALL(ftrace, GetEventNamesForGroup("events/group_two"))
-      .Times(AnyNumber());
-
-  static constexpr int kEventId1 = 1;
-  Event event1;
-  event1.name = "foo";
-  event1.group = "group_one";
-  event1.ftrace_event_id = kEventId1;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_one", "foo")))
-      .WillByDefault(Return(&event1));
-  EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_one", "foo")));
-
-  static constexpr int kEventId2 = 2;
-  Event event2;
-  event2.name = "foo";
-  event2.group = "group_two";
-  event2.ftrace_event_id = kEventId2;
-  ON_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")))
-      .WillByDefault(Return(&event2));
-  EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")));
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-
-  FtraceConfigId id = 23;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              ElementsAreArray({kEventId1, kEventId2}));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  ASSERT_THAT(central_filter->GetEnabledEvents(),
-              ElementsAreArray({kEventId1, kEventId2}));
-}
-
-TEST_F(FtraceConfigMuxerTest, TurnFtraceOnOff) {
-  MockFtraceProcfs ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"sched_switch", "foo"});
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
-
-  FtraceConfigId id = 97;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              ElementsAreArray({kFakeSchedSwitchEventId}));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  ASSERT_THAT(central_filter->GetEnabledEvents(),
-              ElementsAreArray({kFakeSchedSwitchEventId}));
-
-  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace));
-  EXPECT_CALL(ftrace, NumberOfCpus()).Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_percent", _))
-      .WillRepeatedly(Return(true));
-
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_switch/enable", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", PageSizeKb()));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-
-  ASSERT_TRUE(model.RemoveConfig(id));
-}
-
-TEST_F(FtraceConfigMuxerTest, FtraceIsAlreadyOn) {
-  MockFtraceProcfs ftrace;
-
-  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  // If someone is using ftrace already don't stomp on what they are doing.
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("function"));
-  ASSERT_FALSE(model.SetupConfig(/* id= */ 123, config));
-}
-
-TEST_F(FtraceConfigMuxerTest, Atrace) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
+  FtraceConfigMuxer model(&ftrace_, &atrace_wrapper_, fake_table.get(),
+                          GetSyscallTable(), {},
+                          /* secondary_instance= */ true);
 
   FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
   *config.add_atrace_categories() = "sched";
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "sched"}),
-                                _))
-      .WillOnce(Return(true));
-
-  FtraceConfigId id = 57;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-
-  // "ftrace" group events are always enabled, and therefore the "print" event
-  // will show up in the per data source event filter (as we want to record it),
-  // but not the central filter (as we're not enabling/disabling it).
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              Contains(kFakeSchedSwitchEventId));
-  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              Contains(kFakePrintEventId));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  EXPECT_THAT(central_filter->GetEnabledEvents(),
-              Contains(kFakeSchedSwitchEventId));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceTwoApps) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config = CreateFtraceConfig({});
-  *config.add_atrace_apps() = "com.google.android.gms.persistent";
-  *config.add_atrace_apps() = "com.google.android.gms";
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray(
-              {"atrace", "--async_start", "--only_userspace", "-a",
-               "com.google.android.gms,com.google.android.gms.persistent"}),
-          _))
-      .WillOnce(Return(true));
-
-  FtraceConfigId id = 97;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              Contains(kFakePrintEventId));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceMultipleConfigs) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config_a = CreateFtraceConfig({});
-  *config_a.add_atrace_apps() = "app_a";
-  *config_a.add_atrace_categories() = "cat_a";
-
-  FtraceConfig config_b = CreateFtraceConfig({});
-  *config_b.add_atrace_apps() = "app_b";
-  *config_b.add_atrace_categories() = "cat_b";
-
-  FtraceConfig config_c = CreateFtraceConfig({});
-  *config_c.add_atrace_apps() = "app_c";
-  *config_c.add_atrace_categories() = "cat_c";
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "cat_a",
-                                                  "-a", "app_a"}),
-                                _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_a = 3;
-  ASSERT_TRUE(model.SetupConfig(id_a, config_a));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_a", "cat_b", "-a", "app_a,app_b"}),
-                _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_b = 13;
-  ASSERT_TRUE(model.SetupConfig(id_b, config_b));
-
-  EXPECT_CALL(atrace,
-              RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                          "--only_userspace", "cat_a", "cat_b",
-                                          "cat_c", "-a", "app_a,app_b,app_c"}),
-                        _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_c = 23;
-  ASSERT_TRUE(model.SetupConfig(id_c, config_c));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_a", "cat_c", "-a", "app_a,app_c"}),
-                _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_b));
-
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "cat_c",
-                                                  "-a", "app_c"}),
-                                _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_a));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_c));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceFailedConfig) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config_a = CreateFtraceConfig({});
-  *config_a.add_atrace_apps() = "app_1";
-  *config_a.add_atrace_apps() = "app_2";
-  *config_a.add_atrace_categories() = "cat_1";
-  *config_a.add_atrace_categories() = "cat_2";
-
-  FtraceConfig config_b = CreateFtraceConfig({});
-  *config_b.add_atrace_apps() = "app_fail";
-  *config_b.add_atrace_categories() = "cat_fail";
-
-  FtraceConfig config_c = CreateFtraceConfig({});
-  *config_c.add_atrace_apps() = "app_1";
-  *config_c.add_atrace_apps() = "app_3";
-  *config_c.add_atrace_categories() = "cat_1";
-  *config_c.add_atrace_categories() = "cat_3";
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_1", "cat_2", "-a", "app_1,app_2"}),
-                _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_a = 7;
-  ASSERT_TRUE(model.SetupConfig(id_a, config_a));
-
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "cat_1",
-                                                  "cat_2", "cat_fail", "-a",
-                                                  "app_1,app_2,app_fail"}),
-                                _))
-      .WillOnce(Return(false));
-  FtraceConfigId id_b = 17;
-  ASSERT_TRUE(model.SetupConfig(id_b, config_b));
-
-  EXPECT_CALL(atrace,
-              RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                          "--only_userspace", "cat_1", "cat_2",
-                                          "cat_3", "-a", "app_1,app_2,app_3"}),
-                        _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_c = 47;
-  ASSERT_TRUE(model.SetupConfig(id_c, config_c));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_1", "cat_2", "-a", "app_1,app_2"}),
-                _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_c));
-
-  // Removing the config we failed to enable doesn't change the atrace state
-  // so we don't expect a call here.
-  ASSERT_TRUE(model.RemoveConfig(id_b));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_a));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceDuplicateConfigs) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config_a = CreateFtraceConfig({});
-  *config_a.add_atrace_apps() = "app_1";
-  *config_a.add_atrace_categories() = "cat_1";
-
-  FtraceConfig config_b = CreateFtraceConfig({});
-  *config_b.add_atrace_apps() = "app_1";
-  *config_b.add_atrace_categories() = "cat_1";
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "cat_1",
-                                                  "-a", "app_1"}),
-                                _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_a = 19;
-  ASSERT_TRUE(model.SetupConfig(id_a, config_a));
-
-  FtraceConfigId id_b = 29;
-  ASSERT_TRUE(model.SetupConfig(id_b, config_b));
-
-  ASSERT_TRUE(model.RemoveConfig(id_a));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_b));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceAndFtraceConfigs) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config_a = CreateFtraceConfig({"sched/sched_cpu_hotplug"});
-
-  FtraceConfig config_b = CreateFtraceConfig({"sched/sched_switch"});
-  *config_b.add_atrace_categories() = "b";
-
-  FtraceConfig config_c = CreateFtraceConfig({"sched/sched_switch"});
-
-  FtraceConfig config_d = CreateFtraceConfig({"sched/sched_cpu_hotplug"});
-  *config_d.add_atrace_categories() = "d";
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-  FtraceConfigId id_a = 179;
-  ASSERT_TRUE(model.SetupConfig(id_a, config_a));
-
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "b"}),
-                                _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_b = 239;
-  ASSERT_TRUE(model.SetupConfig(id_b, config_b));
-
-  FtraceConfigId id_c = 101;
-  ASSERT_TRUE(model.SetupConfig(id_c, config_c));
-
-  EXPECT_CALL(atrace,
-              RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                          "--only_userspace", "b", "d"}),
-                        _))
-      .WillOnce(Return(true));
-  FtraceConfigId id_d = 47;
-  ASSERT_TRUE(model.SetupConfig(id_d, config_d));
-
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "b"}),
-                                _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_d));
-
-  ASSERT_TRUE(model.RemoveConfig(id_c));
-
-  EXPECT_CALL(
-      atrace,
-      RunAtrace(
-          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
-      .WillOnce(Return(true));
-  ASSERT_TRUE(model.RemoveConfig(id_b));
-
-  ASSERT_TRUE(model.RemoveConfig(id_a));
-}
-
-TEST_F(FtraceConfigMuxerTest, AtraceErrorsPropagated) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  MockRunAtrace atrace;
-
-  FtraceConfig config = CreateFtraceConfig({});
-  *config.add_atrace_categories() = "cat_1";
-  *config.add_atrace_categories() = "cat_2";
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
-      .WillByDefault(Return("0"));
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "cat_1",
-                                                  "cat_2"}),
-                                _))
-      .WillOnce(Invoke([](const std::vector<std::string>&, std::string* err) {
-        EXPECT_NE(err, nullptr);
-        if (err)
-          err->append("foo\nbar\n");
-        return true;
-      }));
-
-  FtraceSetupErrors errors{};
-  FtraceConfigId id_a = 23;
-  ASSERT_TRUE(model.SetupConfig(id_a, config, &errors));
-  EXPECT_EQ(errors.atrace_errors, "foo\nbar\n");
-}
-
-TEST_F(FtraceConfigMuxerTest, SetupClockForTesting) {
-  MockFtraceProcfs ftrace;
-  FtraceConfig config;
-
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-  namespace pb0 = protos::pbzero;
-
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  model.SetupClockForTesting(config);
-  // unspecified = boot.
-  EXPECT_EQ(model.ftrace_clock(),
-            static_cast<int>(pb0::FTRACE_CLOCK_UNSPECIFIED));
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "global"));
-  model.SetupClockForTesting(config);
-  EXPECT_EQ(model.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_GLOBAL));
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return(""));
-  model.SetupClockForTesting(config);
-  EXPECT_EQ(model.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_UNKNOWN));
-
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("local [global]"));
-  model.SetupClockForTesting(config);
-  EXPECT_EQ(model.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_GLOBAL));
-}
-
-TEST_F(FtraceConfigMuxerTest, GetFtraceEvents) {
-  MockFtraceProcfs ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
-  std::set<GroupAndName> events =
-      model.GetFtraceEventsForTesting(config, table_.get());
-
-  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
-  EXPECT_THAT(events, Not(Contains(GroupAndName("ftrace", "print"))));
-}
-
-TEST_F(FtraceConfigMuxerTest, GetFtraceEventsAtrace) {
-  MockFtraceProcfs ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  FtraceConfig config = CreateFtraceConfig({});
-  *config.add_atrace_categories() = "sched";
-  std::set<GroupAndName> events =
-      model.GetFtraceEventsForTesting(config, table_.get());
-
-  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
-  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_cpu_hotplug")));
-  EXPECT_THAT(events, Contains(GroupAndName("ftrace", "print")));
-}
-
-TEST_F(FtraceConfigMuxerTest, GetFtraceEventsAtraceCategories) {
-  MockFtraceProcfs ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
-  FtraceConfig config = CreateFtraceConfig({});
-  *config.add_atrace_categories() = "sched";
-  *config.add_atrace_categories() = "memreclaim";
-  std::set<GroupAndName> events =
-      model.GetFtraceEventsForTesting(config, table_.get());
-
-  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
-  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_cpu_hotplug")));
-  EXPECT_THAT(events, Contains(GroupAndName("cgroup", "cgroup_mkdir")));
-  EXPECT_THAT(events, Contains(GroupAndName("vmscan",
-                                            "mm_vmscan_direct_reclaim_begin")));
-  EXPECT_THAT(events,
-              Contains(GroupAndName("lowmemorykiller", "lowmemory_kill")));
-  EXPECT_THAT(events, Contains(GroupAndName("ftrace", "print")));
-}
-
-// Tests the enabling fallback logic that tries to use the "set_event" interface
-// if writing the individual xxx/enable file fails.
-TEST_F(FtraceConfigMuxerTest, FallbackOnSetEvent) {
-  MockFtraceProcfs ftrace;
-  FtraceConfig config =
-      CreateFtraceConfig({"sched/sched_switch", "cgroup/cgroup_mkdir"});
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_percent", _))
-      .WillRepeatedly(Return(true));
-
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
-      .WillOnce(Return("nop"));
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
-      .Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
-  EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/cgroup/cgroup_mkdir/enable", "1"))
-      .WillOnce(Return(false));
-  EXPECT_CALL(ftrace, AppendToFile("/root/set_event", "cgroup:cgroup_mkdir"))
-      .WillOnce(Return(true));
-  FtraceConfigId id = 97;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-  ASSERT_TRUE(model.ActivateConfig(id));
-
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
-  ASSERT_TRUE(ds_config);
-  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              Contains(kFakeSchedSwitchEventId));
-  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
-              Contains(kCgroupMkdirEventId));
-
-  const EventFilter* central_filter = model.GetCentralEventFilterForTesting();
-  EXPECT_THAT(central_filter->GetEnabledEvents(),
-              Contains(kFakeSchedSwitchEventId));
-  EXPECT_THAT(central_filter->GetEnabledEvents(),
-              Contains(kCgroupMkdirEventId));
-
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_switch/enable", "0"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/cgroup/cgroup_mkdir/enable", "0"))
-      .WillOnce(Return(false));
-  EXPECT_CALL(ftrace, AppendToFile("/root/set_event", "!cgroup:cgroup_mkdir"))
-      .WillOnce(Return(true));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", PageSizeKb()));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
-  ASSERT_TRUE(model.RemoveConfig(id));
+  ASSERT_FALSE(model.SetupConfig(/* id= */ 73, config));
 }
 
 TEST_F(FtraceConfigMuxerTest, CompactSchedConfig) {
@@ -1185,13 +289,14 @@
   auto valid_compact_format = CompactSchedEventFormat{
       /*format_valid=*/true, format_with_id, CompactSchedWakingFormat{}};
 
-  NiceMock<MockFtraceProcfs> ftrace;
-  table_ = CreateFakeTable(valid_compact_format);
-  FtraceConfigMuxer muxer(&ftrace, table_.get(), GetSyscallTable(), {});
+  std::unique_ptr<ProtoTranslationTable> table =
+      CreateFakeTable(valid_compact_format);
+  FtraceConfigMuxer muxer(&ftrace_, &atrace_wrapper_, table.get(),
+                          GetSyscallTable(), {});
 
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
       .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
       .WillByDefault(Return("0"));
 
   {
@@ -1246,38 +351,659 @@
   }
 }
 
-TEST_F(FtraceConfigMuxerTest, CompactSchedConfigWithInvalidFormat) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
+// Fixture that constructs a FtraceConfigMuxer with a fake
+// ProtoTranslationTable.
+class FtraceConfigMuxerFakeTableTest : public FtraceConfigMuxerTest {
+ protected:
+  std::unique_ptr<ProtoTranslationTable> table_ = CreateFakeTable();
+  FtraceConfigMuxer model_ = FtraceConfigMuxer(&ftrace_,
+                                               &atrace_wrapper_,
+                                               table_.get(),
+                                               GetSyscallTable(),
+                                               {});
+};
 
+TEST_F(FtraceConfigMuxerFakeTableTest, GenericSyscallFiltering) {
+  FtraceConfig config = CreateFtraceConfig({"raw_syscalls/sys_enter"});
+  *config.add_syscall_events() = "sys_open";
+  *config.add_syscall_events() = "sys_read";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile(_, _)).WillRepeatedly(Return(true));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/raw_syscalls/sys_enter/filter",
+                                   "id == 0 || id == 1"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/raw_syscalls/sys_exit/filter",
+                                   "id == 0 || id == 1"));
+
+  FtraceConfigId id = 37;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const std::set<size_t>& filter = model_.GetSyscallFilterForTesting();
+  ASSERT_THAT(filter, UnorderedElementsAre(0, 1));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, UnknownSyscallFilter) {
+  FtraceConfig config = CreateFtraceConfig({"raw_syscalls/sys_enter"});
+  config.add_syscall_events("sys_open");
+  config.add_syscall_events("sys_not_a_call");
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+
+  // Unknown syscall is ignored.
+  ASSERT_TRUE(model_.SetupConfig(/*id = */ 73, config));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, SyscallFilterMuxing) {
+  FtraceConfig empty_config = CreateFtraceConfig({});
+
+  FtraceConfig syscall_config = empty_config;
+  syscall_config.add_ftrace_events("raw_syscalls/sys_enter");
+
+  FtraceConfig syscall_open_config = syscall_config;
+  syscall_open_config.add_syscall_events("sys_open");
+
+  FtraceConfig syscall_read_config = syscall_config;
+  syscall_read_config.add_syscall_events("sys_read");
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+
+  // Expect no filter for non-syscall config.
+  ASSERT_TRUE(model_.SetupConfig(/* id= */ 179239, empty_config));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre());
+
+  // Expect no filter for syscall config with no specified events.
+  FtraceConfigId syscall_id = 73;
+  ASSERT_TRUE(model_.SetupConfig(syscall_id, syscall_config));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre());
+
+  // Still expect no filter to satisfy this and the above.
+  FtraceConfigId syscall_open_id = 101;
+  ASSERT_TRUE(model_.SetupConfig(syscall_open_id, syscall_open_config));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre());
+
+  // After removing the generic syscall trace, only the one with filter is left.
+  ASSERT_TRUE(model_.RemoveConfig(syscall_id));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
+
+  // With sys_read and sys_open traced separately, filter includes both.
+  FtraceConfigId syscall_read_id = 57;
+  ASSERT_TRUE(model_.SetupConfig(syscall_read_id, syscall_read_config));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre(0, 1));
+
+  // After removing configs with filters, filter is reset to empty.
+  ASSERT_TRUE(model_.RemoveConfig(syscall_open_id));
+  ASSERT_TRUE(model_.RemoveConfig(syscall_read_id));
+  ASSERT_THAT(model_.GetSyscallFilterForTesting(), UnorderedElementsAre());
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, TurnFtraceOnOff) {
+  FtraceConfig config = CreateFtraceConfig({"sched_switch", "foo"});
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
+
+  FtraceConfigId id = 97;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              ElementsAreArray({kFakeSchedSwitchEventId}));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              ElementsAreArray({kFakeSchedSwitchEventId}));
+
+  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace_));
+  EXPECT_CALL(ftrace_, NumberOfCpus()).Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_percent", _))
+      .WillRepeatedly(Return(true));
+
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_switch/enable", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", PageSizeKb()));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+
+  ASSERT_TRUE(model_.RemoveConfig(id));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, FtraceIsAlreadyOn) {
+  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
+
+  // If someone is using ftrace already don't stomp on what they are doing.
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("function"));
+  ASSERT_FALSE(model_.SetupConfig(/* id= */ 123, config));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, Atrace) {
+  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
+  *config.add_atrace_categories() = "sched";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "sched"}),
+                        _))
+      .WillOnce(Return(true));
+
+  FtraceConfigId id = 57;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  // "ftrace" group events are always enabled, and therefore the "print" event
+  // will show up in the per data source event filter (as we want to record it),
+  // but not the central filter (as we're not enabling/disabling it).
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              Contains(kFakeSchedSwitchEventId));
+  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              Contains(kFakePrintEventId));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  EXPECT_THAT(central_filter->GetEnabledEvents(),
+              Contains(kFakeSchedSwitchEventId));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceTwoApps) {
+  FtraceConfig config = CreateFtraceConfig({});
+  *config.add_atrace_apps() = "com.google.android.gms.persistent";
+  *config.add_atrace_apps() = "com.google.android.gms";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray(
+              {"atrace", "--async_start", "--only_userspace", "-a",
+               "com.google.android.gms,com.google.android.gms.persistent"}),
+          _))
+      .WillOnce(Return(true));
+
+  FtraceConfigId id = 97;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              Contains(kFakePrintEventId));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceMultipleConfigs) {
+  FtraceConfig config_a = CreateFtraceConfig({});
+  *config_a.add_atrace_apps() = "app_a";
+  *config_a.add_atrace_categories() = "cat_a";
+
+  FtraceConfig config_b = CreateFtraceConfig({});
+  *config_b.add_atrace_apps() = "app_b";
+  *config_b.add_atrace_categories() = "cat_b";
+
+  FtraceConfig config_c = CreateFtraceConfig({});
+  *config_c.add_atrace_apps() = "app_c";
+  *config_c.add_atrace_categories() = "cat_c";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_a", "-a", "app_a"}),
+                _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_a = 3;
+  ASSERT_TRUE(model_.SetupConfig(id_a, config_a));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_a", "cat_b", "-a", "app_a,app_b"}),
+                _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_b = 13;
+  ASSERT_TRUE(model_.SetupConfig(id_b, config_b));
+
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "cat_a", "cat_b",
+                                          "cat_c", "-a", "app_a,app_b,app_c"}),
+                        _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_c = 23;
+  ASSERT_TRUE(model_.SetupConfig(id_c, config_c));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_a", "cat_c", "-a", "app_a,app_c"}),
+                _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_b));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_c", "-a", "app_c"}),
+                _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_a));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_c));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceFailedConfig) {
+  FtraceConfig config_a = CreateFtraceConfig({});
+  *config_a.add_atrace_apps() = "app_1";
+  *config_a.add_atrace_apps() = "app_2";
+  *config_a.add_atrace_categories() = "cat_1";
+  *config_a.add_atrace_categories() = "cat_2";
+
+  FtraceConfig config_b = CreateFtraceConfig({});
+  *config_b.add_atrace_apps() = "app_fail";
+  *config_b.add_atrace_categories() = "cat_fail";
+
+  FtraceConfig config_c = CreateFtraceConfig({});
+  *config_c.add_atrace_apps() = "app_1";
+  *config_c.add_atrace_apps() = "app_3";
+  *config_c.add_atrace_categories() = "cat_1";
+  *config_c.add_atrace_categories() = "cat_3";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_1", "cat_2", "-a", "app_1,app_2"}),
+                _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_a = 7;
+  ASSERT_TRUE(model_.SetupConfig(id_a, config_a));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_1", "cat_2", "cat_fail", "-a",
+                                  "app_1,app_2,app_fail"}),
+                _))
+      .WillOnce(Return(false));
+  FtraceConfigId id_b = 17;
+  ASSERT_TRUE(model_.SetupConfig(id_b, config_b));
+
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "cat_1", "cat_2",
+                                          "cat_3", "-a", "app_1,app_2,app_3"}),
+                        _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_c = 47;
+  ASSERT_TRUE(model_.SetupConfig(id_c, config_c));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_1", "cat_2", "-a", "app_1,app_2"}),
+                _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_c));
+
+  // Removing the config we failed to enable doesn't change the atrace state
+  // so we don't expect a call here.
+  ASSERT_TRUE(model_.RemoveConfig(id_b));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_a));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceDuplicateConfigs) {
+  FtraceConfig config_a = CreateFtraceConfig({});
+  *config_a.add_atrace_apps() = "app_1";
+  *config_a.add_atrace_categories() = "cat_1";
+
+  FtraceConfig config_b = CreateFtraceConfig({});
+  *config_b.add_atrace_apps() = "app_1";
+  *config_b.add_atrace_categories() = "cat_1";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_1", "-a", "app_1"}),
+                _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_a = 19;
+  ASSERT_TRUE(model_.SetupConfig(id_a, config_a));
+
+  FtraceConfigId id_b = 29;
+  ASSERT_TRUE(model_.SetupConfig(id_b, config_b));
+
+  ASSERT_TRUE(model_.RemoveConfig(id_a));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_b));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceAndFtraceConfigs) {
+  FtraceConfig config_a = CreateFtraceConfig({"sched/sched_cpu_hotplug"});
+
+  FtraceConfig config_b = CreateFtraceConfig({"sched/sched_switch"});
+  *config_b.add_atrace_categories() = "b";
+
+  FtraceConfig config_c = CreateFtraceConfig({"sched/sched_switch"});
+
+  FtraceConfig config_d = CreateFtraceConfig({"sched/sched_cpu_hotplug"});
+  *config_d.add_atrace_categories() = "d";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+  FtraceConfigId id_a = 179;
+  ASSERT_TRUE(model_.SetupConfig(id_a, config_a));
+
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "b"}),
+                        _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_b = 239;
+  ASSERT_TRUE(model_.SetupConfig(id_b, config_b));
+
+  FtraceConfigId id_c = 101;
+  ASSERT_TRUE(model_.SetupConfig(id_c, config_c));
+
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "b", "d"}),
+                        _))
+      .WillOnce(Return(true));
+  FtraceConfigId id_d = 47;
+  ASSERT_TRUE(model_.SetupConfig(id_d, config_d));
+
+  EXPECT_CALL(atrace_wrapper_,
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "b"}),
+                        _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_d));
+
+  ASSERT_TRUE(model_.RemoveConfig(id_c));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
+      .WillOnce(Return(true));
+  ASSERT_TRUE(model_.RemoveConfig(id_b));
+
+  ASSERT_TRUE(model_.RemoveConfig(id_a));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, AtraceErrorsPropagated) {
+  FtraceConfig config = CreateFtraceConfig({});
+  *config.add_atrace_categories() = "cat_1";
+  *config.add_atrace_categories() = "cat_2";
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
+  EXPECT_CALL(
+      atrace_wrapper_,
+      RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
+                                  "cat_1", "cat_2"}),
+                _))
+      .WillOnce(Invoke([](const std::vector<std::string>&, std::string* err) {
+        EXPECT_NE(err, nullptr);
+        if (err)
+          err->append("foo\nbar\n");
+        return true;
+      }));
+
+  FtraceSetupErrors errors{};
+  FtraceConfigId id_a = 23;
+  ASSERT_TRUE(model_.SetupConfig(id_a, config, &errors));
+  EXPECT_EQ(errors.atrace_errors, "foo\nbar\n");
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, SetupClockForTesting) {
+  FtraceConfig config;
+
+  namespace pb0 = protos::pbzero;
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  model_.SetupClockForTesting(config);
+  // unspecified = boot.
+  EXPECT_EQ(model_.ftrace_clock(),
+            static_cast<int>(pb0::FTRACE_CLOCK_UNSPECIFIED));
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "global"));
+  model_.SetupClockForTesting(config);
+  EXPECT_EQ(model_.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_GLOBAL));
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return(""));
+  model_.SetupClockForTesting(config);
+  EXPECT_EQ(model_.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_UNKNOWN));
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("local [global]"));
+  model_.SetupClockForTesting(config);
+  EXPECT_EQ(model_.ftrace_clock(), static_cast<int>(pb0::FTRACE_CLOCK_GLOBAL));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, GetFtraceEvents) {
+  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
+  std::set<GroupAndName> events =
+      model_.GetFtraceEventsForTesting(config, table_.get());
+
+  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
+  EXPECT_THAT(events, Not(Contains(GroupAndName("ftrace", "print"))));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, GetFtraceEventsAtrace) {
+  FtraceConfig config = CreateFtraceConfig({});
+  *config.add_atrace_categories() = "sched";
+  std::set<GroupAndName> events =
+      model_.GetFtraceEventsForTesting(config, table_.get());
+
+  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
+  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_cpu_hotplug")));
+  EXPECT_THAT(events, Contains(GroupAndName("ftrace", "print")));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, GetFtraceEventsAtraceCategories) {
+  FtraceConfig config = CreateFtraceConfig({});
+  *config.add_atrace_categories() = "sched";
+  *config.add_atrace_categories() = "memreclaim";
+  std::set<GroupAndName> events =
+      model_.GetFtraceEventsForTesting(config, table_.get());
+
+  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_switch")));
+  EXPECT_THAT(events, Contains(GroupAndName("sched", "sched_cpu_hotplug")));
+  EXPECT_THAT(events, Contains(GroupAndName("cgroup", "cgroup_mkdir")));
+  EXPECT_THAT(events, Contains(GroupAndName("vmscan",
+                                            "mm_vmscan_direct_reclaim_begin")));
+  EXPECT_THAT(events,
+              Contains(GroupAndName("lowmemorykiller", "lowmemory_kill")));
+  EXPECT_THAT(events, Contains(GroupAndName("ftrace", "print")));
+}
+
+// Tests the enabling fallback logic that tries to use the "set_event" interface
+// if writing the individual xxx/enable file fails.
+TEST_F(FtraceConfigMuxerFakeTableTest, FallbackOnSetEvent) {
+  FtraceConfig config =
+      CreateFtraceConfig({"sched/sched_switch", "cgroup/cgroup_mkdir"});
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_percent", _))
+      .WillRepeatedly(Return(true));
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/cgroup/cgroup_mkdir/enable", "1"))
+      .WillOnce(Return(false));
+  EXPECT_CALL(ftrace_, AppendToFile("/root/set_event", "cgroup:cgroup_mkdir"))
+      .WillOnce(Return(true));
+  FtraceConfigId id = 97;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              Contains(kFakeSchedSwitchEventId));
+  EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              Contains(kCgroupMkdirEventId));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  EXPECT_THAT(central_filter->GetEnabledEvents(),
+              Contains(kFakeSchedSwitchEventId));
+  EXPECT_THAT(central_filter->GetEnabledEvents(),
+              Contains(kCgroupMkdirEventId));
+
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_switch/enable", "0"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/cgroup/cgroup_mkdir/enable", "0"))
+      .WillOnce(Return(false));
+  EXPECT_CALL(ftrace_, AppendToFile("/root/set_event", "!cgroup:cgroup_mkdir"))
+      .WillOnce(Return(true));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", PageSizeKb()));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.RemoveConfig(id));
+}
+
+TEST_F(FtraceConfigMuxerFakeTableTest, CompactSchedConfigWithInvalidFormat) {
   // Request compact encoding.
   FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
   config.mutable_compact_sched()->set_enabled(true);
 
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
       .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
       .WillByDefault(Return("0"));
 
   FtraceConfigId id = 67;
-  ASSERT_TRUE(model.SetupConfig(id, config));
+  ASSERT_TRUE(model_.SetupConfig(id, config));
 
   // The translation table says that the scheduling events' format didn't match
   // compile-time assumptions, so we won't enable compact events even if
   // requested.
-  const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
   ASSERT_TRUE(ds_config);
   EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
               Contains(kFakeSchedSwitchEventId));
   EXPECT_FALSE(ds_config->compact_sched.enabled);
 }
 
-TEST_F(FtraceConfigMuxerTest, SkipGenericEventsOption) {
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
-
+TEST_F(FtraceConfigMuxerFakeTableTest, SkipGenericEventsOption) {
   static constexpr int kFtraceGenericEventId = 42;
-  ON_CALL(table_procfs_, ReadEventFormat("sched", "generic"))
+  ON_CALL(ftrace_, ReadEventFormat("sched", "generic"))
       .WillByDefault(Return(R"(name: generic
 ID: 42
 format:
@@ -1297,15 +1023,15 @@
       CreateFtraceConfig({"sched/sched_switch", "sched/generic"});
   config_with_disable.set_disable_generic_events(true);
 
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
       .WillByDefault(Return("nop"));
-  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
       .WillByDefault(Return("0"));
 
   {
     FtraceConfigId id = 123;
-    ASSERT_TRUE(model.SetupConfig(id, config_default));
-    const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
+    ASSERT_TRUE(model_.SetupConfig(id, config_default));
+    const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
     ASSERT_TRUE(ds_config);
     // Both events enabled for the data source by default.
     EXPECT_THAT(
@@ -1314,8 +1040,8 @@
   }
   {
     FtraceConfigId id = 321;
-    ASSERT_TRUE(model.SetupConfig(id, config_with_disable));
-    const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
+    ASSERT_TRUE(model_.SetupConfig(id, config_with_disable));
+    const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
     ASSERT_TRUE(ds_config);
     // Only the statically known event is enabled.
     EXPECT_THAT(ds_config->event_filter.GetEnabledEvents(),
@@ -1323,11 +1049,7 @@
   }
 }
 
-TEST_F(FtraceConfigMuxerTest, Funcgraph) {
-  auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
-
+TEST_F(FtraceConfigMuxerFakeTableTest, Funcgraph) {
   FtraceConfig config;
   config.set_enable_function_graph(true);
   *config.add_function_filters() = "sched*";
@@ -1336,76 +1058,276 @@
   *config.add_function_graph_roots() = "sched*";
   *config.add_function_graph_roots() = "*mm_fault";
 
-  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
       .WillByDefault(Return("nop"));
 
-  EXPECT_CALL(ftrace, WriteToFile(_, _)).WillRepeatedly(Return(true));
+  EXPECT_CALL(ftrace_, WriteToFile(_, _)).WillRepeatedly(Return(true));
 
-  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
-  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
 
   // Set up config, assert that the tracefs writes happened:
-  EXPECT_CALL(ftrace, ClearFile("/root/set_ftrace_filter"));
-  EXPECT_CALL(ftrace, ClearFile("/root/set_graph_function"));
-  EXPECT_CALL(ftrace, AppendToFile("/root/set_ftrace_filter",
-                                   "sched*\nhandle_mm_fault"))
+  EXPECT_CALL(ftrace_, ClearFile("/root/set_ftrace_filter"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/set_graph_function"));
+  EXPECT_CALL(ftrace_, AppendToFile("/root/set_ftrace_filter",
+                                    "sched*\nhandle_mm_fault"))
       .WillOnce(Return(true));
-  EXPECT_CALL(ftrace,
+  EXPECT_CALL(ftrace_,
               AppendToFile("/root/set_graph_function", "sched*\n*mm_fault"))
       .WillOnce(Return(true));
-  EXPECT_CALL(ftrace, WriteToFile("/root/current_tracer", "function_graph"))
+  EXPECT_CALL(ftrace_, WriteToFile("/root/current_tracer", "function_graph"))
       .WillOnce(Return(true));
   FtraceConfigId id = 43;
-  ASSERT_TRUE(model.SetupConfig(id, config));
-  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace));
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace_));
   // Toggle config on and off, tracer won't be reset yet:
-  ASSERT_TRUE(model.ActivateConfig(id));
-  ASSERT_TRUE(model.RemoveConfig(id));
-  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+  ASSERT_TRUE(model_.RemoveConfig(id));
+  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace_));
 
   // Emulate ftrace_controller's call to ResetCurrentTracer (see impl comments
   // for why RemoveConfig is insufficient).
-  EXPECT_CALL(ftrace, ClearFile("/root/set_ftrace_filter"));
-  EXPECT_CALL(ftrace, ClearFile("/root/set_graph_function"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/current_tracer", "nop"))
+  EXPECT_CALL(ftrace_, ClearFile("/root/set_ftrace_filter"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/set_graph_function"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/current_tracer", "nop"))
       .WillOnce(Return(true));
-  ASSERT_TRUE(model.ResetCurrentTracer());
-  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace));
+  ASSERT_TRUE(model_.ResetCurrentTracer());
+  ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace_));
 }
 
-TEST_F(FtraceConfigMuxerTest, SecondaryInstanceDoNotSupportAtrace) {
-  auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {},
-                          /* secondary_instance= */ true);
-
-  FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
-  *config.add_atrace_categories() = "sched";
-
-  ASSERT_FALSE(model.SetupConfig(/* id= */ 73, config));
-}
-
-TEST_F(FtraceConfigMuxerTest, PreserveFtraceBufferNotSetBufferSizeKb) {
-  auto fake_table = CreateFakeTable();
-  NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {},
-                          /* secondary_instance= */ false);
-
+TEST_F(FtraceConfigMuxerFakeTableTest, PreserveFtraceBufferNotSetBufferSizeKb) {
   FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
 
   config.set_preserve_ftrace_buffer(true);
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
       .WillOnce(Return('1'));
-  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
       .WillByDefault(Return("[local] global boot"));
-  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
       .Times(AnyNumber());
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _)).Times(0);
-  EXPECT_CALL(ftrace,
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _)).Times(0);
+  EXPECT_CALL(ftrace_,
               WriteToFile("/root/events/sched/sched_switch/enable", "1"));
 
   FtraceConfigId id = 44;
-  ASSERT_TRUE(model.SetupConfig(id, config));
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+}
+
+// Fixture that constructs a FtraceConfigMuxer with a mock
+// ProtoTranslationTable.
+class FtraceConfigMuxerMockTableTest : public FtraceConfigMuxerTest {
+ protected:
+  std::unique_ptr<MockProtoTranslationTable> mock_table_ = GetMockTable();
+  FtraceConfigMuxer model_ = FtraceConfigMuxer(&ftrace_,
+                                               &atrace_wrapper_,
+                                               mock_table_.get(),
+                                               GetSyscallTable(),
+                                               {});
+};
+
+TEST_F(FtraceConfigMuxerMockTableTest, AddGenericEvent) {
+  FtraceConfig config = CreateFtraceConfig({"power/cpu_frequency"});
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/power/cpu_frequency/enable", "1"));
+  EXPECT_CALL(*mock_table_, GetEvent(GroupAndName("power", "cpu_frequency")))
+      .Times(AnyNumber());
+
+  static constexpr int kExpectedEventId = 77;
+  Event event_to_return;
+  event_to_return.name = "cpu_frequency";
+  event_to_return.group = "power";
+  event_to_return.ftrace_event_id = kExpectedEventId;
+  ON_CALL(*mock_table_,
+          GetOrCreateEvent(GroupAndName("power", "cpu_frequency")))
+      .WillByDefault(Return(&event_to_return));
+  EXPECT_CALL(*mock_table_,
+              GetOrCreateEvent(GroupAndName("power", "cpu_frequency")));
+
+  FtraceConfigId id = 7;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              ElementsAreArray({kExpectedEventId}));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              ElementsAreArray({kExpectedEventId}));
+}
+
+TEST_F(FtraceConfigMuxerMockTableTest, AddAllEvents) {
+  FtraceConfig config = CreateFtraceConfig({"sched/*"});
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_switch/enable", "1"));
+  EXPECT_CALL(ftrace_,
+              WriteToFile("/root/events/sched/sched_new_event/enable", "1"));
+
+  std::set<std::string> n = {"sched_switch", "sched_new_event"};
+  ON_CALL(ftrace_, GetEventNamesForGroup("events/sched"))
+      .WillByDefault(Return(n));
+  EXPECT_CALL(ftrace_, GetEventNamesForGroup("events/sched")).Times(1);
+
+  // Non-generic event.
+  static constexpr int kSchedSwitchEventId = 1;
+  Event sched_switch = {"sched_switch", "sched", {}, 0, 0, 0};
+  sched_switch.ftrace_event_id = kSchedSwitchEventId;
+  ON_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("sched", "sched_switch")))
+      .WillByDefault(Return(&sched_switch));
+  EXPECT_CALL(*mock_table_,
+              GetOrCreateEvent(GroupAndName("sched", "sched_switch")))
+      .Times(AnyNumber());
+
+  // Generic event.
+  static constexpr int kGenericEventId = 2;
+  Event event_to_return;
+  event_to_return.name = "sched_new_event";
+  event_to_return.group = "sched";
+  event_to_return.ftrace_event_id = kGenericEventId;
+  ON_CALL(*mock_table_,
+          GetOrCreateEvent(GroupAndName("sched", "sched_new_event")))
+      .WillByDefault(Return(&event_to_return));
+  EXPECT_CALL(*mock_table_,
+              GetOrCreateEvent(GroupAndName("sched", "sched_new_event")));
+
+  FtraceConfigId id = 13;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+  ASSERT_TRUE(id);
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              ElementsAreArray({kSchedSwitchEventId, kGenericEventId}));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              ElementsAreArray({kSchedSwitchEventId, kGenericEventId}));
+}
+
+TEST_F(FtraceConfigMuxerMockTableTest, TwoWildcardGroups) {
+  FtraceConfig config = CreateFtraceConfig({"group_one/*", "group_two/*"});
+
+  std::set<std::string> event_names = {"foo"};
+  ON_CALL(ftrace_, GetEventNamesForGroup("events/group_one"))
+      .WillByDefault(Return(event_names));
+  EXPECT_CALL(ftrace_, GetEventNamesForGroup("events/group_one"))
+      .Times(AnyNumber());
+
+  ON_CALL(ftrace_, GetEventNamesForGroup("events/group_two"))
+      .WillByDefault(Return(event_names));
+  EXPECT_CALL(ftrace_, GetEventNamesForGroup("events/group_two"))
+      .Times(AnyNumber());
+
+  static constexpr int kEventId1 = 1;
+  Event event1;
+  event1.name = "foo";
+  event1.group = "group_one";
+  event1.ftrace_event_id = kEventId1;
+  ON_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_one", "foo")))
+      .WillByDefault(Return(&event1));
+  EXPECT_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_one", "foo")));
+
+  static constexpr int kEventId2 = 2;
+  Event event2;
+  event2.name = "foo";
+  event2.group = "group_two";
+  event2.ftrace_event_id = kEventId2;
+  ON_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_two", "foo")))
+      .WillByDefault(Return(&event2));
+  EXPECT_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_two", "foo")));
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
+  FtraceConfigId id = 23;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              ElementsAreArray({kEventId1, kEventId2}));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              ElementsAreArray({kEventId1, kEventId2}));
+}
+
+TEST_F(FtraceConfigMuxerMockTableTest, AddSameNameEvents) {
+  FtraceConfig config = CreateFtraceConfig({"group_one/foo", "group_two/foo"});
+
+  static constexpr int kEventId1 = 1;
+  Event event1;
+  event1.name = "foo";
+  event1.group = "group_one";
+  event1.ftrace_event_id = kEventId1;
+  ON_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_one", "foo")))
+      .WillByDefault(Return(&event1));
+  EXPECT_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_one", "foo")));
+
+  static constexpr int kEventId2 = 2;
+  Event event2;
+  event2.name = "foo";
+  event2.group = "group_two";
+  event2.ftrace_event_id = kEventId2;
+  ON_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_two", "foo")))
+      .WillByDefault(Return(&event2));
+  EXPECT_CALL(*mock_table_, GetOrCreateEvent(GroupAndName("group_two", "foo")));
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
+  FtraceConfigId id = 5;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              ElementsAreArray({kEventId1, kEventId2}));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              ElementsAreArray({kEventId1, kEventId2}));
 }
 
 }  // namespace
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index ad924fa..750629d 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -179,25 +179,31 @@
   if (!table)
     return nullptr;
 
+  auto atrace_wrapper = std::make_unique<AtraceWrapperImpl>();
+
   std::map<std::string, std::vector<GroupAndName>> vendor_evts =
       GetAtraceVendorEvents(ftrace_procfs.get());
 
   SyscallTable syscalls = SyscallTable::FromCurrentArch();
 
   auto muxer = std::make_unique<FtraceConfigMuxer>(
-      ftrace_procfs.get(), table.get(), std::move(syscalls), vendor_evts);
-  return std::unique_ptr<FtraceController>(
-      new FtraceController(std::move(ftrace_procfs), std::move(table),
-                           std::move(muxer), runner, observer));
+      ftrace_procfs.get(), atrace_wrapper.get(), table.get(),
+      std::move(syscalls), vendor_evts);
+  return std::unique_ptr<FtraceController>(new FtraceController(
+      std::move(ftrace_procfs), std::move(table), std::move(atrace_wrapper),
+      std::move(muxer), runner, observer));
 }
 
-FtraceController::FtraceController(std::unique_ptr<FtraceProcfs> ftrace_procfs,
-                                   std::unique_ptr<ProtoTranslationTable> table,
-                                   std::unique_ptr<FtraceConfigMuxer> muxer,
-                                   base::TaskRunner* task_runner,
-                                   Observer* observer)
+FtraceController::FtraceController(
+    std::unique_ptr<FtraceProcfs> ftrace_procfs,
+    std::unique_ptr<ProtoTranslationTable> table,
+    std::unique_ptr<AtraceWrapper> atrace_wrapper,
+    std::unique_ptr<FtraceConfigMuxer> muxer,
+    base::TaskRunner* task_runner,
+    Observer* observer)
     : task_runner_(task_runner),
       observer_(observer),
+      atrace_wrapper_(std::move(atrace_wrapper)),
       primary_(std::move(ftrace_procfs), std::move(table), std::move(muxer)),
       weak_factory_(this) {}
 
@@ -816,7 +822,8 @@
   auto syscalls = SyscallTable::FromCurrentArch();
 
   auto muxer = std::make_unique<FtraceConfigMuxer>(
-      ftrace_procfs.get(), table.get(), std::move(syscalls), vendor_evts,
+      ftrace_procfs.get(), atrace_wrapper_.get(), table.get(),
+      std::move(syscalls), vendor_evts,
       /* secondary_instance= */ true);
   return std::make_unique<FtraceInstanceState>(
       std::move(ftrace_procfs), std::move(table), std::move(muxer));
diff --git a/src/traced/probes/ftrace/ftrace_controller.h b/src/traced/probes/ftrace/ftrace_controller.h
index 1e7c575..a858dfe 100644
--- a/src/traced/probes/ftrace/ftrace_controller.h
+++ b/src/traced/probes/ftrace/ftrace_controller.h
@@ -29,6 +29,7 @@
 #include "perfetto/ext/base/weak_ptr.h"
 #include "perfetto/ext/tracing/core/basic_types.h"
 #include "src/kallsyms/lazy_kernel_symbolizer.h"
+#include "src/traced/probes/ftrace/atrace_wrapper.h"
 #include "src/traced/probes/ftrace/cpu_reader.h"
 #include "src/traced/probes/ftrace/ftrace_config_utils.h"
 
@@ -99,6 +100,7 @@
 
   FtraceController(std::unique_ptr<FtraceProcfs>,
                    std::unique_ptr<ProtoTranslationTable>,
+                   std::unique_ptr<AtraceWrapper>,
                    std::unique_ptr<FtraceConfigMuxer>,
                    base::TaskRunner*,
                    Observer*);
@@ -123,6 +125,8 @@
 
   virtual uint64_t NowMs() const;
 
+  AtraceWrapper* atrace_wrapper() const { return atrace_wrapper_.get(); }
+
  private:
   friend class TestFtraceController;
   enum class PollSupport { kUntested, kSupported, kUnsupported };
@@ -168,6 +172,7 @@
   bool retain_ksyms_on_stop_ = false;
   PollSupport buffer_watermark_support_ = PollSupport::kUntested;
   std::set<FtraceDataSource*> data_sources_;
+  std::unique_ptr<AtraceWrapper> atrace_wrapper_;
   // Default tracefs instance (normally /sys/kernel/tracing) is valid for as
   // long as the controller is valid.
   // Secondary instances (i.e. /sys/kernel/tracing/instances/...) are created
diff --git a/src/traced/probes/ftrace/ftrace_controller_unittest.cc b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
index 28ca2d4..1a3538e 100644
--- a/src/traced/probes/ftrace/ftrace_controller_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
@@ -102,9 +102,10 @@
 }
 
 std::unique_ptr<FtraceConfigMuxer> FakeMuxer(FtraceProcfs* ftrace,
+                                             AtraceWrapper* atrace_wrapper,
                                              ProtoTranslationTable* table) {
   return std::unique_ptr<FtraceConfigMuxer>(new FtraceConfigMuxer(
-      ftrace, table, SyscallTable(Architecture::kUnknown), {}));
+      ftrace, atrace_wrapper, table, SyscallTable(Architecture::kUnknown), {}));
 }
 
 class MockFtraceProcfs : public FtraceProcfs {
@@ -201,6 +202,12 @@
   std::string current_tracer_ = "nop";
 };
 
+class MockAtraceWrapper : public AtraceWrapper {
+ public:
+  MOCK_METHOD(bool, RunAtrace, (const std::vector<std::string>&, std::string*));
+  MOCK_METHOD(bool, IsOldAtrace, ());
+};
+
 }  // namespace
 
 class TestFtraceController : public FtraceController,
@@ -208,11 +215,13 @@
  public:
   TestFtraceController(std::unique_ptr<MockFtraceProcfs> ftrace_procfs,
                        std::unique_ptr<Table> table,
+                       std::unique_ptr<AtraceWrapper> atrace_wrapper,
                        std::unique_ptr<FtraceConfigMuxer> muxer,
                        std::unique_ptr<MockTaskRunner> runner,
                        MockFtraceProcfs* raw_procfs)
       : FtraceController(std::move(ftrace_procfs),
                          std::move(table),
+                         std::move(atrace_wrapper),
                          std::move(muxer),
                          runner.get(),
                          /*observer=*/this),
@@ -256,7 +265,7 @@
     PERFETTO_CHECK(ftrace_procfs);
 
     auto table = FakeTable(ftrace_procfs.get());
-    auto muxer = FakeMuxer(ftrace_procfs.get(), table.get());
+    auto muxer = FakeMuxer(ftrace_procfs.get(), atrace_wrapper(), table.get());
     return std::unique_ptr<FtraceController::FtraceInstanceState>(
         new FtraceController::FtraceInstanceState(
             std::move(ftrace_procfs), std::move(table), std::move(muxer)));
@@ -289,14 +298,17 @@
         new MockFtraceProcfs("/root/", cpu_count));
   }
 
+  std::unique_ptr<AtraceWrapper> atrace_wrapper;
+
   auto table = FakeTable(ftrace_procfs.get());
 
-  auto muxer = FakeMuxer(ftrace_procfs.get(), table.get());
+  auto muxer =
+      FakeMuxer(ftrace_procfs.get(), atrace_wrapper.get(), table.get());
 
   MockFtraceProcfs* raw_procfs = ftrace_procfs.get();
   return std::unique_ptr<TestFtraceController>(new TestFtraceController(
-      std::move(ftrace_procfs), std::move(table), std::move(muxer),
-      std::move(runner), raw_procfs));
+      std::move(ftrace_procfs), std::move(table), std::move(atrace_wrapper),
+      std::move(muxer), std::move(runner), raw_procfs));
 }
 
 }  // namespace