checkpoint: refactor TextBlock/ParsedText from std::list to std::vector

Reduces heap fragmentation by ~12x fewer allocations per TextBlock.
This fixes crashes when repeatedly navigating dictionary pages.

- Replace std::list with std::vector in TextBlock members
- Replace splice() with move+erase in ParsedText::extractLine()
- Use index-based access in hyphenateWordAtIndex()
This commit is contained in:
cottongin 2026-01-29 09:52:30 -05:00
parent 62643ae933
commit 6ceba56620
No known key found for this signature in database
GPG Key ID: 0ECC91FE4655C262
4 changed files with 50 additions and 56 deletions

View File

@ -281,14 +281,9 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
return false; return false;
} }
// Get iterators to target word and style. // Direct index access for vectors (more efficient than iterator + advance)
auto wordIt = words.begin(); const std::string& word = words[wordIndex];
auto styleIt = wordStyles.begin(); const auto wordStyle = wordStyles[wordIndex];
std::advance(wordIt, wordIndex);
std::advance(styleIt, wordIndex);
const std::string& word = *wordIt;
const auto style = *styleIt;
// Collect candidate breakpoints (byte offsets and hyphen requirements). // Collect candidate breakpoints (byte offsets and hyphen requirements).
auto breakInfos = Hyphenator::breakOffsets(word, allowFallbackBreaks); auto breakInfos = Hyphenator::breakOffsets(word, allowFallbackBreaks);
@ -308,7 +303,7 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
} }
const bool needsHyphen = info.requiresInsertedHyphen; const bool needsHyphen = info.requiresInsertedHyphen;
const int prefixWidth = measureWordWidth(renderer, fontId, word.substr(0, offset), style, needsHyphen); const int prefixWidth = measureWordWidth(renderer, fontId, word.substr(0, offset), wordStyle, needsHyphen);
if (prefixWidth > availableWidth || prefixWidth <= chosenWidth) { if (prefixWidth > availableWidth || prefixWidth <= chosenWidth) {
continue; // Skip if too wide or not an improvement continue; // Skip if too wide or not an improvement
} }
@ -325,20 +320,18 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
// Split the word at the selected breakpoint and append a hyphen if required. // Split the word at the selected breakpoint and append a hyphen if required.
std::string remainder = word.substr(chosenOffset); std::string remainder = word.substr(chosenOffset);
wordIt->resize(chosenOffset); words[wordIndex].resize(chosenOffset);
if (chosenNeedsHyphen) { if (chosenNeedsHyphen) {
wordIt->push_back('-'); words[wordIndex].push_back('-');
} }
// Insert the remainder word (with matching style) directly after the prefix. // Insert the remainder word (with matching style) directly after the prefix.
auto insertWordIt = std::next(wordIt); words.insert(words.begin() + wordIndex + 1, remainder);
auto insertStyleIt = std::next(styleIt); wordStyles.insert(wordStyles.begin() + wordIndex + 1, wordStyle);
words.insert(insertWordIt, remainder);
wordStyles.insert(insertStyleIt, style);
// Update cached widths to reflect the new prefix/remainder pairing. // Update cached widths to reflect the new prefix/remainder pairing.
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth); wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
const uint16_t remainderWidth = measureWordWidth(renderer, fontId, remainder, style); const uint16_t remainderWidth = measureWordWidth(renderer, fontId, remainder, wordStyle);
wordWidths.insert(wordWidths.begin() + wordIndex + 1, remainderWidth); wordWidths.insert(wordWidths.begin() + wordIndex + 1, remainderWidth);
return true; return true;
} }
@ -375,28 +368,30 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
} }
// Pre-calculate X positions for words // Pre-calculate X positions for words
std::list<uint16_t> lineXPos; std::vector<uint16_t> lineXPos;
lineXPos.reserve(lineWordCount);
for (size_t i = lastBreakAt; i < lineBreak; i++) { for (size_t i = lastBreakAt; i < lineBreak; i++) {
const uint16_t currentWordWidth = wordWidths[i]; const uint16_t currentWordWidth = wordWidths[i];
lineXPos.push_back(xpos); lineXPos.push_back(xpos);
xpos += currentWordWidth + spacing; xpos += currentWordWidth + spacing;
} }
// Iterators always start at the beginning as we are moving content with splice below // *** CRITICAL STEP: CONSUME DATA USING MOVE + ERASE ***
auto wordEndIt = words.begin(); // Move first lineWordCount elements from words into lineWords
auto wordStyleEndIt = wordStyles.begin(); std::vector<std::string> lineWords(
auto wordUnderlineEndIt = wordUnderlines.begin(); std::make_move_iterator(words.begin()),
std::advance(wordEndIt, lineWordCount); std::make_move_iterator(words.begin() + lineWordCount));
std::advance(wordStyleEndIt, lineWordCount); words.erase(words.begin(), words.begin() + lineWordCount);
std::advance(wordUnderlineEndIt, lineWordCount);
// *** CRITICAL STEP: CONSUME DATA USING SPLICE *** std::vector<EpdFontFamily::Style> lineWordStyles(
std::list<std::string> lineWords; std::make_move_iterator(wordStyles.begin()),
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt); std::make_move_iterator(wordStyles.begin() + lineWordCount));
std::list<EpdFontFamily::Style> lineWordStyles; wordStyles.erase(wordStyles.begin(), wordStyles.begin() + lineWordCount);
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
std::list<bool> lineWordUnderlines; std::vector<bool> lineWordUnderlines(
lineWordUnderlines.splice(lineWordUnderlines.begin(), wordUnderlines, wordUnderlines.begin(), wordUnderlineEndIt); wordUnderlines.begin(),
wordUnderlines.begin() + lineWordCount);
wordUnderlines.erase(wordUnderlines.begin(), wordUnderlines.begin() + lineWordCount);
for (auto& word : lineWords) { for (auto& word : lineWords) {
if (containsSoftHyphen(word)) { if (containsSoftHyphen(word)) {

View File

@ -3,7 +3,6 @@
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <functional> #include <functional>
#include <list>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
@ -14,9 +13,9 @@
class GfxRenderer; class GfxRenderer;
class ParsedText { class ParsedText {
std::list<std::string> words; std::vector<std::string> words;
std::list<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
std::list<bool> wordUnderlines; // Track underline per word std::vector<bool> wordUnderlines; // Track underline per word
TextBlock::Style style; TextBlock::Style style;
BlockStyle blockStyle; BlockStyle blockStyle;
bool extraParagraphSpacing; bool extraParagraphSpacing;

View File

@ -98,23 +98,23 @@ bool TextBlock::serialize(FsFile& file) const {
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) { std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
uint16_t wc; uint16_t wc;
std::list<std::string> words; std::vector<std::string> words;
std::list<uint16_t> wordXpos; std::vector<uint16_t> wordXpos;
std::list<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
std::list<bool> wordUnderlines; std::vector<bool> wordUnderlines;
Style style; Style style;
BlockStyle blockStyle; BlockStyle blockStyle;
// Word count // Word count
serialization::readPod(file, wc); serialization::readPod(file, wc);
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block) // Sanity check: prevent allocation of unreasonably large vectors (max 10000 words per block)
if (wc > 10000) { if (wc > 10000) {
Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc); Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc);
return nullptr; return nullptr;
} }
// Word data // Word data - reserve capacity then resize
words.resize(wc); words.resize(wc);
wordXpos.resize(wc); wordXpos.resize(wc);
wordStyles.resize(wc); wordStyles.resize(wc);
@ -124,14 +124,14 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
// Underline flags (packed as bytes, 8 words per byte) // Underline flags (packed as bytes, 8 words per byte)
wordUnderlines.resize(wc, false); wordUnderlines.resize(wc, false);
auto underlineIt = wordUnderlines.begin(); size_t underlineIdx = 0;
const int bytesNeeded = (wc + 7) / 8; const int bytesNeeded = (wc + 7) / 8;
for (int byteIdx = 0; byteIdx < bytesNeeded; byteIdx++) { for (int byteIdx = 0; byteIdx < bytesNeeded; byteIdx++) {
uint8_t underlineByte; uint8_t underlineByte;
serialization::readPod(file, underlineByte); serialization::readPod(file, underlineByte);
for (int bit = 0; bit < 8 && underlineIt != wordUnderlines.end(); bit++) { for (int bit = 0; bit < 8 && underlineIdx < wc; bit++) {
*underlineIt = (underlineByte & 1 << bit) != 0; wordUnderlines[underlineIdx] = (underlineByte & (1 << bit)) != 0;
++underlineIt; ++underlineIdx;
} }
} }

View File

@ -2,7 +2,7 @@
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <SdFat.h> #include <SdFat.h>
#include <list> #include <vector>
#include <memory> #include <memory>
#include <string> #include <string>
@ -20,17 +20,17 @@ class TextBlock final : public Block {
}; };
private: private:
std::list<std::string> words; std::vector<std::string> words;
std::list<uint16_t> wordXpos; std::vector<uint16_t> wordXpos;
std::list<EpdFontFamily::Style> wordStyles; std::vector<EpdFontFamily::Style> wordStyles;
std::list<bool> wordUnderlines; // Track underline per word std::vector<bool> wordUnderlines; // Track underline per word
Style style; Style style;
BlockStyle blockStyle; BlockStyle blockStyle;
public: public:
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos, explicit TextBlock(std::vector<std::string> words, std::vector<uint16_t> word_xpos,
std::list<EpdFontFamily::Style> word_styles, const Style style, std::vector<EpdFontFamily::Style> word_styles, const Style style,
const BlockStyle& blockStyle = BlockStyle(), std::list<bool> word_underlines = std::list<bool>()) const BlockStyle& blockStyle = BlockStyle(), std::vector<bool> word_underlines = std::vector<bool>())
: words(std::move(words)), : words(std::move(words)),
wordXpos(std::move(word_xpos)), wordXpos(std::move(word_xpos)),
wordStyles(std::move(word_styles)), wordStyles(std::move(word_styles)),
@ -50,9 +50,9 @@ class TextBlock final : public Block {
bool isEmpty() override { return words.empty(); } bool isEmpty() override { return words.empty(); }
// Getters for word selection support // Getters for word selection support
const std::list<std::string>& getWords() const { return words; } const std::vector<std::string>& getWords() const { return words; }
const std::list<uint16_t>& getWordXPositions() const { return wordXpos; } const std::vector<uint16_t>& getWordXPositions() const { return wordXpos; }
const std::list<EpdFontFamily::Style>& getWordStyles() const { return wordStyles; } const std::vector<EpdFontFamily::Style>& getWordStyles() const { return wordStyles; }
size_t getWordCount() const { return words.size(); } size_t getWordCount() const { return words.size(); }
void layout(GfxRenderer& renderer) override {}; void layout(GfxRenderer& renderer) override {};
// given a renderer works out where to break the words into lines // given a renderer works out where to break the words into lines