From 97ee05a40c3ff48b14f392e197b0e5bb4e161890 Mon Sep 17 00:00:00 2001 From: Julia Nguyen Date: Wed, 15 Jul 2026 12:04:08 -0400 Subject: [PATCH] fix: flush long EPUB text blocks before large vector growth and reduce EPUB parser contiguous heap growth --- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 32 +++++++++++-------- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 6 ++-- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 76a6901d..b9aad8d2 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -21,6 +21,7 @@ // Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB constexpr size_t PARSE_BUFFER_SIZE = 1024; +constexpr size_t MAX_BUFFERED_TEXT_WORDS = 300; // Hard cap on the number of anchor IDs recorded per chapter. Legitimate navigation // anchors (TOC entries, footnotes, cross-references) rarely exceed a few hundred per @@ -201,6 +202,8 @@ void ChapterHtmlSlimParser::flushPendingAnchor() { // flush the contents of partWordBuffer to currentTextBlock void ChapterHtmlSlimParser::flushPartWordBuffer() { + flushLongTextBlockIfNeeded(); + // Determine font style from depth-based tracking and CSS effective style const bool isBold = boldUntilDepth < depth || effectiveBold; const bool isItalic = italicUntilDepth < depth || effectiveItalic; @@ -228,6 +231,20 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() { listItemBulletOnly = false; } +void ChapterHtmlSlimParser::flushLongTextBlockIfNeeded() { + if (!currentTextBlock || currentTextBlock->size() <= MAX_BUFFERED_TEXT_WORDS) { + return; + } + + LOG_DBG("EHP", "Text block too long, splitting into multiple pages"); + const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset(); + const uint16_t effectiveWidth = + (horizontalInset < viewportWidth) ? static_cast(viewportWidth - horizontalInset) : viewportWidth; + currentTextBlock->layoutAndExtractLines( + renderer, fontId, effectiveWidth, + [this](const std::shared_ptr& textBlock) { this->addLineToPage(textBlock); }, false); +} + // start a new text block if needed void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) { nextWordContinues = false; // New block = new paragraph, no continuation @@ -1155,20 +1172,9 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char self->partWordBuffer[self->partWordBufferIndex++] = s[i]; } - // If we have > 750 words buffered up, perform the layout and consume out all but the last line - // There should be enough here to build out 1-2 full pages and doing this will free up a lot of - // memory. + // If we have a large number of words buffered up, perform the layout and consume out all but the last line. // Spotted when reading Intermezzo, there are some really long text blocks in there. - if (self->currentTextBlock->size() > 750) { - LOG_DBG("EHP", "Text block too long, splitting into multiple pages"); - const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset(); - const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth) - ? static_cast(self->viewportWidth - horizontalInset) - : self->viewportWidth; - self->currentTextBlock->layoutAndExtractLines( - self->renderer, self->fontId, effectiveWidth, - [self](const std::shared_ptr& textBlock) { self->addLineToPage(textBlock); }, false); - } + self->flushLongTextBlockIfNeeded(); } void XMLCALL ChapterHtmlSlimParser::defaultHandlerExpand(void* userData, const XML_Char* s, const int len) { diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 9cc01e69..e9496a12 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -84,7 +85,7 @@ class ChapterHtmlSlimParser { // Anchor-to-page mapping: tracks which page each HTML id attribute lands on int completedPageCount = 0; - std::vector> anchorData; + std::deque> anchorData; std::string pendingAnchorId; // deferred until after previous text block is flushed std::vector tocAnchors; // the list of anchors that are TOC chapter boundaries uint16_t xpathParagraphIndex = 0; @@ -112,6 +113,7 @@ class ChapterHtmlSlimParser { void startNewTextBlock(const BlockStyle& blockStyle); void flushPendingAnchor(); void flushPartWordBuffer(); + void flushLongTextBlockIfNeeded(); void makePages(); static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration); static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css); @@ -173,7 +175,7 @@ class ChapterHtmlSlimParser { void abortParse(); // tear down without flushing (error / abandon) void addLineToPage(std::shared_ptr line); - const std::vector>& getAnchors() const { return anchorData; } + const std::deque>& getAnchors() const { return anchorData; } // Byte progress of the in-flight parse, used to estimate a still-building section's total page // count (a giant single-spine book never fully lays out, so its real count is unknown). Valid