Refactor ErrorCounter out from timing report code

This is helpful to allow other applications to track error counts in a
similar manner.

Change-Id: Ifc7127578c08757febc6acdc3a79e42ad7b7cce5
Signed-off-by: Austin Schuh <austin.schuh@bluerivertech.com>
diff --git a/aos/events/BUILD b/aos/events/BUILD
index 6abc075..0f23d5a 100644
--- a/aos/events/BUILD
+++ b/aos/events/BUILD
@@ -24,10 +24,8 @@
     name = "event_loop_fbs",
     srcs = ["event_loop.fbs"],
     gen_reflections = 1,
-    includes = [
-        "//aos:configuration_fbs_includes",
-    ],
     target_compatible_with = ["@platforms//os:linux"],
+    deps = ["//aos:configuration_fbs"],
 )
 
 cc_static_flatbuffer(
@@ -341,6 +339,7 @@
     deps = [
         ":event_loop_fbs",
         "//aos:configuration",
+        "//aos/util:error_counter",
         "@com_github_google_glog//:glog",
     ],
 )
diff --git a/aos/events/event_loop.cc b/aos/events/event_loop.cc
index 09a3834..257313b 100644
--- a/aos/events/event_loop.cc
+++ b/aos/events/event_loop.cc
@@ -389,19 +389,13 @@
 
   // Pre-fill in the defaults for senders.
   std::vector<flatbuffers::Offset<timing::Sender>> sender_offsets;
-  for (const RawSender *sender : senders_) {
+  for (RawSender *sender : senders_) {
     flatbuffers::Offset<timing::Statistic> size_offset =
         timing::CreateStatistic(fbb);
 
-    std::vector<flatbuffers::Offset<timing::SendErrorCount>>
-        error_count_offsets;
-    for (size_t ii = 0; ii < internal::RawSenderTiming::kNumErrors; ++ii) {
-      error_count_offsets.push_back(timing::CreateSendErrorCount(
-          fbb, timing::EnumValuesSendError()[ii], 0));
-    }
     const flatbuffers::Offset<
         flatbuffers::Vector<flatbuffers::Offset<timing::SendErrorCount>>>
-        error_counts_offset = fbb.CreateVector(error_count_offsets);
+        error_counts_offset = sender->timing_.error_counter.Initialize(&fbb);
 
     timing::Sender::Builder sender_builder(fbb);
 
diff --git a/aos/events/timing_statistics.cc b/aos/events/timing_statistics.cc
index fbc49f4..c89bebd 100644
--- a/aos/events/timing_statistics.cc
+++ b/aos/events/timing_statistics.cc
@@ -28,8 +28,10 @@
   sender = new_sender;
   if (!sender) {
     size.set_statistic(nullptr);
+    error_counter.InvalidateBuffer();
   } else {
     size.set_statistic(sender->mutable_size());
+    error_counter.set_mutable_vector(sender->mutable_error_counts());
   }
 }
 
@@ -40,19 +42,14 @@
 
   size.Reset();
   sender->mutate_count(0);
-  for (size_t ii = 0; ii < kNumErrors; ++ii) {
-    sender->mutable_error_counts()->GetMutableObject(ii)->mutate_count(0);
-  }
+  error_counter.ResetCounts();
 }
 
 void RawSenderTiming::IncrementError(timing::SendError error) {
   if (!sender) {
     return;
   }
-  const size_t index = static_cast<size_t>(error);
-  timing::SendErrorCount *counter =
-      sender->mutable_error_counts()->GetMutableObject(index);
-  counter->mutate_count(counter->count() + 1);
+  error_counter.IncrementError(error);
 }
 
 void TimerTiming::set_timing_report(timing::Timer *new_timer) {
diff --git a/aos/events/timing_statistics.h b/aos/events/timing_statistics.h
index e9edff2..3c8c741 100644
--- a/aos/events/timing_statistics.h
+++ b/aos/events/timing_statistics.h
@@ -4,6 +4,7 @@
 #include <cmath>
 
 #include "aos/events/event_loop_generated.h"
+#include "aos/util/error_counter.h"
 
 namespace aos {
 namespace internal {
@@ -79,9 +80,9 @@
 
 // Class to hold timing information for a raw sender.
 struct RawSenderTiming {
-  static constexpr size_t kNumErrors =
-      static_cast<int>(timing::SendError::MAX) -
-      static_cast<int>(timing::SendError::MIN) + 1;
+  typedef util::ErrorCounter<timing::SendError, timing::SendErrorCount>
+      ErrorCounter;
+  static constexpr size_t kNumErrors = ErrorCounter::kNumErrors;
 
   RawSenderTiming(int new_channel_index) : channel_index(new_channel_index) {}
 
@@ -90,8 +91,6 @@
   void IncrementError(timing::SendError error);
   // Sanity check that the enum values are such that we can just use the enum
   // values themselves as array indices without anything weird happening.
-  static_assert(0 == static_cast<int>(timing::SendError::MIN),
-                "Expected error enum values to start at zero.");
   static_assert(
       sizeof(std::invoke_result<decltype(timing::EnumValuesSendError)>::type) /
               sizeof(timing::SendError) ==
@@ -101,6 +100,7 @@
   const int channel_index;
   timing::Sender *sender = nullptr;
   internal::TimingStatistic size;
+  ErrorCounter error_counter;
 };
 
 // Class to hold timing information for timers.
diff --git a/aos/util/BUILD b/aos/util/BUILD
index 0808062..5a87251 100644
--- a/aos/util/BUILD
+++ b/aos/util/BUILD
@@ -54,6 +54,28 @@
 )
 
 cc_library(
+    name = "error_counter",
+    hdrs = ["error_counter.h"],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        "@com_github_google_flatbuffers//:flatbuffers",
+        "@com_github_google_glog//:glog",
+    ],
+)
+
+cc_test(
+    name = "error_counter_test",
+    srcs = ["error_counter_test.cc"],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        ":error_counter",
+        "//aos:flatbuffers",
+        "//aos/events:event_loop_fbs",
+        "//aos/testing:googletest",
+    ],
+)
+
+cc_library(
     name = "mcap_logger",
     srcs = ["mcap_logger.cc"],
     hdrs = ["mcap_logger.h"],
diff --git a/aos/util/error_counter.h b/aos/util/error_counter.h
new file mode 100644
index 0000000..cf6cb7f
--- /dev/null
+++ b/aos/util/error_counter.h
@@ -0,0 +1,71 @@
+#ifndef AOS_UTIL_ERROR_COUNTER_H_
+#define AOS_UTIL_ERROR_COUNTER_H_
+#include "flatbuffers/flatbuffers.h"
+#include "glog/logging.h"
+
+namespace aos::util {
+// Class to manage simple error counters for flatbuffer status message.
+// This presumes that you have a flatbuffer enum type Error which has
+// enum values that are continuous and start at zero. These are then
+// counted by a Count flatbuffer table that is of the format:
+// table Count {
+//   error:Error (id: 0);
+//   count:uint (id: 1);
+// }
+// And which is stored as a vector in the resulting status message,
+// where the index within the vector corresponds with the underlying
+// value of the enum.
+template <typename Error, typename Count>
+class ErrorCounter {
+ public:
+  static constexpr size_t kNumErrors =
+      static_cast<int>(Error::MAX) - static_cast<int>(Error::MIN) + 1;
+  static_assert(0 == static_cast<int>(Error::MIN),
+                "Expected Error enum values to start at zero.");
+  // TODO(james): Is there any good way to check that the values are contiguous?
+  // There's no Error::COUNT, and the method I previously used (checking the
+  // size of the return type of EnumValues*()) requires the user to pass that
+  // method as a template argument.
+  ErrorCounter() = default;
+  static flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Count>>>
+  Initialize(flatbuffers::FlatBufferBuilder *fbb) {
+    std::array<flatbuffers::Offset<Count>, kNumErrors> count_offsets;
+    for (size_t ii = 0; ii < kNumErrors; ++ii) {
+      typename Count::Builder builder(*fbb);
+      builder.add_error(static_cast<Error>(ii));
+      builder.add_count(0);
+      count_offsets[ii] = builder.Finish();
+    }
+    const flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Count>>>
+        offset = fbb->CreateVector(count_offsets.data(), count_offsets.size());
+    return offset;
+  }
+
+  void set_mutable_vector(
+      flatbuffers::Vector<flatbuffers::Offset<Count>> *vector) {
+    vector_ = vector;
+  }
+
+  void InvalidateBuffer() { vector_ = nullptr; }
+
+  void IncrementError(Error error) {
+    CHECK_NOTNULL(vector_);
+    DCHECK_LT(static_cast<size_t>(error), vector_->size());
+    Count *counter = vector_->GetMutableObject(static_cast<size_t>(error));
+    counter->mutate_count(counter->count() + 1);
+  }
+
+  // Sets all the error counts to zero.
+  void ResetCounts() {
+    CHECK_NOTNULL(vector_);
+    DCHECK_EQ(vector_->size(), kNumErrors) << this << " vector " << vector_;
+    for (size_t ii = 0; ii < kNumErrors; ++ii) {
+      vector_->GetMutableObject(ii)->mutate_count(0);
+    }
+  }
+
+ private:
+  flatbuffers::Vector<flatbuffers::Offset<Count>> *vector_ = nullptr;
+};
+}  // namespace aos::util
+#endif  // AOS_UTIL_ERROR_COUNTER_H_
diff --git a/aos/util/error_counter_test.cc b/aos/util/error_counter_test.cc
new file mode 100644
index 0000000..2166cea
--- /dev/null
+++ b/aos/util/error_counter_test.cc
@@ -0,0 +1,37 @@
+#include "aos/util/error_counter.h"
+
+#include "aos/events/event_loop_generated.h"
+#include "aos/flatbuffers.h"
+#include "gtest/gtest.h"
+
+namespace aos::util::testing {
+// Exercises the basic API for the ErrorCounter class, ensuring that everything
+// works in the normal case.
+TEST(ErrorCounterTest, ErrorCounter) {
+  ErrorCounter<aos::timing::SendError, aos::timing::SendErrorCount> counter;
+  flatbuffers::FlatBufferBuilder fbb;
+  fbb.ForceDefaults(true);
+  const flatbuffers::Offset<
+      flatbuffers::Vector<flatbuffers::Offset<aos::timing::SendErrorCount>>>
+      counts_offset = counter.Initialize(&fbb);
+  aos::timing::Sender::Builder builder(fbb);
+  builder.add_error_counts(counts_offset);
+  fbb.Finish(builder.Finish());
+  aos::FlatbufferDetachedBuffer<aos::timing::Sender> message = fbb.Release();
+  counter.set_mutable_vector(message.mutable_message()->mutable_error_counts());
+  counter.IncrementError(aos::timing::SendError::MESSAGE_SENT_TOO_FAST);
+  counter.IncrementError(aos::timing::SendError::MESSAGE_SENT_TOO_FAST);
+  counter.IncrementError(aos::timing::SendError::INVALID_REDZONE);
+  ASSERT_EQ(2u, message.message().error_counts()->size());
+  EXPECT_EQ(aos::timing::SendError::MESSAGE_SENT_TOO_FAST,
+            message.message().error_counts()->Get(0)->error());
+  EXPECT_EQ(2u, message.message().error_counts()->Get(0)->count());
+  EXPECT_EQ(aos::timing::SendError::INVALID_REDZONE,
+            message.message().error_counts()->Get(1)->error());
+  EXPECT_EQ(1u, message.message().error_counts()->Get(1)->count());
+
+  counter.ResetCounts();
+  EXPECT_EQ(0u, message.message().error_counts()->Get(0)->count());
+  EXPECT_EQ(0u, message.message().error_counts()->Get(1)->count());
+}
+}  // namespace aos::util::testing