perf(aw-transform): cache categorize/tag results per event data to fix month-view timeout - #150
Conversation
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.
Greptile SummaryThe PR memoizes category and tag rule results by serialized event data to reduce repeated regex evaluation, while copying cached lists onto each event.
Confidence Score: 4/5The PR should not merge until classification cache keys can handle transformed event data without aborting composed queries. The new unconditional JSON serialization fails when a supported upstream transformation stores Event objects in the data payload; the missing profiling evidence is additionally actionable but non-blocking. Files Needing Attention: aw_transform/classify.py Important Files Changed
Reviews (1): Last reviewed commit: "perf(aw-transform): cache categorize/tag..." | Re-trigger Greptile |
| return [_categorize_one(e, classes) for e in events] | ||
| cache: Dict[str, Category] = {} | ||
| for e in events: | ||
| key = json.dumps(e.data, sort_keys=True) |
There was a problem hiding this comment.
Serialization breaks transformed events
When chunk_events_by_key precedes categorize, data["subevents"] contains Event objects, so serializing the complete payload for the cache key raises TypeError and aborts the query; tag has the same issue at line 68.
Knowledge Base Used: Event grouping and normalization
| cache: Dict[str, Category] = {} | ||
| for e in events: | ||
| key = json.dumps(e.data, sort_keys=True) | ||
| if key not in cache: | ||
| cache[key] = _pick_category( | ||
| [_cls for _cls, rule in classes if rule.match(e)] | ||
| ) | ||
| e.data["$category"] = list(cache[key]) |
There was a problem hiding this comment.
Optimization lacks profiling measurements
These caches implement a performance optimization without profiling or benchmark measurements, so reviewers cannot validate the claimed bottleneck, representative improvement, or the cost of serializing every event payload.
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!
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.
|
CI fixed (pushed d41a406). Root cause: Fix: Wrap the All 31 tests in |
Companion to ActivityWatch/aw-server-rust#657 — same fix requested by @ErikBjare.
Root cause
categorize()andtag()inaw_transform/classify.pywere re-evaluating every regex rule against every event individually. For a typical month with 50 000+ events fromaw-watcher-windowand 20 category rules, this meant ~1 000 000 regex evaluations per query. The vast majority of those events share identicalapp+titledata — heartbeat-based watchers emit the same payload many times per second.Fix
Added a function-local
dictcache inside bothcategorize()andtag(), keyed onjson.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.json.dumps(..., sort_keys=True)guarantees a consistent key regardless of dict insertion order.Expected impact
For a month's worth of data with 50 000 events but only ~200 distinct app/title pairs:
Changes
aw_transform/classify.py: cache added tocategorize()andtag();_categorize_one()and_tag_one()kept as internal helpers (unchanged)test_categorize_cache_correctness: 101 events (50 Firefox + 1 vim + 50 Firefox), verifies correct category per data shape and that mutating one event's category list does not affect others (list-copy correctness)