-
Notifications
You must be signed in to change notification settings - Fork 0
Data Cache and Metrics
Local data caching and perf instrumentation for the Proton Pulse Decky plugin.
The plugin fetches ProtonDB report data and summaries from the CDN and live ProtonDB API, and fetches vote totals from Supabase. Without caching, every game view triggers multiple HTTP requests. The cache system stores fetched data locally so repeated views are instant, and the metrics system tracks timing data for profiling bottlenecks.
Hybrid frontend-backend design:
-
Cache reads: In-memory
Mapin the frontend (sync, fast) backed bylocalStoragefor persistence across plugin reloads -
Prefetch: Frontend enumerates installed games via
SteamClient.collectionStoreon startup, fetches data for recently played games in the background -
Metrics collection: Frontend
metrics.tscollects timing spans and counters in a ring buffer -
Backend persistence: Python
export_metricscallable writes metrics JSON to~/.config/decky-proton-pulse/metrics/for offline analysis
Plugin Init
|
+-------+-------+
| |
initCache() startAutoFlush()
| |
load from localStorage start 5min flush timer
|
(5s delay)
|
runStartupPrefetch()
|
enumerate installed games
via collectionStore
|
filter to recently played
(last 30 days)
|
skip games already cached
|
prefetch up to 50 games
(3 concurrent, 200ms throttle)
| File | Layer | Purpose |
|---|---|---|
src/lib/cache.ts |
Frontend | In-memory + localStorage cache. TTL, LRU eviction, get/set/invalidate |
src/lib/metrics.ts |
Frontend | Timing spans, counters, ring buffer, summary text, disk export |
src/lib/prefetch.ts |
Frontend | Startup prefetch. Enumerates games, warms cache |
lib/metrics_export.py |
Backend | Writes metrics JSON to disk, prunes old files |
src/components/CacheManagerModal.tsx |
Frontend | Advanced Settings UI for cache management |
Two layers:
-
In-memory:
Map<string, CacheEntry>- instant sync reads -
localStorage: Serialized
CacheEntry[]under keyproton-pulse:data-cache- survives plugin reloads
On init, entries are loaded from localStorage into the Map. Expired entries are discarded during load.
Default 24 hours, configurable via Settings (Advanced Settings > Cache TTL slider, 1-168 hours). Stored in proton-pulse:cache-ttl-hours.
When the cache exceeds 200 entries, the least-recently-accessed entries are evicted. Every getCached() call updates lastAccessedAt.
interface CacheEntry {
appId: string;
reports: CdnReport[];
summary: ProtonDBSummary | null;
votes: Record<string, { upvotes: number; downvotes: number }>;
cachedAt: number; // when data was fetched
lastAccessedAt: number; // for LRU
source: 'cdn' | 'live-summary' | 'prefetch';
}All data access in protondb.ts now follows:
- Check
getCached(appId)- returns entry if valid and not expired - On miss: fetch from network as before
- On success:
setCache(appId, ...)to write through to both layers - On error: return empty/fallback as before (no caching of errors)
On plugin startup (5s delay to let Steam settle):
- Enumerate all installed games from
collectionStore.allAppsCollection - Filter to games played in the last 30 days, sorted by most recent first
- Skip games already in cache (from localStorage persistence)
- Fetch up to 50 games with concurrency=3 and 200ms throttle between batches
- Each game fetches: CDN index + year files, ProtonDB summary, and live vote totals
metrics.ts provides:
-
startSpan(category, label)- returns a closer function. Call it when done. -
startDetailedSpan(category, label)- returns{ end(success, metadata) }for tracking success/failure -
Counters:
countCacheHit(),countCacheMiss(),countCacheEviction(),countFetch(),countFetchError(),countPrefetchedGame()
Categories: cache-read, cache-write, fetch-cdn-index, fetch-cdn-year, fetch-cdn-votes, fetch-live-summary, fetch-steam-title, prefetch-game, prefetch-batch
Timing entries are stored in a ring buffer (max 2000 entries). When full, oldest entries are overwritten. Memory-bounded for long Deck sessions.
getSummary() returns per-category aggregates:
- count, totalMs, avgMs, minMs, maxMs, p95Ms, errorCount
- Global counters: cache hits/misses/evictions, fetch errors, prefetched games
getSummaryText() returns a human-readable text block shown in the Logs tab.
Every 5 minutes (and on plugin unload), metrics are flushed to disk via the export_metrics Python callable. Files land at:
~/.config/decky-proton-pulse/metrics/metrics-YYYYMMDD-HHMMSS.json
Old files are pruned to keep the last 50. Each file contains the full summary plus all raw timing entries.
- In-plugin: Logs tab has Show/Hide Metrics button with Refresh and Export to Disk
-
On disk: JSON files in
~/.config/decky-proton-pulse/metrics/for external graphing
Hidden behind the Advanced Settings toggle in General Settings:
- Toggle: Settings > Advanced Settings (off by default)
- Cache TTL slider: 1-168 hours (default 24)
-
Manage Cache: Opens inline cache manager showing all cached games
- Text filter for searching by name or app ID
- Games sorted by most recently accessed
- Per-game refresh button (re-fetches from CDN)
- Per-game delete button (removes from cache)
- Clear All button with confirmation
Every cache decision is logged via logFrontendEvent at DEBUG level:
-
Cache hit- appId, age, source, report count -
Cache miss- appId -
Cache expired- appId, age -
Cache write- appId, report count, source, total entries -
Cache eviction ran- evicted count, remaining -
Prefetch starting- count, first few IDs -
Prefetch complete- succeeded, failed, total -
Metrics flushed to disk- entry count, success
Enable Debug Logs in Settings to see these in the Logs tab.