Improve cache memory management

This commit is contained in:
jpirnay
2026-05-11 22:42:07 +02:00
parent c273e250f2
commit 00d9a4ea05
2 changed files with 95 additions and 21 deletions
+90 -20
View File
@@ -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 <typename Fn>
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<char>(std::tolower(static_cast<unsigned char>(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<unsigned>(hotRuleCache_.size()), static_cast<unsigned>(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<unsigned long>(unsupportedSelectorSkips_), static_cast<unsigned long>(totalSelectorCandidates_),
static_cast<unsigned>(hotRuleCache_.size()), static_cast<unsigned>(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<unsigned>(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<unsigned>(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()) {
+5 -1
View File
@@ -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<std::string, uint32_t> cacheRuleOffsets_;
uint32_t totalSelectorCandidates_ = 0;
uint32_t unsupportedSelectorSkips_ = 0;
// Bounded hot cache of most recently used rules.
mutable std::list<std::string> 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);