From fb69196f961eeaa30432c33db3003effcdbced53 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 22 Aug 2026 00:57:20 +0000 Subject: Pre-compile filter rules instead of re-lowercasing per article per emission ArticleListScreen calls evaluate() once per visible article against the same rule set, re-run on essentially any read-state change to any article in the list. With a couple hundred articles and a handful of rules, calling pattern.lowercase() inside the old per-article evaluate() was thousands of redundant String allocations of the same small set of strings. --- .../java/org/terst/feedreader/data/FilterRuleEngine.kt | 18 +++++++++++++++--- .../java/org/terst/feedreader/ui/ArticleListScreen.kt | 3 ++- 2 files changed, 17 insertions(+), 4 deletions(-) (limited to 'app/src/main/java/org') diff --git a/app/src/main/java/org/terst/feedreader/data/FilterRuleEngine.kt b/app/src/main/java/org/terst/feedreader/data/FilterRuleEngine.kt index f095322..1f45a9f 100644 --- a/app/src/main/java/org/terst/feedreader/data/FilterRuleEngine.kt +++ b/app/src/main/java/org/terst/feedreader/data/FilterRuleEngine.kt @@ -20,15 +20,27 @@ interface Titled { * globally. MUTE wins over HIGHLIGHT if both match (hiding takes priority * over drawing attention). */ +// Pre-lowercased once per rule, not re-derived from FilterRuleEntity.pattern on every +// evaluate() call -- ArticleListScreen calls evaluate() once per VISIBLE ARTICLE against +// the same rule set (re-run on every emission of either the article or filter-rule Flow, +// i.e. on essentially any read-state change to any article in the list), so with a couple +// hundred articles and a handful of rules, calling pattern.lowercase() inside the old +// per-article evaluate() meant thousands of redundant String allocations of the exact same +// small set of strings. +data class CompiledRule(val feedId: String?, val action: FilterAction, val lowerPattern: String) + object FilterRuleEngine { - fun evaluate(article: Titled, rules: List): FilterResult { + fun compile(rules: List): List = + rules.map { CompiledRule(it.feedId, it.action, it.pattern.lowercase()) } + + fun evaluate(article: Titled, rules: List): FilterResult { val applicable = rules.filter { it.feedId == null || it.feedId == article.feedId } val title = article.title.lowercase() - val muted = applicable.any { it.action == FilterAction.MUTE && title.contains(it.pattern.lowercase()) } + val muted = applicable.any { it.action == FilterAction.MUTE && title.contains(it.lowerPattern) } if (muted) return FilterResult.MUTED - val highlighted = applicable.any { it.action == FilterAction.HIGHLIGHT && title.contains(it.pattern.lowercase()) } + val highlighted = applicable.any { it.action == FilterAction.HIGHLIGHT && title.contains(it.lowerPattern) } return if (highlighted) FilterResult.HIGHLIGHTED else FilterResult.NORMAL } } diff --git a/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt index a852288..6f2db12 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt @@ -72,7 +72,8 @@ fun ArticleListScreen( } val visibleArticles by remember(feedId) { combine(articleFlow, db.filterRuleDao().observeAll()) { articles, rules -> - articles.map { it to FilterRuleEngine.evaluate(it, rules) } + val compiledRules = FilterRuleEngine.compile(rules) + articles.map { it to FilterRuleEngine.evaluate(it, compiledRules) } .filter { (_, result) -> result != FilterResult.MUTED } } }.collectAsState(initial = emptyList()) -- cgit v1.2.3