Skip to content

Data Cache and Metrics

Michael DeGuzis edited this page Apr 8, 2026 · 2 revisions

Data Cache and Performance Metrics

Local data caching and perf instrumentation for the Proton Pulse Decky plugin.

Overview

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.

Architecture

Hybrid frontend-backend design:

  • Cache reads: In-memory Map in the frontend (sync, fast) backed by localStorage for persistence across plugin reloads
  • Prefetch: Frontend enumerates installed games via SteamClient.collectionStore on startup, fetches data for recently played games in the background
  • Metrics collection: Frontend metrics.ts collects timing spans and counters in a ring buffer
  • Backend persistence: Python export_metrics callable 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)

Files

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

Cache Design

Storage

Two layers:

  1. In-memory: Map<string, CacheEntry> - instant sync reads
  2. localStorage: Serialized CacheEntry[] under key proton-pulse:data-cache - survives plugin reloads

On init, entries are loaded from localStorage into the Map. Expired entries are discarded during load.

TTL

Default 24 hours, configurable via Settings (Advanced Settings > Cache TTL slider, 1-168 hours). Stored in proton-pulse:cache-ttl-hours.

LRU Eviction

When the cache exceeds 200 entries, the least-recently-accessed entries are evicted. Every getCached() call updates lastAccessedAt.

Cache Entry Shape

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';
}

Read-Through Pattern

All data access in protondb.ts now follows:

  1. Check getCached(appId) - returns entry if valid and not expired
  2. On miss: fetch from network as before
  3. On success: setCache(appId, ...) to write through to both layers
  4. On error: return empty/fallback as before (no caching of errors)

Prefetch

On plugin startup (5s delay to let Steam settle):

  1. Enumerate all installed games from collectionStore.allAppsCollection
  2. Filter to games played in the last 30 days, sorted by most recent first
  3. Skip games already in cache (from localStorage persistence)
  4. Fetch up to 50 games with concurrency=3 and 200ms throttle between batches
  5. Each game fetches: CDN index + year files, ProtonDB summary, and live vote totals

Performance Metrics

Collection

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

Ring Buffer

Timing entries are stored in a ring buffer (max 2000 entries). When full, oldest entries are overwritten. Memory-bounded for long Deck sessions.

Summary

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.

Export

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.

Viewing Metrics

  • 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

Cache Management UI

Hidden behind the Advanced Settings toggle in General Settings:

  1. Toggle: Settings > Advanced Settings (off by default)
  2. Cache TTL slider: 1-168 hours (default 24)
  3. 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

Debug Logging

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.

Clone this wiki locally