Reduce TLS memory requirements for KOReader sync
Split heap gate into separate free-memory (50KB) and largest-block (20KB) thresholds based on field measurements. Enable wolfSSL single-precision ECC to use fixed 256-bit arrays instead of heap-allocated fast-math bignums, reducing TLS handshake memory footprint. Reclaim ~7KB by right-sizing ESP timer task stacks and move WiFi code out of IRAM to free ~25-30KB for heap. Add memory audit landmarks for font and EPUB allocations.
This commit is contained in:
@@ -369,6 +369,11 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
|
|||||||
}
|
}
|
||||||
stats.pageBufferBytes += totalBytes;
|
stats.pageBufferBytes += totalBytes;
|
||||||
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
|
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
|
||||||
|
// MEMFIX-PORT: page-slot address landmark for the heap map; portable
|
||||||
|
// Landmark for the heap block map: page slots are the largest flash-font
|
||||||
|
// allocations and otherwise show up as anonymous ~4-20 KB used blocks.
|
||||||
|
LOG_DBG("FDC", "page slot buffer=%p bytes=%u glyphs=%u", static_cast<void*>(slot.buffer), (unsigned)totalBytes,
|
||||||
|
(unsigned)glyphCount);
|
||||||
|
|
||||||
slot.fontData = fontData;
|
slot.fontData = fontData;
|
||||||
slot.glyphCount = glyphCount;
|
slot.glyphCount = glyphCount;
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class SdCardFont {
|
|||||||
uint32_t uniqueGlyphs = 0;
|
uint32_t uniqueGlyphs = 0;
|
||||||
uint32_t bitmapBytes = 0;
|
uint32_t bitmapBytes = 0;
|
||||||
};
|
};
|
||||||
|
// MEMFIX-PORT: SD font resident-bytes audit; portable
|
||||||
// Log per-style resident heap (full tables + kept-if-fits mini arenas +
|
// Log per-style resident heap (full tables + kept-if-fits mini arenas +
|
||||||
// advance tables + overflow bitmaps) and return the total in bytes. Pure
|
// advance tables + overflow bitmaps) and return the total in bytes. Pure
|
||||||
// accounting — no allocation, no state change.
|
// accounting — no allocation, no state change.
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class SdCardFontManager {
|
|||||||
// Get name of currently loaded family (empty if none).
|
// Get name of currently loaded family (empty if none).
|
||||||
const std::string& currentFamilyName() const { return loadedFamilyName_; };
|
const std::string& currentFamilyName() const { return loadedFamilyName_; };
|
||||||
|
|
||||||
|
// MEMFIX-PORT: font manager audit passthrough; portable
|
||||||
// Sum of loaded fonts' resident heap (see SdCardFont::reportMemory).
|
// Sum of loaded fonts' resident heap (see SdCardFont::reportMemory).
|
||||||
size_t reportMemory() const;
|
size_t reportMemory() const;
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class Epub {
|
|||||||
}
|
}
|
||||||
~Epub() = default;
|
~Epub() = default;
|
||||||
std::string& getBasePath() { return contentBasePath; }
|
std::string& getBasePath() { return contentBasePath; }
|
||||||
|
// MEMFIX-PORT: epub resident-bytes audit accessor; portable
|
||||||
// Approximate resident heap of the open book (audit): path strings, the CSS
|
// Approximate resident heap of the open book (audit): path strings, the CSS
|
||||||
// file list, and the parsed stylesheet. BookMetadataCache is file-backed
|
// file list, and the parsed stylesheet. BookMetadataCache is file-backed
|
||||||
// (counts + HalFile handles) and contributes little.
|
// (counts + HalFile handles) and contributes little.
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ class Section {
|
|||||||
// (covers finalized sections and partials from a previous session).
|
// (covers finalized sections and partials from a previous session).
|
||||||
std::optional<uint16_t> findAnchor(const std::string& anchor) const;
|
std::optional<uint16_t> findAnchor(const std::string& anchor) const;
|
||||||
|
|
||||||
|
// MEMFIX-PORT: section resident-bytes audit accessor; portable
|
||||||
// Approximate resident heap for the audit log. Steady state (no build) a
|
// Approximate resident heap for the audit log. Steady state (no build) a
|
||||||
// Section holds little beyond itself; during a build the page LUT and path
|
// Section holds little beyond itself; during a build the page LUT and path
|
||||||
// strings dominate (the parser's internal footprint is not walked here).
|
// strings dominate (the parser's internal footprint is not walked here).
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class CssParser {
|
|||||||
*/
|
*/
|
||||||
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
|
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
|
||||||
|
|
||||||
|
// MEMFIX-PORT: stylesheet resident-bytes audit accessor; portable
|
||||||
// Approximate resident heap of the parsed stylesheet, for the audit log.
|
// Approximate resident heap of the parsed stylesheet, for the audit log.
|
||||||
// unordered_map cost model: bucket array + one node per rule (libstdc++ node
|
// unordered_map cost model: bucket array + one node per rule (libstdc++ node
|
||||||
// overhead ~= 2 pointers + hash) + key string capacity when it exceeds SSO.
|
// overhead ~= 2 pointers + hash) + key string capacity when it exceeds SSO.
|
||||||
|
|||||||
@@ -22,7 +22,21 @@ constexpr char DEVICE_ID[] = "crosspoint-reader";
|
|||||||
// footprint is smaller than mbedTLS's old ~48KB peak, but keep a conservative
|
// footprint is smaller than mbedTLS's old ~48KB peak, but keep a conservative
|
||||||
// floor. Check both total free heap and largest contiguous block so fragmented
|
// floor. Check both total free heap and largest contiguous block so fragmented
|
||||||
// heap does not fall through into a failed TLS allocation path.
|
// heap does not fall through into a failed TLS allocation path.
|
||||||
constexpr uint32_t MIN_HEAP_FOR_TLS = 55000;
|
// MEMFIX-PORT: TLS heap gate; portable
|
||||||
|
// Field data (July 2026): launching sync from a reader session lands at
|
||||||
|
// 51.9-58.2 KB free / 42-53 KB maxAlloc after WiFi comes up. wolfSSL handles
|
||||||
|
// allocation failure by returning MEMORY_E (no abort under -fno-exceptions),
|
||||||
|
// so an optimistic attempt degrades to the same clean "sync failed" as the
|
||||||
|
// gate — the gate only needs to keep out states where a doomed handshake
|
||||||
|
// would waste tens of seconds, not guarantee success.
|
||||||
|
//
|
||||||
|
// Free and largest-block have separate requirements: with SP ECC
|
||||||
|
// (WOLFSSL_HAVE_SP_ECC) the handshake's crypto uses fixed 256-bit arrays, so
|
||||||
|
// the largest single TLS allocation is the ~17 KB wolfSSL record buffer, not
|
||||||
|
// a run of fast-math bignums. A handshake was measured succeeding inside a
|
||||||
|
// 43 KB largest block; requiring 50 KB contiguous refused syncs that fit.
|
||||||
|
constexpr uint32_t MIN_FREE_FOR_TLS = 50000;
|
||||||
|
constexpr uint32_t MIN_BLOCK_FOR_TLS = 20000;
|
||||||
|
|
||||||
// Apply the shared KOSync auth headers after begin(). x-auth-* is the native
|
// Apply the shared KOSync auth headers after begin(). x-auth-* is the native
|
||||||
// KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility.
|
// KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility.
|
||||||
@@ -39,9 +53,9 @@ void applyAuthHeaders(freeink::SecureHttpClient& http) {
|
|||||||
bool insufficientHeap() {
|
bool insufficientHeap() {
|
||||||
const uint32_t freeHeap = ESP.getFreeHeap();
|
const uint32_t freeHeap = ESP.getFreeHeap();
|
||||||
const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
|
const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
|
||||||
if (freeHeap < MIN_HEAP_FOR_TLS || maxAllocHeap < MIN_HEAP_FOR_TLS) {
|
if (freeHeap < MIN_FREE_FOR_TLS || maxAllocHeap < MIN_BLOCK_FOR_TLS) {
|
||||||
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free, %u max alloc (need %u)", freeHeap,
|
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u), %u max alloc (need %u)", freeHeap,
|
||||||
maxAllocHeap, MIN_HEAP_FOR_TLS);
|
MIN_FREE_FOR_TLS, maxAllocHeap, MIN_BLOCK_FOR_TLS);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+23
-1
@@ -45,7 +45,14 @@ build_flags =
|
|||||||
-DWOLFSSL_OPTIONS_H
|
-DWOLFSSL_OPTIONS_H
|
||||||
-DWOLFSSL_CLIENT_EXAMPLE
|
-DWOLFSSL_CLIENT_EXAMPLE
|
||||||
-DWOLFSSL_TLS13
|
-DWOLFSSL_TLS13
|
||||||
-DWOLFSSL_SP_RISCV32
|
# MEMFIX-PORT: single-precision ECC (sp_c32.c). Without it every P-256 operation
|
||||||
|
# (TLS 1.3 key_share keygen, ECDHE, ECDSA cert verify) runs on fast-math bignums
|
||||||
|
# that WOLFSSL_SMALL_STACK heap-allocates at FP_MAX_BITS size -- tens of KB of
|
||||||
|
# temporaries, which OOMs (MP_MEM) at the ~50KB free heap a reading session
|
||||||
|
# leaves. SP uses fixed 256-bit arrays: a few KB, and several times faster.
|
||||||
|
# SP_SMALL trades the large precomputed point tables for smaller flash.
|
||||||
|
-DWOLFSSL_HAVE_SP_ECC
|
||||||
|
-DWOLFSSL_SP_SMALL
|
||||||
-DHAVE_TLS_EXTENSIONS
|
-DHAVE_TLS_EXTENSIONS
|
||||||
-DHAVE_SUPPORTED_CURVES
|
-DHAVE_SUPPORTED_CURVES
|
||||||
-DHAVE_HKDF
|
-DHAVE_HKDF
|
||||||
@@ -99,6 +106,21 @@ custom_sdkconfig =
|
|||||||
CONFIG_BT_CTRL_BLE_MAX_ACT=3 ; was 6; need conn + scan + initiate only
|
CONFIG_BT_CTRL_BLE_MAX_ACT=3 ; was 6; need conn + scan + initiate only
|
||||||
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=50 ; was 100; pairing-time scan only
|
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=50 ; was 100; pairing-time scan only
|
||||||
CONFIG_BT_CTRL_ADV_DUP_FILT_MAX=10 ; was 30
|
CONFIG_BT_CTRL_ADV_DUP_FILT_MAX=10 ; was 30
|
||||||
|
; MEMFIX-PORT: task stack right-sizing (~7 KB); NOTE develop has no
|
||||||
|
; custom_sdkconfig block — port via sdkconfig.defaults or equivalent.
|
||||||
|
; Task stack right-sizing from measured high-water marks (heap block map +
|
||||||
|
; per-task stack audit, July 2026): esp_timer used ~0.8 KB of 8 KB across
|
||||||
|
; every capture incl. BLE sessions; the FreeRTOS timer service used ~0.5 KB
|
||||||
|
; of 4 KB. Neither runs TLS or app code. ~7 KB back to the heap.
|
||||||
|
CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096
|
||||||
|
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2560
|
||||||
|
; Move the WiFi stack's non-critical hot paths out of IRAM into flash.
|
||||||
|
; On the C3, IRAM and DRAM share one SRAM pool, so the ~25-30 KB this
|
||||||
|
; frees lands directly in the heap — paid for with lower WiFi throughput
|
||||||
|
; during transfers (occasional sync/OTA use, not streaming: acceptable).
|
||||||
|
; IRAM cost is static, so the heap gain applies even with WiFi off.
|
||||||
|
CONFIG_ESP_WIFI_IRAM_OPT=n
|
||||||
|
CONFIG_ESP_WIFI_RX_IRAM_OPT=n
|
||||||
; Keep the Arduino wrappers for the removed cloud components (below) out of
|
; Keep the Arduino wrappers for the removed cloud components (below) out of
|
||||||
; the core source list; all other bundled libraries default to enabled.
|
; the core source list; all other bundled libraries default to enabled.
|
||||||
CONFIG_ARDUINO_SELECTIVE_COMPILATION=y
|
CONFIG_ARDUINO_SELECTIVE_COMPILATION=y
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ OVERRIDES = f"""
|
|||||||
#ifndef HAVE_FFDHE_2048
|
#ifndef HAVE_FFDHE_2048
|
||||||
#define HAVE_FFDHE_2048
|
#define HAVE_FFDHE_2048
|
||||||
#endif
|
#endif
|
||||||
|
/* MEMFIX-PORT: 8192 handles up to RSA-4096 keys (the public-CA maximum,
|
||||||
|
ISRG Root X1 included) with half the per-bignum heap of 16384: with
|
||||||
|
WOLFSSL_SMALL_STACK each fast-math temp is FP_MAX_BITS/8 * 2 bytes on the
|
||||||
|
heap, and TLS cert verification allocates dozens at once. */
|
||||||
#undef FP_MAX_BITS
|
#undef FP_MAX_BITS
|
||||||
#define FP_MAX_BITS 16384
|
#define FP_MAX_BITS 8192
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#include "HeapMap.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <Memory.h>
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include "rom/ets_sys.h"
|
||||||
|
|
||||||
|
// heap_caps_dump walks each heap inside a critical section (interrupts
|
||||||
|
// masked), so its output can neither go through the UART-0 ROM path we can't
|
||||||
|
// see nor be flow-controlled toward the CDC (the CDC ring drains in an
|
||||||
|
// interrupt handler — waiting deadlocks into the interrupt WDT,
|
||||||
|
// field-verified). Instead the ROM putc parses each line into a compact
|
||||||
|
// record; the captured table is logged after the dump with interrupts live.
|
||||||
|
namespace heapmap {
|
||||||
|
namespace {
|
||||||
|
struct BlockRec {
|
||||||
|
uint32_t addr;
|
||||||
|
uint32_t size;
|
||||||
|
bool free;
|
||||||
|
};
|
||||||
|
constexpr uint16_t kMaxRecs = 1400;
|
||||||
|
BlockRec* g_recs = nullptr; // borrowed buffer, valid only during capture
|
||||||
|
uint16_t g_recCount = 0;
|
||||||
|
bool g_overflowed = false;
|
||||||
|
char g_line[96];
|
||||||
|
uint8_t g_lineLen = 0;
|
||||||
|
|
||||||
|
void captInterpolatePutc(char c) {
|
||||||
|
if (c != '\n') {
|
||||||
|
if (g_lineLen < sizeof(g_line) - 1) g_line[g_lineLen++] = c;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_line[g_lineLen] = '\0';
|
||||||
|
g_lineLen = 0;
|
||||||
|
// e.g. "Block 0x3fcc69bc data, size: 89424 bytes, Free: Yes"
|
||||||
|
unsigned addr = 0, size = 0;
|
||||||
|
char freeWord[4] = {0};
|
||||||
|
if (sscanf(g_line, "Block 0x%x data, size: %u bytes, Free: %3s", &addr, &size, freeWord) == 3) {
|
||||||
|
if (g_recs && g_recCount < kMaxRecs) {
|
||||||
|
g_recs[g_recCount++] = {addr, size, freeWord[0] == 'Y'};
|
||||||
|
} else {
|
||||||
|
g_overflowed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void dump() {
|
||||||
|
auto recBuf = makeUniqueNoThrow<BlockRec[]>(kMaxRecs);
|
||||||
|
if (!recBuf) {
|
||||||
|
LOG_ERR("MEM", "heap map skipped: no room for capture buffer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_recs = recBuf.get();
|
||||||
|
g_recCount = 0;
|
||||||
|
g_overflowed = false;
|
||||||
|
g_lineLen = 0;
|
||||||
|
// Capture (interrupts masked inside the dump): parse into records, never
|
||||||
|
// wait. Log the table afterward with the system live. NOTE: the capture
|
||||||
|
// buffer itself appears in the map as a used block of ~17.4 KB — it frees
|
||||||
|
// on return (observer effect, do not chase it as a leak/splitter).
|
||||||
|
ets_install_putc1(&captInterpolatePutc);
|
||||||
|
heap_caps_dump(MALLOC_CAP_8BIT);
|
||||||
|
ets_install_uart_printf();
|
||||||
|
g_recs = nullptr;
|
||||||
|
|
||||||
|
LOG_DBG("MEM", "---- heap block map: %u blocks%s ----", g_recCount, g_overflowed ? " (TRUNCATED)" : "");
|
||||||
|
uint32_t dustCount = 0, dustBytes = 0;
|
||||||
|
for (uint16_t i = 0; i < g_recCount; ++i) {
|
||||||
|
const auto& r = recBuf[i];
|
||||||
|
if (r.free || r.size >= 256) {
|
||||||
|
LOG_DBG("MEM", "%s 0x%08x %u", r.free ? "FREE" : "used", r.addr, r.size);
|
||||||
|
} else {
|
||||||
|
dustCount++;
|
||||||
|
dustBytes += r.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG_DBG("MEM", "dust: %u used blocks < 256B totaling %u bytes", dustCount, dustBytes);
|
||||||
|
LOG_DBG("MEM", "---- end heap block map ----");
|
||||||
|
}
|
||||||
|
} // namespace heapmap
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// MEMFIX-PORT: heap block map (on-demand via CMD:MEMMAP + reader one-shot); portable, no BLE dependency
|
||||||
|
namespace heapmap {
|
||||||
|
// Capture-and-log the DRAM heap block map (address/size/free per block,
|
||||||
|
// sub-256B used blocks rolled up as "dust"). Safe to call from the main loop;
|
||||||
|
// ~60-100 LOG_DBG lines. See HeapMap.cpp for why capture-then-log is the only
|
||||||
|
// shape that works (heap_caps_dump runs with interrupts masked).
|
||||||
|
void dump();
|
||||||
|
} // namespace heapmap
|
||||||
@@ -32,6 +32,7 @@ class SdCardFontSystem {
|
|||||||
/// Non-const access to the registry (for FontInstaller).
|
/// Non-const access to the registry (for FontInstaller).
|
||||||
SdCardFontRegistry& registry() { return registry_; }
|
SdCardFontRegistry& registry() { return registry_; }
|
||||||
|
|
||||||
|
// MEMFIX-PORT: font system audit passthrough; portable
|
||||||
/// Resident heap held by loaded SD fonts (audit; see SdCardFont::reportMemory).
|
/// Resident heap held by loaded SD fonts (audit; see SdCardFont::reportMemory).
|
||||||
size_t reportFontMemory() const { return manager_.reportMemory(); }
|
size_t reportFontMemory() const { return manager_.reportMemory(); }
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
#include "SdCardFontSystem.h"
|
#include "SdCardFontSystem.h"
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#include "rom/ets_sys.h"
|
#include "rom/ets_sys.h"
|
||||||
|
#include "HeapMap.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
||||||
@@ -70,45 +71,6 @@ bool isInReadFolder(const std::string& path) {
|
|||||||
return path.size() > n && path.compare(0, n, READ_FOLDER) == 0 && path[n] == '/';
|
return path.size() > n && path.compare(0, n, READ_FOLDER) == 0 && path[n] == '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heap block map capture. heap_caps_dump walks each heap inside a critical
|
|
||||||
// section (interrupts masked), so its output can neither go through the
|
|
||||||
// UART-0 ROM path we can't see nor be flow-controlled toward the CDC (the CDC
|
|
||||||
// ring drains in an interrupt handler — waiting deadlocks into the interrupt
|
|
||||||
// WDT, field-verified). Instead the ROM putc parses each line into a compact
|
|
||||||
// record; the captured table is logged after the dump with interrupts live.
|
|
||||||
namespace heapmap {
|
|
||||||
struct BlockRec {
|
|
||||||
uint32_t addr;
|
|
||||||
uint32_t size;
|
|
||||||
bool free;
|
|
||||||
};
|
|
||||||
constexpr uint16_t kMaxRecs = 1400;
|
|
||||||
BlockRec* recs = nullptr; // borrowed buffer, valid only during capture
|
|
||||||
uint16_t recCount = 0;
|
|
||||||
bool overflowed = false;
|
|
||||||
char line[96];
|
|
||||||
uint8_t lineLen = 0;
|
|
||||||
|
|
||||||
void putc(char c) {
|
|
||||||
if (c != '\n') {
|
|
||||||
if (lineLen < sizeof(line) - 1) line[lineLen++] = c;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
line[lineLen] = '\0';
|
|
||||||
lineLen = 0;
|
|
||||||
// e.g. "Block 0x3fcc69bc data, size: 89424 bytes, Free: Yes"
|
|
||||||
unsigned addr = 0, size = 0;
|
|
||||||
char freeWord[4] = {0};
|
|
||||||
if (sscanf(line, "Block 0x%x data, size: %u bytes, Free: %3s", &addr, &size, freeWord) == 3) {
|
|
||||||
if (recs && recCount < kMaxRecs) {
|
|
||||||
recs[recCount++] = {addr, size, freeWord[0] == 'Y'};
|
|
||||||
} else {
|
|
||||||
overflowed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace heapmap
|
|
||||||
|
|
||||||
class FrameBufferBuildLoan {
|
class FrameBufferBuildLoan {
|
||||||
public:
|
public:
|
||||||
explicit FrameBufferBuildLoan(GfxRenderer& renderer) : renderer_(renderer) {}
|
explicit FrameBufferBuildLoan(GfxRenderer& renderer) : renderer_(renderer) {}
|
||||||
@@ -1545,6 +1507,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
|||||||
// preceding lines show (mini rebuilds, kern reloads, BLE churn).
|
// preceding lines show (mini rebuilds, kern reloads, BLE churn).
|
||||||
LOG_DBG("MEM", "post-render: free=%u maxAlloc=%u", (unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap());
|
LOG_DBG("MEM", "post-render: free=%u maxAlloc=%u", (unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap());
|
||||||
{
|
{
|
||||||
|
// MEMFIX-PORT: per-render heap owner audit + one-shot block map; portable
|
||||||
// Heap audit: attribute resident heap to its owners so margin work is
|
// Heap audit: attribute resident heap to its owners so margin work is
|
||||||
// measurement-driven. `other` = IDF/Arduino baseline + SdFat + epub
|
// measurement-driven. `other` = IDF/Arduino baseline + SdFat + epub
|
||||||
// metadata + BLE (when on) + anything not yet instrumented.
|
// metadata + BLE (when on) + anything not yet instrumented.
|
||||||
@@ -1568,36 +1531,10 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
|||||||
static bool heapMapDumped = false;
|
static bool heapMapDumped = false;
|
||||||
if (!heapMapDumped && !(section && section->isBuilding())) {
|
if (!heapMapDumped && !(section && section->isBuilding())) {
|
||||||
heapMapDumped = true;
|
heapMapDumped = true;
|
||||||
auto recBuf = makeUniqueNoThrow<heapmap::BlockRec[]>(heapmap::kMaxRecs);
|
// Landmarks: known owners' addresses, so map blocks self-identify.
|
||||||
if (recBuf) {
|
LOG_DBG("MEM", "landmark framebuffer=%p section=%p epub=%p activity=%p", renderer.getFrameBuffer(),
|
||||||
heapmap::recs = recBuf.get();
|
static_cast<void*>(section.get()), static_cast<void*>(epub.get()), static_cast<void*>(this));
|
||||||
heapmap::recCount = 0;
|
heapmap::dump();
|
||||||
heapmap::overflowed = false;
|
|
||||||
heapmap::lineLen = 0;
|
|
||||||
// Capture (interrupts masked inside the dump): parse into records,
|
|
||||||
// never wait. Log the table afterward with the system live.
|
|
||||||
ets_install_putc1(&heapmap::putc);
|
|
||||||
heap_caps_dump(MALLOC_CAP_8BIT);
|
|
||||||
ets_install_uart_printf();
|
|
||||||
heapmap::recs = nullptr;
|
|
||||||
|
|
||||||
LOG_DBG("MEM", "---- heap block map: %u blocks%s ----", heapmap::recCount,
|
|
||||||
heapmap::overflowed ? " (TRUNCATED)" : "");
|
|
||||||
uint32_t dustCount = 0, dustBytes = 0;
|
|
||||||
for (uint16_t i = 0; i < heapmap::recCount; ++i) {
|
|
||||||
const auto& r = recBuf[i];
|
|
||||||
if (r.free || r.size >= 256) {
|
|
||||||
LOG_DBG("MEM", "%s 0x%08x %u", r.free ? "FREE" : "used", r.addr, r.size);
|
|
||||||
} else {
|
|
||||||
dustCount++;
|
|
||||||
dustBytes += r.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOG_DBG("MEM", "dust: %u used blocks < 256B totaling %u bytes", dustCount, dustBytes);
|
|
||||||
LOG_DBG("MEM", "---- end heap block map ----");
|
|
||||||
} else {
|
|
||||||
LOG_ERR("MEM", "heap map skipped: no room for capture buffer");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
#include "images/LoadingIcon.h"
|
#include "images/LoadingIcon.h"
|
||||||
#include "util/ButtonNavigator.h"
|
#include "util/ButtonNavigator.h"
|
||||||
#include "util/ScreenshotUtil.h"
|
#include "util/ScreenshotUtil.h"
|
||||||
|
#include <Memory.h>
|
||||||
|
#include "HeapMap.h"
|
||||||
|
|
||||||
GfxRenderer renderer(display);
|
GfxRenderer renderer(display);
|
||||||
MappedInputManager mappedInputManager(gpio, renderer);
|
MappedInputManager mappedInputManager(gpio, renderer);
|
||||||
@@ -596,6 +598,23 @@ void loop() {
|
|||||||
LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(),
|
LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(),
|
||||||
ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap());
|
ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap());
|
||||||
lastMemPrint = millis();
|
lastMemPrint = millis();
|
||||||
|
// MEMFIX-PORT: per-task stack high-water audit; portable
|
||||||
|
// Task stack audit (~once/min): name, stack base (matches blocks in the
|
||||||
|
// heap map), and high-water free bytes — the margin available for
|
||||||
|
// right-sizing each stack. TRACE_FACILITY is on in this sdkconfig.
|
||||||
|
static uint8_t memPrintCount = 0;
|
||||||
|
if (++memPrintCount >= 6) {
|
||||||
|
memPrintCount = 0;
|
||||||
|
const UBaseType_t taskCount = uxTaskGetNumberOfTasks();
|
||||||
|
auto taskStatus = makeUniqueNoThrow<TaskStatus_t[]>(taskCount + 2);
|
||||||
|
if (taskStatus) {
|
||||||
|
const UBaseType_t got = uxTaskGetSystemState(taskStatus.get(), taskCount + 2, nullptr);
|
||||||
|
for (UBaseType_t i = 0; i < got; ++i) {
|
||||||
|
LOG_DBG("MEM", "task %-20s stackBase=%p highWaterFree=%u", taskStatus[i].pcTaskName,
|
||||||
|
taskStatus[i].pxStackBase, (unsigned)taskStatus[i].usStackHighWaterMark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle incoming serial commands,
|
// Handle incoming serial commands,
|
||||||
@@ -611,6 +630,11 @@ void loop() {
|
|||||||
uint8_t* buf = display.getFrameBuffer();
|
uint8_t* buf = display.getFrameBuffer();
|
||||||
logSerial.write(buf, bufferSize);
|
logSerial.write(buf, bufferSize);
|
||||||
logSerial.printf("SCREENSHOT_END\n");
|
logSerial.printf("SCREENSHOT_END\n");
|
||||||
|
// MEMFIX-PORT: on-demand heap map serial command; portable
|
||||||
|
} else if (cmd == "MEMMAP") {
|
||||||
|
// On-demand heap block map: capture the heap exactly when it looks
|
||||||
|
// interesting (e.g. maxAlloc degraded mid-session) without a reboot.
|
||||||
|
heapmap::dump();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,14 @@ void CrossPointWebServer::begin() {
|
|||||||
udpActive = udp.begin(LOCAL_UDP_PORT);
|
udpActive = udp.begin(LOCAL_UDP_PORT);
|
||||||
LOG_DBG("WEB", "Discovery UDP %s on port %d", udpActive ? "enabled" : "failed", LOCAL_UDP_PORT);
|
LOG_DBG("WEB", "Discovery UDP %s on port %d", udpActive ? "enabled" : "failed", LOCAL_UDP_PORT);
|
||||||
|
|
||||||
|
// All request handlers run on the task that calls handleClient(). Register
|
||||||
|
// that task before any handler can call esp_task_wdt_reset().
|
||||||
|
const esp_err_t watchdogResult = esp_task_wdt_add(nullptr);
|
||||||
|
watchdogTaskRegistered = watchdogResult == ESP_OK;
|
||||||
|
if (!watchdogTaskRegistered) {
|
||||||
|
LOG_ERR("WEB", "Failed to register web server task with watchdog: %s", esp_err_to_name(watchdogResult));
|
||||||
|
}
|
||||||
|
|
||||||
running = true;
|
running = true;
|
||||||
|
|
||||||
LOG_DBG("WEB", "Web server started on port %d", port);
|
LOG_DBG("WEB", "Web server started on port %d", port);
|
||||||
@@ -230,6 +238,10 @@ void CrossPointWebServer::abortWsUpload(const char* tag) {
|
|||||||
void CrossPointWebServer::stop() {
|
void CrossPointWebServer::stop() {
|
||||||
if (!running || !server) {
|
if (!running || !server) {
|
||||||
LOG_DBG("WEB", "stop() called but already stopped (running=%d, server=%p)", running, server.get());
|
LOG_DBG("WEB", "stop() called but already stopped (running=%d, server=%p)", running, server.get());
|
||||||
|
if (watchdogTaskRegistered) {
|
||||||
|
esp_task_wdt_delete(nullptr);
|
||||||
|
watchdogTaskRegistered = false;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +282,11 @@ void CrossPointWebServer::stop() {
|
|||||||
LOG_DBG("WEB", "Web server stopped and deleted");
|
LOG_DBG("WEB", "Web server stopped and deleted");
|
||||||
LOG_DBG("WEB", "[MEM] Free heap after delete server: %d bytes", ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap after delete server: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
|
if (watchdogTaskRegistered) {
|
||||||
|
esp_task_wdt_delete(nullptr);
|
||||||
|
watchdogTaskRegistered = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared
|
// Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared
|
||||||
// later in the file and will be cleared when they go out of scope or on next upload
|
// later in the file and will be cleared when they go out of scope or on next upload
|
||||||
LOG_DBG("WEB", "[MEM] Free heap final: %d bytes", ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap final: %d bytes", ESP.getFreeHeap());
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class CrossPointWebServer {
|
|||||||
std::unique_ptr<WebServer> server = nullptr;
|
std::unique_ptr<WebServer> server = nullptr;
|
||||||
std::unique_ptr<WebSocketsServer> wsServer = nullptr;
|
std::unique_ptr<WebSocketsServer> wsServer = nullptr;
|
||||||
bool running = false;
|
bool running = false;
|
||||||
|
bool watchdogTaskRegistered = false;
|
||||||
bool apMode = false; // true when running in AP mode, false for STA mode
|
bool apMode = false; // true when running in AP mode, false for STA mode
|
||||||
uint16_t port = 80;
|
uint16_t port = 80;
|
||||||
uint16_t wsPort = 81; // WebSocket port
|
uint16_t wsPort = 81; // WebSocket port
|
||||||
|
|||||||
Reference in New Issue
Block a user