pw_trace: improve locks

Add an internal queue with a separate lock, this is a small section
which just copies the arguments making it suitable for a critical
section.

Emptying this queue is handled from a separate lock, which is a trylock.
If the queue is already being emptied, it is safe to return after adding
to the queue (no need to block).

Change-Id: I45bbf749b2cb8687bb5d8e485adde2256b2c99a8
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/21881
Reviewed-by: Paul Mathieu <paulmathieu@google.com>
Commit-Queue: Rob Oliver <rgoliver@google.com>
diff --git a/pw_trace_tokenized/BUILD.gn b/pw_trace_tokenized/BUILD.gn
index 0ca9cf8..c031e89 100644
--- a/pw_trace_tokenized/BUILD.gn
+++ b/pw_trace_tokenized/BUILD.gn
@@ -146,12 +146,14 @@
     ":backend_config",
     ":public_include_path",
   ]
-  public_deps = [ "$dir_pw_tokenizer" ]
+  public_deps = [
+    "$dir_pw_status",
+    "$dir_pw_tokenizer",
+  ]
   deps = [
     ":config",
     "$dir_pw_assert",
     "$dir_pw_ring_buffer",
-    "$dir_pw_status",
     "$dir_pw_trace:facade",
     "$dir_pw_varint",
   ]
diff --git a/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h b/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h
index 8ea52e4..300792a 100644
--- a/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h
+++ b/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h
@@ -28,6 +28,7 @@
 #endif  // __cplusplus
 #endif  // PW_TRACE_GET_TIME_DELTA
 
+#include "pw_status/status.h"
 #include "pw_tokenizer/tokenize.h"
 #include "pw_trace_tokenized/config.h"
 #include "pw_trace_tokenized/internal/trace_tokenized_internal.h"
@@ -38,9 +39,88 @@
 
 using EventType = pw_trace_EventType;
 
+namespace internal {
+
+// Simple ring buffer which is suitable for use in a critical section.
+template <size_t kSize>
+class TraceQueue {
+ public:
+  struct QueueEventBlock {
+    uint32_t trace_token;
+    EventType event_type;
+    const char* module;
+    uint32_t trace_id;
+    uint8_t flags;
+    size_t data_size;
+    std::byte data_buffer[PW_TRACE_BUFFER_MAX_DATA_SIZE_BYTES];
+  };
+
+  pw::Status TryPushBack(uint32_t trace_token,
+                         EventType event_type,
+                         const char* module,
+                         uint32_t trace_id,
+                         uint8_t flags,
+                         const void* data_buffer,
+                         size_t data_size) {
+    if (IsFull()) {
+      return pw::Status::RESOURCE_EXHAUSTED;
+    }
+    event_queue_[head_].trace_token = trace_token;
+    event_queue_[head_].event_type = event_type;
+    event_queue_[head_].module = module;
+    event_queue_[head_].trace_id = trace_id;
+    event_queue_[head_].flags = flags;
+    event_queue_[head_].data_size = data_size;
+    for (size_t i = 0; i < data_size; i++) {
+      event_queue_[head_].data_buffer[i] =
+          reinterpret_cast<const std::byte*>(data_buffer)[i];
+    }
+    head_ = (head_ + 1) % kSize;
+    is_empty_ = false;
+    return pw::Status::OK;
+  }
+
+  const volatile QueueEventBlock* PeekFront() const {
+    if (IsEmpty()) {
+      return nullptr;
+    }
+    return &event_queue_[tail_];
+  }
+
+  void PopFront() {
+    if (!IsEmpty()) {
+      tail_ = (tail_ + 1) % kSize;
+      is_empty_ = (tail_ == head_);
+    }
+  }
+
+  void Clear() {
+    head_ = 0;
+    tail_ = 0;
+    is_empty_ = true;
+  }
+
+  bool IsEmpty() const { return is_empty_; }
+  bool IsFull() const { return !is_empty_ && (head_ == tail_); }
+
+ private:
+  std::array<volatile QueueEventBlock, kSize> event_queue_;
+  volatile size_t head_ = 0;  // Next write
+  volatile size_t tail_ = 0;  // Next read
+  volatile bool is_empty_ =
+      true;  // Used to distinquish if head==tail is empty or full
+};
+
+}  // namespace internal
+
 class TokenizedTraceImpl {
  public:
-  void Enable(bool enable) { enabled_ = enable; }
+  void Enable(bool enable) {
+    if (enable == enabled_ && enable) {
+      event_queue_.Clear();
+    }
+    enabled_ = enable;
+  }
   bool IsEnabled() const { return enabled_; }
 
   void HandleTraceEvent(uint32_t trace_token,
@@ -52,8 +132,13 @@
                         size_t data_size);
 
  private:
+  using TraceQueue = internal::TraceQueue<PW_TRACE_QUEUE_SIZE_EVENTS>;
   PW_TRACE_TIME_TYPE last_trace_time_ = 0;
   bool enabled_ = false;
+  TraceQueue event_queue_;
+
+  void HandleNextItemInQueue(
+      const volatile TraceQueue::QueueEventBlock* event_block);
 };
 
 // A singleton object of the TokenizedTraceImpl class which can be used to
diff --git a/pw_trace_tokenized/trace.cc b/pw_trace_tokenized/trace.cc
index c9f9094..b017a73 100644
--- a/pw_trace_tokenized/trace.cc
+++ b/pw_trace_tokenized/trace.cc
@@ -40,10 +40,48 @@
     return;
   }
 
-  pw_trace_TraceEventReturnFlags ret_flags = 0;
+  // Create trace event
+  PW_TRACE_QUEUE_LOCK();
+  if (!event_queue_
+           .TryPushBack(trace_token,
+                        event_type,
+                        module,
+                        trace_id,
+                        flags,
+                        data_buffer,
+                        data_size)
+           .Ok()) {
+    // Queue full dropping sample
+    // TODO(rgoliver): Allow other strategies, for example: drop oldest, try
+    // empty queue, or block.
+  }
+  PW_TRACE_QUEUE_UNLOCK();
 
-  PW_TRACE_LOCK();
+  // Sample is now in queue (if not dropped), try to empty the queue if not
+  // already being emptied.
+  if (PW_TRACE_TRY_LOCK()) {
+    while (!event_queue_.IsEmpty()) {
+      HandleNextItemInQueue(event_queue_.PeekFront());
+      event_queue_.PopFront();
+    }
+    PW_TRACE_UNLOCK();
+  }
+}
+
+void TokenizedTraceImpl::HandleNextItemInQueue(
+    const volatile TraceQueue::QueueEventBlock* event_block) {
+  // Get next item in queue
+  uint32_t trace_token = event_block->trace_token;
+  EventType event_type = event_block->event_type;
+  const char* module = event_block->module;
+  uint32_t trace_id = event_block->trace_id;
+  uint8_t flags = event_block->flags;
+  const std::byte* data_buffer =
+      const_cast<const std::byte*>(event_block->data_buffer);
+  size_t data_size = event_block->data_size;
+
   // Call any event callback which is registered to receive every event.
+  pw_trace_TraceEventReturnFlags ret_flags = 0;
   ret_flags |=
       Callbacks::Instance().CallEventCallbacks(CallbacksImpl::kCallOnEveryEvent,
                                                trace_token,
@@ -53,11 +91,10 @@
                                                flags);
   // Return if disabled.
   if ((PW_TRACE_EVENT_RETURN_FLAGS_SKIP_EVENT & ret_flags) || !enabled_) {
-    PW_TRACE_UNLOCK();
     return;
   }
 
-  // Call any event callback which is registered to receive every event.
+  // Call any event callback not already called.
   ret_flags |= Callbacks::Instance().CallEventCallbacks(
       CallbacksImpl::kCallOnlyWhenEnabled,
       trace_token,
@@ -68,7 +105,6 @@
   // Return if disabled (from a callback) or if a callback has indicated the
   // sample should be skipped.
   if ((PW_TRACE_EVENT_RETURN_FLAGS_SKIP_EVENT & ret_flags) || !enabled_) {
-    PW_TRACE_UNLOCK();
     return;
   }
 
@@ -108,8 +144,6 @@
   if (PW_TRACE_EVENT_RETURN_FLAGS_DISABLE_AFTER_PROCESSING & ret_flags) {
     enabled_ = false;
   }
-
-  PW_TRACE_UNLOCK();
 }
 
 pw_trace_TraceEventReturnFlags CallbacksImpl::CallEventCallbacks(
diff --git a/pw_trace_tokenized/trace_test.cc b/pw_trace_tokenized/trace_test.cc
index fa8f64d..5e6d2a4 100644
--- a/pw_trace_tokenized/trace_test.cc
+++ b/pw_trace_tokenized/trace_test.cc
@@ -537,3 +537,73 @@
                     "i");  // TODO(rgoliver): check data
   EXPECT_TRUE(test_interface.GetEvents().empty());
 }
+
+// Create some helper macros that generated some test trace data based from a
+// number, and can check that it is correct.
+constexpr std::byte kTestData[] = {
+    std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}};
+#define QUEUE_TESTS_ARGS(num)                               \
+  (num), static_cast<pw_trace_EventType>((num) % 10),       \
+      "module_" PW_STRINGIFY(num), (num), (num), kTestData, \
+      (num) % PW_ARRAY_SIZE(kTestData)
+#define QUEUE_CHECK_RESULT(queue_size, result, num)                            \
+  result && ((result->trace_token) == (num)) &&                                \
+      ((result->event_type) == static_cast<pw_trace_EventType>((num) % 10)) && \
+      (strncmp(result->module,                                                 \
+               "module_" PW_STRINGIFY(num),                                    \
+               strlen("module_" PW_STRINGIFY(num))) == 0) &&                   \
+      ((result->trace_id) == (num)) && ((result->flags) == (num)) &&           \
+      (memcmp(const_cast<const pw::trace::internal::TraceQueue<                \
+                  queue_size>::QueueEventBlock*>(result)                       \
+                  ->data_buffer,                                               \
+              kTestData,                                                       \
+              result->data_size) == 0) &&                                      \
+      (result->data_size == (num) % PW_ARRAY_SIZE(kTestData))
+
+TEST(TokenizedTrace, QueueSimple) {
+  constexpr size_t kQueueSize = 5;
+  pw::trace::internal::TraceQueue<kQueueSize> queue;
+  constexpr size_t kTestNum = 1;
+  queue.TryPushBack(QUEUE_TESTS_ARGS(kTestNum));
+  EXPECT_FALSE(queue.IsEmpty());
+  EXPECT_FALSE(queue.IsFull());
+  EXPECT_TRUE(QUEUE_CHECK_RESULT(kQueueSize, queue.PeekFront(), kTestNum));
+  queue.PopFront();
+  EXPECT_TRUE(queue.IsEmpty());
+  EXPECT_TRUE(queue.PeekFront() == nullptr);
+  EXPECT_FALSE(queue.IsFull());
+}
+
+TEST(TokenizedTrace, QueueFull) {
+  constexpr size_t kQueueSize = 5;
+  pw::trace::internal::TraceQueue<kQueueSize> queue;
+  for (size_t i = 0; i < kQueueSize; i++) {
+    EXPECT_EQ(queue.TryPushBack(QUEUE_TESTS_ARGS(i)), pw::Status::OK);
+  }
+  EXPECT_FALSE(queue.IsEmpty());
+  EXPECT_TRUE(queue.IsFull());
+  EXPECT_EQ(queue.TryPushBack(QUEUE_TESTS_ARGS(1)),
+            pw::Status::RESOURCE_EXHAUSTED);
+
+  for (size_t i = 0; i < kQueueSize; i++) {
+    EXPECT_TRUE(QUEUE_CHECK_RESULT(kQueueSize, queue.PeekFront(), i));
+    queue.PopFront();
+  }
+  EXPECT_TRUE(queue.IsEmpty());
+  EXPECT_TRUE(queue.PeekFront() == nullptr);
+  EXPECT_FALSE(queue.IsFull());
+}
+
+TEST(TokenizedTrace, Clear) {
+  constexpr size_t kQueueSize = 5;
+  pw::trace::internal::TraceQueue<kQueueSize> queue;
+  for (size_t i = 0; i < kQueueSize; i++) {
+    EXPECT_EQ(queue.TryPushBack(QUEUE_TESTS_ARGS(i)), pw::Status::OK);
+  }
+  EXPECT_FALSE(queue.IsEmpty());
+  EXPECT_TRUE(queue.IsFull());
+  queue.Clear();
+  EXPECT_TRUE(queue.IsEmpty());
+  EXPECT_TRUE(queue.PeekFront() == nullptr);
+  EXPECT_FALSE(queue.IsFull());
+}