Introduce toc caching

This commit is contained in:
jpirnay
2026-04-27 11:19:42 +02:00
parent cec083a8bb
commit ad7789f53d
3 changed files with 41 additions and 35 deletions
+4 -29
View File
@@ -868,35 +868,10 @@ bool Epub::hasReliableToc() const {
return false; return false;
} }
const int spineCount = bookMetadataCache->getSpineCount(); // Reliability is computed once at indexing time and persisted in book.bin's header.
const int tocCount = bookMetadataCache->getTocCount(); // 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.
if (spineCount <= 0 || tocCount <= 0) { const bool reliable = bookMetadataCache->isTocReliable();
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<bool> spineReferenced(static_cast<size_t>(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<size_t>(toc.spineIndex);
if (!spineReferenced[idx]) {
spineReferenced[idx] = true;
distinctSpinesReferenced++;
}
}
}
// Require at least 25% spine coverage from TOC references.
const bool reliable = (distinctSpinesReferenced * 4 >= spineCount);
tocReliabilityState = reliable ? 1 : 0; tocReliabilityState = reliable ? 1 : 0;
return reliable; return reliable;
} }
+28 -5
View File
@@ -9,7 +9,7 @@
#include "FsHelpers.h" #include "FsHelpers.h"
namespace { namespace {
constexpr uint8_t BOOK_CACHE_VERSION = 6; constexpr uint8_t BOOK_CACHE_VERSION = 7;
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";
@@ -113,8 +113,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
return false; return false;
} }
constexpr uint32_t headerASize = constexpr uint32_t headerASize = sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) +
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount); sizeof(tocCount) + sizeof(uint8_t) /* tocReliable */;
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() + const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
metadata.coverItemHref.size() + metadata.textReferenceHref.size() + metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
metadata.series.size() + metadata.seriesIndex.size() + metadata.description.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 lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
const uint32_t lutOffset = headerASize + metadataSize; 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, BOOK_CACHE_VERSION);
serialization::writePod(bookFile, lutOffset); serialization::writePod(bookFile, lutOffset);
serialization::writePod(bookFile, spineCount); serialization::writePod(bookFile, spineCount);
serialization::writePod(bookFile, tocCount); serialization::writePod(bookFile, tocCount);
serialization::writePod(bookFile, static_cast<uint8_t>(0)); // placeholder for tocReliable
// Metadata // Metadata
serialization::writeString(bookFile, metadata.title); serialization::writeString(bookFile, metadata.title);
serialization::writeString(bookFile, metadata.author); serialization::writeString(bookFile, metadata.author);
@@ -156,18 +159,31 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
// LUTs complete // LUTs complete
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin // 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<int16_t> spineToTocIndex(spineCount, -1); std::deque<int16_t> spineToTocIndex(spineCount, -1);
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); auto tocEntry = readTocEntry(tocFile);
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) { if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
if (spineToTocIndex[tocEntry.spineIndex] == -1) { if (spineToTocIndex[tocEntry.spineIndex] == -1) {
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j); spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(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); ZipFile zip(epubPath);
// Pre-open zip file to speed up size calculations // Pre-open zip file to speed up size calculations
if (!zip.open()) { if (!zip.open()) {
@@ -269,6 +285,10 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
writeTocEntry(bookFile, tocEntry); writeTocEntry(bookFile, tocEntry);
} }
// Patch tocReliable placeholder in header A
bookFile.seek(tocReliableHeaderPos);
serialization::writePod(bookFile, static_cast<uint8_t>(tocReliable ? 1 : 0));
bookFile.close(); bookFile.close();
spineFile.close(); spineFile.close();
tocFile.close(); tocFile.close();
@@ -384,6 +404,9 @@ bool BookMetadataCache::load() {
serialization::readPod(bookFile, lutOffset); serialization::readPod(bookFile, lutOffset);
serialization::readPod(bookFile, spineCount); serialization::readPod(bookFile, spineCount);
serialization::readPod(bookFile, tocCount); serialization::readPod(bookFile, tocCount);
uint8_t tocReliableByte;
serialization::readPod(bookFile, tocReliableByte);
tocReliable = (tocReliableByte != 0);
serialization::readString(bookFile, coreMetadata.title); serialization::readString(bookFile, coreMetadata.title);
serialization::readString(bookFile, coreMetadata.author); serialization::readString(bookFile, coreMetadata.author);
+9 -1
View File
@@ -50,6 +50,7 @@ class BookMetadataCache {
size_t lutOffset; size_t lutOffset;
uint16_t spineCount; uint16_t spineCount;
uint16_t tocCount; uint16_t tocCount;
bool tocReliable;
bool loaded; bool loaded;
bool buildMode; bool buildMode;
@@ -88,7 +89,13 @@ class BookMetadataCache {
BookMetadata coreMetadata; BookMetadata coreMetadata;
explicit BookMetadataCache(std::string cachePath) 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; ~BookMetadataCache() = default;
// Building phase (stream to disk immediately) // Building phase (stream to disk immediately)
@@ -111,5 +118,6 @@ class BookMetadataCache {
TocEntry getTocEntry(int index); TocEntry getTocEntry(int index);
int getSpineCount() const { return spineCount; } int getSpineCount() const { return spineCount; }
int getTocCount() const { return tocCount; } int getTocCount() const { return tocCount; }
bool isTocReliable() const { return tocReliable; }
bool isLoaded() const { return loaded; } bool isLoaded() const { return loaded; }
}; };