From 5fbc657aeb16d12631e89eb590d218139138959b Mon Sep 17 00:00:00 2001 From: hooligan333 <59103913+hooligan333@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:56:28 -0700 Subject: [PATCH] perf: always binary-search idref lookups in content.opf (#2433) Co-authored-by: Erica Jensen --- lib/Epub/Epub/parsers/ContentOpfParser.cpp | 11 ++++++----- lib/Epub/Epub/parsers/ContentOpfParser.h | 4 +--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 85bc7a68..04a0a3de 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -137,8 +137,10 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error."); } - // Sort item index for binary search if we have enough items - if (self->itemIndex.size() >= LARGE_SPINE_THRESHOLD) { + // Sort the (unconditionally-built) item index so every idref lookup uses binary + // search. Without this, small/medium manifests fell back to an O(spine × manifest) + // linear rescan of .items.bin per itemref (up to ~200ms/item at large scale). + if (!self->itemIndex.empty()) { std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) { return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen); }); @@ -284,9 +286,8 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name ++it; } } else { - // Slow path: linear scan (for small manifests, keeps original behavior) - // TODO: This lookup is slow as need to scan through all items each time. - // It can take up to 200ms per item when getting to 1500 items. + // Fallback linear scan, only reached when the index is empty (no manifest + // items). The fast binary-search path above is used for all real manifests. self->tempItemStore.seek(0); std::string itemId; while (self->tempItemStore.available()) { diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.h b/lib/Epub/Epub/parsers/ContentOpfParser.h index 0a0e51ba..c0373133 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.h +++ b/lib/Epub/Epub/parsers/ContentOpfParser.h @@ -32,7 +32,7 @@ class ContentOpfParser final : public Print { HalFile tempItemStore; std::string coverItemId; - // Index for fast idref→href lookup (used only for large EPUBs) + // Index for fast idref→href lookup (binary search over .items.bin) struct ItemIndexEntry { uint32_t idHash; // FNV-1a hash of itemId uint16_t idLen; // length for collision reduction @@ -41,8 +41,6 @@ class ContentOpfParser final : public Print { std::deque itemIndex; bool useItemIndex = false; - static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400; - // FNV-1a hash function static uint32_t fnvHash(const std::string& s) { uint32_t hash = 2166136261u;