mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 08:41:59 +00:00
host logger thread safe
This commit is contained in:
@@ -73,6 +73,65 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
// Reset the recursion guard for this task
|
||||
this->reset_task_log_recursion_(is_main_task);
|
||||
}
|
||||
#elif defined(USE_HOST)
|
||||
// Implementation for host platform (multi-threaded with pthread support)
|
||||
// Main thread always uses direct buffer access for console output and callbacks
|
||||
//
|
||||
// For non-main threads:
|
||||
// - WITH task log buffer: Queue message to lock-free ring buffer for async processing
|
||||
// - Prevents console corruption from concurrent writes by multiple threads
|
||||
// - Messages are serialized through main loop for proper console output
|
||||
// - Fallback to emergency console logging only if ring buffer is full
|
||||
// - WITHOUT task log buffer: Only emergency console output, no callbacks
|
||||
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag))
|
||||
return;
|
||||
|
||||
pthread_t current_thread = pthread_self();
|
||||
bool is_main_thread = pthread_equal(current_thread, main_thread_);
|
||||
|
||||
// Check and set recursion guard - uses pthread TLS for per-thread state
|
||||
if (this->check_and_set_task_log_recursion_(is_main_thread)) {
|
||||
return; // Recursion detected
|
||||
}
|
||||
|
||||
// Main thread uses the shared buffer for efficiency
|
||||
if (is_main_thread) {
|
||||
this->log_message_to_buffer_and_send_(level, tag, line, format, args);
|
||||
this->reset_task_log_recursion_(is_main_thread);
|
||||
return;
|
||||
}
|
||||
|
||||
bool message_sent = false;
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
// For non-main threads, queue the message for callbacks
|
||||
message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), format, args);
|
||||
if (message_sent) {
|
||||
// Enable logger loop to process the buffered message
|
||||
this->enable_loop_soon_any_context();
|
||||
}
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
// Emergency console logging for non-main threads when ring buffer is full or disabled
|
||||
// This is a fallback mechanism to ensure critical log messages are visible
|
||||
// Note: This may cause interleaved/corrupted console output if multiple threads
|
||||
// log simultaneously, but it's better than losing important messages entirely
|
||||
if (!message_sent) {
|
||||
// Host always has console output - no baud_rate check needed
|
||||
// Use larger buffer for host since memory is plentiful
|
||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 1024;
|
||||
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
|
||||
uint16_t buffer_at = 0; // Initialize buffer position
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at,
|
||||
MAX_CONSOLE_LOG_MSG_SIZE);
|
||||
// Add newline before writing to console
|
||||
this->add_newline_to_buffer_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE);
|
||||
this->write_msg_(console_buffer, buffer_at);
|
||||
}
|
||||
|
||||
// Reset the recursion guard for this thread
|
||||
this->reset_task_log_recursion_(is_main_thread);
|
||||
}
|
||||
#else
|
||||
// Implementation for all other platforms
|
||||
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||
@@ -86,7 +145,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
|
||||
global_recursion_guard_ = false;
|
||||
}
|
||||
#endif // !USE_ESP32
|
||||
#endif // USE_ESP32 / USE_HOST
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// Implementation for ESP8266 with flash string support.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <cstdarg>
|
||||
#include <map>
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
#include "esphome/core/automation.h"
|
||||
@@ -12,7 +12,11 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#include "task_log_buffer.h"
|
||||
#ifdef USE_HOST
|
||||
#include "task_log_buffer_host.h"
|
||||
#elif defined(USE_ESP32)
|
||||
#include "task_log_buffer_esp32.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
@@ -181,6 +185,9 @@ class Logger : public Component {
|
||||
uart_port_t get_uart_num() const { return uart_num_; }
|
||||
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
|
||||
#endif
|
||||
#ifdef USE_HOST
|
||||
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
|
||||
/// Get the UART used by the logger.
|
||||
@@ -228,7 +235,7 @@ class Logger : public Component {
|
||||
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
|
||||
va_list args, char *buffer, uint16_t *buffer_at,
|
||||
uint16_t buffer_size) {
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST)
|
||||
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
char buff[MAX_POINTER_REPRESENTATION];
|
||||
@@ -325,6 +332,9 @@ class Logger : public Component {
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
void *main_task_ = nullptr; // Only used for thread name identification
|
||||
#endif
|
||||
#ifdef USE_HOST
|
||||
pthread_t main_thread_{}; // Main thread for identification
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
// Task-specific recursion guards:
|
||||
// - Main task uses a dedicated member variable for efficiency
|
||||
@@ -332,6 +342,10 @@ class Logger : public Component {
|
||||
pthread_key_t log_recursion_key_; // 4 bytes
|
||||
uart_port_t uart_num_; // 4 bytes (enum defaults to int size)
|
||||
#endif
|
||||
#ifdef USE_HOST
|
||||
// Thread-specific recursion guards using pthread TLS
|
||||
pthread_key_t log_recursion_key_;
|
||||
#endif
|
||||
|
||||
// Large objects (internally aligned)
|
||||
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||
@@ -342,7 +356,11 @@ class Logger : public Component {
|
||||
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners
|
||||
#endif
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#ifdef USE_HOST
|
||||
std::unique_ptr<logger::TaskLogBufferHost> log_buffer_; // Will be initialized with init_log_buffer
|
||||
#elif defined(USE_ESP32)
|
||||
std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Group smaller types together at the end
|
||||
@@ -355,7 +373,7 @@ class Logger : public Component {
|
||||
#ifdef USE_LIBRETINY
|
||||
UARTSelection uart_{UART_SELECTION_DEFAULT};
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
bool main_task_recursion_guard_{false};
|
||||
#else
|
||||
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
|
||||
@@ -392,7 +410,7 @@ class Logger : public Component {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
inline bool HOT check_and_set_task_log_recursion_(bool is_main_task) {
|
||||
if (is_main_task) {
|
||||
const bool was_recursive = main_task_recursion_guard_;
|
||||
@@ -418,6 +436,22 @@ class Logger : public Component {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_HOST
|
||||
const char *HOT get_thread_name_() {
|
||||
pthread_t current_thread = pthread_self();
|
||||
if (pthread_equal(current_thread, main_thread_)) {
|
||||
return nullptr; // Main thread
|
||||
}
|
||||
// For non-main threads, return the thread name
|
||||
// We store it in thread-local storage to avoid allocation
|
||||
static thread_local char thread_name_buf[32];
|
||||
if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) {
|
||||
return thread_name_buf;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
static inline void copy_string(char *buffer, uint16_t &pos, const char *str) {
|
||||
const size_t len = strlen(str);
|
||||
// Intentionally no null terminator, building larger string
|
||||
@@ -475,7 +509,7 @@ class Logger : public Component {
|
||||
buffer[pos++] = '0' + (remainder - tens * 10);
|
||||
buffer[pos++] = ']';
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
|
||||
if (thread_name != nullptr) {
|
||||
write_ansi_color_for_level(buffer, pos, 1); // Always use bold red for thread name
|
||||
buffer[pos++] = '[';
|
||||
|
||||
108
esphome/components/logger/task_log_buffer_host.h
Normal file
108
esphome/components/logger/task_log_buffer_host.h
Normal file
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <pthread.h>
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
/**
|
||||
* @brief Lock-free task log buffer for host platform.
|
||||
*
|
||||
* This implements a Multi-Producer Single-Consumer (MPSC) lock-free ring buffer
|
||||
* for log messages on the host platform. It uses atomic operations for thread-safety
|
||||
* without requiring mutexes in the hot path.
|
||||
*
|
||||
* Design:
|
||||
* - Fixed number of pre-allocated message slots to avoid dynamic allocation
|
||||
* - Each slot contains a header and fixed-size text buffer
|
||||
* - Atomic indices for lock-free push/pop operations
|
||||
* - Thread-safe for multiple producers, single consumer (main loop)
|
||||
*
|
||||
* Host platform has much more memory than embedded devices, so we use larger
|
||||
* buffer sizes for better log message handling.
|
||||
*/
|
||||
class TaskLogBufferHost {
|
||||
public:
|
||||
// Default number of message slots - host has plenty of memory
|
||||
static constexpr size_t DEFAULT_SLOT_COUNT = 64;
|
||||
|
||||
// Structure for a log message (fixed size for lock-free operation)
|
||||
struct LogMessage {
|
||||
// Size constants - host has plenty of memory, so use larger sizes
|
||||
static constexpr size_t MAX_THREAD_NAME_SIZE = 32;
|
||||
static constexpr size_t MAX_TEXT_SIZE = 1024;
|
||||
|
||||
const char *tag; // Pointer to static tag string
|
||||
char thread_name[MAX_THREAD_NAME_SIZE]; // Thread name (copied)
|
||||
char text[MAX_TEXT_SIZE + 1]; // Message text with null terminator
|
||||
uint16_t text_length; // Actual length of text
|
||||
uint16_t line; // Source line number
|
||||
uint8_t level; // Log level
|
||||
std::atomic<bool> ready; // Message is ready to be consumed
|
||||
|
||||
LogMessage() : tag(nullptr), text_length(0), line(0), level(0), ready(false) {
|
||||
thread_name[0] = '\0';
|
||||
text[0] = '\0';
|
||||
}
|
||||
};
|
||||
|
||||
/// Constructor that takes the number of message slots
|
||||
explicit TaskLogBufferHost(size_t slot_count);
|
||||
~TaskLogBufferHost();
|
||||
|
||||
// NOT thread-safe - get next message from buffer, only call from main loop
|
||||
// Returns true if a message was retrieved, false if buffer is empty
|
||||
bool get_message_main_loop(LogMessage **message);
|
||||
|
||||
// NOT thread-safe - release the message after processing, only call from main loop
|
||||
void release_message_main_loop();
|
||||
|
||||
// Thread-safe - send a message to the buffer from any thread
|
||||
// Returns true if message was queued, false if buffer is full
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args);
|
||||
|
||||
// Check if there are messages ready to be processed
|
||||
inline bool HOT has_messages() const {
|
||||
return read_index_.load(std::memory_order_acquire) != write_index_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
// Get the buffer size (number of slots)
|
||||
inline size_t size() const { return slot_count_; }
|
||||
|
||||
private:
|
||||
// Acquire a slot for writing (thread-safe)
|
||||
// Returns slot index or -1 if buffer is full
|
||||
int acquire_write_slot_();
|
||||
|
||||
// Commit a slot after writing (thread-safe)
|
||||
void commit_write_slot_(int slot_index);
|
||||
|
||||
std::unique_ptr<LogMessage[]> slots_; // Pre-allocated message slots
|
||||
size_t slot_count_; // Number of slots
|
||||
|
||||
// Lock-free indices using atomics
|
||||
// We use a simple approach: write_index_ is where the next write will go,
|
||||
// read_index_ is where the next read will come from
|
||||
std::atomic<size_t> write_index_{0}; // Next slot to write to
|
||||
std::atomic<size_t> read_index_{0}; // Next slot to read from
|
||||
std::atomic<size_t> commit_index_{0}; // Last committed write
|
||||
|
||||
// For thread-safe slot acquisition
|
||||
std::atomic<size_t> reserve_index_{0}; // Next slot to reserve for writing
|
||||
};
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#endif // USE_HOST
|
||||
Reference in New Issue
Block a user