Reduce memory footprint
This commit is contained in:
+68
-15
@@ -12,7 +12,7 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 26;
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 27;
|
||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
|
||||
sizeof(int) + // fontId
|
||||
sizeof(float) + // lineCompression
|
||||
@@ -20,6 +20,7 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
|
||||
sizeof(uint8_t) + // paragraphAlignment
|
||||
sizeof(uint16_t) + // viewportWidth
|
||||
sizeof(uint16_t) + // viewportHeight
|
||||
sizeof(bool) + // parseComplete
|
||||
sizeof(uint16_t) + // pageCount (stored as 16-bit in header)
|
||||
sizeof(bool) + // hyphenationEnabled
|
||||
sizeof(bool) + // embeddedStyle
|
||||
@@ -195,9 +196,9 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
}
|
||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
||||
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
|
||||
sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) + sizeof(imageRendering) +
|
||||
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||
sizeof(viewportHeight) + sizeof(bool) + sizeof(pageCount) +
|
||||
sizeof(hyphenationEnabled) + sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) +
|
||||
sizeof(imageRendering) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||
"Header size mismatch");
|
||||
serialization::writePod(file, SECTION_FILE_VERSION);
|
||||
serialization::writePod(file, fontId);
|
||||
@@ -210,6 +211,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
serialization::writePod(file, embeddedStyle);
|
||||
serialization::writePod(file, bionicReadingEnabled);
|
||||
serialization::writePod(file, imageRendering);
|
||||
serialization::writePod(file, false); // Placeholder for parseComplete (patched later)
|
||||
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
|
||||
@@ -220,13 +222,30 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const bool bionicReadingEnabled, const uint8_t imageRendering) {
|
||||
truncatedCache = false;
|
||||
uint32_t propertyHash =
|
||||
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering);
|
||||
filePath = getSectionFilePath(propertyHash);
|
||||
|
||||
bool usingEmbeddedStyleFallback = false;
|
||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||
return false;
|
||||
// Fallback: allow loading a no-CSS cache variant when embedded CSS is enabled.
|
||||
if (embeddedStyle) {
|
||||
const uint32_t fallbackHash =
|
||||
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, false, bionicReadingEnabled, imageRendering);
|
||||
const std::string fallbackPath = getSectionFilePath(fallbackHash);
|
||||
if (Storage.openFileForRead("SCT", fallbackPath, file)) {
|
||||
filePath = fallbackPath;
|
||||
usingEmbeddedStyleFallback = true;
|
||||
LOG_ERR("SCT", "Using no-CSS section cache fallback: %s", filePath.c_str());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Match parameters
|
||||
@@ -248,6 +267,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
bool fileEmbeddedStyle;
|
||||
bool fileBionicReadingEnabled;
|
||||
uint8_t fileImageRendering;
|
||||
bool fileParseComplete;
|
||||
serialization::readPod(file, fileFontId);
|
||||
serialization::readPod(file, fileLineCompression);
|
||||
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||
@@ -258,16 +278,21 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
serialization::readPod(file, fileEmbeddedStyle);
|
||||
serialization::readPod(file, fileBionicReadingEnabled);
|
||||
serialization::readPod(file, fileImageRendering);
|
||||
serialization::readPod(file, fileParseComplete);
|
||||
|
||||
const bool embeddedStyleMatches =
|
||||
(embeddedStyle == fileEmbeddedStyle) || (usingEmbeddedStyleFallback && !fileEmbeddedStyle);
|
||||
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
|
||||
hyphenationEnabled != fileHyphenationEnabled || !embeddedStyleMatches ||
|
||||
bionicReadingEnabled != fileBionicReadingEnabled || imageRendering != fileImageRendering) {
|
||||
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||
clearCache(); // closes file before removal
|
||||
return false;
|
||||
}
|
||||
|
||||
truncatedCache = !fileParseComplete;
|
||||
}
|
||||
|
||||
serialization::readPod(file, pageCount);
|
||||
@@ -311,6 +336,7 @@ bool Section::clearCache() {
|
||||
lut.clear();
|
||||
pageCount = 0;
|
||||
currentPage = 0;
|
||||
truncatedCache = false;
|
||||
|
||||
if (!Storage.exists(filePath.c_str())) {
|
||||
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
||||
@@ -413,7 +439,10 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
const uint32_t phaseParseStart = millis();
|
||||
const bool streamOk = epub->readItemContentsToStream(localPath, visitor, 1024);
|
||||
const bool finalizeOk = visitor.finalize();
|
||||
bool success = streamOk && finalizeOk && visitor.streamSucceeded();
|
||||
const bool parserStreamOk = visitor.streamSucceeded();
|
||||
const bool parseComplete = streamOk && finalizeOk && parserStreamOk;
|
||||
bool success = parseComplete;
|
||||
const bool hasParsedPages = pageCount > 0;
|
||||
const uint32_t parseMs = millis() - phaseParseStart;
|
||||
// streamMs is no longer a separate phase (SD-write of temp file is gone); keep the
|
||||
// log breakdown stable by reporting it as 0.
|
||||
@@ -421,13 +450,35 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
|
||||
const uint32_t phaseFinalizeStart = millis();
|
||||
if (!success) {
|
||||
LOG_ERR("SCT", "Failed to parse XML and build pages (stream=%d finalize=%d)", streamOk ? 1 : 0, finalizeOk ? 1 : 0);
|
||||
file.close();
|
||||
Storage.remove(filePath.c_str());
|
||||
if (cssParser) {
|
||||
cssParser->clear();
|
||||
// If parsing fails mid-stream due low memory but some pages were already serialized,
|
||||
// keep the partial section cache so the chapter remains readable instead of failing hard.
|
||||
if (hasParsedPages) {
|
||||
LOG_ERR("SCT", "Parse incomplete; keeping partial section cache with %u pages (stream=%d finalize=%d parser=%d)",
|
||||
pageCount, streamOk ? 1 : 0, finalizeOk ? 1 : 0, parserStreamOk ? 1 : 0);
|
||||
success = true;
|
||||
} else if (embeddedStyle) {
|
||||
LOG_ERR("SCT",
|
||||
"Parse failed with embedded CSS enabled; retrying section creation with embeddedStyle=0 "
|
||||
"(stream=%d finalize=%d)",
|
||||
streamOk ? 1 : 0, finalizeOk ? 1 : 0);
|
||||
file.close();
|
||||
Storage.remove(filePath.c_str());
|
||||
if (cssParser) {
|
||||
cssParser->clear();
|
||||
}
|
||||
return createSectionFile(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, false, bionicReadingEnabled, imageRendering,
|
||||
progressFn);
|
||||
} else {
|
||||
LOG_ERR("SCT", "Failed to parse XML and build pages (stream=%d finalize=%d)", streamOk ? 1 : 0,
|
||||
finalizeOk ? 1 : 0);
|
||||
file.close();
|
||||
Storage.remove(filePath.c_str());
|
||||
if (cssParser) {
|
||||
cssParser->clear();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const uint32_t fileSize = static_cast<uint32_t>(inflatedSize);
|
||||
|
||||
@@ -477,8 +528,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
serialization::writePod(file, entry.listItemIndex);
|
||||
}
|
||||
|
||||
// Patch header with final pageCount, lutOffset, anchorMapOffset, and paragraphLutOffset
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t) * 3 - sizeof(pageCount));
|
||||
// Patch header with final parseComplete/pageCount and offsets.
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t) * 3 - sizeof(pageCount) - sizeof(bool));
|
||||
serialization::writePod(file, parseComplete);
|
||||
serialization::writePod(file, pageCount);
|
||||
serialization::writePod(file, lutOffset);
|
||||
serialization::writePod(file, anchorMapOffset);
|
||||
@@ -499,6 +551,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
LOG_ERR("SCT", "Failed to open section file for reading after creation");
|
||||
return false;
|
||||
}
|
||||
truncatedCache = !parseComplete;
|
||||
this->lut = std::move(lut);
|
||||
const uint32_t finalizeMs = millis() - phaseFinalizeStart;
|
||||
const uint32_t totalMs = millis() - phaseTotalStart;
|
||||
|
||||
@@ -17,6 +17,7 @@ class Section {
|
||||
std::string filePath;
|
||||
FsFile file;
|
||||
std::vector<uint32_t> lut; // Cached page byte-offsets; loaded once, avoids per-page LUT seek
|
||||
bool truncatedCache = false;
|
||||
|
||||
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
|
||||
@@ -68,6 +69,7 @@ class Section {
|
||||
bool bionicReadingEnabled, uint8_t imageRendering,
|
||||
const std::function<void(int)>& progressFn = nullptr);
|
||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||
bool isTruncatedCache() const { return truncatedCache; }
|
||||
|
||||
// Given a page in this section, return the TOC index for that page.
|
||||
int getTocIndexForPage(int page) const;
|
||||
|
||||
@@ -26,6 +26,7 @@ struct JpegContext {
|
||||
const RenderConfig* config{nullptr};
|
||||
int screenWidth{0};
|
||||
int screenHeight{0};
|
||||
ImageDitherMode effectiveDitherMode{ImageDitherMode::Bayer};
|
||||
|
||||
// Source dimensions after JPEGDEC's built-in scaling
|
||||
int scaledSrcWidth{0};
|
||||
@@ -103,7 +104,7 @@ uint8_t ditherGray(JpegContext& ctx, uint8_t gray, int localX, int outX, int out
|
||||
return quantizeGray4Level(gray);
|
||||
}
|
||||
|
||||
switch (ctx.config->ditherMode) {
|
||||
switch (ctx.effectiveDitherMode) {
|
||||
case ImageDitherMode::Atkinson:
|
||||
if (ctx.atkinsonDitherer) {
|
||||
return ctx.atkinsonDitherer->processPixel(gray, localX);
|
||||
@@ -179,6 +180,71 @@ int32_t jpegSeek(JPEGFILE* pFile, int32_t pos) {
|
||||
constexpr size_t JPEG_DECODER_APPROX_SIZE = 20 * 1024;
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_JPEG = JPEG_DECODER_APPROX_SIZE + 16 * 1024;
|
||||
|
||||
// Optional memory-behavior knobs for embedded targets.
|
||||
#ifndef JPEG_ENABLE_FIRST_RENDER_NO_CACHE
|
||||
#define JPEG_ENABLE_FIRST_RENDER_NO_CACHE 1
|
||||
#endif
|
||||
|
||||
#ifndef JPEG_CACHE_MIN_FREE_HEAP_MARGIN
|
||||
#define JPEG_CACHE_MIN_FREE_HEAP_MARGIN (24 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef JPEG_CACHE_MIN_MAX_ALLOC_MARGIN
|
||||
#define JPEG_CACHE_MIN_MAX_ALLOC_MARGIN (20 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef JPEG_DITHER_LOW_MEM_MIN_FREE_HEAP
|
||||
#define JPEG_DITHER_LOW_MEM_MIN_FREE_HEAP (MIN_FREE_HEAP_FOR_JPEG + 8 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef JPEG_DITHER_LOW_MEM_MIN_MAX_ALLOC
|
||||
#define JPEG_DITHER_LOW_MEM_MIN_MAX_ALLOC (48 * 1024)
|
||||
#endif
|
||||
|
||||
size_t jpegCacheBytes(int width, int height) {
|
||||
return static_cast<size_t>((width + 3) / 4) * static_cast<size_t>(height);
|
||||
}
|
||||
|
||||
bool shouldEnableJpegCache(const RenderConfig& config, int width, int height) {
|
||||
if (config.cachePath.empty()) return false;
|
||||
|
||||
#if JPEG_ENABLE_FIRST_RENDER_NO_CACHE
|
||||
if (!Storage.exists(config.cachePath.c_str())) {
|
||||
LOG_DBG("JPG", "Skipping cache on first render (compile-time policy): %s", config.cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
const size_t cacheBytes = jpegCacheBytes(width, height);
|
||||
const size_t freeHeap = ESP.getFreeHeap();
|
||||
const size_t maxAlloc = ESP.getMaxAllocHeap();
|
||||
const size_t minFreeForCaching = MIN_FREE_HEAP_FOR_JPEG + JPEG_CACHE_MIN_FREE_HEAP_MARGIN;
|
||||
const size_t minMaxAllocForCaching = cacheBytes + JPEG_CACHE_MIN_MAX_ALLOC_MARGIN;
|
||||
|
||||
if (freeHeap < minFreeForCaching) {
|
||||
LOG_DBG("JPG", "Skipping cache: free heap %u < %u (cache %u bytes)", static_cast<unsigned>(freeHeap),
|
||||
static_cast<unsigned>(minFreeForCaching), static_cast<unsigned>(cacheBytes));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (maxAlloc < minMaxAllocForCaching) {
|
||||
LOG_DBG("JPG", "Skipping cache: max alloc %u < %u (cache %u bytes)", static_cast<unsigned>(maxAlloc),
|
||||
static_cast<unsigned>(minMaxAllocForCaching), static_cast<unsigned>(cacheBytes));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool shouldForceBayerDither(const RenderConfig& config) {
|
||||
if (!config.useDithering) return false;
|
||||
if (config.ditherMode == ImageDitherMode::Bayer) return false;
|
||||
|
||||
const size_t freeHeap = ESP.getFreeHeap();
|
||||
const size_t maxAlloc = ESP.getMaxAllocHeap();
|
||||
return freeHeap < JPEG_DITHER_LOW_MEM_MIN_FREE_HEAP || maxAlloc < JPEG_DITHER_LOW_MEM_MIN_MAX_ALLOC;
|
||||
}
|
||||
|
||||
bool readJpegDimensionsFromHeader(const std::string& imagePath, ImageDimensions& out) {
|
||||
FsFile f;
|
||||
if (!Storage.openFileForRead("JPG", imagePath, f)) {
|
||||
@@ -526,6 +592,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
ctx.config = &config;
|
||||
ctx.screenWidth = renderer.getScreenWidth();
|
||||
ctx.screenHeight = renderer.getScreenHeight();
|
||||
ctx.effectiveDitherMode = config.ditherMode;
|
||||
|
||||
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, jpegDrawCallback);
|
||||
if (rc != 1) {
|
||||
@@ -605,7 +672,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
jpeg->setUserPointer(&ctx);
|
||||
|
||||
// Allocate cache buffer using final output dimensions
|
||||
ctx.caching = !config.cachePath.empty();
|
||||
ctx.caching = shouldEnableJpegCache(config, destWidth, destHeight);
|
||||
if (ctx.caching) {
|
||||
if (!ctx.cache.allocate(destWidth, destHeight, config.x, config.y)) {
|
||||
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
|
||||
@@ -613,6 +680,12 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldForceBayerDither(config)) {
|
||||
LOG_DBG("JPG", "Low-memory mode: forcing Bayer dithering (%u free, %u max alloc)",
|
||||
static_cast<unsigned>(ESP.getFreeHeap()), static_cast<unsigned>(ESP.getMaxAllocHeap()));
|
||||
ctx.effectiveDitherMode = ImageDitherMode::Bayer;
|
||||
}
|
||||
|
||||
// See PngToFramebufferConverter for rationale: BW-only display needs a 1-bit
|
||||
// dither so mid-grays don't collapse to black under DirectPixelWriter's `< 3` rule.
|
||||
if (config.monochromeOutput) {
|
||||
@@ -624,7 +697,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
|
||||
if (config.useDithering && !ctx.atkinson1BitDitherer) {
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
switch (config.ditherMode) {
|
||||
switch (ctx.effectiveDitherMode) {
|
||||
case ImageDitherMode::Atkinson:
|
||||
ctx.atkinsonDitherer.reset(new (std::nothrow) AtkinsonDitherer(destWidth));
|
||||
if (!ctx.atkinsonDitherer) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <expat.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -30,11 +31,42 @@ constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||
constexpr size_t MIN_SIZE_FOR_POPUP = 15 * 1024;
|
||||
constexpr size_t SIZE_FOR_PROGRESS_HEARTBEAT = 30 * 1024;
|
||||
constexpr size_t SIZE_FOR_PROGRESS_FINE = 80 * 1024;
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_INDEXING_POPUP = 32 * 1024;
|
||||
constexpr size_t MIN_CONTIG_HEAP_FOR_INDEXING_POPUP = 12 * 1024;
|
||||
|
||||
// Parser progress popup/ticks trigger full-screen refreshes that can temporarily
|
||||
// collapse heap during section cache builds on constrained targets. Keep disabled
|
||||
// by default; enable only when debugging parse progress behavior.
|
||||
#ifndef EHP_ENABLE_PARSE_PROGRESS_UI
|
||||
#define EHP_ENABLE_PARSE_PROGRESS_UI 0
|
||||
#endif
|
||||
|
||||
constexpr size_t PARSE_BUFFER_SIZE = 1024;
|
||||
constexpr size_t IMAGE_EXTRACT_CHUNK_SIZE = 1024;
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_IMAGE_EXTRACT = 48 * 1024;
|
||||
constexpr size_t MIN_MAX_ALLOC_FOR_IMAGE_EXTRACT = 36 * 1024;
|
||||
|
||||
#ifndef EHP_TEXT_LAYOUT_SOFT_MIN_FREE_HEAP
|
||||
#define EHP_TEXT_LAYOUT_SOFT_MIN_FREE_HEAP (18 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef EHP_TEXT_LAYOUT_SOFT_MIN_MAX_ALLOC
|
||||
#define EHP_TEXT_LAYOUT_SOFT_MIN_MAX_ALLOC (12 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef EHP_TEXT_LAYOUT_HARD_MIN_FREE_HEAP
|
||||
#define EHP_TEXT_LAYOUT_HARD_MIN_FREE_HEAP (9 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef EHP_TEXT_LAYOUT_HARD_MIN_MAX_ALLOC
|
||||
#define EHP_TEXT_LAYOUT_HARD_MIN_MAX_ALLOC (6 * 1024)
|
||||
#endif
|
||||
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_TEXT_LAYOUT = EHP_TEXT_LAYOUT_SOFT_MIN_FREE_HEAP;
|
||||
constexpr size_t MIN_MAX_ALLOC_FOR_TEXT_LAYOUT = EHP_TEXT_LAYOUT_SOFT_MIN_MAX_ALLOC;
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_TEXT_LAYOUT_HARD = EHP_TEXT_LAYOUT_HARD_MIN_FREE_HEAP;
|
||||
constexpr size_t MIN_MAX_ALLOC_FOR_TEXT_LAYOUT_HARD = EHP_TEXT_LAYOUT_HARD_MIN_MAX_ALLOC;
|
||||
|
||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote", "pre"};
|
||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||
|
||||
@@ -198,6 +230,34 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ChapterHtmlSlimParser::ensureHeapForTextLayout(const char* phase) {
|
||||
if (streamFailed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t freeHeap = ESP.getFreeHeap();
|
||||
const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
|
||||
if (freeHeap >= MIN_FREE_HEAP_FOR_TEXT_LAYOUT || maxAllocHeap >= MIN_MAX_ALLOC_FOR_TEXT_LAYOUT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Soft low-memory zone: keep parsing in degraded mode and only hard-abort when
|
||||
// both free and contiguous heap fall to critical levels.
|
||||
if (freeHeap >= MIN_FREE_HEAP_FOR_TEXT_LAYOUT_HARD && maxAllocHeap >= MIN_MAX_ALLOC_FOR_TEXT_LAYOUT_HARD) {
|
||||
lowMemoryImageFallback = true;
|
||||
LOG_DBG("EHP", "Low heap (%u free, %u max alloc) before %s; continuing in degraded mode", freeHeap, maxAllocHeap,
|
||||
phase);
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_ERR("EHP", "Low heap (%u free, %u max alloc), aborting parse before %s", freeHeap, maxAllocHeap, phase);
|
||||
streamFailed = true;
|
||||
if (activeParser) {
|
||||
XML_StopParser(activeParser, XML_FALSE);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// flush the contents of partWordBuffer to currentTextBlock
|
||||
void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
// Determine font style from depth-based tracking and CSS effective style
|
||||
@@ -229,6 +289,11 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||
|
||||
if (currentTextBlock->size() > 96) {
|
||||
if (!ensureHeapForTextLayout("long-block split")) {
|
||||
partWordBufferIndex = 0;
|
||||
nextWordContinues = false;
|
||||
return;
|
||||
}
|
||||
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
||||
const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset();
|
||||
const uint16_t effectiveWidth =
|
||||
@@ -1572,8 +1637,25 @@ bool ChapterHtmlSlimParser::setup(const size_t totalInflatedSize) {
|
||||
progressStepPercent = 50;
|
||||
}
|
||||
|
||||
const uint32_t popupFreeHeap = ESP.getFreeHeap();
|
||||
const uint32_t popupContigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||
#if EHP_ENABLE_PARSE_PROGRESS_UI
|
||||
progressUiEnabled =
|
||||
popupFreeHeap >= MIN_FREE_HEAP_FOR_INDEXING_POPUP && popupContigHeap >= MIN_CONTIG_HEAP_FOR_INDEXING_POPUP;
|
||||
if (!progressUiEnabled) {
|
||||
LOG_DBG("EHP", "Skipping indexing popup due to low heap (free=%u contig=%u)", popupFreeHeap, popupContigHeap);
|
||||
// When popup is disabled, also disable mid-parse ticks.
|
||||
progressStepPercent = 0;
|
||||
}
|
||||
#else
|
||||
progressUiEnabled = false;
|
||||
progressStepPercent = 0;
|
||||
LOG_DBG("EHP", "Skipping parser progress popup/ticks (EHP_ENABLE_PARSE_PROGRESS_UI=0, free=%u contig=%u)",
|
||||
popupFreeHeap, popupContigHeap);
|
||||
#endif
|
||||
|
||||
// Show initial progress popup for files above threshold.
|
||||
if (progressFn && totalStreamSize >= MIN_SIZE_FOR_POPUP) {
|
||||
if (progressFn && progressUiEnabled && totalStreamSize >= MIN_SIZE_FOR_POPUP) {
|
||||
progressFn(0);
|
||||
}
|
||||
return true;
|
||||
@@ -1614,7 +1696,7 @@ size_t ChapterHtmlSlimParser::write(const uint8_t* buffer, const size_t size) {
|
||||
// Report progress at the granularity chosen up-front (see progressStepPercent).
|
||||
// Skip the 100% callback — the page render that follows immediately replaces the popup,
|
||||
// so the final tick is wasted work.
|
||||
if (progressFn && progressStepPercent > 0 && totalStreamSize > 0) {
|
||||
if (progressFn && progressUiEnabled && progressStepPercent > 0 && totalStreamSize > 0) {
|
||||
const int progress = static_cast<int>(bytesStreamed * 100 / totalStreamSize);
|
||||
if (progress < 100 && progress / progressStepPercent > lastReportedProgress / progressStepPercent) {
|
||||
lastReportedProgress = progress;
|
||||
@@ -1736,6 +1818,10 @@ void ChapterHtmlSlimParser::makePages() {
|
||||
const uint16_t effectiveWidth =
|
||||
(horizontalInset < viewportWidth) ? static_cast<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
|
||||
|
||||
if (!ensureHeapForTextLayout("paragraph layout")) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentTextBlock->layoutAndExtractLines(
|
||||
renderer, fontId, effectiveWidth,
|
||||
[this](const std::shared_ptr<TextBlock>& textBlock, const bool lineEndsWithHyphenatedWord,
|
||||
|
||||
@@ -137,6 +137,7 @@ class ChapterHtmlSlimParser final : public Print {
|
||||
size_t bytesStreamed = 0;
|
||||
int lastReportedProgress = -1;
|
||||
int progressStepPercent = 0;
|
||||
bool progressUiEnabled = true;
|
||||
bool streamFailed = false;
|
||||
uint32_t streamStartTimeMs = 0;
|
||||
|
||||
@@ -155,6 +156,7 @@ class ChapterHtmlSlimParser final : public Print {
|
||||
std::unordered_map<std::string, CssStyle> inlineStyleCache_;
|
||||
|
||||
void updateEffectiveInlineStyle();
|
||||
bool ensureHeapForTextLayout(const char* phase);
|
||||
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||
void flushPartWordBuffer();
|
||||
void makePages();
|
||||
|
||||
Reference in New Issue
Block a user