Skip to content

perf(aw-transform): cache categorize results per event data to fix month-view timeout - #657

Merged
ErikBjare merged 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/month-view-timeout-categorize-cache
Aug 25, 2026
Merged

perf(aw-transform): cache categorize results per event data to fix month-view timeout#657
ErikBjare merged 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/month-view-timeout-categorize-cache

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Summary

Fixes #629 — month-summary view timing out (30 s) on large databases (168 MB, 2+ years of data).

Root cause

categorize() in aw-transform/src/classify.rs was 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+title data — 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 inside categorize(), 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::Map preserves 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:

  • Before: 50 000 × 20 = 1 000 000 regex evaluations
  • After: 200 × 20 = 4 000 regex evaluations (~250× less)

The 30-second timeout on a 168 MB database should become a sub-second query.

Changes

  • aw-transform/src/classify.rs: cache added to categorize(); dead categorize_one() helper removed (logic inlined into the cached loop)
  • New test test_categorize_cache_correctness: 101 events (50 + 1 + 50), two distinct data shapes → verifies correct category for each shape and no false cache collisions

Testing

cargo test -p aw-transform --lib
# 42 passed; 0 failed

…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-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown

Greptile Summary

The PR accelerates event categorization by caching categories under serialized event-data keys and reusing them for repeated payloads.

  • Replaces per-event rule evaluation with a function-local category cache.
  • Preserves category hierarchy selection while inserting the cached result into each event.
  • Adds a correctness test covering repeated payloads and two distinct categories.

Confidence Score: 4/5

The 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

Filename Overview
aw-transform/src/classify.rs Adds a data-keyed categorization cache and regression test; behavior appears correct for current data-only rules, but the performance claim lacks measurement and the map-order comment is inaccurate.

Reviews (1): Last reviewed commit: "perf(aw-transform): cache categorize res..." | Re-trigger Greptile

Comment on lines +121 to +125
///
/// 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%.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Comment on lines +131 to +133
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
// 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

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.37%. Comparing base (656f3c9) to head (b81642d).
⚠️ Report is 90 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ErikBjare

Copy link
Copy Markdown
Member

@TimeToBuildBob Do the same PR for aw-server-python/aw-core

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Done — ActivityWatch/aw-core#150 applies the same cache to categorize() and tag() in aw_transform/classify.py.

One note on this PR's CI: the macOS failure is a pre-existing SQLite schema issue in aw-datastore ("duplicate column name: data" in test_push_does_not_reexport_synced_buckets) — it's in the aw-sync crate and unrelated to the aw-transform changes here.

@ErikBjare
ErikBjare merged commit 7429039 into ActivityWatch:master Aug 25, 2026
6 of 7 checks passed
ErikBjare pushed a commit to ActivityWatch/aw-core that referenced this pull request Aug 25, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AxiosError: timeout of 30000ms exceeded while trying to see activity for one month

2 participants