Merge "ftrace: record failed atrace & ftrace categories in TraceStats"
diff --git a/protos/perfetto/trace/ftrace/ftrace_stats.proto b/protos/perfetto/trace/ftrace/ftrace_stats.proto
index 1f65456..11b7cbc 100644
--- a/protos/perfetto/trace/ftrace/ftrace_stats.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_stats.proto
@@ -80,4 +80,14 @@
 
   // The memory used by the kernel symbolizer (KernelSymbolMap.size_bytes()).
   optional uint32 kernel_symbols_mem_kb = 4;
+
+  // Atrace errors (even non-fatal ones) are reported here. A typical example is
+  // one or more atrace categories not available on the device.
+  optional string atrace_errors = 5;
+
+  // Ftrace events requested by the config but not present on device.
+  repeated string unknown_ftrace_events = 6;
+
+  // Ftrace events requested by the config, present, which we failed to enable.
+  repeated string failed_ftrace_events = 7;
 }
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 0ee96f8..ea46bd2 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -6516,6 +6516,16 @@
 
   // The memory used by the kernel symbolizer (KernelSymbolMap.size_bytes()).
   optional uint32 kernel_symbols_mem_kb = 4;
+
+  // Atrace errors (even non-fatal ones) are reported here. A typical example is
+  // one or more atrace categories not available on the device.
+  optional string atrace_errors = 5;
+
+  // Ftrace events requested by the config but not present on device.
+  repeated string unknown_ftrace_events = 6;
+
+  // Ftrace events requested by the config, present, which we failed to enable.
+  repeated string failed_ftrace_events = 7;
 }
 
 // End of protos/perfetto/trace/ftrace/ftrace_stats.proto
diff --git a/src/traced/probes/ftrace/atrace_wrapper.cc b/src/traced/probes/ftrace/atrace_wrapper.cc
index 22523f1..632c131 100644
--- a/src/traced/probes/ftrace/atrace_wrapper.cc
+++ b/src/traced/probes/ftrace/atrace_wrapper.cc
@@ -43,7 +43,8 @@
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
 // Args should include "atrace" for argv[0].
-bool ExecvAtrace(const std::vector<std::string>& args) {
+bool ExecvAtrace(const std::vector<std::string>& args,
+                 std::string* atrace_errors) {
   int status = 1;
 
   std::vector<char*> argv;
@@ -163,20 +164,22 @@
   PERFETTO_EINTR(waitpid(pid, &status, 0));
 
   bool ok = WIFEXITED(status) && WEXITSTATUS(status) == 0;
-  if (!ok) {
+  if (!ok)
     PERFETTO_ELOG("%s", error.c_str());
-  }
+  if (atrace_errors)
+    atrace_errors->append(error);
   return ok;
 }
 #endif
 
 }  // namespace
 
-bool RunAtrace(const std::vector<std::string>& args) {
+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);
+    return g_run_atrace_for_testing(args, atrace_errors);
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-  return ExecvAtrace(args);
+  return ExecvAtrace(args, atrace_errors);
 #else
   PERFETTO_LOG("Atrace only supported on Android.");
   return false;
diff --git a/src/traced/probes/ftrace/atrace_wrapper.h b/src/traced/probes/ftrace/atrace_wrapper.h
index 91f02cb..29847aa 100644
--- a/src/traced/probes/ftrace/atrace_wrapper.h
+++ b/src/traced/probes/ftrace/atrace_wrapper.h
@@ -24,7 +24,8 @@
 namespace perfetto {
 
 using RunAtraceFunction =
-    std::add_pointer<bool(const std::vector<std::string>& /*args*/)>::type;
+    std::add_pointer<bool(const std::vector<std::string>& /*args*/,
+                          std::string* /*atrace_errors*/)>::type;
 
 // 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:
@@ -35,7 +36,8 @@
 void SetIsOldAtraceForTesting(bool);
 void ClearIsOldAtraceForTesting();
 
-bool RunAtrace(const std::vector<std::string>& args);
+bool RunAtrace(const std::vector<std::string>& args,
+               std::string* atrace_errors);
 void SetRunAtraceForTesting(RunAtraceFunction);
 
 }  // namespace perfetto
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index 4ebd30f..ba48c44 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -29,6 +29,7 @@
 #include "protos/perfetto/trace/ftrace/sched.pbzero.h"
 #include "src/traced/probes/ftrace/atrace_wrapper.h"
 #include "src/traced/probes/ftrace/compact_sched.h"
+#include "src/traced/probes/ftrace/ftrace_stats.h"
 
 namespace perfetto {
 namespace {
@@ -485,7 +486,8 @@
       vendor_events_(vendor_events) {}
 FtraceConfigMuxer::~FtraceConfigMuxer() = default;
 
-FtraceConfigId FtraceConfigMuxer::SetupConfig(const FtraceConfig& request) {
+FtraceConfigId FtraceConfigMuxer::SetupConfig(const FtraceConfig& request,
+                                              FtraceSetupErrors* errors) {
   EventFilter filter;
   bool is_ftrace_enabled = ftrace_->IsTracingEnabled();
   if (ds_configs_.empty()) {
@@ -530,7 +532,7 @@
           "bailing out.");
       return 0;
     }
-    UpdateAtrace(request);
+    UpdateAtrace(request, errors ? &errors->atrace_errors : nullptr);
   }
 
   for (const auto& group_and_name : events) {
@@ -538,6 +540,8 @@
     if (!event) {
       PERFETTO_DLOG("Can't enable %s, event not known",
                     group_and_name.ToString().c_str());
+      if (errors)
+        errors->unknown_ftrace_events.push_back(group_and_name.ToString());
       continue;
     }
     // Note: ftrace events are always implicitly enabled (and don't have an
@@ -554,6 +558,8 @@
       filter.AddEnabledEvent(event->ftrace_event_id);
     } else {
       PERFETTO_DPLOG("Failed to enable %s.", group_and_name.ToString().c_str());
+      if (errors)
+        errors->failed_ftrace_events.push_back(group_and_name.ToString());
     }
   }
 
@@ -662,7 +668,8 @@
       // some categories this won't disable them (e.g. categories that just
       // enable ftrace events) for those there is nothing we can do till the
       // last ftrace config is removed.
-      if (StartAtrace(expected_apps, expected_categories)) {
+      if (StartAtrace(expected_apps, expected_categories,
+                      /*atrace_errors=*/nullptr)) {
         // Update current_state_ to reflect this change.
         current_state_.atrace_apps = expected_apps;
         current_state_.atrace_categories = expected_categories;
@@ -720,7 +727,8 @@
   return current_state_.cpu_buffer_size_pages;
 }
 
-void FtraceConfigMuxer::UpdateAtrace(const FtraceConfig& request) {
+void FtraceConfigMuxer::UpdateAtrace(const FtraceConfig& request,
+                                     std::string* atrace_errors) {
   // We want to avoid poisoning current_state_.atrace_{categories, apps}
   // if for some reason these args make atrace unhappy so we stash the
   // union into temps and only update current_state_ if we successfully
@@ -738,7 +746,7 @@
     return;
   }
 
-  if (StartAtrace(combined_apps, combined_categories)) {
+  if (StartAtrace(combined_apps, combined_categories, atrace_errors)) {
     current_state_.atrace_categories = combined_categories;
     current_state_.atrace_apps = combined_apps;
     current_state_.atrace_on = true;
@@ -746,9 +754,9 @@
 }
 
 // static
-bool FtraceConfigMuxer::StartAtrace(
-    const std::vector<std::string>& apps,
-    const std::vector<std::string>& categories) {
+bool FtraceConfigMuxer::StartAtrace(const std::vector<std::string>& apps,
+                                    const std::vector<std::string>& categories,
+                                    std::string* atrace_errors) {
   PERFETTO_DLOG("Update atrace config...");
 
   std::vector<std::string> args;
@@ -771,7 +779,7 @@
     args.push_back(arg);
   }
 
-  bool result = RunAtrace(args);
+  bool result = RunAtrace(args, atrace_errors);
   PERFETTO_DLOG("...done (%s)", result ? "success" : "fail");
   return result;
 }
@@ -784,7 +792,7 @@
   std::vector<std::string> args{"atrace", "--async_stop"};
   if (!IsOldAtrace())
     args.push_back("--only_userspace");
-  if (RunAtrace(args)) {
+  if (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 bcaf1e5..7b21357 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.h
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.h
@@ -34,6 +34,8 @@
 }  // namespace pbzero
 }  // namespace protos
 
+struct FtraceSetupErrors;
+
 // State held by the muxer per data source, used to parse ftrace according to
 // that data source's config.
 struct FtraceDataSourceConfig {
@@ -93,7 +95,8 @@
   // (if you enable an atrace category we try to give you the matching events).
   // If someone else is tracing we won't touch atrace (since it resets the
   // buffer).
-  FtraceConfigId SetupConfig(const FtraceConfig& request);
+  FtraceConfigId SetupConfig(const FtraceConfig& request,
+                             FtraceSetupErrors* = nullptr);
 
   // Activate ftrace for the given config (if not already active).
   bool ActivateConfig(FtraceConfigId);
@@ -131,7 +134,8 @@
 
  private:
   static bool StartAtrace(const std::vector<std::string>& apps,
-                          const std::vector<std::string>& categories);
+                          const std::vector<std::string>& categories,
+                          std::string* atrace_errors);
 
   struct FtraceState {
     EventFilter ftrace_events;
@@ -148,7 +152,7 @@
 
   void SetupClock(const FtraceConfig& request);
   void SetupBufferSize(const FtraceConfig& request);
-  void UpdateAtrace(const FtraceConfig& request);
+  void UpdateAtrace(const FtraceConfig& request, std::string* atrace_errors);
   void DisableAtrace();
 
   // This processes the config to get the exact events.
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
index 006ee59..af01bd9 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
@@ -18,9 +18,11 @@
 
 #include <memory>
 
+#include "ftrace_config_muxer.h"
 #include "src/traced/probes/ftrace/atrace_wrapper.h"
 #include "src/traced/probes/ftrace/compact_sched.h"
 #include "src/traced/probes/ftrace/ftrace_procfs.h"
+#include "src/traced/probes/ftrace/ftrace_stats.h"
 #include "src/traced/probes/ftrace/proto_translation_table.h"
 #include "test/gtest_and_gmock.h"
 
@@ -29,6 +31,7 @@
 using testing::Contains;
 using testing::ElementsAreArray;
 using testing::Eq;
+using testing::Invoke;
 using testing::IsEmpty;
 using testing::MatchesRegex;
 using testing::NiceMock;
@@ -63,14 +66,15 @@
   MockRunAtrace() {
     static MockRunAtrace* instance;
     instance = this;
-    SetRunAtraceForTesting([](const std::vector<std::string>& args) {
-      return instance->RunAtrace(args);
-    });
+    SetRunAtraceForTesting(
+        [](const std::vector<std::string>& args, std::string* atrace_errors) {
+          return instance->RunAtrace(args, atrace_errors);
+        });
   }
 
   ~MockRunAtrace() { SetRunAtraceForTesting(nullptr); }
 
-  MOCK_METHOD1(RunAtrace, bool(const std::vector<std::string>&));
+  MOCK_METHOD2(RunAtrace, bool(const std::vector<std::string>&, std::string*));
 };
 
 class MockProtoTranslationTable : public ProtoTranslationTable {
@@ -474,9 +478,9 @@
 
   EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
       .WillOnce(Return('0'));
-  EXPECT_CALL(atrace,
-              RunAtrace(ElementsAreArray(
-                  {"atrace", "--async_start", "--only_userspace", "sched"})))
+  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                                  "--only_userspace", "sched"}),
+                                _))
       .WillOnce(Return(true));
 
   FtraceConfigId id = model.SetupConfig(config);
@@ -496,8 +500,10 @@
   EXPECT_THAT(central_filter->GetEnabledEvents(),
               Contains(FtraceConfigMuxerTest::kFakeSchedSwitchEventId));
 
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray(
-                          {"atrace", "--async_stop", "--only_userspace"})))
+  EXPECT_CALL(
+      atrace,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
       .WillOnce(Return(true));
   ASSERT_TRUE(model.RemoveConfig(id));
 }
@@ -516,9 +522,11 @@
       .WillOnce(Return('0'));
   EXPECT_CALL(
       atrace,
-      RunAtrace(ElementsAreArray(
-          {"atrace", "--async_start", "--only_userspace", "-a",
-           "com.google.android.gms,com.google.android.gms.persistent"})))
+      RunAtrace(
+          ElementsAreArray(
+              {"atrace", "--async_start", "--only_userspace", "-a",
+               "com.google.android.gms,com.google.android.gms.persistent"}),
+          _))
       .WillOnce(Return(true));
 
   FtraceConfigId id = model.SetupConfig(config);
@@ -529,8 +537,10 @@
   ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
               Contains(FtraceConfigMuxerTest::kFakePrintEventId));
 
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray(
-                          {"atrace", "--async_stop", "--only_userspace"})))
+  EXPECT_CALL(
+      atrace,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
       .WillOnce(Return(true));
   ASSERT_TRUE(model.RemoveConfig(id));
 }
@@ -555,7 +565,8 @@
 
   EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
                                                   "--only_userspace", "cat_a",
-                                                  "-a", "app_a"})))
+                                                  "-a", "app_a"}),
+                                _))
       .WillOnce(Return(true));
   FtraceConfigId id_a = model.SetupConfig(config_a);
   ASSERT_TRUE(id_a);
@@ -563,7 +574,8 @@
   EXPECT_CALL(
       atrace,
       RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_a", "cat_b", "-a", "app_a,app_b"})))
+                                  "cat_a", "cat_b", "-a", "app_a,app_b"}),
+                _))
       .WillOnce(Return(true));
   FtraceConfigId id_b = model.SetupConfig(config_b);
   ASSERT_TRUE(id_b);
@@ -571,7 +583,8 @@
   EXPECT_CALL(atrace,
               RunAtrace(ElementsAreArray({"atrace", "--async_start",
                                           "--only_userspace", "cat_a", "cat_b",
-                                          "cat_c", "-a", "app_a,app_b,app_c"})))
+                                          "cat_c", "-a", "app_a,app_b,app_c"}),
+                        _))
       .WillOnce(Return(true));
   FtraceConfigId id_c = model.SetupConfig(config_c);
   ASSERT_TRUE(id_c);
@@ -579,18 +592,22 @@
   EXPECT_CALL(
       atrace,
       RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_a", "cat_c", "-a", "app_a,app_c"})))
+                                  "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"})))
+                                                  "-a", "app_c"}),
+                                _))
       .WillOnce(Return(true));
   ASSERT_TRUE(model.RemoveConfig(id_a));
 
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray(
-                          {"atrace", "--async_stop", "--only_userspace"})))
+  EXPECT_CALL(
+      atrace,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
       .WillOnce(Return(true));
   ASSERT_TRUE(model.RemoveConfig(id_c));
 }
@@ -620,15 +637,17 @@
   EXPECT_CALL(
       atrace,
       RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_1", "cat_2", "-a", "app_1,app_2"})))
+                                  "cat_1", "cat_2", "-a", "app_1,app_2"}),
+                _))
       .WillOnce(Return(true));
   FtraceConfigId id_a = model.SetupConfig(config_a);
   ASSERT_TRUE(id_a);
 
-  EXPECT_CALL(atrace,
-              RunAtrace(ElementsAreArray(
-                  {"atrace", "--async_start", "--only_userspace", "cat_1",
-                   "cat_2", "cat_fail", "-a", "app_1,app_2,app_fail"})))
+  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 = model.SetupConfig(config_b);
   ASSERT_TRUE(id_b);
@@ -636,7 +655,8 @@
   EXPECT_CALL(atrace,
               RunAtrace(ElementsAreArray({"atrace", "--async_start",
                                           "--only_userspace", "cat_1", "cat_2",
-                                          "cat_3", "-a", "app_1,app_2,app_3"})))
+                                          "cat_3", "-a", "app_1,app_2,app_3"}),
+                        _))
       .WillOnce(Return(true));
   FtraceConfigId id_c = model.SetupConfig(config_c);
   ASSERT_TRUE(id_c);
@@ -644,7 +664,8 @@
   EXPECT_CALL(
       atrace,
       RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
-                                  "cat_1", "cat_2", "-a", "app_1,app_2"})))
+                                  "cat_1", "cat_2", "-a", "app_1,app_2"}),
+                _))
       .WillOnce(Return(true));
   ASSERT_TRUE(model.RemoveConfig(id_c));
 
@@ -652,8 +673,10 @@
   // so we don't expect a call here.
   ASSERT_TRUE(model.RemoveConfig(id_b));
 
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray(
-                          {"atrace", "--async_stop", "--only_userspace"})))
+  EXPECT_CALL(
+      atrace,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
       .WillOnce(Return(true));
   ASSERT_TRUE(model.RemoveConfig(id_a));
 }
@@ -674,7 +697,8 @@
 
   EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
                                                   "--only_userspace", "cat_1",
-                                                  "-a", "app_1"})))
+                                                  "-a", "app_1"}),
+                                _))
       .WillOnce(Return(true));
   FtraceConfigId id_a = model.SetupConfig(config_a);
   ASSERT_TRUE(id_a);
@@ -684,8 +708,10 @@
 
   ASSERT_TRUE(model.RemoveConfig(id_a));
 
-  EXPECT_CALL(atrace, RunAtrace(ElementsAreArray(
-                          {"atrace", "--async_stop", "--only_userspace"})))
+  EXPECT_CALL(
+      atrace,
+      RunAtrace(
+          ElementsAreArray({"atrace", "--async_stop", "--only_userspace"}), _))
       .WillOnce(Return(true));
   ASSERT_TRUE(model.RemoveConfig(id_b));
 }
@@ -710,7 +736,8 @@
   ASSERT_TRUE(id_a);
 
   EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "b"})))
+                                                  "--only_userspace", "b"}),
+                                _))
       .WillOnce(Return(true));
   FtraceConfigId id_b = model.SetupConfig(config_b);
   ASSERT_TRUE(id_b);
@@ -719,27 +746,61 @@
   ASSERT_TRUE(id_c);
 
   EXPECT_CALL(atrace,
-              RunAtrace(ElementsAreArray(
-                  {"atrace", "--async_start", "--only_userspace", "b", "d"})))
+              RunAtrace(ElementsAreArray({"atrace", "--async_start",
+                                          "--only_userspace", "b", "d"}),
+                        _))
       .WillOnce(Return(true));
   FtraceConfigId id_d = model.SetupConfig(config_d);
   ASSERT_TRUE(id_d);
 
   EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
-                                                  "--only_userspace", "b"})))
+                                                  "--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"})))
+  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";
+
+  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+      .WillRepeatedly(Return('0'));
+
+  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+
+  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 = model.SetupConfig(config, &errors);
+  ASSERT_TRUE(id_a);
+  EXPECT_EQ(errors.atrace_errors, "foo\nbar\n");
+}
+
 TEST_F(FtraceConfigMuxerTest, SetupClockForTesting) {
   MockFtraceProcfs ftrace;
   FtraceConfig config;
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index 0d8cb73..fc25d3b 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -393,7 +393,8 @@
   if (!ValidConfig(data_source->config()))
     return false;
 
-  auto config_id = ftrace_config_muxer_->SetupConfig(data_source->config());
+  auto config_id = ftrace_config_muxer_->SetupConfig(
+      data_source->config(), data_source->mutable_setup_errors());
   if (!config_id)
     return false;
 
diff --git a/src/traced/probes/ftrace/ftrace_data_source.cc b/src/traced/probes/ftrace/ftrace_data_source.cc
index 5b6ac34..e5ba144 100644
--- a/src/traced/probes/ftrace/ftrace_data_source.cc
+++ b/src/traced/probes/ftrace/ftrace_data_source.cc
@@ -111,11 +111,13 @@
   if (!ftrace->StartDataSource(this))
     return;
   DumpFtraceStats(&stats_before_);
+  setup_errors_ = FtraceSetupErrors();  // Dump only on START_OF_TRACE.
 }
 
 void FtraceDataSource::DumpFtraceStats(FtraceStats* stats) {
   if (controller_weak_)
     controller_weak_->DumpFtraceStats(stats);
+  stats->setup_errors = std::move(setup_errors_);
 }
 
 void FtraceDataSource::Flush(FlushRequestID flush_request_id,
diff --git a/src/traced/probes/ftrace/ftrace_data_source.h b/src/traced/probes/ftrace/ftrace_data_source.h
index cadcd5f..b1a1c45 100644
--- a/src/traced/probes/ftrace/ftrace_data_source.h
+++ b/src/traced/probes/ftrace/ftrace_data_source.h
@@ -81,6 +81,7 @@
   }
 
   FtraceMetadata* mutable_metadata() { return &metadata_; }
+  FtraceSetupErrors* mutable_setup_errors() { return &setup_errors_; }
   TraceWriter* trace_writer() { return writer_.get(); }
 
  private:
@@ -95,7 +96,8 @@
 
   const FtraceConfig config_;
   FtraceMetadata metadata_;
-  FtraceStats stats_before_ = {};
+  FtraceStats stats_before_{};
+  FtraceSetupErrors setup_errors_{};
   std::map<FlushRequestID, std::function<void()>> pending_flushes_;
 
   // -- Fields initialized by the Initialize() call:
diff --git a/src/traced/probes/ftrace/ftrace_stats.cc b/src/traced/probes/ftrace/ftrace_stats.cc
index a424cc9..bba09ac 100644
--- a/src/traced/probes/ftrace/ftrace_stats.cc
+++ b/src/traced/probes/ftrace/ftrace_stats.cc
@@ -26,6 +26,12 @@
   }
   writer->set_kernel_symbols_parsed(kernel_symbols_parsed);
   writer->set_kernel_symbols_mem_kb(kernel_symbols_mem_kb);
+  if (!setup_errors.atrace_errors.empty())
+    writer->set_atrace_errors(setup_errors.atrace_errors);
+  for (const std::string& err : setup_errors.unknown_ftrace_events)
+    writer->add_unknown_ftrace_events(err);
+  for (const std::string& err : setup_errors.failed_ftrace_events)
+    writer->add_failed_ftrace_events(err);
 }
 
 void FtraceCpuStats::Write(protos::pbzero::FtraceCpuStats* writer) const {
diff --git a/src/traced/probes/ftrace/ftrace_stats.h b/src/traced/probes/ftrace/ftrace_stats.h
index ce68fe8..c7a16d8 100644
--- a/src/traced/probes/ftrace/ftrace_stats.h
+++ b/src/traced/probes/ftrace/ftrace_stats.h
@@ -18,6 +18,7 @@
 #define SRC_TRACED_PROBES_FTRACE_FTRACE_STATS_H_
 
 #include <cinttypes>
+#include <string>
 #include <vector>
 
 namespace perfetto {
@@ -43,8 +44,15 @@
   void Write(protos::pbzero::FtraceCpuStats*) const;
 };
 
+struct FtraceSetupErrors {
+  std::string atrace_errors;
+  std::vector<std::string> unknown_ftrace_events;
+  std::vector<std::string> failed_ftrace_events;
+};
+
 struct FtraceStats {
   std::vector<FtraceCpuStats> cpu_stats;
+  FtraceSetupErrors setup_errors;
   uint32_t kernel_symbols_parsed = 0;
   uint32_t kernel_symbols_mem_kb = 0;
 
diff --git a/test/ftrace_integrationtest.cc b/test/ftrace_integrationtest.cc
index 2553d40..42d251f 100644
--- a/test/ftrace_integrationtest.cc
+++ b/test/ftrace_integrationtest.cc
@@ -65,6 +65,7 @@
 using ::testing::HasSubstr;
 using ::testing::Property;
 using ::testing::SizeIs;
+using ::testing::UnorderedElementsAreArray;
 
 class PerfettoFtraceIntegrationTest : public ::testing::Test {
  public:
@@ -277,5 +278,66 @@
   ASSERT_GT(symbols_parsed, 100);
 }
 
+TEST_F(PerfettoFtraceIntegrationTest, ReportFtraceFailuresInStats) {
+  base::TestTaskRunner task_runner;
+
+  TestHelper helper(&task_runner);
+  helper.StartServiceIfRequired();
+
+#if PERFETTO_BUILDFLAG(PERFETTO_START_DAEMONS)
+  ProbesProducerThread probes(GetTestProducerSockName());
+  probes.Connect();
+#endif
+
+  helper.ConnectConsumer();
+  helper.WaitForConsumerConnect();
+
+  // Wait for the traced_probes service to connect. We want to start tracing
+  // only after it connects, otherwise we'll start a tracing session with 0
+  // producers connected (which is valid but not what we want here).
+  helper.WaitForDataSourceConnected("linux.ftrace");
+
+  TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(32);
+  trace_config.set_duration_ms(1);
+
+  auto* ds_config = trace_config.add_data_sources()->mutable_config();
+  ds_config->set_name("linux.ftrace");
+
+  protos::gen::FtraceConfig ftrace_config;
+  ftrace_config.add_ftrace_events("sched/sched_process_fork");    // Good.
+  ftrace_config.add_ftrace_events("sched/does_not_exist");        // Bad.
+  ftrace_config.add_ftrace_events("foobar/i_just_made_this_up");  // Bad.
+  ftrace_config.add_atrace_categories("madeup_atrace_cat");       // Bad.
+  ds_config->set_ftrace_config_raw(ftrace_config.SerializeAsString());
+
+  helper.StartTracing(trace_config);
+  helper.WaitForTracingDisabled(kDefaultTestTimeoutMs);
+
+  helper.ReadData();
+  helper.WaitForReadData();
+  const auto& packets = helper.trace();
+  ASSERT_GT(packets.size(), 0u);
+
+  base::Optional<protos::gen::FtraceStats> stats;
+  for (const auto& packet : packets) {
+    if (!packet.has_ftrace_stats() ||
+        packet.ftrace_stats().phase() !=
+            protos::gen::FtraceStats::START_OF_TRACE) {
+      continue;
+    }
+    stats = packet.ftrace_stats();
+  }
+  ASSERT_TRUE(stats.has_value());
+  EXPECT_THAT(stats->unknown_ftrace_events(),
+              UnorderedElementsAreArray(
+                  {"sched/does_not_exist", "foobar/i_just_made_this_up"}));
+
+  // Atrace is not available on Linux and on the O-based emulator on the CI.
+  if (base::FileExists("/system/bin/atrace")) {
+    EXPECT_THAT(stats->atrace_errors(), HasSubstr("madeup_atrace_cat"));
+  }
+}
+
 }  // namespace perfetto
 #endif  // OS_ANDROID || OS_LINUX