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.