Skip to content

perf(cache): skip rewriting unchanged CacheManager.set() records - #730

Merged
ChuckBuilds merged 7 commits into
mainfrom
perf/cache-write-skip
Oct 3, 2026
Merged

ChuckBuilds merged 7 commits into
mainfrom
perf/cache-write-skip

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

Summary

DiskCache.set() already skips rewriting a payload identical to the last one it wrote, but for CacheManager.set() that check never fired: every record embeds timestamp: time.time(), so the bytes always differed and every unchanged re-save was a full SD-card write. The digest now leaves out a header-first record's timestamp. A skipped write moves the file's mtime to the new timestamp instead, readers take a record's age from the newer of the two, and the mtime is never trusted more than an hour past the embedded timestamp (see the "UNCHANGED RE-SAVES" comment in disk_cache.py).

Also in this PR:

  • per-plugin plugin_metrics:<id> records become one plugin_metrics_snapshot written at most once a minute (the old per-plugin records are still read as a fallback, never written)
  • CacheManager builds its ConfigManager on first use. Nothing in the cache reads it, and the sports plugins that use cache_manager.config_manager get the same object, just later.

Type of change

  • Bug fix

Test plan

  • Ran on a real Raspberry Pi with hardware: hdpi (Pi 4, 512x64), 40-min A/B/B/A soak against main, each window starting from a fresh restart. Counts are from inotifywait on /var/cache/ledmatrix, after a 5-min warmup:

    main (A1) this branch main (A2)
    cache file writes / min 38.3 8.6 36.4
    mtime-only touches / min 0 13.3 0
    plugin_metrics* writes 816 33 807

    No frame-timing change in Vegas mode (same music load, minus warmup): 81.3 fps mean / 20.7 ms median p99, against 81.7 / 20.9 on main. RSS 333 MB vs 326 and 351 MB on main. Journal errors were unchanged (4–6 per window, all youtube-stats "API key not configured").

  • Ran the test suite (pytest): new test_cache_unchanged_resave.py (skip, mtime lift, cap, copied/touched/replaced files, other process's file), test_cache_manager_lazy_config.py and test_resource_monitor.py additions. The full suite passes, apart from 8 test_live_status_fields.py tests that need psutil and fail the same way on main.

Plugin compatibility

  • No plugin breakage expected (nothing in ledmatrix-plugins reads plugin_metrics:*. cache_manager.config_manager is still public.)

Notes for reviewer

  • The display and web processes both read, modify and write the metrics snapshot with no cross-process lock, so a reset from the web UI can be overwritten by the display's next write. The old per-plugin records had the same race, and the display rewrites every minute.
  • A file copied without -p or touched reads at most an hour fresher than its contents. Short-TTL records from yesterday stay stale.
  • Before the soak I stopped one aborted run after a music plugin (ledmatrix-plugins fix(install): parse the sudoers rules before installing them #602) added scroll stalls that had nothing to do with this branch. Its cache-write count agreed: 40.4 → 15.2 / min.

🤖 Generated with Claude Code

ChuckBuilds and others added 4 commits October 2, 2026 15:13
DiskCache skipped a payload identical to its last write, but
CacheManager.set() stamps each record with time.time(), so the payload
never matched and every unchanged re-save rewrote the file. The digest
now leaves out a header-first record's timestamp (ttl and data still
count) and the newer timestamp lives in the file's mtime: a skipped save
touches mtime to the record's timestamp, and a real write pins mtime to
the record's own timestamp so an old-stamped record cannot borrow the
write time's freshness.

Readers age a record from the newer of the two, bounded at one hour
past the embedded timestamp (unchanged data is rewritten hourly, so an
honest lift never reaches the bound and a copy without mtime reads at
most an hour fresher). DiskCache.get returns the record with the
effective timestamp, so the header check, CacheManager max_age logic,
the memory tier and plugins reading record['timestamp'] all agree. The
skip also checks the file's inode and size, so a write by another
process is never mistaken for ours.

100 identical set() of a 32 KB record: 100 writes before, 1 after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uc5DAbSwGGUTm3MrCtpC2m
The resource monitor wrote a plugin_metrics:<id> record per plugin,
throttled to 30 s each: two SD writes a minute per plugin. All plugins
now share one plugin_metrics_snapshot record ({"schema": 1, "plugins":
{id: record}}, each record shaped as before), written at most once a
minute.

Readers (the web UI's /plugins/metrics routes, through get_metrics)
return the same fields: they read the snapshot and fall back to the old
per-plugin record for a plugin it does not have yet, so an upgrade
loses nothing. Each write starts from the snapshot on disk and replaces
only the plugins this process recorded, so plugins not run since a
restart keep their numbers and a web-side reset of an idle plugin
sticks. Entries idle for 30 days are dropped, mirroring the retention
that used to age out their files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uc5DAbSwGGUTm3MrCtpC2m
Every CacheManager built a ConfigManager and loaded the config in
__init__, for a CacheStrategy that keeps the parameter but reads
nothing from it. config_manager is now a property that builds and loads
it on first access, so plugins resolving the global timezone through
cache_manager.config_manager get the same object as before; assignment
still replaces it, an ImportError still yields None, and a failing
load_config() still raises (now at first access) and is retried on the
next one. CacheStrategy is no longer handed the config manager.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uc5DAbSwGGUTm3MrCtpC2m
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 33 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bc92e5b9-0f94-4567-9ac8-d6a6b7ee09ef
📥 Commits

Reviewing files that changed from the base of the PR and between 0d179fd and 9953175.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • src/cache/disk_cache.py
  • src/cache_manager.py
  • src/error_aggregator.py
  • src/plugin_system/resource_monitor.py
  • test/test_cache_manager_lazy_config.py
  • test/test_cache_unchanged_resave.py
  • test/test_espn_scoreboard_cache.py
  • test/test_resource_monitor.py
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 42 complexity

Metric Results
Complexity 42

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

ChuckBuilds and others added 3 commits October 3, 2026 12:19
The disk cache now reads a record's age from the newer of its embedded
timestamp and the file's mtime (an unchanged re-save only touches the
file). The #728 tests aged a record by rewriting its timestamp, which
moved the mtime to now, so the record read as fresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ChuckBuilds
ChuckBuilds merged commit 7bb85c0 into main Oct 3, 2026
15 checks passed
@ChuckBuilds
ChuckBuilds deleted the perf/cache-write-skip branch October 3, 2026 18:14
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