fix: reduce CSS parse-time OOM risk in chapter layout (#2606)
This commit is contained in:
@@ -287,7 +287,30 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
|||||||
effectiveNoSpaceBefore = true;
|
effectiveNoSpaceBefore = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto ensureTokenCapacity = [&](const size_t additionalTokens) {
|
||||||
|
if (additionalTokens == 0) return;
|
||||||
|
const size_t requiredSize = words.size() + additionalTokens;
|
||||||
|
if (words.capacity() >= requiredSize) return;
|
||||||
|
|
||||||
|
size_t newCapacity = words.capacity();
|
||||||
|
if (newCapacity < 16) {
|
||||||
|
newCapacity = 16;
|
||||||
|
}
|
||||||
|
while (newCapacity < requiredSize) {
|
||||||
|
newCapacity *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
words.reserve(newCapacity);
|
||||||
|
wordStyles.reserve(newCapacity);
|
||||||
|
wordContinues.reserve(newCapacity);
|
||||||
|
wordNoSpaceBefore.reserve(newCapacity);
|
||||||
|
wordIsFocusSuffix.reserve(newCapacity);
|
||||||
|
};
|
||||||
|
|
||||||
if (auto breakOffsets = cjkCharacterBreakByteOffsets(word); !breakOffsets.empty()) {
|
if (auto breakOffsets = cjkCharacterBreakByteOffsets(word); !breakOffsets.empty()) {
|
||||||
|
// CJK-heavy paragraphs can push hundreds of tiny tokens quickly when CSS toggles
|
||||||
|
// inline styles. Reserve once up front to avoid repeated vector growth reallocations.
|
||||||
|
ensureTokenCapacity(breakOffsets.size() + 1);
|
||||||
bool firstToken = true;
|
bool firstToken = true;
|
||||||
size_t tokenStart = 0;
|
size_t tokenStart = 0;
|
||||||
for (const size_t breakOffset : breakOffsets) {
|
for (const size_t breakOffset : breakOffsets) {
|
||||||
@@ -326,29 +349,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
|||||||
|
|
||||||
// --- FOCUS READING LOGIC BELOW ---
|
// --- FOCUS READING LOGIC BELOW ---
|
||||||
|
|
||||||
// Pre-reserve capacity to prevent mid-word heap reallocations.
|
// Worst case: a segment boundary on each byte (highly punctuated UTF-8 text).
|
||||||
size_t maxPossibleNewTokens = word.length();
|
ensureTokenCapacity(word.length());
|
||||||
size_t requiredSize = words.size() + maxPossibleNewTokens;
|
|
||||||
|
|
||||||
if (words.capacity() < requiredSize) {
|
|
||||||
// Emulate standard geometric growth (doubling) to ensure we don't reallocate on every word.
|
|
||||||
size_t newCapacity = words.capacity() * 2;
|
|
||||||
|
|
||||||
// Ensure the doubled capacity is actually enough for this specific word
|
|
||||||
if (newCapacity < requiredSize) {
|
|
||||||
newCapacity = requiredSize;
|
|
||||||
}
|
|
||||||
// Set a sensible minimum starting size so the first few words don't trigger tiny reallocations
|
|
||||||
if (newCapacity < 16) {
|
|
||||||
newCapacity = 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
words.reserve(newCapacity);
|
|
||||||
wordStyles.reserve(newCapacity);
|
|
||||||
wordContinues.reserve(newCapacity);
|
|
||||||
wordNoSpaceBefore.reserve(newCapacity);
|
|
||||||
wordIsFocusSuffix.reserve(newCapacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lambda helper to process and push individual sub-segments of the string
|
// Lambda helper to process and push individual sub-segments of the string
|
||||||
// Use std::string_view to avoid heap allocations when slicing
|
// Use std::string_view to avoid heap allocations when slicing
|
||||||
|
|||||||
@@ -22,6 +22,17 @@
|
|||||||
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
|
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
|
||||||
constexpr size_t PARSE_BUFFER_SIZE = 1024;
|
constexpr size_t PARSE_BUFFER_SIZE = 1024;
|
||||||
|
|
||||||
|
// This number comes from PR #73
|
||||||
|
// 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.
|
||||||
|
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
||||||
|
constexpr size_t TEXT_BLOCK_SOFT_FLUSH_WORDS = 750;
|
||||||
|
|
||||||
|
// When CSS is enabled, flush earlier to save RAM. 320 is still more than enough to build a CJK
|
||||||
|
// page at font size 14
|
||||||
|
constexpr size_t TEXT_BLOCK_SOFT_FLUSH_WORDS_WITH_CSS = 320;
|
||||||
|
|
||||||
// Hard cap on the number of anchor IDs recorded per chapter. Legitimate navigation
|
// 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
|
// anchors (TOC entries, footnotes, cross-references) rarely exceed a few hundred per
|
||||||
// chapter. A runaway count usually means a converter injected machine-generated IDs on
|
// chapter. A runaway count usually means a converter injected machine-generated IDs on
|
||||||
@@ -1155,12 +1166,14 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
|
// Keep token growth bounded: CSS-heavy spans can fragment text into many tiny
|
||||||
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
|
// words, so flush earlier when embedded CSS is active. We still keep the
|
||||||
// memory.
|
// "exclude last line" behavior to preserve paragraph flow across chunks.
|
||||||
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
const size_t blockWordCount = self->currentTextBlock->size();
|
||||||
if (self->currentTextBlock->size() > 750) {
|
const size_t softFlushThreshold =
|
||||||
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
self->embeddedStyle ? TEXT_BLOCK_SOFT_FLUSH_WORDS_WITH_CSS : TEXT_BLOCK_SOFT_FLUSH_WORDS;
|
||||||
|
if (blockWordCount > softFlushThreshold) {
|
||||||
|
LOG_DBG("EHP", "Text block soft flush (%u words)", static_cast<unsigned>(blockWordCount));
|
||||||
const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
|
const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
|
||||||
const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth)
|
const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth)
|
||||||
? static_cast<uint16_t>(self->viewportWidth - horizontalInset)
|
? static_cast<uint16_t>(self->viewportWidth - horizontalInset)
|
||||||
|
|||||||
Reference in New Issue
Block a user