From ad8039136f84a60a954d0ac1d1b3b5554f1e1548 Mon Sep 17 00:00:00 2001 From: zekageri Date: Mon, 6 Jul 2026 09:15:14 +0200 Subject: [PATCH 1/3] ring-buffer refactor --- .github/workflows/ci.yml | 2 +- .github/workflows/lint.yml | 3 +- README.md | 10 +- docs/api.md | 2 + docs/configuration.md | 26 +++- library.json | 2 +- library.properties | 2 +- src/Trace.h | 38 +++++- src/TraceFlush.cpp | 22 +-- src/TraceFormatting.cpp | 48 ++++--- src/TraceLifecycle.cpp | 37 +++++- src/TraceLogging.cpp | 117 ++++++++++------ src/TraceQueries.cpp | 41 ++++-- src/internal/TraceImpl.h | 21 +-- src/internal/TraceStorage.h | 229 ++++++++++++++++++++++++++++++++ src/internal/TraceTaskSupport.h | 3 +- tests/host/esp_heap_caps.h | 64 ++++++++- tests/host/trace_host_tests.cpp | 191 ++++++++++++++++++++++++++ 18 files changed, 755 insertions(+), 103 deletions(-) create mode 100644 src/internal/TraceStorage.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c65ff92..1bb3f66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [ main, master, 'feature/**' ] + branches: [ '**' ] tags: ['v*'] pull_request: workflow_dispatch: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d780494..52b796a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,8 +4,7 @@ on: pull_request: push: branches: - - main - - feature/** + - '**' permissions: contents: read diff --git a/README.md b/README.md index 3ddc70e..c641ee4 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Trace helps you collect structured runtime logs in Arduino ESP32 projects with b ## Why use Trace? -* **Bounded memory** - recent history, realtime delivery, pending flush logs, and payload lengths have separate configured limits. +* **Bounded memory** - recent history, realtime delivery, pending flush logs, and payload lengths use fixed-capacity storage. * **Structured output** - log records keep level, tag, message, formatted text, sequence, and uptime. * **Task-side callbacks** - realtime observation and persistence callbacks run from the internal Trace task. * **ESP32 task control** - configure byte stack size, priority, core affinity, and stack memory preference. @@ -80,7 +80,7 @@ void loop() { * `maxRecentLogs` controls queryable in-RAM history only. * `maxRealtimeLogs` controls the realtime delivery queue used by `onLog()` and stream output. * `maxPendingLogs` controls unsaved logs waiting for flush. -* Queue-count `0` disables that queue. Payload-cap `0` means unlimited. +* Queue-count `0` disables that queue. Payload-cap `0` uses the compiled maximum for that payload type. * `setStream()` writes formatted realtime logs to any Arduino `Print` stream such as `Serial`, `Serial1`, `WiFiClient`, or a custom sink. * Stream output uses ANSI colors by default. Callback, flush, and query `TraceLog::formatted` values stay plain text. * `onLog()` is for realtime observation; `onFlush()` is for persistence. @@ -89,6 +89,7 @@ void loop() { * Detaching or replacing `Print` or `Tempo` while Trace is active does not synchronize already snapshotted worker use. * Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes. * `TraceStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM. +* `TraceStorageMemory::PreferPsram` opts recent and pending log buffers into PSRAM with internal fallback. Realtime delivery stays internal. ## Examples @@ -149,16 +150,17 @@ For the full API, see [`docs/api.md`](docs/api.md). | Platform | `espressif32` | | Language | C++20 | | Filesystem | none | -| PSRAM | Optional for task stacks when ESP-IDF support is available | +| PSRAM | Optional for task stacks and opt-in recent/pending log storage | | Dependencies | `bblanchon/ArduinoJson >= 7.0.0` | | Exceptions | Not used | -| Status | Early-stage `0.1.0` | +| Status | Release candidate `0.2.0-rc.1` | ## Configuration ```cpp TraceConfig config; config.stackSize = 4096; +config.storageMemory = TraceStorageMemory::Internal; config.maxRecentLogs = 100; config.maxRealtimeLogs = 100; config.maxPendingLogs = 50; diff --git a/docs/api.md b/docs/api.md index 9e980ba..159cfba 100644 --- a/docs/api.md +++ b/docs/api.md @@ -67,6 +67,8 @@ std::vector getLastLogs(size_t count); std::vector getLogsByTag(const char *tag); ``` +`TraceDiag` includes queue counts, drop and flush counters, task stack information, queue allocation byte counts, and PSRAM placement flags for recent, realtime, and pending queues. + ## TraceLog `TraceLog` stores `sequence`, `level`, `tag`, `message`, `formatted`, `timeText`, `uptimeMs`, and `truncated`. diff --git a/docs/configuration.md b/docs/configuration.md index adbea05..a0a26de 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,6 @@ # Configuration -`TraceConfig` controls the internal task, log limits, flush timing, overflow behavior, JSON formatting, level filtering, and stream colors. +`TraceConfig` controls the internal task, log storage memory, log limits, flush timing, overflow behavior, JSON formatting, level filtering, and stream colors. ```cpp TraceConfig config; @@ -8,6 +8,7 @@ config.stackSize = 4096; config.priority = 1; config.coreId = tskNO_AFFINITY; config.stackType = TraceStackType::Auto; +config.storageMemory = TraceStorageMemory::Internal; config.maxRecentLogs = 100; config.maxRealtimeLogs = 100; config.maxPendingLogs = 50; @@ -35,7 +36,17 @@ config.maxFormattedLength = 384; `maxPendingLogs = 0` disables persistence buffering. Logs are still accepted for recent history and realtime delivery, but they are counted as dropped for persistence. -Queue-count `0` means disabled. Payload-cap `0` means unlimited. +Queue-count `0` means disabled. Payload-cap `0` means use the compiled maximum for that payload type. + +## Storage memory + +Trace stores recent, realtime, and pending logs in fixed-capacity ring buffers. Each enabled queue is allocated once during `init()`. + +`TraceStorageMemory::Internal` is the deterministic default. Recent, realtime, and pending queues use internal-capable memory. + +`TraceStorageMemory::PreferPsram` uses PSRAM for recent and pending queues when PSRAM is available, otherwise it falls back to internal-capable memory. The realtime queue remains internal. + +`TraceStorageMemory::RequirePsram` requires recent and pending queues to allocate in PSRAM. `init()` returns `TraceStatus::OutOfMemory` if PSRAM is unavailable or allocation fails. The realtime queue remains internal. ## Payload limits @@ -45,6 +56,17 @@ Queue-count `0` means disabled. Payload-cap `0` means unlimited. `maxFormattedLength` applies to `printf`-style and JSON-formatted input before it becomes `TraceLog::message`. +Runtime payload limits are bounded by compile-time caps: + +```cpp +TRACE_RECORD_MAX_TAG_LENGTH +TRACE_RECORD_MAX_MESSAGE_LENGTH +TRACE_FORMATTED_BUFFER_LENGTH +TRACE_TIME_TEXT_BUFFER_LENGTH +``` + +Setting a runtime limit above the compiled cap clamps to the compiled cap. Setting a runtime payload limit to `0` uses the compiled cap. + When Trace truncates a log, `TraceLog::truncated` is set. `TraceDiag::truncatedLogCount` increments once per log record, even if both tag and message were truncated. ## Flush triggers diff --git a/library.json b/library.json index 8cf9705..78da5f9 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "Trace", - "version": "0.1.0", + "version": "0.2.0-rc.1", "description": "Logging and diagnostics library for ESP32 devices with bounded buffers and task-side flushing.", "keywords": [ "esp32", diff --git a/library.properties b/library.properties index 1f964aa..52831bb 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=Trace -version=0.1.0 +version=0.2.0-rc.1 author=zekageri maintainer=zekageri sentence=Logging and diagnostics library for ESP32 devices. diff --git a/src/Trace.h b/src/Trace.h index e4019d9..a9008b4 100644 --- a/src/Trace.h +++ b/src/Trace.h @@ -13,6 +13,22 @@ #include #include +#ifndef TRACE_RECORD_MAX_TAG_LENGTH +#define TRACE_RECORD_MAX_TAG_LENGTH 32 +#endif + +#ifndef TRACE_RECORD_MAX_MESSAGE_LENGTH +#define TRACE_RECORD_MAX_MESSAGE_LENGTH 256 +#endif + +#ifndef TRACE_FORMATTED_BUFFER_LENGTH +#define TRACE_FORMATTED_BUFFER_LENGTH 384 +#endif + +#ifndef TRACE_TIME_TEXT_BUFFER_LENGTH +#define TRACE_TIME_TEXT_BUFFER_LENGTH 48 +#endif + class Tempo; struct TraceImpl; @@ -43,6 +59,12 @@ enum class TraceStackType : uint8_t { Psram, }; +enum class TraceStorageMemory : uint8_t { + Internal, + PreferPsram, + RequirePsram, +}; + enum class TraceOverflowPolicy : uint8_t { DropOldestPending, DropNewest, @@ -89,6 +111,7 @@ struct TraceConfig { UBaseType_t priority = 1; BaseType_t coreId = tskNO_AFFINITY; TraceStackType stackType = TraceStackType::Auto; + TraceStorageMemory storageMemory = TraceStorageMemory::Internal; size_t maxRecentLogs = 100; size_t maxRealtimeLogs = 100; size_t maxPendingLogs = 50; @@ -146,6 +169,12 @@ struct TraceDiag { size_t stackHighWaterMarkBytes = 0; TraceStackType requestedStackType = TraceStackType::Auto; TraceStackType actualStackType = TraceStackType::Internal; + size_t recentAllocatedBytes = 0; + size_t realtimeAllocatedBytes = 0; + size_t pendingAllocatedBytes = 0; + bool recentLogsInPsram = false; + bool realtimeLogsInPsram = false; + bool pendingLogsInPsram = false; }; using TraceTimeFormatter = bool (*)(const Tempo &tempo, char *buffer, size_t bufferSize); @@ -260,12 +289,11 @@ class Trace { return TraceResult::failure(TraceStatus::InvalidArgument, "format failed"); } const size_t limit = getMaxFormattedLength(); - const bool unlimited = limit == 0; const size_t outputLength = static_cast(needed); - const size_t boundedLength = unlimited ? outputLength : std::min(outputLength, limit); - std::vector buffer(boundedLength + 1); - snprintf(buffer.data(), buffer.size(), format, args...); - return log(level, tag, std::string(buffer.data()), !unlimited && outputLength > limit); + const size_t boundedLength = std::min(outputLength, limit); + char buffer[TRACE_FORMATTED_BUFFER_LENGTH + 1] = {}; + snprintf(buffer, boundedLength + 1, format, args...); + return log(level, tag, std::string(buffer), outputLength > limit); } size_t getMaxFormattedLength() const; diff --git a/src/TraceFlush.cpp b/src/TraceFlush.cpp index ce7c382..6e0cffc 100644 --- a/src/TraceFlush.cpp +++ b/src/TraceFlush.cpp @@ -17,8 +17,14 @@ void TraceImpl::performFlush() { return; } callback = onFlush; - batch.logs = pendingLogs; batch.createdAtUptimeMs = millis(); + batch.logs.reserve(pendingLogs.size()); + for (size_t i = 0; i < pendingLogs.size(); ++i) { + TraceRecord record; + if (pendingLogs.peek(i, record)) { + batch.logs.push_back(toPublicLog(record)); + } + } if (!batch.logs.empty()) { maxSequence = batch.logs.back().sequence; } @@ -46,16 +52,10 @@ void TraceImpl::performFlush() { if (flushResult == TraceFlushResult::Ok) { nextFlushAttemptMs = 0; if (maxSequence > 0) { - pendingLogs.erase( - std::remove_if( - pendingLogs.begin(), - pendingLogs.end(), - [maxSequence](const TraceLog &log) { - return log.sequence <= maxSequence; - } - ), - pendingLogs.end() - ); + TraceRecord record; + while (pendingLogs.peek(0, record) && record.sequence <= maxSequence) { + pendingLogs.pop(record); + } } flushSuccessCount++; } else if (flushResult == TraceFlushResult::Retry) { diff --git a/src/TraceFormatting.cpp b/src/TraceFormatting.cpp index 8d55309..31dc410 100644 --- a/src/TraceFormatting.cpp +++ b/src/TraceFormatting.cpp @@ -19,6 +19,13 @@ bool isErrorLevel(TraceLevel level) { return level == TraceLevel::Error || level == TraceLevel::Fatal; } +size_t effectiveLimit(size_t limit, size_t maximum) { + if (limit == 0 || limit > maximum) { + return maximum; + } + return limit; +} + size_t clampedLimit(size_t length, size_t limit) { if (limit == 0 || length <= limit) { return length; @@ -47,25 +54,20 @@ std::string formatPrintf(const char *format, va_list args, size_t limit, bool &t return std::string(); } - char stackBuffer[kFormatBufferSize]; + char stackBuffer[kFormatBufferSize + 1] = {}; + const size_t effective = effectiveLimit(limit, TRACE_FORMATTED_BUFFER_LENGTH); va_list copy; va_copy(copy, args); - const int needed = vsnprintf(stackBuffer, sizeof(stackBuffer), format, copy); + const int needed = vsnprintf(stackBuffer, effective + 1, format, copy); va_end(copy); if (needed < 0) { return std::string(); } const size_t outputLength = static_cast(needed); - const size_t boundedLength = clampedLimit(outputLength, limit); - truncated = limit > 0 && outputLength > limit; - if (boundedLength < sizeof(stackBuffer)) { - return std::string(stackBuffer, boundedLength); - } - - std::vector buffer(boundedLength + 1); - vsnprintf(buffer.data(), buffer.size(), format, args); - return std::string(buffer.data()); + const size_t boundedLength = std::min(outputLength, effective); + truncated = outputLength > effective; + return std::string(stackBuffer, boundedLength); } std::string jsonToString( @@ -76,20 +78,32 @@ std::string jsonToString( ) { const size_t measuredLength = format == TraceJsonFormat::Pretty ? measureJsonPretty(doc) : measureJson(doc); - const size_t boundedLength = clampedLimit(measuredLength, limit); - truncated = limit > 0 && measuredLength > limit; + const size_t effective = effectiveLimit(limit, TRACE_FORMATTED_BUFFER_LENGTH); + const size_t boundedLength = std::min(measuredLength, effective); + truncated = measuredLength > effective; - std::vector buffer(boundedLength + 1); + char buffer[TRACE_FORMATTED_BUFFER_LENGTH + 1] = {}; if (format == TraceJsonFormat::Pretty) { - serializeJsonPretty(doc, buffer.data(), buffer.size()); + serializeJsonPretty(doc, buffer, boundedLength + 1); } else { - serializeJson(doc, buffer.data(), buffer.size()); + serializeJson(doc, buffer, boundedLength + 1); } buffer[boundedLength] = '\0'; - return std::string(buffer.data()); + return std::string(buffer); } } // namespace trace_detail +TraceLog TraceImpl::toPublicLog(const TraceRecord &record) { + TraceLog log; + log.sequence = record.sequence; + log.level = record.level; + log.tag = record.tag; + log.message = record.message; + log.uptimeMs = record.uptimeMs; + log.truncated = record.truncated; + return log; +} + void TraceImpl::formatLog(TraceLog &log) { char timeBuffer[trace_detail::kTimeBufferSize] = {}; bool hasTime = false; diff --git a/src/TraceLifecycle.cpp b/src/TraceLifecycle.cpp index 20798c1..298e4c4 100644 --- a/src/TraceLifecycle.cpp +++ b/src/TraceLifecycle.cpp @@ -1,5 +1,28 @@ #include "internal/TraceImpl.h" +bool TraceImpl::initBuffers() { + deinitBuffers(); + if (!recentLogs.init(config.maxRecentLogs, config.storageMemory, false)) { + deinitBuffers(); + return false; + } + if (!realtimeLogs.init(config.maxRealtimeLogs, TraceStorageMemory::Internal, true)) { + deinitBuffers(); + return false; + } + if (!pendingLogs.init(config.maxPendingLogs, config.storageMemory, false)) { + deinitBuffers(); + return false; + } + return true; +} + +void TraceImpl::deinitBuffers() { + recentLogs.deinit(); + realtimeLogs.deinit(); + pendingLogs.deinit(); +} + void TraceImpl::wakeTask() { TaskHandle_t handle = nullptr; { @@ -135,9 +158,6 @@ TraceResult Trace::init(const TraceConfig &config) { _impl->stopping = false; _impl->flushRequested = false; _impl->urgentFlushRequested = false; - _impl->recentLogs.clear(); - _impl->realtimeLogs.clear(); - _impl->pendingLogs.clear(); _impl->nextSequence = 1; _impl->droppedLogCount = 0; _impl->realtimeLogCount = 0; @@ -155,6 +175,12 @@ TraceResult Trace::init(const TraceConfig &config) { _impl->shutdownDeadlineMs = 0; _impl->shutdownTimedOut = false; _impl->shutdownFlushFailed = false; + if (!_impl->initBuffers()) { + return TraceResult::failure( + TraceStatus::OutOfMemory, + "failed to allocate trace buffers" + ); + } } TaskHandle_t handle = nullptr; @@ -171,6 +197,10 @@ TraceResult Trace::init(const TraceConfig &config) { createdWithCaps ); if (created != pdPASS || handle == nullptr) { + TraceLock lock(_impl->mutex); + if (lock) { + _impl->deinitBuffers(); + } return TraceResult::failure(TraceStatus::TaskCreateFailed, "failed to create trace task"); } @@ -217,6 +247,7 @@ TraceResult Trace::end(uint32_t timeoutMs) { _impl->initialized = false; _impl->stopping = false; _impl->shutdownDeadlineMs = 0; + _impl->deinitBuffers(); if (timedOut) { return TraceResult::failure(TraceStatus::Timeout, "trace end timed out"); } diff --git a/src/TraceLogging.cpp b/src/TraceLogging.cpp index 85bd3fd..d1b548d 100644 --- a/src/TraceLogging.cpp +++ b/src/TraceLogging.cpp @@ -1,6 +1,36 @@ #include "internal/TraceImpl.h" -TraceResult TraceImpl::appendLog(TraceLog log) { +#include + +namespace { +void copyToRecordField( + const char *source, + size_t sourceLength, + size_t configuredLimit, + char *target, + size_t targetCapacity, + bool &truncated +) { + if (target == nullptr || targetCapacity == 0) { + return; + } + target[0] = '\0'; + if (source == nullptr) { + return; + } + const size_t effective = trace_detail::effectiveLimit(configuredLimit, targetCapacity - 1); + const size_t copied = std::min(sourceLength, effective); + if (copied > 0) { + memcpy(target, source, copied); + } + target[copied] = '\0'; + if (sourceLength > effective) { + truncated = true; + } +} +} // namespace + +TraceResult TraceImpl::appendLog(TraceRecord record) { bool recentAdded = false; const uint64_t startedMs = millis(); @@ -20,11 +50,11 @@ TraceResult TraceImpl::appendLog(TraceLog log) { } if (!recentAdded) { - log.sequence = nextSequence++; - addRecentLocked(log); - addRealtimeLocked(log); - lastLogAtMs = log.uptimeMs; - if (log.truncated) { + record.sequence = nextSequence++; + addRecentLocked(record); + addRealtimeLocked(record); + lastLogAtMs = record.uptimeMs; + if (record.truncated) { truncatedLogCount++; } recentAdded = true; @@ -33,12 +63,10 @@ TraceResult TraceImpl::appendLog(TraceLog log) { if (config.maxPendingLogs == 0) { droppedLogCount++; shouldNotify = true; - } else if (pendingLogs.size() < config.maxPendingLogs) { - pendingLogs.push_back(log); + } else if (pendingLogs.pushDropNewest(record)) { shouldNotify = true; } else if (config.overflowPolicy == TraceOverflowPolicy::DropOldestPending) { - pendingLogs.erase(pendingLogs.begin()); - pendingLogs.push_back(log); + pendingLogs.pushDropOldest(record); droppedLogCount++; shouldNotify = true; } else if (config.overflowPolicy == TraceOverflowPolicy::DropNewest) { @@ -56,7 +84,7 @@ TraceResult TraceImpl::appendLog(TraceLog log) { if (config.flushEveryLogs > 0 && pendingLogs.size() >= config.flushEveryLogs) { flushRequested = true; } - if (config.flushOnError && trace_detail::isErrorLevel(log.level)) { + if (config.flushOnError && trace_detail::isErrorLevel(record.level)) { urgentFlushRequested = true; flushRequested = true; } @@ -82,25 +110,21 @@ TraceResult TraceImpl::appendLog(TraceLog log) { } } -void TraceImpl::addRecentLocked(const TraceLog &log) { +void TraceImpl::addRecentLocked(const TraceRecord &record) { if (config.maxRecentLogs == 0) { return; } - while (recentLogs.size() >= config.maxRecentLogs) { - recentLogs.erase(recentLogs.begin()); - } - recentLogs.push_back(log); + recentLogs.pushDropOldest(record); } -void TraceImpl::addRealtimeLocked(const TraceLog &log) { +void TraceImpl::addRealtimeLocked(const TraceRecord &record) { if (config.maxRealtimeLogs == 0 || (!onLog && stream == nullptr)) { return; } - while (realtimeLogs.size() >= config.maxRealtimeLogs) { - realtimeLogs.erase(realtimeLogs.begin()); + if (realtimeLogs.full()) { droppedRealtimeLogCount++; } - realtimeLogs.push_back(log); + realtimeLogs.pushDropOldest(record); realtimeLogCount++; } @@ -108,7 +132,7 @@ void TraceImpl::processRealtimeLogs() { TraceLogCallback callback; Print *streamSnapshot = nullptr; bool colorsEnabled = true; - std::vector logs; + std::vector records; { TraceLock lock(mutex); if (!lock) { @@ -121,10 +145,15 @@ void TraceImpl::processRealtimeLogs() { realtimeLogs.clear(); return; } - logs.swap(realtimeLogs); + records.reserve(realtimeLogs.size()); + TraceRecord record; + while (realtimeLogs.pop(record)) { + records.push_back(record); + } } - for (TraceLog &log : logs) { + for (const TraceRecord &record : records) { + TraceLog log = toPublicLog(record); formatLog(log); if (streamSnapshot != nullptr) { const char *color = colorsEnabled ? levelColor(log.level) : ""; @@ -145,9 +174,12 @@ void TraceImpl::processRealtimeLogs() { size_t Trace::getMaxFormattedLength() const { TraceLock lock(_impl->mutex); if (!lock) { - return TraceConfig().maxFormattedLength; + return TRACE_FORMATTED_BUFFER_LENGTH; } - return _impl->config.maxFormattedLength; + return trace_detail::effectiveLimit( + _impl->config.maxFormattedLength, + TRACE_FORMATTED_BUFFER_LENGTH + ); } TraceResult Trace::log(TraceLevel level, const char *tag, const std::string &message) { @@ -158,10 +190,7 @@ TraceResult Trace::log(TraceLevel level, const char *tag, const std::string &mes config = _impl->config; } } - bool messageTruncated = false; - const std::string boundedMessage = - trace_detail::truncateString(message, config.maxMessageLength, messageTruncated); - return log(level, tag, boundedMessage, messageTruncated); + return log(level, tag, message, false); } TraceResult Trace::log( @@ -190,14 +219,28 @@ TraceResult Trace::log( return TraceResult::success("log filtered"); } - bool tagTruncated = false; - TraceLog log; - log.level = level; - log.tag = trace_detail::copyLimited(tag, config.maxTagLength, tagTruncated); - log.message = message; - log.uptimeMs = millis(); - log.truncated = tagTruncated || messageTruncated; - return _impl->appendLog(log); + bool truncated = messageTruncated; + TraceRecord record; + record.level = level; + record.uptimeMs = millis(); + copyToRecordField( + tag, + strlen(tag), + config.maxTagLength, + record.tag, + sizeof(record.tag), + truncated + ); + copyToRecordField( + message.c_str(), + message.size(), + config.maxMessageLength, + record.message, + sizeof(record.message), + truncated + ); + record.truncated = truncated; + return _impl->appendLog(record); } TraceResult Trace::logJson(TraceLevel level, const char *tag, const JsonDocument &doc) { diff --git a/src/TraceQueries.cpp b/src/TraceQueries.cpp index 9393490..8f52adc 100644 --- a/src/TraceQueries.cpp +++ b/src/TraceQueries.cpp @@ -1,5 +1,7 @@ #include "internal/TraceImpl.h" +#include + TraceDiag Trace::getDiagnostics() { TraceDiag diag; TraceLock lock(_impl->mutex); @@ -20,6 +22,12 @@ TraceDiag Trace::getDiagnostics() { diag.stackHighWaterMarkBytes = _impl->stackHighWaterMarkBytes; diag.requestedStackType = _impl->config.stackType; diag.actualStackType = _impl->actualStackType; + diag.recentAllocatedBytes = _impl->recentLogs.allocatedBytes(); + diag.realtimeAllocatedBytes = _impl->realtimeLogs.allocatedBytes(); + diag.pendingAllocatedBytes = _impl->pendingLogs.allocatedBytes(); + diag.recentLogsInPsram = _impl->recentLogs.usingPsram(); + diag.realtimeLogsInPsram = _impl->realtimeLogs.usingPsram(); + diag.pendingLogsInPsram = _impl->pendingLogs.usingPsram(); return diag; } @@ -30,7 +38,11 @@ TraceLog Trace::getLastLog() { if (!lock || _impl->recentLogs.empty()) { return log; } - log = _impl->recentLogs.back(); + TraceRecord record; + if (!_impl->recentLogs.peek(_impl->recentLogs.size() - 1, record)) { + return log; + } + log = _impl->toPublicLog(record); } _impl->formatLog(log); return log; @@ -43,7 +55,13 @@ std::vector Trace::getLogs() { if (!lock) { return logs; } - logs = _impl->recentLogs; + logs.reserve(_impl->recentLogs.size()); + for (size_t i = 0; i < _impl->recentLogs.size(); ++i) { + TraceRecord record; + if (_impl->recentLogs.peek(i, record)) { + logs.push_back(_impl->toPublicLog(record)); + } + } } for (TraceLog &log : logs) { _impl->formatLog(log); @@ -58,9 +76,10 @@ std::vector Trace::getLogs(TraceLevel level) { if (!lock) { return logs; } - for (const TraceLog &log : _impl->recentLogs) { - if (log.level == level) { - logs.push_back(log); + for (size_t i = 0; i < _impl->recentLogs.size(); ++i) { + TraceRecord record; + if (_impl->recentLogs.peek(i, record) && record.level == level) { + logs.push_back(_impl->toPublicLog(record)); } } } @@ -79,7 +98,10 @@ std::vector Trace::getLastLogs(size_t count) { } const size_t start = count >= _impl->recentLogs.size() ? 0 : _impl->recentLogs.size() - count; for (size_t i = start; i < _impl->recentLogs.size(); ++i) { - logs.push_back(_impl->recentLogs[i]); + TraceRecord record; + if (_impl->recentLogs.peek(i, record)) { + logs.push_back(_impl->toPublicLog(record)); + } } } for (TraceLog &log : logs) { @@ -98,9 +120,10 @@ std::vector Trace::getLogsByTag(const char *tag) { if (!lock) { return logs; } - for (const TraceLog &log : _impl->recentLogs) { - if (log.tag == tag) { - logs.push_back(log); + for (size_t i = 0; i < _impl->recentLogs.size(); ++i) { + TraceRecord record; + if (_impl->recentLogs.peek(i, record) && strcmp(record.tag, tag) == 0) { + logs.push_back(_impl->toPublicLog(record)); } } } diff --git a/src/internal/TraceImpl.h b/src/internal/TraceImpl.h index ff2109c..e5e15cb 100644 --- a/src/internal/TraceImpl.h +++ b/src/internal/TraceImpl.h @@ -2,6 +2,7 @@ #include "../Trace.h" #include "TraceMutex.h" +#include "TraceStorage.h" #include "TraceTaskSupport.h" #include @@ -10,11 +11,12 @@ namespace trace_detail { inline constexpr uint32_t kWaitPollMs = 10; -inline constexpr size_t kFormatBufferSize = 256; -inline constexpr size_t kTimeBufferSize = 48; +inline constexpr size_t kFormatBufferSize = TRACE_FORMATTED_BUFFER_LENGTH; +inline constexpr size_t kTimeBufferSize = TRACE_TIME_TEXT_BUFFER_LENGTH; bool levelEnabled(TraceLevel level, TraceLevel minLevel); bool isErrorLevel(TraceLevel level); +size_t effectiveLimit(size_t limit, size_t maximum); size_t clampedLimit(size_t length, size_t limit); std::string copyLimited(const char *value, size_t limit, bool &truncated); std::string truncateString(const std::string &value, size_t limit, bool &truncated); @@ -31,9 +33,9 @@ struct TraceImpl { TraceConfig config{}; TraceTempoConfig tempoConfig{}; TraceMutex mutex; - std::vector recentLogs; - std::vector realtimeLogs; - std::vector pendingLogs; + TraceRingBuffer recentLogs; + TraceRingBuffer realtimeLogs; + TraceRingBuffer pendingLogs; TraceFlushCallback onFlush; TraceLogCallback onLog; Print *stream = nullptr; @@ -63,12 +65,15 @@ struct TraceImpl { bool shutdownTimedOut = false; bool shutdownFlushFailed = false; + bool initBuffers(); + void deinitBuffers(); void wakeTask(); - void addRecentLocked(const TraceLog &log); - void addRealtimeLocked(const TraceLog &log); + void addRecentLocked(const TraceRecord &record); + void addRealtimeLocked(const TraceRecord &record); uint32_t retryIntervalMsLocked() const; - TraceResult appendLog(TraceLog log); + TraceResult appendLog(TraceRecord record); + TraceLog toPublicLog(const TraceRecord &record); void formatLog(TraceLog &log); bool formatTempoTime( const Tempo &tempoRef, diff --git a/src/internal/TraceStorage.h b/src/internal/TraceStorage.h new file mode 100644 index 0000000..c92f248 --- /dev/null +++ b/src/internal/TraceStorage.h @@ -0,0 +1,229 @@ +#pragma once + +#include "../Trace.h" + +#include +#include +#include +#include +#include +#include + +#include "esp_heap_caps.h" + +struct TraceRecord { + uint64_t sequence = 0; + uint64_t uptimeMs = 0; + uint32_t epochSeconds = 0; + TraceLevel level = TraceLevel::Info; + bool hasEpochTime = false; + bool truncated = false; + char tag[TRACE_RECORD_MAX_TAG_LENGTH + 1] = {}; + char message[TRACE_RECORD_MAX_MESSAGE_LENGTH + 1] = {}; +}; + +namespace trace_storage { +#if defined(MALLOC_CAP_INTERNAL) +inline constexpr int kInternalCaps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT; +#else +inline constexpr int kInternalCaps = MALLOC_CAP_8BIT; +#endif + +#if defined(MALLOC_CAP_SPIRAM) +inline constexpr int kPsramCaps = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; +#else +inline constexpr int kPsramCaps = MALLOC_CAP_8BIT; +#endif + +struct AllocationInfo { + size_t bytes = 0; + bool psram = false; +}; + +inline bool psramAvailable() { +#if defined(MALLOC_CAP_SPIRAM) + return heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0; +#else + return false; +#endif +} + +inline void *allocate( + size_t bytes, + TraceStorageMemory memory, + bool realtime, + AllocationInfo &info +) { + info = AllocationInfo(); + if (bytes == 0) { + return nullptr; + } + + void *ptr = nullptr; + if (!realtime && memory != TraceStorageMemory::Internal) { +#if defined(MALLOC_CAP_SPIRAM) + if (psramAvailable()) { + ptr = heap_caps_malloc(bytes, kPsramCaps); + if (ptr != nullptr) { + info.bytes = bytes; + info.psram = true; + return ptr; + } + } +#endif + if (memory == TraceStorageMemory::RequirePsram) { + return nullptr; + } + } + + ptr = heap_caps_malloc(bytes, kInternalCaps); + if (ptr != nullptr) { + info.bytes = bytes; + info.psram = false; + } + return ptr; +} + +inline void deallocate(void *ptr) { + if (ptr != nullptr) { + heap_caps_free(ptr); + } +} +} // namespace trace_storage + +template +class TraceRingBuffer { + public: + TraceRingBuffer() = default; + ~TraceRingBuffer() { + deinit(); + } + + TraceRingBuffer(const TraceRingBuffer &) = delete; + TraceRingBuffer &operator=(const TraceRingBuffer &) = delete; + + bool init(size_t capacity, TraceStorageMemory memoryType, bool realtime) { + static_assert(std::is_trivially_copyable::value, "TraceRingBuffer requires trivial items"); + static_assert( + std::is_trivially_destructible::value, + "TraceRingBuffer requires trivially destructible items" + ); + deinit(); + if (capacity == 0) { + return true; + } + const size_t bytes = sizeof(T) * capacity; + trace_storage::AllocationInfo allocation; + void *items = trace_storage::allocate(bytes, memoryType, realtime, allocation); + if (items == nullptr) { + return false; + } + items_ = static_cast(items); + capacity_ = capacity; + allocation_ = allocation; + clearStorage(); + return true; + } + + void deinit() { + if (items_ != nullptr) { + for (size_t i = 0; i < capacity_; ++i) { + items_[i].~T(); + } + } + trace_storage::deallocate(items_); + items_ = nullptr; + capacity_ = 0; + head_ = 0; + count_ = 0; + allocation_ = trace_storage::AllocationInfo(); + } + + bool pushDropOldest(const T &item) { + if (capacity_ == 0 || items_ == nullptr) { + return false; + } + if (count_ < capacity_) { + items_[physicalIndex(count_)] = item; + count_++; + return true; + } + items_[head_] = item; + head_ = (head_ + 1) % capacity_; + return true; + } + + bool pushDropNewest(const T &item) { + if (full()) { + return false; + } + return pushDropOldest(item); + } + + bool pop(T &out) { + if (empty()) { + return false; + } + out = items_[head_]; + head_ = (head_ + 1) % capacity_; + count_--; + if (count_ == 0) { + head_ = 0; + } + return true; + } + + bool peek(size_t index, T &out) const { + if (index >= count_ || items_ == nullptr) { + return false; + } + out = items_[physicalIndex(index)]; + return true; + } + + void clear() { + head_ = 0; + count_ = 0; + } + + size_t size() const { + return count_; + } + + size_t capacity() const { + return capacity_; + } + + bool empty() const { + return count_ == 0; + } + + bool full() const { + return capacity_ > 0 && count_ >= capacity_; + } + + size_t allocatedBytes() const { + return allocation_.bytes; + } + + bool usingPsram() const { + return allocation_.psram; + } + + private: + size_t physicalIndex(size_t logicalIndex) const { + return (head_ + logicalIndex) % capacity_; + } + + T *items_ = nullptr; + size_t capacity_ = 0; + size_t head_ = 0; + size_t count_ = 0; + trace_storage::AllocationInfo allocation_; + + void clearStorage() { + for (size_t i = 0; i < capacity_; ++i) { + ::new (static_cast(&items_[i])) T(); + } + } +}; diff --git a/src/internal/TraceTaskSupport.h b/src/internal/TraceTaskSupport.h index c20ad4e..17220ec 100644 --- a/src/internal/TraceTaskSupport.h +++ b/src/internal/TraceTaskSupport.h @@ -3,8 +3,9 @@ #include #include -extern "C" { #include "esp_heap_caps.h" + +extern "C" { #include "freertos/FreeRTOS.h" #include "freertos/task.h" } diff --git a/tests/host/esp_heap_caps.h b/tests/host/esp_heap_caps.h index 8d48acb..fcdd195 100644 --- a/tests/host/esp_heap_caps.h +++ b/tests/host/esp_heap_caps.h @@ -1,11 +1,73 @@ #pragma once +#include #include +#include #define MALLOC_CAP_SPIRAM 1 #define MALLOC_CAP_8BIT 2 +#define MALLOC_CAP_INTERNAL 4 + +namespace trace_host_heap { +inline bool psramAvailable = false; +inline bool failNextAllocation = false; +inline int failCaps = 0; +inline int allocationsBeforeFailure = -1; +inline size_t allocationCount = 0; +inline size_t freeCount = 0; +inline size_t activeAllocations = 0; +inline std::unordered_map allocationCaps; + +inline void reset() { + psramAvailable = false; + failNextAllocation = false; + failCaps = 0; + allocationsBeforeFailure = -1; + allocationCount = 0; + freeCount = 0; + activeAllocations = 0; + allocationCaps.clear(); +} +} // namespace trace_host_heap inline size_t heap_caps_get_total_size(int caps) { - (void)caps; + if ((caps & MALLOC_CAP_SPIRAM) != 0 && trace_host_heap::psramAvailable) { + return 4 * 1024 * 1024; + } return 0; } + +inline void *heap_caps_malloc(size_t size, int caps) { + if (trace_host_heap::failNextAllocation && + (trace_host_heap::failCaps == 0 || (caps & trace_host_heap::failCaps) != 0)) { + trace_host_heap::failNextAllocation = false; + return nullptr; + } + if (trace_host_heap::allocationsBeforeFailure >= 0 && + (trace_host_heap::failCaps == 0 || (caps & trace_host_heap::failCaps) != 0)) { + if (trace_host_heap::allocationsBeforeFailure == 0) { + return nullptr; + } + trace_host_heap::allocationsBeforeFailure--; + } + void *ptr = std::malloc(size); + if (ptr == nullptr) { + return nullptr; + } + trace_host_heap::allocationCount++; + trace_host_heap::activeAllocations++; + trace_host_heap::allocationCaps[ptr] = caps; + return ptr; +} + +inline void heap_caps_free(void *ptr) { + if (ptr == nullptr) { + return; + } + trace_host_heap::freeCount++; + if (trace_host_heap::activeAllocations > 0) { + trace_host_heap::activeAllocations--; + } + trace_host_heap::allocationCaps.erase(ptr); + std::free(ptr); +} diff --git a/tests/host/trace_host_tests.cpp b/tests/host/trace_host_tests.cpp index 09e726f..5571f13 100644 --- a/tests/host/trace_host_tests.cpp +++ b/tests/host/trace_host_tests.cpp @@ -1,4 +1,5 @@ #include +#include "esp_heap_caps.h" #include #include @@ -350,6 +351,191 @@ void shutdownResults(TestRunner &runner) { } } +void storageAllocationPolicies(TestRunner &runner) { + resetClock(); + trace_host_heap::reset(); + trace_host_heap::psramAvailable = true; + { + Trace trace; + TraceConfig config = baseConfig(); + config.maxRecentLogs = 2; + config.maxRealtimeLogs = 2; + config.maxPendingLogs = 2; + runner.check(trace.init(config), "init default internal storage"); + TraceDiag diag = trace.getDiagnostics(); + runner.check(!diag.recentLogsInPsram, "default recent storage is internal"); + runner.check(!diag.realtimeLogsInPsram, "realtime storage is internal"); + runner.check(!diag.pendingLogsInPsram, "default pending storage is internal"); + runner.check(diag.recentAllocatedBytes > 0, "recent allocated bytes reported"); + runner.check(diag.realtimeAllocatedBytes > 0, "realtime allocated bytes reported"); + runner.check(diag.pendingAllocatedBytes > 0, "pending allocated bytes reported"); + runner.check(trace.end(), "end default internal storage"); + } + + resetClock(); + trace_host_heap::reset(); + trace_host_heap::psramAvailable = true; + { + Trace trace; + TraceConfig config = baseConfig(); + config.storageMemory = TraceStorageMemory::PreferPsram; + config.maxRecentLogs = 2; + config.maxRealtimeLogs = 2; + config.maxPendingLogs = 2; + runner.check(trace.init(config), "init PreferPsram storage"); + TraceDiag diag = trace.getDiagnostics(); + runner.check(diag.recentLogsInPsram, "PreferPsram recent uses PSRAM"); + runner.check(!diag.realtimeLogsInPsram, "PreferPsram realtime remains internal"); + runner.check(diag.pendingLogsInPsram, "PreferPsram pending uses PSRAM"); + runner.check(trace.end(), "end PreferPsram storage"); + } + + resetClock(); + trace_host_heap::reset(); + { + Trace trace; + TraceConfig config = baseConfig(); + config.storageMemory = TraceStorageMemory::PreferPsram; + config.maxRecentLogs = 2; + config.maxRealtimeLogs = 2; + config.maxPendingLogs = 2; + runner.check(trace.init(config), "init PreferPsram fallback storage"); + TraceDiag diag = trace.getDiagnostics(); + runner.check(!diag.recentLogsInPsram, "PreferPsram recent falls back to internal"); + runner.check(!diag.pendingLogsInPsram, "PreferPsram pending falls back to internal"); + runner.check(trace.end(), "end PreferPsram fallback storage"); + } + + resetClock(); + trace_host_heap::reset(); + { + Trace trace; + TraceConfig config = baseConfig(); + config.storageMemory = TraceStorageMemory::RequirePsram; + config.maxRecentLogs = 2; + config.maxRealtimeLogs = 2; + config.maxPendingLogs = 2; + TraceResult result = trace.init(config); + runner.check( + !result && result.status == TraceStatus::OutOfMemory, + "RequirePsram fails without PSRAM" + ); + runner.check( + trace_host_heap::activeAllocations == 0, + "RequirePsram failure leaves no allocations" + ); + } +} + +void partialAllocationCleanup(TestRunner &runner) { + resetClock(); + trace_host_heap::reset(); + trace_host_heap::allocationsBeforeFailure = 2; + Trace trace; + TraceConfig config = baseConfig(); + config.maxRecentLogs = 2; + config.maxRealtimeLogs = 2; + config.maxPendingLogs = 2; + TraceResult result = trace.init(config); + runner.check( + !result && result.status == TraceStatus::OutOfMemory, + "partial allocation init fails" + ); + runner.check( + trace_host_heap::activeAllocations == 0, + "partial allocation failure frees previous buffers" + ); +} + +void enqueueDoesNotAllocateAfterInit(TestRunner &runner) { + resetClock(); + trace_host_heap::reset(); + Trace trace; + TraceConfig config = baseConfig(); + config.maxRecentLogs = 4; + config.maxRealtimeLogs = 4; + config.maxPendingLogs = 4; + runner.check(trace.init(config), "init enqueueDoesNotAllocateAfterInit"); + const size_t allocationsAfterInit = trace_host_heap::allocationCount; + runner.check(trace.info("HOT", "ok"), "log enqueueDoesNotAllocateAfterInit"); + runner.check( + trace_host_heap::allocationCount == allocationsAfterInit, + "accepted internal enqueue does not heap-cap allocate" + ); + runner.check(trace.end(), "end enqueueDoesNotAllocateAfterInit"); +} + +void ringOrderAndSequenceConsistency(TestRunner &runner) { + resetClock(); + trace_host_heap::reset(); + Trace trace; + std::vector realtimeSequences; + std::vector flushedSequences; + trace.onLog([&realtimeSequences](const TraceLog &log) { + realtimeSequences.push_back(log.sequence); + }); + trace.onFlush([&flushedSequences](const TraceLogBatch &batch) { + for (const TraceLog &log : batch.logs) { + flushedSequences.push_back(log.sequence); + } + return TraceFlushResult::Ok; + }); + + TraceConfig config = baseConfig(); + config.maxRecentLogs = 3; + config.maxRealtimeLogs = 5; + config.maxPendingLogs = 3; + config.overflowPolicy = TraceOverflowPolicy::DropOldestPending; + runner.check(trace.init(config), "init ringOrderAndSequenceConsistency"); + for (int i = 1; i <= 5; ++i) { + runner.check(trace.infof("SEQ", "item=%d", i), "sequence log"); + } + runner.check( + waitUntil([&realtimeSequences]() { return realtimeSequences.size() == 5; }), + "realtime sees five logs" + ); + std::vector recent = trace.getLogs(); + runner.check( + recent.size() == 3 && recent[0].sequence == 3 && recent[1].sequence == 4 && + recent[2].sequence == 5, + "recent query returns wrapped records oldest-to-newest" + ); + runner.check( + realtimeSequences.size() == 5 && realtimeSequences[0] == 1 && realtimeSequences[1] == 2 && + realtimeSequences[2] == 3 && realtimeSequences[3] == 4 && realtimeSequences[4] == 5, + "realtime callback order is oldest-to-newest" + ); + runner.check(trace.flushAndWait(1000), "flush ringOrderAndSequenceConsistency"); + runner.check( + flushedSequences.size() == 3 && flushedSequences[0] == 3 && flushedSequences[1] == 4 && + flushedSequences[2] == 5, + "flush batch returns wrapped pending records oldest-to-newest" + ); + runner.check(trace.end(), "end ringOrderAndSequenceConsistency"); +} + +void runtimeCapClamping(TestRunner &runner) { + resetClock(); + trace_host_heap::reset(); + Trace trace; + TraceConfig config = baseConfig(); + config.maxTagLength = TRACE_RECORD_MAX_TAG_LENGTH + 100; + config.maxMessageLength = 0; + config.maxFormattedLength = 0; + runner.check(trace.init(config), "init runtimeCapClamping"); + std::string longTag(TRACE_RECORD_MAX_TAG_LENGTH + 10, 'T'); + std::string longMessage(TRACE_RECORD_MAX_MESSAGE_LENGTH + 10, 'M'); + runner.check(trace.info(longTag.c_str(), longMessage), "log runtimeCapClamping"); + TraceLog log = trace.getLastLog(); + runner.check(log.tag.size() == TRACE_RECORD_MAX_TAG_LENGTH, "tag cap clamps to compile maximum"); + runner.check( + log.message.size() == TRACE_RECORD_MAX_MESSAGE_LENGTH, + "message zero runtime cap uses compile maximum" + ); + runner.check(log.truncated, "runtime cap clamping marks truncation"); + runner.check(trace.end(), "end runtimeCapClamping"); +} + } // namespace int main() { @@ -364,6 +550,11 @@ int main() { callbacksRunOutsideLock(runner); truncation(runner); shutdownResults(runner); + storageAllocationPolicies(runner); + partialAllocationCleanup(runner); + enqueueDoesNotAllocateAfterInit(runner); + ringOrderAndSequenceConsistency(runner); + runtimeCapClamping(runner); if (runner.failed != 0) { std::cerr << runner.failed << " host Trace tests failed\n"; From 7fee1048e101f81b51e81e93bd1a6fc717237305 Mon Sep 17 00:00:00 2001 From: zekageri Date: Mon, 6 Jul 2026 09:41:39 +0200 Subject: [PATCH 2/3] 0.2.0 blocker fixes --- README.md | 2 + docs/api.md | 2 +- docs/configuration.md | 8 +- src/Trace.h | 15 ++- src/TraceFlush.cpp | 8 +- src/TraceFormatting.cpp | 69 ----------- src/TraceLogging.cpp | 168 +++++++++++++++++++-------- src/internal/TraceImpl.h | 11 -- tests/host/trace_host_tests.cpp | 196 ++++++++++++++++++++++++++++++++ 9 files changed, 348 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index c641ee4..624de68 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ void loop() { * Queue-count `0` disables that queue. Payload-cap `0` uses the compiled maximum for that payload type. * `setStream()` writes formatted realtime logs to any Arduino `Print` stream such as `Serial`, `Serial1`, `WiFiClient`, or a custom sink. * Stream output uses ANSI colors by default. Callback, flush, and query `TraceLog::formatted` values stay plain text. +* After `Trace::init()`, accepted direct C-string log calls do not allocate on the internal enqueue path when all target queues have capacity and no output-boundary conversion is triggered. +* Query APIs, flush batch conversion, `onLog()` conversion, stream implementations, and user-created `std::string` values may still allocate. * `onLog()` is for realtime observation; `onFlush()` is for persistence. * Callbacks should avoid long blocking work and should not recursively call Trace logging methods. * Trace does not own attached `Print` or `Tempo` instances. Keep them alive until `Trace::end()` completes. diff --git a/docs/api.md b/docs/api.md index 159cfba..593a846 100644 --- a/docs/api.md +++ b/docs/api.md @@ -12,7 +12,7 @@ `TraceFlushResult` values are `Ok`, `Failed`, and `Retry`. -`Ok` removes flushed pending logs. `Failed` is terminal for the current `flushAndWait()` call but retains pending logs. `Retry` retains pending logs, schedules another attempt using `TraceConfig::retryIntervalMs`, and keeps `flushAndWait()` waiting until `Ok`, `Failed`, or timeout. +`Ok` removes flushed pending logs. `Failed` is terminal for the current `flushAndWait()` call but retains pending logs. `Retry` retains pending logs, schedules another attempt using `TraceConfig::retryIntervalMs`, and keeps `flushAndWait()` waiting until `Ok`, `Failed`, or timeout. Normal flush requests do not bypass the retry deadline; urgent error and fatal flush requests may bypass it. ## Main methods diff --git a/docs/configuration.md b/docs/configuration.md index a0a26de..c8db44e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -42,6 +42,10 @@ Queue-count `0` means disabled. Payload-cap `0` means use the compiled maximum f Trace stores recent, realtime, and pending logs in fixed-capacity ring buffers. Each enabled queue is allocated once during `init()`. +After `Trace::init()`, accepted direct C-string log calls do not allocate on the internal enqueue path when all target queues have capacity and no output-boundary conversion is triggered. + +This guarantee is intentionally narrow. Query APIs allocate `std::vector`, flush batch conversion and `onLog()` conversion can allocate because public `TraceLog` contains `std::string`, stream implementations may allocate internally, and user code may allocate before passing `std::string` values to Trace. + `TraceStorageMemory::Internal` is the deterministic default. Recent, realtime, and pending queues use internal-capable memory. `TraceStorageMemory::PreferPsram` uses PSRAM for recent and pending queues when PSRAM is available, otherwise it falls back to internal-capable memory. The realtime queue remains internal. @@ -78,7 +82,7 @@ Trace flushes pending logs when: * `flushIntervalMs` elapses with pending logs. * `flushOnError` is enabled and an `Error` or `Fatal` log is queued. -`retryIntervalMs` controls the delay after `TraceFlushResult::Retry`. Values smaller than the worker poll interval are clamped so retry cannot spin in a tight loop. +`retryIntervalMs` controls the delay after `TraceFlushResult::Retry`. Values smaller than the worker poll interval are clamped so retry cannot spin in a tight loop. Normal flush requests set `flushRequested` but do not clear or bypass the retry deadline. Urgent error and fatal flush requests may bypass the retry deadline. ## Flush results @@ -96,7 +100,7 @@ Trace flushes pending logs when: `BlockCaller` requests a flush and waits up to `blockCallerTimeoutMs` for pending space. -`FlushImmediately` requests a flush immediately and retries until space appears or `blockCallerTimeoutMs` expires. +`FlushImmediately` requests a flush immediately and queues the new record only if pending space becomes available before `blockCallerTimeoutMs` expires. ## Stack policy diff --git a/src/Trace.h b/src/Trace.h index a9008b4..894ba02 100644 --- a/src/Trace.h +++ b/src/Trace.h @@ -271,6 +271,14 @@ class Trace { private: TraceResult log(TraceLevel level, const char *tag, const std::string &message); + TraceResult logRaw( + TraceLevel level, + const char *tag, + size_t tagLen, + const char *message, + size_t messageLen, + bool alreadyTruncated + ); TraceResult logJson(TraceLevel level, const char *tag, const JsonDocument &doc); TraceResult logVPrintf(TraceLevel level, const char *tag, const char *format, va_list args); @@ -293,9 +301,14 @@ class Trace { const size_t boundedLength = std::min(outputLength, limit); char buffer[TRACE_FORMATTED_BUFFER_LENGTH + 1] = {}; snprintf(buffer, boundedLength + 1, format, args...); - return log(level, tag, std::string(buffer), outputLength > limit); + const size_t tagLimit = getMaxTagLength(); + const size_t tagLen = boundedStrLen(tag, tagLimit + 1); + return logRaw(level, tag, tagLen, buffer, boundedLength, outputLength > limit); } + static size_t boundedStrLen(const char *value, size_t maxLen); + size_t getMaxTagLength() const; + size_t getMaxMessageLength() const; size_t getMaxFormattedLength() const; TraceResult log( TraceLevel level, diff --git a/src/TraceFlush.cpp b/src/TraceFlush.cpp index 6e0cffc..31c59ca 100644 --- a/src/TraceFlush.cpp +++ b/src/TraceFlush.cpp @@ -76,13 +76,19 @@ bool TraceImpl::shouldFlushNow() { if (!lock) { return false; } + const uint64_t nowMs = millis(); + if ( + !pendingLogs.empty() && nextFlushAttemptMs > 0 && nowMs < nextFlushAttemptMs && + !urgentFlushRequested + ) { + return false; + } if (flushRequested || urgentFlushRequested) { return true; } if (pendingLogs.empty()) { return false; } - const uint64_t nowMs = millis(); if (nextFlushAttemptMs > 0) { return nowMs >= nextFlushAttemptMs; } diff --git a/src/TraceFormatting.cpp b/src/TraceFormatting.cpp index 31dc410..65d04cb 100644 --- a/src/TraceFormatting.cpp +++ b/src/TraceFormatting.cpp @@ -1,8 +1,5 @@ #include "internal/TraceImpl.h" -#include -#include - #if __has_include() #include #define TRACE_HAS_TEMPO 1 @@ -25,72 +22,6 @@ size_t effectiveLimit(size_t limit, size_t maximum) { } return limit; } - -size_t clampedLimit(size_t length, size_t limit) { - if (limit == 0 || length <= limit) { - return length; - } - return limit; -} - -std::string copyLimited(const char *value, size_t limit, bool &truncated) { - if (value == nullptr) { - return std::string(); - } - const size_t length = strlen(value); - const size_t boundedLength = clampedLimit(length, limit); - truncated = limit > 0 && length > limit; - return std::string(value, boundedLength); -} - -std::string truncateString(const std::string &value, size_t limit, bool &truncated) { - const size_t boundedLength = clampedLimit(value.size(), limit); - truncated = limit > 0 && value.size() > limit; - return value.substr(0, boundedLength); -} - -std::string formatPrintf(const char *format, va_list args, size_t limit, bool &truncated) { - if (format == nullptr) { - return std::string(); - } - - char stackBuffer[kFormatBufferSize + 1] = {}; - const size_t effective = effectiveLimit(limit, TRACE_FORMATTED_BUFFER_LENGTH); - va_list copy; - va_copy(copy, args); - const int needed = vsnprintf(stackBuffer, effective + 1, format, copy); - va_end(copy); - - if (needed < 0) { - return std::string(); - } - const size_t outputLength = static_cast(needed); - const size_t boundedLength = std::min(outputLength, effective); - truncated = outputLength > effective; - return std::string(stackBuffer, boundedLength); -} - -std::string jsonToString( - const JsonDocument &doc, - TraceJsonFormat format, - size_t limit, - bool &truncated -) { - const size_t measuredLength = - format == TraceJsonFormat::Pretty ? measureJsonPretty(doc) : measureJson(doc); - const size_t effective = effectiveLimit(limit, TRACE_FORMATTED_BUFFER_LENGTH); - const size_t boundedLength = std::min(measuredLength, effective); - truncated = measuredLength > effective; - - char buffer[TRACE_FORMATTED_BUFFER_LENGTH + 1] = {}; - if (format == TraceJsonFormat::Pretty) { - serializeJsonPretty(doc, buffer, boundedLength + 1); - } else { - serializeJson(doc, buffer, boundedLength + 1); - } - buffer[boundedLength] = '\0'; - return std::string(buffer); -} } // namespace trace_detail TraceLog TraceImpl::toPublicLog(const TraceRecord &record) { diff --git a/src/TraceLogging.cpp b/src/TraceLogging.cpp index d1b548d..3bb6137 100644 --- a/src/TraceLogging.cpp +++ b/src/TraceLogging.cpp @@ -28,6 +28,7 @@ void copyToRecordField( truncated = true; } } + } // namespace TraceResult TraceImpl::appendLog(TraceRecord record) { @@ -129,30 +130,28 @@ void TraceImpl::addRealtimeLocked(const TraceRecord &record) { } void TraceImpl::processRealtimeLogs() { - TraceLogCallback callback; - Print *streamSnapshot = nullptr; - bool colorsEnabled = true; - std::vector records; - { - TraceLock lock(mutex); - if (!lock) { - return; - } - callback = onLog; - streamSnapshot = stream; - colorsEnabled = config.enableColors; - if (!callback && streamSnapshot == nullptr) { - realtimeLogs.clear(); - return; - } - records.reserve(realtimeLogs.size()); + while (true) { + TraceLogCallback callback; + Print *streamSnapshot = nullptr; + bool colorsEnabled = true; TraceRecord record; - while (realtimeLogs.pop(record)) { - records.push_back(record); + { + TraceLock lock(mutex); + if (!lock || stopping || realtimeLogs.empty()) { + return; + } + callback = onLog; + streamSnapshot = stream; + colorsEnabled = config.enableColors; + if (!callback && streamSnapshot == nullptr) { + realtimeLogs.clear(); + return; + } + if (!realtimeLogs.pop(record)) { + return; + } } - } - for (const TraceRecord &record : records) { TraceLog log = toPublicLog(record); formatLog(log); if (streamSnapshot != nullptr) { @@ -171,6 +170,36 @@ void TraceImpl::processRealtimeLogs() { } } +size_t Trace::boundedStrLen(const char *value, size_t maxLen) { + if (value == nullptr) { + return 0; + } + size_t length = 0; + while (length < maxLen && value[length] != '\0') { + length++; + } + return length; +} + +size_t Trace::getMaxTagLength() const { + TraceLock lock(_impl->mutex); + if (!lock) { + return TRACE_RECORD_MAX_TAG_LENGTH; + } + return trace_detail::effectiveLimit(_impl->config.maxTagLength, TRACE_RECORD_MAX_TAG_LENGTH); +} + +size_t Trace::getMaxMessageLength() const { + TraceLock lock(_impl->mutex); + if (!lock) { + return TRACE_RECORD_MAX_MESSAGE_LENGTH; + } + return trace_detail::effectiveLimit( + _impl->config.maxMessageLength, + TRACE_RECORD_MAX_MESSAGE_LENGTH + ); +} + size_t Trace::getMaxFormattedLength() const { TraceLock lock(_impl->mutex); if (!lock) { @@ -183,13 +212,6 @@ size_t Trace::getMaxFormattedLength() const { } TraceResult Trace::log(TraceLevel level, const char *tag, const std::string &message) { - TraceConfig config; - { - TraceLock lock(_impl->mutex); - if (lock) { - config = _impl->config; - } - } return log(level, tag, message, false); } @@ -199,10 +221,22 @@ TraceResult Trace::log( const std::string &message, bool messageTruncated ) { - if (tag == nullptr || tag[0] == '\0') { + const size_t tagLimit = getMaxTagLength(); + const size_t tagLen = boundedStrLen(tag, tagLimit + 1); + return logRaw(level, tag, tagLen, message.data(), message.size(), messageTruncated); +} + +TraceResult Trace::logRaw( + TraceLevel level, + const char *tag, + size_t tagLen, + const char *message, + size_t messageLen, + bool alreadyTruncated +) { + if (tag == nullptr || tagLen == 0) { return TraceResult::failure(TraceStatus::InvalidArgument, "tag is required"); } - TraceConfig config; { TraceLock lock(_impl->mutex); @@ -219,21 +253,21 @@ TraceResult Trace::log( return TraceResult::success("log filtered"); } - bool truncated = messageTruncated; + bool truncated = alreadyTruncated; TraceRecord record; record.level = level; record.uptimeMs = millis(); copyToRecordField( tag, - strlen(tag), + tagLen, config.maxTagLength, record.tag, sizeof(record.tag), truncated ); copyToRecordField( - message.c_str(), - message.size(), + message, + messageLen, config.maxMessageLength, record.message, sizeof(record.message), @@ -253,9 +287,23 @@ TraceResult Trace::logJson(TraceLevel level, const char *tag, const JsonDocument maxFormattedLength = _impl->config.maxFormattedLength; } } - bool truncated = false; - const std::string message = trace_detail::jsonToString(doc, format, maxFormattedLength, truncated); - return log(level, tag, message, truncated); + const size_t effective = trace_detail::effectiveLimit( + maxFormattedLength, + TRACE_FORMATTED_BUFFER_LENGTH + ); + const size_t measuredLength = + format == TraceJsonFormat::Pretty ? measureJsonPretty(doc) : measureJson(doc); + const size_t boundedLength = std::min(measuredLength, effective); + const bool truncated = measuredLength > effective; + char buffer[TRACE_FORMATTED_BUFFER_LENGTH + 1] = {}; + const size_t written = format == TraceJsonFormat::Pretty + ? serializeJsonPretty(doc, buffer, boundedLength + 1) + : serializeJson(doc, buffer, boundedLength + 1); + buffer[boundedLength] = '\0'; + const size_t messageLen = std::min(written, boundedLength); + const size_t tagLimit = getMaxTagLength(); + const size_t tagLen = boundedStrLen(tag, tagLimit + 1); + return logRaw(level, tag, tagLen, buffer, messageLen, truncated); } TraceResult Trace::logVPrintf( @@ -268,30 +316,58 @@ TraceResult Trace::logVPrintf( return TraceResult::failure(TraceStatus::InvalidArgument, "format is required"); } const size_t maxFormattedLength = getMaxFormattedLength(); - bool truncated = false; - const std::string message = - trace_detail::formatPrintf(format, args, maxFormattedLength, truncated); - return log(level, tag, message, truncated); + char buffer[TRACE_FORMATTED_BUFFER_LENGTH + 1] = {}; + va_list measureArgs; + va_copy(measureArgs, args); + const int needed = vsnprintf(nullptr, 0, format, measureArgs); + va_end(measureArgs); + if (needed < 0) { + return TraceResult::failure(TraceStatus::InvalidArgument, "format failed"); + } + const size_t outputLength = static_cast(needed); + const size_t boundedLength = std::min(outputLength, maxFormattedLength); + va_list formatArgs; + va_copy(formatArgs, args); + vsnprintf(buffer, boundedLength + 1, format, formatArgs); + va_end(formatArgs); + const size_t tagLimit = getMaxTagLength(); + const size_t tagLen = boundedStrLen(tag, tagLimit + 1); + return logRaw(level, tag, tagLen, buffer, boundedLength, outputLength > maxFormattedLength); } TraceResult Trace::debug(const char *tag, const char *message) { - return log(TraceLevel::Debug, tag, message != nullptr ? message : ""); + const size_t tagLen = boundedStrLen(tag, getMaxTagLength() + 1); + const char *safeMessage = message != nullptr ? message : ""; + const size_t messageLen = boundedStrLen(safeMessage, getMaxMessageLength() + 1); + return logRaw(TraceLevel::Debug, tag, tagLen, safeMessage, messageLen, false); } TraceResult Trace::info(const char *tag, const char *message) { - return log(TraceLevel::Info, tag, message != nullptr ? message : ""); + const size_t tagLen = boundedStrLen(tag, getMaxTagLength() + 1); + const char *safeMessage = message != nullptr ? message : ""; + const size_t messageLen = boundedStrLen(safeMessage, getMaxMessageLength() + 1); + return logRaw(TraceLevel::Info, tag, tagLen, safeMessage, messageLen, false); } TraceResult Trace::warn(const char *tag, const char *message) { - return log(TraceLevel::Warn, tag, message != nullptr ? message : ""); + const size_t tagLen = boundedStrLen(tag, getMaxTagLength() + 1); + const char *safeMessage = message != nullptr ? message : ""; + const size_t messageLen = boundedStrLen(safeMessage, getMaxMessageLength() + 1); + return logRaw(TraceLevel::Warn, tag, tagLen, safeMessage, messageLen, false); } TraceResult Trace::error(const char *tag, const char *message) { - return log(TraceLevel::Error, tag, message != nullptr ? message : ""); + const size_t tagLen = boundedStrLen(tag, getMaxTagLength() + 1); + const char *safeMessage = message != nullptr ? message : ""; + const size_t messageLen = boundedStrLen(safeMessage, getMaxMessageLength() + 1); + return logRaw(TraceLevel::Error, tag, tagLen, safeMessage, messageLen, false); } TraceResult Trace::fatal(const char *tag, const char *message) { - return log(TraceLevel::Fatal, tag, message != nullptr ? message : ""); + const size_t tagLen = boundedStrLen(tag, getMaxTagLength() + 1); + const char *safeMessage = message != nullptr ? message : ""; + const size_t messageLen = boundedStrLen(safeMessage, getMaxMessageLength() + 1); + return logRaw(TraceLevel::Fatal, tag, tagLen, safeMessage, messageLen, false); } TraceResult Trace::debug(const char *tag, const std::string &message) { diff --git a/src/internal/TraceImpl.h b/src/internal/TraceImpl.h index e5e15cb..2549342 100644 --- a/src/internal/TraceImpl.h +++ b/src/internal/TraceImpl.h @@ -11,22 +11,11 @@ namespace trace_detail { inline constexpr uint32_t kWaitPollMs = 10; -inline constexpr size_t kFormatBufferSize = TRACE_FORMATTED_BUFFER_LENGTH; inline constexpr size_t kTimeBufferSize = TRACE_TIME_TEXT_BUFFER_LENGTH; bool levelEnabled(TraceLevel level, TraceLevel minLevel); bool isErrorLevel(TraceLevel level); size_t effectiveLimit(size_t limit, size_t maximum); -size_t clampedLimit(size_t length, size_t limit); -std::string copyLimited(const char *value, size_t limit, bool &truncated); -std::string truncateString(const std::string &value, size_t limit, bool &truncated); -std::string formatPrintf(const char *format, va_list args, size_t limit, bool &truncated); -std::string jsonToString( - const JsonDocument &doc, - TraceJsonFormat format, - size_t limit, - bool &truncated -); } // namespace trace_detail struct TraceImpl { diff --git a/tests/host/trace_host_tests.cpp b/tests/host/trace_host_tests.cpp index 5571f13..9acf664 100644 --- a/tests/host/trace_host_tests.cpp +++ b/tests/host/trace_host_tests.cpp @@ -5,13 +5,66 @@ #include #include #include +#include #include #include #include std::atomic trace_host_millis{0}; +std::atomic trace_host_global_allocations{0}; + +void *operator new(std::size_t size) { + if (size == 0) { + size = 1; + } + void *ptr = std::malloc(size); + if (ptr == nullptr) { + throw std::bad_alloc(); + } + trace_host_global_allocations.fetch_add(1, std::memory_order_relaxed); + return ptr; +} + +void *operator new[](std::size_t size) { + if (size == 0) { + size = 1; + } + void *ptr = std::malloc(size); + if (ptr == nullptr) { + throw std::bad_alloc(); + } + trace_host_global_allocations.fetch_add(1, std::memory_order_relaxed); + return ptr; +} + +void operator delete(void *ptr) noexcept { + std::free(ptr); +} + +void operator delete[](void *ptr) noexcept { + std::free(ptr); +} + +void operator delete(void *ptr, std::size_t) noexcept { + std::free(ptr); +} + +void operator delete[](void *ptr, std::size_t) noexcept { + std::free(ptr); +} namespace { +struct AllocationScope { + size_t start; + + AllocationScope() : start(trace_host_global_allocations.load(std::memory_order_relaxed)) { + } + + size_t delta() const { + return trace_host_global_allocations.load(std::memory_order_relaxed) - start; + } +}; + struct FakePrint : Print { std::string output; @@ -271,6 +324,76 @@ void flushResults(TestRunner &runner) { } } +void retryBackoffRequests(TestRunner &runner) { + resetClock(); + { + Trace trace; + int attempts = 0; + std::vector attemptTimes; + trace.onFlush([&attempts, &attemptTimes](const TraceLogBatch &) { + attempts++; + attemptTimes.push_back(trace_host_millis.load()); + return TraceFlushResult::Retry; + }); + TraceConfig config = baseConfig(); + config.maxPendingLogs = 1; + config.overflowPolicy = TraceOverflowPolicy::BlockCaller; + config.retryIntervalMs = 1000; + config.blockCallerTimeoutMs = 20; + runner.check(trace.init(config), "init normal retry gating"); + runner.check(trace.info("R", "one"), "first normal retry gating log"); + TraceResult flushResult = trace.flushAndWait(20); + runner.check( + !flushResult && flushResult.status == TraceStatus::Timeout, + "initial retry waits for retry interval" + ); + TraceResult blocked = trace.info("R", "two"); + runner.check( + !blocked && blocked.status == TraceStatus::Timeout, + "normal full-queue request waits for space" + ); + bool retrySpacingOk = true; + for (size_t i = 1; i < attemptTimes.size(); ++i) { + if (attemptTimes[i] < attemptTimes[i - 1] + config.retryIntervalMs) { + retrySpacingOk = false; + } + } + runner.check( + retrySpacingOk, + "normal full-queue request does not bypass retry interval" + ); + trace.end(20); + } + resetClock(); + { + Trace trace; + int attempts = 0; + trace.onFlush([&attempts](const TraceLogBatch &) { + attempts++; + return attempts == 1 ? TraceFlushResult::Retry : TraceFlushResult::Ok; + }); + TraceConfig config = baseConfig(); + config.maxPendingLogs = 2; + config.retryIntervalMs = 1000; + runner.check(trace.init(config), "init urgent retry bypass"); + runner.check(trace.info("R", "one"), "first urgent retry bypass log"); + TraceResult flushResult = trace.flushAndWait(20); + runner.check( + !flushResult && flushResult.status == TraceStatus::Timeout, + "urgent retry bypass setup times out" + ); + const int attemptsBeforeUrgent = attempts; + runner.check(trace.error("R", "urgent"), "urgent log accepted"); + runner.check( + waitUntil([&attempts, attemptsBeforeUrgent]() { + return attempts > attemptsBeforeUrgent; + }, 100), + "urgent request triggers another flush attempt" + ); + runner.check(trace.end(), "end urgent retry bypass"); + } +} + void callbacksRunOutsideLock(TestRunner &runner) { resetClock(); Trace trace; @@ -293,6 +416,28 @@ void callbacksRunOutsideLock(TestRunner &runner) { runner.check(trace.end(), "end callbacksRunOutsideLock"); } +void realtimeDetachStopsFutureRecords(TestRunner &runner) { + resetClock(); + Trace trace; + int observed = 0; + trace.onLog([&trace, &observed](const TraceLog &) { + observed++; + trace.onLog(nullptr); + }); + TraceConfig config = baseConfig(); + config.maxRealtimeLogs = 4; + runner.check(trace.init(config), "init realtimeDetachStopsFutureRecords"); + runner.check(trace.info("RT", "one"), "first realtime detach log"); + runner.check(trace.info("RT", "two"), "second realtime detach log"); + runner.check( + waitUntil([&observed]() { return observed == 1; }), + "detached realtime callback receives first record" + ); + vTaskDelay(10); + runner.check(observed == 1, "detached realtime callback does not receive later records"); + runner.check(trace.end(), "end realtimeDetachStopsFutureRecords"); +} + void truncation(TestRunner &runner) { resetClock(); Trace trace; @@ -465,6 +610,54 @@ void enqueueDoesNotAllocateAfterInit(TestRunner &runner) { runner.check(trace.end(), "end enqueueDoesNotAllocateAfterInit"); } +void directCStringDoesNotAllocateAfterInit(TestRunner &runner) { + resetClock(); + trace_host_heap::reset(); + Trace trace; + TraceConfig config = baseConfig(); + config.maxRecentLogs = 4; + config.maxRealtimeLogs = 4; + config.maxPendingLogs = 4; + runner.check(trace.init(config), "init directCStringDoesNotAllocateAfterInit short"); + TraceResult shortResult; + size_t shortAllocations = 0; + { + AllocationScope scope; + shortResult = trace.info("HOT", "message"); + shortAllocations = scope.delta(); + } + runner.check(shortResult, "short direct C-string log accepted"); + runner.check(shortAllocations == 0, "short direct C-string log does not allocate"); + runner.check(trace.end(), "end directCStringDoesNotAllocateAfterInit short"); + + resetClock(); + trace_host_heap::reset(); + Trace longTrace; + TraceConfig longConfig = baseConfig(); + longConfig.maxRecentLogs = 4; + longConfig.maxRealtimeLogs = 4; + longConfig.maxPendingLogs = 4; + longConfig.maxMessageLength = 5; + runner.check(longTrace.init(longConfig), "init directCStringDoesNotAllocateAfterInit long"); + TraceResult longResult; + size_t longAllocations = 0; + { + AllocationScope scope; + longResult = longTrace.info("HOT", "message longer than the configured message cap"); + longAllocations = scope.delta(); + } + runner.check(longResult, "long direct C-string log accepted"); + runner.check(longAllocations == 0, "long direct C-string log does not allocate"); + TraceLog log = longTrace.getLastLog(); + runner.check(log.message == "messa", "long direct C-string log is truncated"); + runner.check(log.truncated, "long direct C-string log marks truncation"); + runner.check( + longTrace.getDiagnostics().truncatedLogCount == 1, + "long direct C-string truncation counted once" + ); + runner.check(longTrace.end(), "end directCStringDoesNotAllocateAfterInit long"); +} + void ringOrderAndSequenceConsistency(TestRunner &runner) { resetClock(); trace_host_heap::reset(); @@ -547,12 +740,15 @@ int main() { pendingOverflowPolicies(runner); blockingAndFlushImmediately(runner); flushResults(runner); + retryBackoffRequests(runner); callbacksRunOutsideLock(runner); + realtimeDetachStopsFutureRecords(runner); truncation(runner); shutdownResults(runner); storageAllocationPolicies(runner); partialAllocationCleanup(runner); enqueueDoesNotAllocateAfterInit(runner); + directCStringDoesNotAllocateAfterInit(runner); ringOrderAndSequenceConsistency(runner); runtimeCapClamping(runner); From fb8f8c5040203070629239ddf6bb56d1ff109c4c Mon Sep 17 00:00:00 2001 From: zekageri Date: Mon, 6 Jul 2026 09:51:15 +0200 Subject: [PATCH 3/3] 0.2.0 --- README.md | 2 +- library.json | 2 +- library.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 624de68..ea22b09 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ For the full API, see [`docs/api.md`](docs/api.md). | PSRAM | Optional for task stacks and opt-in recent/pending log storage | | Dependencies | `bblanchon/ArduinoJson >= 7.0.0` | | Exceptions | Not used | -| Status | Release candidate `0.2.0-rc.1` | +| Status | Release `0.2.0` | ## Configuration diff --git a/library.json b/library.json index 78da5f9..063f841 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "Trace", - "version": "0.2.0-rc.1", + "version": "0.2.0", "description": "Logging and diagnostics library for ESP32 devices with bounded buffers and task-side flushing.", "keywords": [ "esp32", diff --git a/library.properties b/library.properties index 52831bb..4a2740b 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=Trace -version=0.2.0-rc.1 +version=0.2.0 author=zekageri maintainer=zekageri sentence=Logging and diagnostics library for ESP32 devices.