-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
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:
- Mutex locks can block - If the drainer thread holds the lock, the audio thread will wait, causing audio dropouts
- Priority inversion - A low-priority logging thread can block the high-priority audio thread
- 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
-
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::atomicfor head/tail indices - Memory barriers for correct ordering
-
Fixed-size log entries
struct LogEntry { Level level; std::chrono::steady_clock::time_point timestamp; char message[256]; // Fixed size, no allocation };
-
Drainer thread
- Sleeps when buffer empty (condition variable or polling)
- Wakes on buffer threshold or flush request
- Batch writes for efficiency
-
Overflow handling
- When buffer full, either:
- Drop oldest entries (prefer for audio - never block)
- Increment overflow counter for diagnostics
- When buffer full, either:
-
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()
- Keep existing
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
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels