From ba92cb902e96a21fb32430e73491ec8b45feeb48 Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Tue, 14 Jul 2026 12:08:29 -0400 Subject: [PATCH] Add heap memory reporting for fonts and sections Implements reportMemory() methods across SdCardFont, SdCardFontManager, and Section classes to track resident heap usage. Adds heap audit logging in EpubReaderActivity to attribute memory to fonts, framebuffer, section data, and other components for measurement-driven optimization. --- lib/EpdFont/SdCardFont.cpp | 27 ++++++++++++++++++++ lib/EpdFont/SdCardFont.h | 4 +++ lib/EpdFont/SdCardFontManager.cpp | 8 ++++++ lib/EpdFont/SdCardFontManager.h | 3 +++ lib/Epub/Epub/Section.h | 14 ++++++++++ src/SdCardFontSystem.h | 3 +++ src/activities/reader/EpubReaderActivity.cpp | 15 +++++++++++ 7 files changed, 74 insertions(+) diff --git a/lib/EpdFont/SdCardFont.cpp b/lib/EpdFont/SdCardFont.cpp index f48028e2..20ef8e0a 100644 --- a/lib/EpdFont/SdCardFont.cpp +++ b/lib/EpdFont/SdCardFont.cpp @@ -1406,6 +1406,33 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) { return &self->overflow_[slot].glyph; } +size_t SdCardFont::reportMemory() const { + size_t total = 0; + for (uint8_t si = 0; si < MAX_STYLES; ++si) { + const auto& s = styles_[si]; + if (!s.present) continue; + size_t fixed = 0; // loaded once per family: interval/kern/lig tables + if (s.fullIntervals) fixed += s.header.intervalCount * sizeof(EpdUnicodeInterval); + if (s.bmpIntervals) fixed += s.header.intervalCount * sizeof(PerStyle::BmpInterval16); + if (s.kernLeftClasses) fixed += s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry); + if (s.kernRightClasses) fixed += s.header.kernRightEntryCount * sizeof(EpdKernClassEntry); + if (s.ligaturePairs) fixed += s.header.ligaturePairCount * sizeof(EpdLigaturePair); + // kept-if-fits mini arenas: capacity (not count) is what stays resident + size_t mini = s.miniIntervalCapacity * sizeof(EpdUnicodeInterval) + s.miniGlyphCapacity * sizeof(EpdGlyph) + + s.miniBitmapCapacity + s.miniKernLeftCapacity * sizeof(EpdKernClassEntry) + + s.miniKernRightCapacity * sizeof(EpdKernClassEntry) + s.miniKernMatrixCapacity; + const size_t adv = advanceTableSize_[si] * sizeof(AdvanceEntry); + LOG_DBG("SDCF", "mem style%u: fixed=%u mini=%u adv=%u", si, (unsigned)fixed, (unsigned)mini, (unsigned)adv); + total += fixed + mini + adv; + } + size_t overflowBytes = 0; + for (uint32_t i = 0; i < overflowCount_; ++i) { + if (overflow_[i].bitmap) overflowBytes += overflow_[i].glyph.dataLength; + } + total += overflowBytes + overflowCount_ * sizeof(OverflowEntry); + return total; +} + bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const { for (uint32_t i = 0; i < overflowCount_; i++) { if (&overflow_[i].glyph == glyph) return true; diff --git a/lib/EpdFont/SdCardFont.h b/lib/EpdFont/SdCardFont.h index e3c91298..68d2d960 100644 --- a/lib/EpdFont/SdCardFont.h +++ b/lib/EpdFont/SdCardFont.h @@ -104,6 +104,10 @@ class SdCardFont { uint32_t uniqueGlyphs = 0; uint32_t bitmapBytes = 0; }; + // Log per-style resident heap (full tables + kept-if-fits mini arenas + + // advance tables + overflow bitmaps) and return the total in bytes. Pure + // accounting — no allocation, no state change. + size_t reportMemory() const; void logStats(const char* label = "SDCF"); void resetStats(); const Stats& getStats() const { return stats_; } diff --git a/lib/EpdFont/SdCardFontManager.cpp b/lib/EpdFont/SdCardFontManager.cpp index 2d221386..40e7ad6c 100644 --- a/lib/EpdFont/SdCardFontManager.cpp +++ b/lib/EpdFont/SdCardFontManager.cpp @@ -88,6 +88,14 @@ void SdCardFontManager::unloadAll(GfxRenderer& renderer) { loadedPointSize_ = 0; } +size_t SdCardFontManager::reportMemory() const { + size_t total = 0; + for (const auto& lf : loaded_) { + if (lf.font) total += lf.font->reportMemory(); + } + return total; +} + int SdCardFontManager::getFontId(const std::string& familyName) const { if (familyName != loadedFamilyName_ || loaded_.empty()) return 0; return loaded_.front().fontId; diff --git a/lib/EpdFont/SdCardFontManager.h b/lib/EpdFont/SdCardFontManager.h index f9d2b3f2..ce186efe 100644 --- a/lib/EpdFont/SdCardFontManager.h +++ b/lib/EpdFont/SdCardFontManager.h @@ -32,6 +32,9 @@ class SdCardFontManager { // Get name of currently loaded family (empty if none). const std::string& currentFamilyName() const { return loadedFamilyName_; }; + // Sum of loaded fonts' resident heap (see SdCardFont::reportMemory). + size_t reportMemory() const; + // Point size that was actually loaded. // 0 if nothing loaded. uint8_t currentPointSize() const { return loadedPointSize_; }; diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index d90b4558..77ee12e1 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -72,6 +72,7 @@ class Section { // Builds write here and are swapped over filePath only on commit, so a prior // partial/finalized file stays readable while a rebuild is in progress. std::string binTmpPath() const { return filePath + ".part"; } + std::unique_ptr loadPageAt(int page) const; // Read a page already laid out by the in-progress build (page < build LUT size), from // the partially-written tmp .bin without disturbing the build's write cursor. @@ -131,6 +132,19 @@ class Section { // (covers finalized sections and partials from a previous session). std::optional findAnchor(const std::string& anchor) const; + // 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 + // strings dominate (the parser's internal footprint is not walked here). + size_t residentBytes() const { + size_t total = sizeof(Section) + filePath.capacity(); + if (build_) { + total += sizeof(BuildContext) + build_->lut.capacity() * sizeof(PageLutEntry) + + build_->parsePath.capacity() + build_->contentBase.capacity() + build_->imageBasePath.capacity() + + build_->htmlPath.capacity() + build_->tmpHtmlPath.capacity(); + } + return total; + } + // True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a // giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild. bool hasHtmlCache() const; diff --git a/src/SdCardFontSystem.h b/src/SdCardFontSystem.h index 6b8aef7f..59acbefd 100644 --- a/src/SdCardFontSystem.h +++ b/src/SdCardFontSystem.h @@ -32,6 +32,9 @@ class SdCardFontSystem { /// Non-const access to the registry (for FontInstaller). SdCardFontRegistry& registry() { return registry_; } + /// Resident heap held by loaded SD fonts (audit; see SdCardFont::reportMemory). + size_t reportFontMemory() const { return manager_.reportMemory(); } + /// Mark the registry as needing re-discovery. /// Thread-safe: can be called from the web server task. void markRegistryDirty() { registryDirty_.store(true, std::memory_order_release); } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 05ae5234..e37d6e15 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -38,6 +38,7 @@ #include "fontIds.h" #include "util/BookmarkUtil.h" #include "util/ScreenshotUtil.h" +#include "SdCardFontSystem.h" namespace { // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() @@ -1502,6 +1503,20 @@ void EpubReaderActivity::render(RenderLock&& lock) { // maxAlloc/free ratio across pages points at whichever allocation pattern the // preceding lines show (mini rebuilds, kern reloads, BLE churn). LOG_DBG("MEM", "post-render: free=%u maxAlloc=%u", (unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap()); + { + // Heap audit: attribute resident heap to its owners so margin work is + // measurement-driven. `other` = IDF/Arduino baseline + SdFat + epub + // 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 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, + section && section->isBuilding() ? 1 : 0, BleHid.isRunning() ? 1 : 0, + (int)(heapTotal - heapFree - fbBytes - fontBytes - sectionBytes)); + } } // Only persist when the position actually changed. render() also runs on menu, // bookmark and screenshot re-renders, and writeAtomic is several FAT ops for 6 bytes.