Implement smarter section caching
This commit is contained in:
+142
-1
@@ -36,6 +36,134 @@ inline uint32_t paragraphLutEntryOffset(uint32_t lutStart, uint16_t page) {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint32_t FNV_PRIME = 0x01000193; // 16777619
|
||||||
|
constexpr uint32_t FNV_OFFSET_BASIS = 0x811C9DC5; // 2166136261
|
||||||
|
|
||||||
|
uint32_t fnv1a(const uint8_t* data, size_t length) {
|
||||||
|
uint32_t hash = FNV_OFFSET_BASIS;
|
||||||
|
for (size_t i = 0; i < length; ++i) {
|
||||||
|
hash ^= data[i];
|
||||||
|
hash *= FNV_PRIME;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
uint32_t Section::calculatePropertyHash(int fontId, float lineCompression, bool extraParagraphSpacing,
|
||||||
|
uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight,
|
||||||
|
bool hyphenationEnabled, bool embeddedStyle, bool bionicReadingEnabled,
|
||||||
|
uint8_t imageRendering) {
|
||||||
|
uint8_t buffer[64];
|
||||||
|
size_t offset = 0;
|
||||||
|
|
||||||
|
auto append = [&](const void* ptr, size_t size) {
|
||||||
|
memcpy(buffer + offset, ptr, size);
|
||||||
|
offset += size;
|
||||||
|
};
|
||||||
|
|
||||||
|
append(&fontId, sizeof(fontId));
|
||||||
|
append(&lineCompression, sizeof(lineCompression));
|
||||||
|
append(&extraParagraphSpacing, sizeof(extraParagraphSpacing));
|
||||||
|
append(¶graphAlignment, sizeof(paragraphAlignment));
|
||||||
|
append(&viewportWidth, sizeof(viewportWidth));
|
||||||
|
append(&viewportHeight, sizeof(viewportHeight));
|
||||||
|
append(&hyphenationEnabled, sizeof(hyphenationEnabled));
|
||||||
|
append(&embeddedStyle, sizeof(embeddedStyle));
|
||||||
|
append(&bionicReadingEnabled, sizeof(bionicReadingEnabled));
|
||||||
|
append(&imageRendering, sizeof(imageRendering));
|
||||||
|
|
||||||
|
return fnv1a(buffer, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Section::getSectionFilePath(uint32_t propertyHash) const {
|
||||||
|
char buf[32];
|
||||||
|
snprintf(buf, sizeof(buf), "%d_%08x", spineIndex, propertyHash);
|
||||||
|
return epub->getCachePath() + "/sections/" + buf + ".bin";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Section::getImageBasePath(uint32_t propertyHash) const {
|
||||||
|
char buf[32];
|
||||||
|
snprintf(buf, sizeof(buf), "img_%d_%08x_", spineIndex, propertyHash);
|
||||||
|
return epub->getCachePath() + "/" + buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SectionVariant {
|
||||||
|
std::string filename;
|
||||||
|
uint16_t date;
|
||||||
|
uint16_t time;
|
||||||
|
};
|
||||||
|
|
||||||
|
void Section::evictOldVariants() const {
|
||||||
|
// We keep up to 3 most recently accessed/modified variants to prevent SD card bloat
|
||||||
|
constexpr size_t MAX_VARIANTS = 3;
|
||||||
|
|
||||||
|
std::string sectionsDir = epub->getCachePath() + "/sections";
|
||||||
|
auto files = Storage.listFiles(sectionsDir.c_str(), 100);
|
||||||
|
|
||||||
|
std::vector<SectionVariant> variants;
|
||||||
|
|
||||||
|
// Find all cache variants belonging to this spineIndex
|
||||||
|
char prefix[16];
|
||||||
|
snprintf(prefix, sizeof(prefix), "%d_", spineIndex);
|
||||||
|
size_t prefixLen = strlen(prefix);
|
||||||
|
|
||||||
|
for (const auto& file : files) {
|
||||||
|
if (file.startsWith(prefix) && file.endsWith(".bin")) {
|
||||||
|
HalFile hf = Storage.open((sectionsDir + "/" + file.c_str()).c_str(), O_RDONLY);
|
||||||
|
if (hf) {
|
||||||
|
uint16_t md, mt;
|
||||||
|
if (hf.getModifyDateTime(&md, &mt)) {
|
||||||
|
variants.push_back({file.c_str(), md, mt});
|
||||||
|
} else {
|
||||||
|
// If we can't get modified time, assume it's very old to evict it
|
||||||
|
variants.push_back({file.c_str(), 0, 0});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variants.size() <= MAX_VARIANTS) return;
|
||||||
|
|
||||||
|
// Sort descending by modified date and time
|
||||||
|
std::sort(variants.begin(), variants.end(), [](const SectionVariant& a, const SectionVariant& b) {
|
||||||
|
if (a.date != b.date) return a.date > b.date;
|
||||||
|
return a.time > b.time;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete everything after MAX_VARIANTS limit
|
||||||
|
for (size_t i = MAX_VARIANTS; i < variants.size(); ++i) {
|
||||||
|
std::string targetPath = sectionsDir + "/" + variants[i].filename;
|
||||||
|
Storage.remove(targetPath.c_str());
|
||||||
|
LOG_DBG("SCT", "Evicted old section cache: %s", targetPath.c_str());
|
||||||
|
|
||||||
|
// Extract the hash to also clean up associated images
|
||||||
|
// Filename format: spineIndex_hash.bin
|
||||||
|
size_t underscore = variants[i].filename.find('_');
|
||||||
|
size_t dot = variants[i].filename.find('.');
|
||||||
|
if (underscore != std::string::npos && dot != std::string::npos && dot > underscore) {
|
||||||
|
std::string hashStr = variants[i].filename.substr(underscore + 1, dot - underscore - 1);
|
||||||
|
uint32_t parsedHash = strtoul(hashStr.c_str(), nullptr, 16);
|
||||||
|
if (parsedHash != 0 || hashStr == "00000000") {
|
||||||
|
std::string imgBasePath = getImageBasePath(parsedHash);
|
||||||
|
// Find and delete matching images
|
||||||
|
auto rootFiles = Storage.listFiles(epub->getCachePath().c_str(), 100);
|
||||||
|
size_t lastSlash = imgBasePath.find_last_of('/');
|
||||||
|
std::string imgPrefix = (lastSlash != std::string::npos) ? imgBasePath.substr(lastSlash + 1) : imgBasePath;
|
||||||
|
|
||||||
|
for (const auto& rf : rootFiles) {
|
||||||
|
if (rf.startsWith(imgPrefix.c_str())) {
|
||||||
|
Storage.remove((epub->getCachePath() + "/" + rf.c_str()).c_str());
|
||||||
|
LOG_DBG("SCT", "Evicted old image cache: %s", rf.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LOG_ERR("SCT", "File not open for writing page %d", pageCount);
|
LOG_ERR("SCT", "File not open for writing page %d", pageCount);
|
||||||
@@ -89,6 +217,11 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||||
const bool bionicReadingEnabled, const uint8_t imageRendering) {
|
const bool bionicReadingEnabled, const uint8_t imageRendering) {
|
||||||
|
uint32_t propertyHash =
|
||||||
|
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
|
viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering);
|
||||||
|
filePath = getSectionFilePath(propertyHash);
|
||||||
|
|
||||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -195,6 +328,11 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||||
const bool bionicReadingEnabled, const uint8_t imageRendering,
|
const bool bionicReadingEnabled, const uint8_t imageRendering,
|
||||||
const std::function<void(int)>& progressFn) {
|
const std::function<void(int)>& progressFn) {
|
||||||
|
uint32_t propertyHash =
|
||||||
|
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
|
viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering);
|
||||||
|
filePath = getSectionFilePath(propertyHash);
|
||||||
|
|
||||||
const uint32_t phaseTotalStart = millis();
|
const uint32_t phaseTotalStart = millis();
|
||||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||||
|
|
||||||
@@ -222,7 +360,10 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
// Derive the content base directory and image cache path prefix for the parser
|
// Derive the content base directory and image cache path prefix for the parser
|
||||||
size_t lastSlash = localPath.find_last_of('/');
|
size_t lastSlash = localPath.find_last_of('/');
|
||||||
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
||||||
std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
std::string imageBasePath = getImageBasePath(propertyHash);
|
||||||
|
|
||||||
|
// Evict old variants for this spine to keep cache size controlled
|
||||||
|
evictOldVariants();
|
||||||
|
|
||||||
CssParser* cssParser = nullptr;
|
CssParser* cssParser = nullptr;
|
||||||
if (embeddedStyle) {
|
if (embeddedStyle) {
|
||||||
|
|||||||
+15
-4
@@ -38,15 +38,26 @@ class Section {
|
|||||||
// Caller is responsible for closing `outFile`. Returns false on any I/O or validation error.
|
// Caller is responsible for closing `outFile`. Returns false on any I/O or validation error.
|
||||||
bool readParagraphLutHeader(FsFile& outFile, uint16_t& outCount, uint32_t& outLutStart) const;
|
bool readParagraphLutHeader(FsFile& outFile, uint16_t& outCount, uint32_t& outLutStart) const;
|
||||||
|
|
||||||
|
// Calculates a stable hash for a given set of rendering properties.
|
||||||
|
// Used to suffix cache files so multiple variants can coexist safely without constant recompilation.
|
||||||
|
static uint32_t calculatePropertyHash(int fontId, float lineCompression, bool extraParagraphSpacing,
|
||||||
|
uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight,
|
||||||
|
bool hyphenationEnabled, bool embeddedStyle, bool bionicReadingEnabled,
|
||||||
|
uint8_t imageRendering);
|
||||||
|
|
||||||
|
// Computes the active file path for this section based on rendering properties
|
||||||
|
std::string getSectionFilePath(uint32_t propertyHash) const;
|
||||||
|
// Computes the image base path for extract images related to this specific section variant
|
||||||
|
std::string getImageBasePath(uint32_t propertyHash) const;
|
||||||
|
// Garbage collection: Keep only the most recent N variants per chapter
|
||||||
|
void evictOldVariants() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
uint16_t pageCount = 0;
|
uint16_t pageCount = 0;
|
||||||
int currentPage = 0;
|
int currentPage = 0;
|
||||||
|
|
||||||
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
||||||
: epub(epub),
|
: epub(epub), spineIndex(spineIndex), renderer(renderer) {}
|
||||||
spineIndex(spineIndex),
|
|
||||||
renderer(renderer),
|
|
||||||
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
|
||||||
~Section() = default;
|
~Section() = default;
|
||||||
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||||
|
|||||||
+5
-5
@@ -141,12 +141,12 @@ inline const std::vector<SettingInfo> list = {
|
|||||||
// All entries share the same ordered action-label list; the submenu groups them behind
|
// All entries share the same ordered action-label list; the submenu groups them behind
|
||||||
// a single placeholder row in the device UI.
|
// a single placeholder row in the device UI.
|
||||||
// Shared action options (everything except the first "default" entry).
|
// Shared action options (everything except the first "default" entry).
|
||||||
#define BTN_ACT_OPTIONS \
|
#define BTN_ACT_OPTIONS \
|
||||||
StrId::STR_BTN_ACT_PAGE_FORWARD, StrId::STR_BTN_ACT_PAGE_BACK, StrId::STR_BTN_ACT_PAGE_FORWARD_10, \
|
StrId::STR_BTN_ACT_PAGE_FORWARD, StrId::STR_BTN_ACT_PAGE_BACK, StrId::STR_BTN_ACT_PAGE_FORWARD_10, \
|
||||||
StrId::STR_BTN_ACT_PAGE_BACK_10, StrId::STR_BTN_ACT_GO_HOME, StrId::STR_BTN_ACT_SLEEP, \
|
StrId::STR_BTN_ACT_PAGE_BACK_10, StrId::STR_BTN_ACT_GO_HOME, StrId::STR_BTN_ACT_SLEEP, \
|
||||||
StrId::STR_BTN_ACT_FORCE_REFRESH, StrId::STR_BTN_ACT_FORCE_FAST_REFRESH, StrId::STR_BTN_ACT_OPEN_TOC, \
|
StrId::STR_BTN_ACT_FORCE_REFRESH, StrId::STR_BTN_ACT_FORCE_FAST_REFRESH, StrId::STR_BTN_ACT_OPEN_TOC, \
|
||||||
StrId::STR_BTN_ACT_OPEN_BOOKMARKS, StrId::STR_BTN_ACT_STAR_PAGE, StrId::STR_BTN_ACT_FOOTNOTES, \
|
StrId::STR_BTN_ACT_OPEN_BOOKMARKS, StrId::STR_BTN_ACT_STAR_PAGE, StrId::STR_BTN_ACT_FOOTNOTES, \
|
||||||
StrId::STR_BTN_ACT_NEXT_SECTION, StrId::STR_BTN_ACT_PREV_SECTION, StrId::STR_BTN_ACT_EXIT_READER, \
|
StrId::STR_BTN_ACT_NEXT_SECTION, StrId::STR_BTN_ACT_PREV_SECTION, StrId::STR_BTN_ACT_EXIT_READER, \
|
||||||
StrId::STR_BTN_ACT_READER_MENU, StrId::STR_BTN_ACT_TOGGLE_BIONIC_READING, StrId::STR_BTN_ACT_KOREADER_SYNC
|
StrId::STR_BTN_ACT_READER_MENU, StrId::STR_BTN_ACT_TOGGLE_BIONIC_READING, StrId::STR_BTN_ACT_KOREADER_SYNC
|
||||||
|
|
||||||
// Back button: short=exit reader, double=ignore, long=go home
|
// Back button: short=exit reader, double=ignore, long=go home
|
||||||
|
|||||||
Reference in New Issue
Block a user