diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 3c8ed39b..b3621c09 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -44,6 +44,17 @@ class Epub { } ~Epub() = default; std::string& getBasePath() { return contentBasePath; } + // Approximate resident heap of the open book (audit): path strings, the CSS + // file list, and the parsed stylesheet. BookMetadataCache is file-backed + // (counts + HalFile handles) and contributes little. + size_t residentBytes() const { + size_t total = sizeof(Epub) + tocNcxItem.capacity() + tocNavItem.capacity() + filepath.capacity() + + contentBasePath.capacity() + cachePath.capacity(); + for (const auto& f : cssFiles) total += sizeof(f) + (f.capacity() > 15 ? f.capacity() : 0); + if (cssParser) total += cssParser->residentBytes(); + return total; + } + size_t cssRuleCount() const { return cssParser ? cssParser->ruleCount() : 0; } bool load(bool buildIfMissing = true, bool skipLoadingCss = false); bool clearCache() const; void setupCacheDir() const; diff --git a/lib/Epub/Epub/css/CssParser.h b/lib/Epub/Epub/css/CssParser.h index 41797fd3..3fe74f04 100644 --- a/lib/Epub/Epub/css/CssParser.h +++ b/lib/Epub/Epub/css/CssParser.h @@ -67,6 +67,19 @@ class CssParser { */ [[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue); + // Approximate resident heap of the parsed stylesheet, for the audit log. + // unordered_map cost model: bucket array + one node per rule (libstdc++ node + // overhead ~= 2 pointers + hash) + key string capacity when it exceeds SSO. + size_t residentBytes() const { + size_t total = rulesBySelector_.bucket_count() * sizeof(void*); + for (const auto& kv : rulesBySelector_) { + total += sizeof(void*) * 2 + sizeof(size_t); // node overhead + total += sizeof(kv); + if (kv.first.capacity() > 15) total += kv.first.capacity(); // beyond SSO + } + return total; + } + /** * Check if any rules have been loaded */ diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index e37d6e15..68b24165 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -39,6 +39,8 @@ #include "util/BookmarkUtil.h" #include "util/ScreenshotUtil.h" #include "SdCardFontSystem.h" +#include +#include "rom/ets_sys.h" namespace { // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() @@ -68,6 +70,45 @@ bool isInReadFolder(const std::string& path) { 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 { public: explicit FrameBufferBuildLoan(GfxRenderer& renderer) : renderer_(renderer) {} @@ -1509,13 +1550,55 @@ void EpubReaderActivity::render(RenderLock&& lock) { // metadata + BLE (when on) + anything not yet instrumented. const size_t fontBytes = sdFontSystem.reportFontMemory(); const size_t sectionBytes = section ? section->residentBytes() : 0; + const size_t epubBytes = epub ? epub->residentBytes() : 0; const size_t fbBytes = renderer.hasFrameBuffer() ? renderer.getBufferSize() : 0; const uint32_t heapTotal = ESP.getHeapSize(); const uint32_t heapFree = ESP.getFreeHeap(); - LOG_DBG("MEM", "audit: total=%u free=%u fb=%u fonts=%u section=%u building=%d ble=%d other=%d", heapTotal, - heapFree, (unsigned)fbBytes, (unsigned)fontBytes, (unsigned)sectionBytes, + LOG_DBG("MEM", + "audit: total=%u free=%u maxAlloc=%u fb=%u fonts=%u section=%u epub=%u cssRules=%u building=%d ble=%d " + "other=%d", + heapTotal, heapFree, (unsigned)ESP.getMaxAllocHeap(), (unsigned)fbBytes, (unsigned)fontBytes, + (unsigned)sectionBytes, (unsigned)epubBytes, (unsigned)(epub ? epub->cssRuleCount() : 0), section && section->isBuilding() ? 1 : 0, BleHid.isRunning() ? 1 : 0, - (int)(heapTotal - heapFree - fbBytes - fontBytes - sectionBytes)); + (int)(heapTotal - heapFree - fbBytes - fontBytes - sectionBytes - epubBytes)); + // One-shot heap block map for fragmentation analysis: dumps every block + // (address/size/free) once per boot, at the first settled (non-building) + // audit, so mid-heap long-lived allocations identify themselves. ~2-4 s + // of serial output; debug builds only. + static bool heapMapDumped = false; + if (!heapMapDumped && !(section && section->isBuilding())) { + heapMapDumped = true; + auto recBuf = makeUniqueNoThrow(heapmap::kMaxRecs); + if (recBuf) { + heapmap::recs = recBuf.get(); + heapmap::recCount = 0; + 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"); + } + } } } // Only persist when the position actually changed. render() also runs on menu,