diff --git a/lib/Epub/Epub/css/CssParser.cpp b/lib/Epub/Epub/css/CssParser.cpp index a10c5deb..27182ab1 100644 --- a/lib/Epub/Epub/css/CssParser.cpp +++ b/lib/Epub/Epub/css/CssParser.cpp @@ -43,7 +43,11 @@ constexpr size_t MAX_RULES = 1500; // Minimum free heap required to apply CSS during rendering // If below this threshold, we skip CSS to avoid display artifacts. -constexpr size_t MIN_FREE_HEAP_FOR_CSS = 48 * 1024; +#ifndef CSS_MIN_FREE_HEAP_FOR_CSS +#define CSS_MIN_FREE_HEAP_FOR_CSS (40 * 1024) +#endif + +constexpr size_t MIN_FREE_HEAP_FOR_CSS = CSS_MIN_FREE_HEAP_FOR_CSS; // In-memory CSS rule cache sizing for disk-backed lookup mode. // Keeps memory bounded on large books while retaining hot selectors. @@ -74,6 +78,33 @@ constexpr char compileTempRulesCache[] = "/css_rules.compile.tmp"; // Check if character is CSS whitespace bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; } +template +void forEachNormalizedClassToken(const std::string& classAttr, std::string& normalizedBuf, Fn&& fn) { + size_t i = 0; + while (i < classAttr.size()) { + while (i < classAttr.size() && isCssWhitespace(classAttr[i])) { + ++i; + } + if (i >= classAttr.size()) { + break; + } + + const size_t start = i; + while (i < classAttr.size() && !isCssWhitespace(classAttr[i])) { + ++i; + } + + normalizedBuf.clear(); + normalizedBuf.reserve(i - start); + for (size_t j = start; j < i; ++j) { + normalizedBuf.push_back(static_cast(std::tolower(static_cast(classAttr[j])))); + } + if (!normalizedBuf.empty()) { + fn(normalizedBuf); + } + } +} + std::string_view stripTrailingImportant(std::string_view value) { constexpr std::string_view IMPORTANT = "!important"; @@ -406,9 +437,11 @@ void CssParser::processRuleBlockWithStyle(const std::string& selectorGroup, cons const auto selectors = splitOnChar(selectorGroup, ','); for (const auto& sel : selectors) { + totalSelectorCandidates_++; // Validate selector length before processing if (sel.size() > MAX_SELECTOR_LENGTH) { LOG_DBG("CSS", "Selector too long (%zu > %zu), skipping", sel.size(), MAX_SELECTOR_LENGTH); + unsupportedSelectorSkips_++; continue; } @@ -419,42 +452,49 @@ void CssParser::processRuleBlockWithStyle(const std::string& selectorGroup, cons // TODO: Consider adding support for sibling css selectors in the future // Ensure no + in selector as we don't support adjacent CSS selectors for now if (key.find('+') != std::string_view::npos) { + unsupportedSelectorSkips_++; continue; } // TODO: Consider adding support for direct nested css selectors in the future // Ensure no > in selector as we don't support nested CSS selectors for now if (key.find('>') != std::string_view::npos) { + unsupportedSelectorSkips_++; continue; } // TODO: Consider adding support for attribute css selectors in the future // Ensure no [ in selector as we don't support attribute CSS selectors for now if (key.find('[') != std::string_view::npos) { + unsupportedSelectorSkips_++; continue; } // TODO: Consider adding support for pseudo selectors in the future // Ensure no : in selector as we don't support pseudo CSS selectors for now if (key.find(':') != std::string_view::npos) { + unsupportedSelectorSkips_++; continue; } // TODO: Consider adding support for ID css selectors in the future // Ensure no # in selector as we don't support ID CSS selectors for now if (key.find('#') != std::string_view::npos) { + unsupportedSelectorSkips_++; continue; } // TODO: Consider adding support for general sibling combinator selectors in the future // Ensure no ~ in selector as we don't support general sibling combinator CSS selectors for now if (key.find('~') != std::string_view::npos) { + unsupportedSelectorSkips_++; continue; } // TODO: Consider adding support for wildcard css selectors in the future // Ensure no * in selector as we don't support wildcard CSS selectors for now if (key.find('*') != std::string_view::npos) { + unsupportedSelectorSkips_++; continue; } @@ -463,6 +503,7 @@ void CssParser::processRuleBlockWithStyle(const std::string& selectorGroup, cons // If the selector has whitespace in it, then it's either a CSS selector for a descendant element (e.g. `tag1 tag2`) // or some other slightly more advanced CSS selector which we don't support yet if (key.find(' ') != std::string_view::npos) { + unsupportedSelectorSkips_++; continue; } @@ -785,6 +826,8 @@ void CssParser::clear() { compileModeActive_ = false; compileModeFailed_ = false; compileSelectorOffsets_.clear(); + totalSelectorCandidates_ = 0; + unsupportedSelectorSkips_ = 0; } void CssParser::resetResolveStats() const { resolveStats_ = {}; } @@ -794,10 +837,13 @@ CssParser::ResolveStats CssParser::getResolveStats() const { return resolveStats void CssParser::logResolveStats(const char* context) const { const auto s = getResolveStats(); LOG_DBG("CSS", - "resolve stats[%s]: calls=%lu lowHeapSkips=%lu mapHits=%lu hotHits=%lu diskHits=%lu misses=%lu " - "negativeHits=%lu hotSize=%u indexSize=%u", - context ? context : "n/a", s.resolveCalls, s.lowHeapSkips, s.mapHits, s.hotHits, s.diskHits, s.misses, - s.negativeHits, static_cast(hotRuleCache_.size()), static_cast(cachedRuleCount_)); + "resolve stats[%s]: calls=%lu lowHeapSkips=%lu lowHeapRescuedHits=%lu lowHeapDiskBypasses=%lu " + "mapHits=%lu hotHits=%lu diskHits=%lu misses=%lu negativeHits=%lu " + "unsupportedSelectorsSkipped=%lu totalSelectorCandidates=%lu hotSize=%u indexSize=%u", + context ? context : "n/a", s.resolveCalls, s.lowHeapSkips, s.lowHeapRescuedHits, s.lowHeapDiskBypasses, + s.mapHits, s.hotHits, s.diskHits, s.misses, s.negativeHits, + static_cast(unsupportedSelectorSkips_), static_cast(totalSelectorCandidates_), + static_cast(hotRuleCache_.size()), static_cast(cachedRuleCount_)); } bool CssParser::readCssStylePayload(FsFile& file, CssStyle& style) { @@ -955,7 +1001,7 @@ bool CssParser::readRuleFromDiskAtOffset(const uint32_t styleOffset, CssStyle& o return ok; } -bool CssParser::lookupRule(const std::string& selector, CssStyle& outStyle) const { +bool CssParser::lookupRule(const std::string& selector, CssStyle& outStyle, const bool allowDiskLookup) const { auto mapIt = rulesBySelector_.find(selector); if (mapIt != rulesBySelector_.end()) { outStyle = mapIt->second; @@ -976,6 +1022,11 @@ bool CssParser::lookupRule(const std::string& selector, CssStyle& outStyle) cons return false; } + if (!allowDiskLookup) { + resolveStats_.lowHeapDiskBypasses++; + return false; + } + if (!ensureCacheIndexLoaded()) { return false; } @@ -1066,14 +1117,15 @@ bool CssParser::ensureCacheIndexLoaded() const { CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& classAttr) const { static bool lowHeapWarningLogged = false; resolveStats_.resolveCalls++; - if (ESP.getFreeHeap() < MIN_FREE_HEAP_FOR_CSS) { + const uint32_t freeHeap = ESP.getFreeHeap(); + const bool lowHeapMode = freeHeap < MIN_FREE_HEAP_FOR_CSS; + if (lowHeapMode) { if (!lowHeapWarningLogged) { lowHeapWarningLogged = true; - LOG_DBG("CSS", "Warning: low heap (%u bytes) below MIN_FREE_HEAP_FOR_CSS (%u), returning empty style", - ESP.getFreeHeap(), static_cast(MIN_FREE_HEAP_FOR_CSS)); + LOG_DBG("CSS", "Warning: low heap (%u bytes) below MIN_FREE_HEAP_FOR_CSS (%u), skipping disk CSS lookups", + freeHeap, static_cast(MIN_FREE_HEAP_FOR_CSS)); } resolveStats_.lowHeapSkips++; - return CssStyle{}; } CssStyle result; const std::string tag = normalized(tagName); @@ -1081,7 +1133,10 @@ CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& // 1. Apply element-level style (lowest priority) { CssStyle tagStyle; - if (lookupRule(tag, tagStyle)) { + if (lookupRule(tag, tagStyle, !lowHeapMode)) { + if (lowHeapMode) { + resolveStats_.lowHeapRescuedHits++; + } result.applyOver(tagStyle); } } @@ -1089,27 +1144,42 @@ CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& // TODO: Support combinations of classes (e.g. style on .class1.class2) // 2. Apply class styles (medium priority) if (!classAttr.empty()) { - const auto classes = splitWhitespace(classAttr); + std::string classToken; + std::string classKey; + classKey.reserve(32); - for (const auto& cls : classes) { - std::string classKey = "." + normalized(cls); + forEachNormalizedClassToken(classAttr, classToken, [&](const std::string& cls) { + classKey.clear(); + classKey.push_back('.'); + classKey.append(cls); CssStyle classStyle; - if (lookupRule(classKey, classStyle)) { + if (lookupRule(classKey, classStyle, !lowHeapMode)) { + if (lowHeapMode) { + resolveStats_.lowHeapRescuedHits++; + } result.applyOver(classStyle); } - } + }); // TODO: Support combinations of classes (e.g. style on p.class1.class2) // 3. Apply element.class styles (higher priority) - for (const auto& cls : classes) { - std::string combinedKey = tag + "." + normalized(cls); + std::string combinedKey; + combinedKey.reserve(tag.size() + 1 + 32); + forEachNormalizedClassToken(classAttr, classToken, [&](const std::string& cls) { + combinedKey.clear(); + combinedKey.append(tag); + combinedKey.push_back('.'); + combinedKey.append(cls); CssStyle combinedStyle; - if (lookupRule(combinedKey, combinedStyle)) { + if (lookupRule(combinedKey, combinedStyle, !lowHeapMode)) { + if (lowHeapMode) { + resolveStats_.lowHeapRescuedHits++; + } result.applyOver(combinedStyle); } - } + }); } if (!result.defined.anySet()) { diff --git a/lib/Epub/Epub/css/CssParser.h b/lib/Epub/Epub/css/CssParser.h index ffb9f27a..244ab1ba 100644 --- a/lib/Epub/Epub/css/CssParser.h +++ b/lib/Epub/Epub/css/CssParser.h @@ -35,6 +35,8 @@ class CssParser { struct ResolveStats { uint32_t resolveCalls = 0; uint32_t lowHeapSkips = 0; + uint32_t lowHeapRescuedHits = 0; + uint32_t lowHeapDiskBypasses = 0; uint32_t mapHits = 0; uint32_t hotHits = 0; uint32_t diskHits = 0; @@ -139,6 +141,8 @@ class CssParser { mutable bool cacheIndexLoaded_ = false; mutable size_t cachedRuleCount_ = 0; mutable std::unordered_map cacheRuleOffsets_; + uint32_t totalSelectorCandidates_ = 0; + uint32_t unsupportedSelectorSkips_ = 0; // Bounded hot cache of most recently used rules. mutable std::list hotRuleLru_; @@ -175,7 +179,7 @@ class CssParser { // On-demand rule loading helpers bool ensureCacheIndexLoaded() const; - bool lookupRule(const std::string& selector, CssStyle& outStyle) const; + bool lookupRule(const std::string& selector, CssStyle& outStyle, bool allowDiskLookup = true) const; bool readRuleFromDiskAtOffset(uint32_t styleOffset, CssStyle& outStyle) const; static bool readCssStylePayload(FsFile& file, CssStyle& style); static void writeCssStylePayload(FsFile& file, const CssStyle& style);