Skip to content

feat(plugin): implement lock-free ring buffer logger for audio-safe logging #3

@zfogg

Description

@zfogg

Problem

The current Log utility (plugin/source/util/Log.cpp) uses a std::mutex for thread safety. While correct, this is not suitable for logging from the audio thread because:

  1. Mutex locks can block - If the drainer thread holds the lock, the audio thread will wait, causing audio dropouts
  2. Priority inversion - A low-priority logging thread can block the high-priority audio thread
  3. Unbounded latency - Lock contention adds unpredictable latency to the audio callback

Proposed Solution

Implement a lock-free ring buffer with a separate drainer thread:

┌─────────────────┐     ┌──────────────────┐     ┌─────────────┐
│  Audio Thread   │────▶│  Ring Buffer     │────▶│   Drainer   │───▶ File/Console
│  (lock-free     │     │  (SPSC or MPSC)  │     │   Thread    │
│   push only)    │     │                  │     │             │
└─────────────────┘     └──────────────────┘     └─────────────┘
         │
         ▼
    No blocking!

Key Design Points

  1. Lock-free SPSC (Single Producer Single Consumer) ring buffer

    • Audio thread = producer (push log entries)
    • Drainer thread = consumer (pop and write to file/console)
    • Use std::atomic for head/tail indices
    • Memory barriers for correct ordering
  2. Fixed-size log entries

    struct LogEntry {
        Level level;
        std::chrono::steady_clock::time_point timestamp;
        char message[256];  // Fixed size, no allocation
    };
  3. Drainer thread

    • Sleeps when buffer empty (condition variable or polling)
    • Wakes on buffer threshold or flush request
    • Batch writes for efficiency
  4. Overflow handling

    • When buffer full, either:
      • Drop oldest entries (prefer for audio - never block)
      • Increment overflow counter for diagnostics
  5. API compatibility

    • Keep existing Log::debug(), Log::info(), etc. interface
    • Add Log::audioDebug(), Log::audioInfo() for explicit audio-safe logging
    • Or auto-detect audio thread via juce::MessageManager::existsAndIsCurrentThread()

Technical Details

Ring Buffer Implementation

template<typename T, size_t Capacity>
class LockFreeRingBuffer {
    std::array<T, Capacity> buffer;
    std::atomic<size_t> head{0};  // Write position
    std::atomic<size_t> tail{0};  // Read position
    
public:
    bool tryPush(const T& item);  // Returns false if full
    bool tryPop(T& item);         // Returns false if empty
    size_t size() const;
};

Memory Ordering

  • Producer: store(head, release) after writing data
  • Consumer: load(head, acquire) before reading data
  • This ensures the consumer sees the data the producer wrote

Acceptance Criteria

  • Lock-free ring buffer with configurable capacity (default 4096 entries)
  • Drainer thread that writes to file and console
  • No allocations in the log path (pre-allocated buffer)
  • Overflow counter exposed via Log::getOverflowCount()
  • Unit tests for ring buffer correctness
  • Benchmark showing no audio thread blocking
  • Documentation on when to use audio-safe vs regular logging

References

Labels

  • enhancement
  • audio
  • performance

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions