perf(aw-transform): cache categorize results per event data to fix month-view timeout - #657
Conversation
…redundant regex matching For a typical month with 50k+ events (from aw-watcher-window) and 20 category rules, categorize() was performing ~1M regex evaluations because every event was matched against every rule individually. Most events in a heartbeat-based watcher share identical data (same app+title). This adds an in-function HashMap cache keyed on the serialized event data JSON so that only the first occurrence of each distinct data fingerprint is matched against the rule set; subsequent identical events reuse the cached category. Expected speedup for a month-view query with 50k events and O(100) distinct app/title pairs: >99% reduction in regex work, turning a 30+ second query into a sub-second one. serde_json::Map preserves insertion order, so events produced by the same watcher in the same session produce a consistent JSON key without normalization. Adds test_categorize_cache_correctness: cache hits on identical data, correct distinct categories for differing data, and no false cache collisions. Fixes ActivityWatch#629
Greptile SummaryThe PR accelerates event categorization by caching categories under serialized event-data keys and reusing them for repeated payloads.
Confidence Score: 4/5The PR appears safe to merge, with non-blocking follow-up needed to validate its performance claims and correct the cache-key documentation. Current classification rules depend exclusively on event data, so reusing categories for identical serialized maps preserves behavior; the remaining concerns concern measurement and documentation rather than a demonstrated runtime defect. Files Needing Attention: aw-transform/src/classify.rs Important Files Changed
Reviews (1): Last reviewed commit: "perf(aw-transform): cache categorize res..." | Re-trigger Greptile |
| /// | ||
| /// Performance: builds an in-memory cache keyed on the event's data JSON so that | ||
| /// events with identical data (same app/title — very common in practice) are only | ||
| /// matched against the rule set once. On a month's data with 50k+ events but only | ||
| /// a few hundred distinct app/title pairs this reduces regex work by >99%. |
There was a problem hiding this comment.
Benchmark the cache optimization
The new implementation adds JSON serialization, hashing, and category cloning for every event, while the test covers only output correctness. Add representative profiling or benchmark results to validate the stated greater-than-99-percent reduction and guard the month-view improvement against regression.
Rule Used: Before implementing performance optimizations, mea... (source)
Learned From
gptme/gptme#707
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| // Key on the full event data. serde_json::Map preserves insertion order, so | ||
| // events with the same fields in the same order produce the same key — which | ||
| // is the normal case for heartbeat-based watchers. |
There was a problem hiding this comment.
Correct the map-order explanation
The current serde_json::Map configuration uses a key-sorted map rather than preserving insertion order. The comment therefore gives maintainers the wrong reason equivalent maps produce stable cache keys and becomes especially misleading when evaluating feature changes.
| // Key on the full event data. serde_json::Map preserves insertion order, so | |
| // events with the same fields in the same order produce the same key — which | |
| // is the normal case for heartbeat-based watchers. | |
| // Key on the full event data. With serde_json's default map implementation, | |
| // keys are sorted, so equivalent event data produces the same serialized key. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #657 +/- ##
==========================================
+ Coverage 70.81% 78.37% +7.55%
==========================================
Files 51 66 +15
Lines 2916 5419 +2503
==========================================
+ Hits 2065 4247 +2182
- Misses 851 1172 +321 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@TimeToBuildBob Do the same PR for aw-server-python/aw-core |
|
Done — ActivityWatch/aw-core#150 applies the same cache to One note on this PR's CI: the macOS failure is a pre-existing SQLite schema issue in |
…x month-view timeout (#150) * perf(aw-transform): cache categorize/tag results per event data Same fix as ActivityWatch/aw-server-rust#657 — categorize() and tag() were re-evaluating every regex rule against every event individually. Heartbeat-based watchers emit the same app+title payload repeatedly, so the same regex matches were being recomputed thousands of times. Added a function-local cache keyed on json.dumps(e.data, sort_keys=True). Only the first occurrence of each distinct data fingerprint is matched against the rule set; subsequent events with identical data reuse the cached result in O(1). Each event receives its own list copy to prevent mutation aliasing across events. For a month with 50 000 events but ~200 distinct app/title pairs and 20 category rules: 1 000 000 → 4 000 regex evaluations (~250× less). Adds test_categorize_cache_correctness: 101 events (50 + 1 + 50), two distinct data shapes, verifies correct category for each and that mutating one event's category list does not affect others. * fix(classify): handle non-JSON-serializable event data in cache key json.dumps raises TypeError when e.data contains non-serializable values (e.g. nested Event objects from chunk_events_by_key). query2.py catches TypeError and maps it to 'invalid amount of arguments', which obscures the real error and breaks test_query2_query_functions. Fall back to str(id(e.data)) for non-serializable data — no caching benefit for those events, but correctness is preserved.
Summary
Fixes #629 — month-summary view timing out (30 s) on large databases (168 MB, 2+ years of data).
Root cause
categorize()inaw-transform/src/classify.rswas processing every event individually against every regex rule. For a typical month with 50 000+ events (from aw-watcher-window) and 20 category rules, this meant ~1 000 000 regex evaluations per query.The vast majority of those events share identical
app+titledata — heartbeat-based watchers emit the same payload many times per second. So the same regex match was being recomputed thousands of times for no reason.Fix
Added a
HashMap<String, Vec<String>>category cache insidecategorize(), keyed on the serialized event data (serde_json::to_string(&event.data)). Only the first occurrence of each distinct data fingerprint is matched against the rule set; subsequent events with identical data reuse the cached result in O(1).serde_json::Mappreserves insertion order (IndexMap semantics), so events produced by the same watcher in the same session produce a consistent JSON key without any normalization step.Expected impact
For a month's worth of data with 50 000 events but only ~200 distinct app/title pairs:
The 30-second timeout on a 168 MB database should become a sub-second query.
Changes
aw-transform/src/classify.rs: cache added tocategorize(); deadcategorize_one()helper removed (logic inlined into the cached loop)test_categorize_cache_correctness: 101 events (50 + 1 + 50), two distinct data shapes → verifies correct category for each shape and no false cache collisionsTesting