Skip to content

perf(cache): mutex-guard memory map + periodic cleanup + 24h home TTL (E4.1)#548

Merged
rainxchzed merged 2 commits into
mainfrom
perf/e4-1-caching-audit
May 8, 2026
Merged

perf(cache): mutex-guard memory map + periodic cleanup + 24h home TTL (E4.1)#548
rainxchzed merged 2 commits into
mainfrom
perf/e4-1-caching-audit

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 8, 2026

First slice of E4 Performance Pass. Pure static-friendly changes; no profiler required.

What & why

1. Thread-safe memory cache

CacheManager.memoryCache is a plain HashMap mutated from any caller's coroutine — Home, Details, Search, Profile repositories all hit it concurrently from their respective ViewModel scopes. Concurrent structural mutations (put during another caller's rehash) can corrupt internal state. Added kotlinx.coroutines.sync.Mutex and wrapped every read/write touching the map.

memoryCache, memoryCacheMutex, cacheDao, and json are now @PublishedApi internal so the inline reified get<T>() / put<T>() keep working from feature-module call sites without the map being effectively public-API.

2. Periodic janitor for the in-memory cache

cleanupExpired() was defined but had no callers. Memory map grew unbounded over a long session. CacheManager constructor now takes an optional appScope: CoroutineScope? and launches a 1h janitor loop that runs cleanupExpired() off the critical path. Mutex serialises it with foreground reads/writes. Wired in coreModule Koin via appScope = get().

3. HOME_REPOS TTL 12h → 24h

Per E4 spec recommendation. Home grid is browsed often, tolerates stale data well, longer TTL cuts cold-start network round-trips on the highest-traffic surface.

Test plan

  • :core:data:compileDebugKotlinAndroid
  • :core:data:compileKotlinJvm
  • Local CodeRabbit: 0 findings (after applying the @PublishedApi internal visibility fix from the first pass).
  • Manual smoke deferred until full E4 lands — the change is ABI-compatible for callers and the mutex semantics preserve the original happy-path behaviour.

Out of scope (later E4 slices)

  • E4.2 — bouncy Spring sweep (survey Refactor: Introduce BrowserHelper and ClipboardHelper #16).
  • E4.3 — defer non-critical Application.onCreate / MainActivity.onCreate work.
  • E4.4 — verify DetailsViewModel.loadDetails parallel async {}.
  • E4.5 — Macrobenchmark + scroll profile (needs Pixel 6 device).
  • LRU eviction on memoryCache — bounded growth is good-enough with periodic cleanup; LRU adds complexity, defer until profile shows it matters.
  • Stale-while-revalidate fallback in Home / Search repositories — Details already uses getStale correctly; Home / Search don't. Separate PR.

Summary by CodeRabbit

  • New Features

    • Automatic periodic background cache cleanup to remove expired entries.
  • Bug Fixes

    • Safer in-memory cache concurrency handling to prevent race conditions and accidental evictions.
    • Conditional eviction behavior to avoid removing freshly written entries after decode failures.
  • Performance

    • Increased home repository cache retention from 12 to 24 hours.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

CacheManager now protects the in-memory cache with a Mutex across all operations (get, put, invalidate, invalidateByPrefix, clearAll, cleanupExpired). An optional appScope parameter enables a background janitor coroutine that periodically calls cleanupExpired(). Cache TTL for HOME_REPOS increased to 24 hours, and the DI wiring passes appScope to CacheManager on construction.

Changes

Cache Concurrency & Background Cleanup

Layer / File(s) Summary
Constructor & Thread Safety Setup
core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt
CacheManager constructor adds optional appScope, initializes memoryCache and memoryCacheMutex, and launches periodic cleanup coroutine when appScope is provided.
Get: Mutex-Protected Read & Conditional Eviction
core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt
get performs all in-memory reads/writes/removals under memoryCacheMutex, conditionally evicts snapshots only when unchanged, uses cacheDao.deleteIfMatches on DB decode failure, and caches DB hits under the mutex.
Put: In-Memory Then Persist
core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt
put writes the in-memory snapshot under memoryCacheMutex before persisting via cacheDao.put.
Invalidate (single key)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt
invalidate removes the in-memory entry under memoryCacheMutex and calls cacheDao.delete.
Invalidate By Prefix
core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt
invalidateByPrefix computes matching in-memory keys and removes them under memoryCacheMutex, then calls cacheDao.deleteByPrefix.
Clear All
core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt
clearAll clears the in-memory cache under memoryCacheMutex then calls cacheDao.deleteAll.
Cleanup Expired
core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt
cleanupExpired computes expired keys from the mutex-protected memoryCache, removes them under the mutex, then calls cacheDao.deleteExpired(currentTime).
Configuration Constants
core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt
CacheTtl.HOME_REPOS increased to 24 hours; added CacheTtl.CLEANUP_INTERVAL_MS = 60 minutes.
Dependency Injection
core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt
Koin binding for CacheManager updated to pass both cacheDao and appScope when constructing the singleton.
DAO: Conditional Delete
core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/CacheDao.kt
Added suspend fun deleteIfMatches(key: String, cachedAt: Long) Room query to delete a row only when key and cachedAt both match.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hop between maps and locks so tight,

I guard each snapshot through the long night,
A janitor rabbit hums a tidy tune,
Sweeps expired crumbs by the light of the moon,
Threads now sleep safe beneath my watchful sprite.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: mutex-guarding the memory cache, adding periodic cleanup, and extending HOME_REPOS TTL to 24 hours, all of which are the core changes reflected in the file modifications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/e4-1-caching-audit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt`:
- Around line 55-67: In CacheManager.get(), avoid unconditionally removing an
in-memory or DB entry based on a stale snapshot: when you read the snapshot from
memoryCache under memoryCacheMutex (the local cached / (expiresAt, jsonData)
pair), decode jsonData before mutating any cache or DB, and only remove from
memoryCache if the current memoryCache[key] still exactly equals the snapshot
you read (compare the tuple) to avoid evicting a newer put(); for the DB-backed
branch, when purging malformed or expired rows use a conditional delete tied to
the exact row/version (e.g., include the original row id/version in the WHERE)
rather than a plain delete(key). Ensure these checks are applied in both the
decode-failure and expiry branches so remove(key)/delete(key) is only executed
when the stored value still matches the snapshot.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 253098d0-6ffd-4b68-93f6-99179d4c452e

📥 Commits

Reviewing files that changed from the base of the PR and between a6d2be3 and 1768c2f.

📒 Files selected for processing (2)
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt

@rainxchzed rainxchzed merged commit 6d1197b into main May 8, 2026
1 check was pending
@rainxchzed rainxchzed deleted the perf/e4-1-caching-audit branch May 8, 2026 13:57
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.

1 participant