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