Introducing scratchpad to reuse already allocated memory

This commit is contained in:
jpirnay
2026-05-04 17:08:51 +02:00
parent 42a9c09e4c
commit 5486eee4e3
6 changed files with 111 additions and 76 deletions
+66 -42
View File
@@ -3,13 +3,15 @@
#include <Logging.h> #include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
#include <ZipFile.h> #include <ZipFile.h>
#include <esp_heap_caps.h>
#include <esp_system.h>
#include <deque> #include <vector>
#include "FsHelpers.h" #include "FsHelpers.h"
namespace { namespace {
constexpr uint8_t BOOK_CACHE_VERSION = 7; constexpr uint8_t BOOK_CACHE_VERSION = 8;
constexpr char bookBinFile[] = "/book.bin"; constexpr char bookBinFile[] = "/book.bin";
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
@@ -52,11 +54,12 @@ bool BookMetadataCache::beginTocPass() {
spineHrefIndex.clear(); spineHrefIndex.clear();
spineHrefIndex.resize(spineCount); spineHrefIndex.resize(spineCount);
spineFile.seek(0); spineFile.seek(0);
SpineEntry scratch;
for (int i = 0; i < spineCount; i++) { for (int i = 0; i < spineCount; i++) {
auto entry = readSpineEntry(spineFile); readSpineEntry(spineFile, scratch);
SpineHrefIndexEntry idx; SpineHrefIndexEntry idx;
idx.hrefHash = fnvHash64(entry.href); idx.hrefHash = fnvHash64(scratch.href);
idx.hrefLen = static_cast<uint16_t>(entry.href.size()); idx.hrefLen = static_cast<uint16_t>(scratch.href.size());
idx.spineIndex = static_cast<int16_t>(i); idx.spineIndex = static_cast<int16_t>(i);
spineHrefIndex[i] = idx; spineHrefIndex[i] = idx;
} }
@@ -97,6 +100,8 @@ bool BookMetadataCache::endWrite() {
} }
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) { bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
LOG_DBG("BMC", "buildBookBin start: free=%lu contig=%lu", static_cast<unsigned long>(esp_get_free_heap_size()),
static_cast<unsigned long>(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT)));
// Open all three files, writing to meta, reading from spine and toc // Open all three files, writing to meta, reading from spine and toc
if (!Storage.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) { if (!Storage.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
return false; return false;
@@ -140,11 +145,18 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
serialization::writeString(bookFile, metadata.seriesIndex); serialization::writeString(bookFile, metadata.seriesIndex);
serialization::writeString(bookFile, metadata.description); serialization::writeString(bookFile, metadata.description);
// Scratch entries reused across all read loops below. Their internal
// std::strings keep the capacity of the longest href/title encountered, so
// each subsequent readSpineEntry / readTocEntry call reuses that allocation
// instead of churning hundreds of small heap blocks during the build.
SpineEntry spineScratch;
TocEntry tocScratch;
// Loop through spine entries, writing LUT positions // Loop through spine entries, writing LUT positions
spineFile.seek(0); spineFile.seek(0);
for (int i = 0; i < spineCount; i++) { for (int i = 0; i < spineCount; i++) {
uint32_t pos = spineFile.position(); uint32_t pos = spineFile.position();
auto spineEntry = readSpineEntry(spineFile); readSpineEntry(spineFile, spineScratch);
serialization::writePod(bookFile, pos + lutOffset + lutSize); serialization::writePod(bookFile, pos + lutOffset + lutSize);
} }
@@ -152,7 +164,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
tocFile.seek(0); tocFile.seek(0);
for (int i = 0; i < tocCount; i++) { for (int i = 0; i < tocCount; i++) {
uint32_t pos = tocFile.position(); uint32_t pos = tocFile.position();
auto tocEntry = readTocEntry(tocFile); readTocEntry(tocFile, tocScratch);
serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast<uint32_t>(spineFile.position())); serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast<uint32_t>(spineFile.position()));
} }
@@ -163,14 +175,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
// Also count distinct spines referenced by the TOC so tocReliable can be persisted in the // Also count distinct spines referenced by the TOC so tocReliable can be persisted in the
// header below — without this, every first-page load on a large book pays an O(tocCount) // header below — without this, every first-page load on a large book pays an O(tocCount)
// seek-heavy scan in Epub::hasReliableToc(). // seek-heavy scan in Epub::hasReliableToc().
std::deque<int16_t> spineToTocIndex(spineCount, -1); std::vector<int16_t> spineToTocIndex(spineCount, -1);
int distinctSpinesReferenced = 0; int distinctSpinesReferenced = 0;
tocFile.seek(0); tocFile.seek(0);
for (int j = 0; j < tocCount; j++) { for (int j = 0; j < tocCount; j++) {
auto tocEntry = readTocEntry(tocFile); readTocEntry(tocFile, tocScratch);
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) { if (tocScratch.spineIndex >= 0 && tocScratch.spineIndex < spineCount) {
if (spineToTocIndex[tocEntry.spineIndex] == -1) { if (spineToTocIndex[tocScratch.spineIndex] == -1) {
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j); spineToTocIndex[tocScratch.spineIndex] = static_cast<int16_t>(j);
distinctSpinesReferenced++; distinctSpinesReferenced++;
} }
} }
@@ -201,23 +213,24 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
// This is O(n*log(m)) instead of O(n*m) while avoiding memory exhaustion. // This is O(n*log(m)) instead of O(n*m) while avoiding memory exhaustion.
// See: https://github.com/crosspoint-reader/crosspoint-reader/issues/134 // See: https://github.com/crosspoint-reader/crosspoint-reader/issues/134
std::deque<uint32_t> spineSizes; std::vector<uint32_t> spineSizes;
bool useBatchSizes = false; bool useBatchSizes = false;
if (spineCount >= LARGE_SPINE_THRESHOLD) { if (spineCount >= LARGE_SPINE_THRESHOLD) {
LOG_DBG("BMC", "Using batch size lookup for %d spine items", spineCount); LOG_DBG("BMC", "Using batch size lookup for %d spine items", spineCount);
std::deque<ZipFile::SizeTarget> targets; std::vector<ZipFile::SizeTarget> targets;
targets.resize(spineCount); targets.resize(spineCount);
std::string pathScratch;
spineFile.seek(0); spineFile.seek(0);
for (int i = 0; i < spineCount; i++) { for (int i = 0; i < spineCount; i++) {
auto entry = readSpineEntry(spineFile); readSpineEntry(spineFile, spineScratch);
std::string path = FsHelpers::normalisePath(entry.href); FsHelpers::normalisePath(spineScratch.href, pathScratch);
ZipFile::SizeTarget t; ZipFile::SizeTarget t;
t.hash = ZipFile::fnvHash64(path.c_str(), path.size()); t.hash = ZipFile::fnvHash64(pathScratch.c_str(), pathScratch.size());
t.len = static_cast<uint16_t>(path.size()); t.len = static_cast<uint16_t>(pathScratch.size());
t.index = static_cast<uint16_t>(i); t.index = static_cast<uint16_t>(i);
targets[i] = t; targets[i] = t;
} }
@@ -239,41 +252,42 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
uint32_t cumSize = 0; uint32_t cumSize = 0;
spineFile.seek(0); spineFile.seek(0);
int lastSpineTocIndex = -1; int lastSpineTocIndex = -1;
std::string pathScratch;
for (int i = 0; i < spineCount; i++) { for (int i = 0; i < spineCount; i++) {
auto spineEntry = readSpineEntry(spineFile); readSpineEntry(spineFile, spineScratch);
spineEntry.tocIndex = spineToTocIndex[i]; spineScratch.tocIndex = spineToTocIndex[i];
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs // Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
// Logging here is for debugging // Logging here is for debugging
if (spineEntry.tocIndex == -1) { if (spineScratch.tocIndex == -1) {
LOG_DBG("BMC", "Warning: Could not find TOC entry for spine item %d: %s, using title from last section", i, LOG_DBG("BMC", "Warning: Could not find TOC entry for spine item %d: %s, using title from last section", i,
spineEntry.href.c_str()); spineScratch.href.c_str());
spineEntry.tocIndex = lastSpineTocIndex; spineScratch.tocIndex = lastSpineTocIndex;
} }
lastSpineTocIndex = spineEntry.tocIndex; lastSpineTocIndex = spineScratch.tocIndex;
size_t itemSize = 0; size_t itemSize = 0;
if (useBatchSizes) { if (useBatchSizes) {
itemSize = spineSizes[i]; itemSize = spineSizes[i];
if (itemSize == 0) { if (itemSize == 0) {
const std::string path = FsHelpers::normalisePath(spineEntry.href); FsHelpers::normalisePath(spineScratch.href, pathScratch);
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) { if (!zip.getInflatedFileSize(pathScratch.c_str(), &itemSize)) {
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str()); LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", pathScratch.c_str());
} }
} }
} else { } else {
const std::string path = FsHelpers::normalisePath(spineEntry.href); FsHelpers::normalisePath(spineScratch.href, pathScratch);
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) { if (!zip.getInflatedFileSize(pathScratch.c_str(), &itemSize)) {
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str()); LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", pathScratch.c_str());
} }
} }
cumSize += itemSize; cumSize += itemSize;
spineEntry.cumulativeSize = cumSize; spineScratch.cumulativeSize = cumSize;
// Write out spine data to book.bin // Write out spine data to book.bin
writeSpineEntry(bookFile, spineEntry); writeSpineEntry(bookFile, spineScratch);
} }
// Close opened zip file // Close opened zip file
zip.close(); zip.close();
@@ -281,8 +295,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
// Loop through toc entries from toc file writing to book.bin // Loop through toc entries from toc file writing to book.bin
tocFile.seek(0); tocFile.seek(0);
for (int i = 0; i < tocCount; i++) { for (int i = 0; i < tocCount; i++) {
auto tocEntry = readTocEntry(tocFile); readTocEntry(tocFile, tocScratch);
writeTocEntry(bookFile, tocEntry); writeTocEntry(bookFile, tocScratch);
} }
// Patch tocReliable placeholder in header A // Patch tocReliable placeholder in header A
@@ -294,6 +308,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
tocFile.close(); tocFile.close();
LOG_DBG("BMC", "Successfully built book.bin"); LOG_DBG("BMC", "Successfully built book.bin");
LOG_DBG("BMC", "buildBookBin end: free=%lu contig=%lu", static_cast<unsigned long>(esp_get_free_heap_size()),
static_cast<unsigned long>(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT)));
return true; return true;
} }
@@ -460,20 +476,28 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
return readTocEntry(bookFile); return readTocEntry(bookFile);
} }
void BookMetadataCache::readSpineEntry(FsFile& file, SpineEntry& out) const {
serialization::readString(file, out.href);
serialization::readPod(file, out.cumulativeSize);
serialization::readPod(file, out.tocIndex);
}
void BookMetadataCache::readTocEntry(FsFile& file, TocEntry& out) const {
serialization::readString(file, out.title);
serialization::readString(file, out.href);
serialization::readString(file, out.anchor);
serialization::readPod(file, out.level);
serialization::readPod(file, out.spineIndex);
}
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(FsFile& file) const { BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(FsFile& file) const {
SpineEntry entry; SpineEntry entry;
serialization::readString(file, entry.href); readSpineEntry(file, entry);
serialization::readPod(file, entry.cumulativeSize);
serialization::readPod(file, entry.tocIndex);
return entry; return entry;
} }
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(FsFile& file) const { BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(FsFile& file) const {
TocEntry entry; TocEntry entry;
serialization::readString(file, entry.title); readTocEntry(file, entry);
serialization::readString(file, entry.href);
serialization::readString(file, entry.anchor);
serialization::readPod(file, entry.level);
serialization::readPod(file, entry.spineIndex);
return entry; return entry;
} }
+7 -2
View File
@@ -3,8 +3,8 @@
#include <HalStorage.h> #include <HalStorage.h>
#include <algorithm> #include <algorithm>
#include <deque>
#include <string> #include <string>
#include <vector>
class BookMetadataCache { class BookMetadataCache {
public: public:
@@ -65,7 +65,7 @@ class BookMetadataCache {
uint16_t hrefLen; // length for collision reduction uint16_t hrefLen; // length for collision reduction
int16_t spineIndex; int16_t spineIndex;
}; };
std::deque<SpineHrefIndexEntry> spineHrefIndex; std::vector<SpineHrefIndexEntry> spineHrefIndex;
bool useSpineHrefIndex = false; bool useSpineHrefIndex = false;
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400; static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
@@ -84,6 +84,11 @@ class BookMetadataCache {
uint32_t writeTocEntry(FsFile& file, const TocEntry& entry) const; uint32_t writeTocEntry(FsFile& file, const TocEntry& entry) const;
SpineEntry readSpineEntry(FsFile& file) const; SpineEntry readSpineEntry(FsFile& file) const;
TocEntry readTocEntry(FsFile& file) const; TocEntry readTocEntry(FsFile& file) const;
// Out-parameter overloads reuse the caller's string capacity inside hot
// build loops, eliminating per-iteration std::string allocation churn that
// would otherwise fragment the heap during book.bin construction.
void readSpineEntry(FsFile& file, SpineEntry& out) const;
void readTocEntry(FsFile& file, TocEntry& out) const;
public: public:
BookMetadata coreMetadata; BookMetadata coreMetadata;
+31 -29
View File
@@ -2,43 +2,45 @@
#include <cctype> #include <cctype>
#include <cstring> #include <cstring>
#include <vector>
namespace FsHelpers { namespace FsHelpers {
std::string normalisePath(const std::string& path) { // Process a finalised component in-place, appending it to `out` (preceded by
std::vector<std::string> components; // '/' if `out` is non-empty) or popping the last component for "..". Used by
std::string component; // both normalisePath overloads so the parsing rules stay in one place.
static void appendOrPopComponent(std::string& out, const char* compData, size_t compLen) {
for (const auto c : path) { if (compLen == 0) return;
if (c == '/') { if (compLen == 2 && compData[0] == '.' && compData[1] == '.') {
if (!component.empty()) { if (out.empty()) return;
if (component == "..") { const auto lastSlash = out.find_last_of('/');
if (!components.empty()) { if (lastSlash == std::string::npos) {
components.pop_back(); out.clear();
}
} else {
components.push_back(component);
}
component.clear();
}
} else { } else {
component += c; out.resize(lastSlash);
}
return;
}
if (!out.empty()) {
out.push_back('/');
}
out.append(compData, compLen);
}
void normalisePath(const std::string& path, std::string& out) {
out.clear();
size_t componentStart = 0;
for (size_t i = 0; i < path.size(); i++) {
if (path[i] == '/') {
appendOrPopComponent(out, path.data() + componentStart, i - componentStart);
componentStart = i + 1;
} }
} }
appendOrPopComponent(out, path.data() + componentStart, path.size() - componentStart);
}
if (!component.empty()) { std::string normalisePath(const std::string& path) {
components.push_back(component);
}
std::string result; std::string result;
for (const auto& c : components) { normalisePath(path, result);
if (!result.empty()) {
result += "/";
}
result += c;
}
return result; return result;
} }
+4
View File
@@ -7,6 +7,10 @@
namespace FsHelpers { namespace FsHelpers {
std::string normalisePath(const std::string& path); std::string normalisePath(const std::string& path);
// Out-parameter overload that reuses `out`'s capacity and performs the
// normalisation in-place without allocating a temporary components vector.
// Use inside hot loops to keep heap fragmentation bounded.
void normalisePath(const std::string& path, std::string& out);
/** /**
* Check if the given filename ends with the specified extension (case-insensitive). * Check if the given filename ends with the specified extension (case-insensitive).
+1 -1
View File
@@ -295,7 +295,7 @@ bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) {
return true; return true;
} }
int ZipFile::fillUncompressedSizes(std::deque<SizeTarget>& targets, std::deque<uint32_t>& sizes) { int ZipFile::fillUncompressedSizes(std::vector<SizeTarget>& targets, std::vector<uint32_t>& sizes) {
if (targets.empty()) { if (targets.empty()) {
return 0; return 0;
} }
+2 -2
View File
@@ -1,9 +1,9 @@
#pragma once #pragma once
#include <HalStorage.h> #include <HalStorage.h>
#include <deque>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector>
class ZipFile { class ZipFile {
public: public:
@@ -64,7 +64,7 @@ class ZipFile {
// Batch lookup: scan ZIP central dir once and fill sizes for matching targets. // Batch lookup: scan ZIP central dir once and fill sizes for matching targets.
// targets must be sorted by (hash, len). sizes[target.index] receives uncompressedSize. // targets must be sorted by (hash, len). sizes[target.index] receives uncompressedSize.
// Returns number of targets matched. // Returns number of targets matched.
int fillUncompressedSizes(std::deque<SizeTarget>& targets, std::deque<uint32_t>& sizes); int fillUncompressedSizes(std::vector<SizeTarget>& targets, std::vector<uint32_t>& sizes);
// Due to the memory required to run each of these, it is recommended to not preopen the zip file for multiple // Due to the memory required to run each of these, it is recommended to not preopen the zip file for multiple
// These functions will open and close the zip as needed // These functions will open and close the zip as needed
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false); uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false);