From ad7789f53dd29848f4a67ccfc5883da1c746e2c9 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 27 Apr 2026 11:19:42 +0200 Subject: [PATCH] Introduce toc caching --- lib/Epub/Epub.cpp | 33 ++++------------------------- lib/Epub/Epub/BookMetadataCache.cpp | 33 ++++++++++++++++++++++++----- lib/Epub/Epub/BookMetadataCache.h | 10 ++++++++- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 0f52d8f8..de26c410 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -868,35 +868,10 @@ bool Epub::hasReliableToc() const { return false; } - const int spineCount = bookMetadataCache->getSpineCount(); - const int tocCount = bookMetadataCache->getTocCount(); - - if (spineCount <= 0 || tocCount <= 0) { - tocReliabilityState = 0; - return false; - } - - // If a larger book only exposes one TOC entry, treat TOC as unusable for chapter UX. - if (spineCount >= 8 && tocCount <= 1) { - tocReliabilityState = 0; - return false; - } - - std::vector spineReferenced(static_cast(spineCount), false); - int distinctSpinesReferenced = 0; - for (int i = 0; i < tocCount; i++) { - const auto toc = bookMetadataCache->getTocEntry(i); - if (toc.spineIndex >= 0 && toc.spineIndex < spineCount) { - const size_t idx = static_cast(toc.spineIndex); - if (!spineReferenced[idx]) { - spineReferenced[idx] = true; - distinctSpinesReferenced++; - } - } - } - - // Require at least 25% spine coverage from TOC references. - const bool reliable = (distinctSpinesReferenced * 4 >= spineCount); + // Reliability is computed once at indexing time and persisted in book.bin's header. + // This avoids the O(tocCount) seek-heavy scan that previously fired on first page load + // for every book — a large web-novel TOC (~3000 entries) added several seconds of latency. + const bool reliable = bookMetadataCache->isTocReliable(); tocReliabilityState = reliable ? 1 : 0; return reliable; } diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index 02b25e2d..593ca70d 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -9,7 +9,7 @@ #include "FsHelpers.h" namespace { -constexpr uint8_t BOOK_CACHE_VERSION = 6; +constexpr uint8_t BOOK_CACHE_VERSION = 7; constexpr char bookBinFile[] = "/book.bin"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; @@ -113,8 +113,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta return false; } - constexpr uint32_t headerASize = - sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount); + constexpr uint32_t headerASize = sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + + sizeof(tocCount) + sizeof(uint8_t) /* tocReliable */; const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() + metadata.coverItemHref.size() + metadata.textReferenceHref.size() + metadata.series.size() + metadata.seriesIndex.size() + metadata.description.size() + @@ -122,11 +122,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount; const uint32_t lutOffset = headerASize + metadataSize; - // Header A + // Header A. tocReliable is patched at the end once the TOC scan below has computed it. + const uint32_t tocReliableHeaderPos = + sizeof(BOOK_CACHE_VERSION) + sizeof(uint32_t) /* lutOffset */ + sizeof(spineCount) + sizeof(tocCount); serialization::writePod(bookFile, BOOK_CACHE_VERSION); serialization::writePod(bookFile, lutOffset); serialization::writePod(bookFile, spineCount); serialization::writePod(bookFile, tocCount); + serialization::writePod(bookFile, static_cast(0)); // placeholder for tocReliable // Metadata serialization::writeString(bookFile, metadata.title); serialization::writeString(bookFile, metadata.author); @@ -156,18 +159,31 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta // LUTs complete // Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin - // Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m)) + // Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m)). + // 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) + // seek-heavy scan in Epub::hasReliableToc(). std::deque spineToTocIndex(spineCount, -1); + int distinctSpinesReferenced = 0; tocFile.seek(0); for (int j = 0; j < tocCount; j++) { auto tocEntry = readTocEntry(tocFile); if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) { if (spineToTocIndex[tocEntry.spineIndex] == -1) { spineToTocIndex[tocEntry.spineIndex] = static_cast(j); + distinctSpinesReferenced++; } } } + // Mirrors the heuristic in Epub::hasReliableToc(): require >=25% distinct spine coverage, + // with short-circuits for edge cases (no entries, or large book with a single TOC entry). + if (spineCount > 0 && tocCount > 0 && !(spineCount >= 8 && tocCount <= 1)) { + tocReliable = (distinctSpinesReferenced * 4 >= spineCount); + } else { + tocReliable = false; + } + ZipFile zip(epubPath); // Pre-open zip file to speed up size calculations if (!zip.open()) { @@ -269,6 +285,10 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta writeTocEntry(bookFile, tocEntry); } + // Patch tocReliable placeholder in header A + bookFile.seek(tocReliableHeaderPos); + serialization::writePod(bookFile, static_cast(tocReliable ? 1 : 0)); + bookFile.close(); spineFile.close(); tocFile.close(); @@ -384,6 +404,9 @@ bool BookMetadataCache::load() { serialization::readPod(bookFile, lutOffset); serialization::readPod(bookFile, spineCount); serialization::readPod(bookFile, tocCount); + uint8_t tocReliableByte; + serialization::readPod(bookFile, tocReliableByte); + tocReliable = (tocReliableByte != 0); serialization::readString(bookFile, coreMetadata.title); serialization::readString(bookFile, coreMetadata.author); diff --git a/lib/Epub/Epub/BookMetadataCache.h b/lib/Epub/Epub/BookMetadataCache.h index feb39a23..fa54d9b0 100644 --- a/lib/Epub/Epub/BookMetadataCache.h +++ b/lib/Epub/Epub/BookMetadataCache.h @@ -50,6 +50,7 @@ class BookMetadataCache { size_t lutOffset; uint16_t spineCount; uint16_t tocCount; + bool tocReliable; bool loaded; bool buildMode; @@ -88,7 +89,13 @@ class BookMetadataCache { BookMetadata coreMetadata; explicit BookMetadataCache(std::string cachePath) - : cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {} + : cachePath(std::move(cachePath)), + lutOffset(0), + spineCount(0), + tocCount(0), + tocReliable(false), + loaded(false), + buildMode(false) {} ~BookMetadataCache() = default; // Building phase (stream to disk immediately) @@ -111,5 +118,6 @@ class BookMetadataCache { TocEntry getTocEntry(int index); int getSpineCount() const { return spineCount; } int getTocCount() const { return tocCount; } + bool isTocReliable() const { return tocReliable; } bool isLoaded() const { return loaded; } };