Skip to content

perf(store): cache GitHub API calls for faster plugin store loading - #251

Merged
ChuckBuilds merged 1 commit into
mainfrom
perf/plugin-store-caching
Feb 15, 2026
Merged

perf(store): cache GitHub API calls for faster plugin store loading#251
ChuckBuilds merged 1 commit into
mainfrom
perf/plugin-store-caching

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Feb 15, 2026

Copy link
Copy Markdown
Owner

Summary

The plugin store pages were extremely slow (10-30 seconds) due to excessive GitHub API calls:

  • Installed plugins endpoint: Called get_plugin_info() for each plugin (3 GitHub API calls each) just to read verified — which is already in the cached registry. Now uses new get_registry_info() (zero API calls)
  • _get_latest_commit_info(): No cache. All 31 monorepo plugins share the same repo URL, causing 31 identical API calls. Now cached for 5 minutes (keyed by repo:branch)
  • _fetch_manifest_from_github(): No cache. 31 calls per store load. Now cached for 5 minutes
  • load_config(): Called inside the plugin loop (once per plugin) in both api_v3.py and pages_v3.py. Hoisted outside the loop
  • Install/update safety: These operations pass force_refresh=True to bypass caches and always get the real latest commit SHA from GitHub

Expected improvement

Scenario Before After
Installed plugins ~6-15s ~50ms
Store list (first load) ~13-30s ~1-2s
Store list (cached) ~13-30s ~50ms

Test plan

  • Open Plugin Manager tab — installed plugins should load near-instantly
  • Switch to Store tab — should load in 1-2s instead of 10-30s
  • Refresh Store tab — should be near-instant (cached)
  • Verify installed plugins show: name, version, enabled, verified badge, last updated, commit info
  • Verify store plugins show: stars, last updated, commit info, description
  • Push a plugin update to GitHub, then click Update on the Pi — should pick up the new commit immediately
  • Install a new plugin — should work normally

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance

    • Plugin metadata is now cached to improve load times and reduce API requests.
    • Cache automatically refreshes at configurable intervals.
  • New Features

    • Added ability to force refresh plugin information on demand.
  • Improvements

    • Plugin lookups now use registry-based retrieval for improved reliability and consistency.

The plugin store was making excessive GitHub API calls causing slow
page loads (10-30s):

- Installed plugins endpoint called get_plugin_info() per plugin (3
  GitHub API calls each) just to read the `verified` field from the
  registry. Use new get_registry_info() instead (zero API calls).
- _get_latest_commit_info() had no cache — all 31 monorepo plugins
  share the same repo URL, causing 31 identical API calls. Add 5-min
  cache keyed by repo:branch.
- _fetch_manifest_from_github() also uncached — add 5-min cache.
- load_config() called inside loop per-plugin — hoist outside loop.
- Install/update operations pass force_refresh=True to bypass caches
  and always get the latest commit SHA from GitHub.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 15, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces caching for GitHub commit and manifest data in the plugin store manager, with optional force_refresh parameter to bypass caches during installation and updates. It also adds a new get_registry_info() method for registry-only lookups and updates the web interface to use this method, reducing unnecessary GitHub API calls.

Changes

Cohort / File(s) Summary
Plugin Store Caching
src/plugin_system/store_manager.py
Added commit_info_cache and manifest_cache with timeout-based expiration. Extended _fetch_manifest_from_github() and _get_latest_commit_info() with force_refresh parameter to bypass caches. Introduced new get_registry_info() helper for registry-only metadata retrieval. Propagated force_refresh through install_plugin(), update_plugin(), and get_plugin_info().
Web Interface API Updates
web_interface/blueprints/api_v3.py, web_interface/blueprints/pages_v3.py
Replaced get_plugin_info() calls with get_registry_info() for plugin verification. Optimized config loading by loading configuration once before iterating plugins instead of per-plugin.

Sequence Diagram

sequenceDiagram
    participant Client
    participant StoreManager as Store Manager
    participant Cache as Cache Layer
    participant GitHub as GitHub API
    
    Client->>StoreManager: install_plugin(id, force_refresh=True)
    StoreManager->>Cache: Check manifest_cache
    alt force_refresh=True
        StoreManager->>GitHub: Fetch manifest
        GitHub-->>StoreManager: manifest.json
        StoreManager->>Cache: Update manifest_cache
    else cache hit and valid
        Cache-->>StoreManager: Cached manifest
    end
    StoreManager-->>Client: Installation result
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ 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 captures the main objective: introducing caching for GitHub API calls to improve performance. It directly relates to the core changes across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf/plugin-store-caching

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/plugin_system/store_manager.py (1)

611-636: Manifest caching logic looks correct, with one minor observation.

When the primary branch returns 404 and the fallback to "main" succeeds (line 628–633), the result is cached under the original branch key (e.g., owner/repo:master:manifest.json). A subsequent request explicitly for branch="main" will miss this cached entry and make another HTTP call. This is a minor inefficiency—not a bug—and only affects the uncommon case where a branch doesn't exist.


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.

@ChuckBuilds
ChuckBuilds merged commit 22c495e into main Feb 15, 2026
1 check passed
@ChuckBuilds
ChuckBuilds deleted the perf/plugin-store-caching branch February 15, 2026 19:46
ChuckBuilds added a commit that referenced this pull request Aug 21, 2026
The heading read "B5 — adoption is safe by construction", which this same
document disproves two sections later: four of the eight adopted plugins
shipped with scroll mode broken on a 3.2.0 core and were repaired in
plugins #251.

The body was already careful -- it says fallback compatibility is what is
guaranteed, and that correctness on a core which *does* ship the module
needs object-level and scroll-mode validation. The heading was not, and a
heading is what a reader scanning the plan actually takes away.

Retitled to name both halves, with a sentence up front saying why the
unqualified claim is false and pointing at the retrospective that shows
it. The phase intro said "one of them is safe by construction and the
other is not"; that now says what it actually means -- one cannot break a
user on an old core, the other can.

The second review point, MD018 on the ATX heading at line 409, does not
reproduce: that line now begins "(#431, #433)" rather than "#433)", so
there is no bare-hash heading. `grep -cE '^#+[^ #]'` returns 0 for the
whole file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
ChuckBuilds added a commit that referenced this pull request Sep 2, 2026
* docs(sports): record where B6 stands, and why it is waiting

The phase table had B4 as "next" and B5 as "after B4" while both had shipped,
and described B6 as blocked on B4's gate — which is now merged and released. A
plan that misreports which phase it is in is worse than no plan: the next
person reads it and repeats finished work.

Corrected, and three things that were only ever decided in conversation are now
written down:

  * **B6 is deliberately held.** 3.2.0 published 2026-08-03; 3.1.0 ran nine
    months before it. B6's premise is that cores without the module are gone,
    and there is no release-asset count or install telemetry to show that.
    Running it now strands users on their current plugin versions. The gate
    that makes it safe is already built and tested — it is the calendar that is
    missing, and no amount of further code changes that.
  * **Stop adopting further shared modules** (data_sources, game_renderer,
    base_odds_manager) until B6 closes. Each adoption adds a copy to keep in
    step against a payoff contingent on B6.
  * **A B5 retrospective**, because "the adoption went fine" is not what
    happened: four of eight shipped with scroll mode broken on a 3.2.0 core.
    The bundled fallback did not protect against it — the break was on the
    modern path — which is an argument for the sunset, not against it. Records
    the ledger too: net negative on disk until B6 runs.

Also replaces the "what's next" list, whose first five items were all done,
with what actually remains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5

* fix: apply CodeRabbit auto-fixes

Fixed 1 file(s) based on 2 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

* docs(sports): stop a wrapped PR reference reading as a heading

A line wrapped onto "#433), the newest manifest entry ...", which
markdownlint reads as a malformed ATX heading (MD018). Reflowed so the
line starts with "(#431, #433)" instead.

Not the suggested fix: adding a space after the hash would have turned
the PR reference into "# 433". The B5 safety claim raised alongside this
was already corrected in ac44b5a.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5

* docs(sports): scope the B5 safety claim to the fallback

The heading read "B5 — adoption is safe by construction", which this same
document disproves two sections later: four of the eight adopted plugins
shipped with scroll mode broken on a 3.2.0 core and were repaired in
plugins #251.

The body was already careful -- it says fallback compatibility is what is
guaranteed, and that correctness on a core which *does* ship the module
needs object-level and scroll-mode validation. The heading was not, and a
heading is what a reader scanning the plan actually takes away.

Retitled to name both halves, with a sentence up front saying why the
unqualified claim is false and pointing at the retrospective that shows
it. The phase intro said "one of them is safe by construction and the
other is not"; that now says what it actually means -- one cannot break a
user on an old core, the other can.

The second review point, MD018 on the ATX heading at line 409, does not
reproduce: that line now begins "(#431, #433)" rather than "#433)", so
there is no bare-hash heading. `grep -cE '^#+[^ #]'` returns 0 for the
whole file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW

* docs(sports): re-check the hold, and close two items that are already done

The remaining-work list had two entries that finished without the doc noticing,
which is the failure mode this file exists to prevent.

- The stale plugin-test tranche is gone. run_plugin_tests.py --all now reports
  174 passed, 2 skipped, 0 failed across the whole fleet. Recorded how to
  re-check it too: these are standalone scripts, not a pytest suite, and one
  calls sys.exit(1) at import, so pointing pytest at a plugin directory
  collapses into an INTERNALERROR that looks nothing like the real state.
- CLAUDE.md already says eight panel sizes.

That leaves the hardware soaks as the only open item needing work rather than
calendar time.

B6's prerequisite is now built -- core test/test_sports_sunset_matrix.py
(#505) -- so the phase table and the regression-test section say so, and the
two modelling traps it had to work through are recorded for whoever touches it
next: the copy-removed shape must be an unguarded import or the failure names
scroll_display_legacy instead of the core module, and only the leaf module may
be hidden because a pre-3.2.0 core still ships src/common/.

The hold itself is re-checked and unchanged: v3.2.0 is still latest,
__version__ is still 3.2.0, no 3.3.0, 23 days rather than the few months the
gate asks for. Also worth stating plainly -- the core updates by git pull, not
by downloading a release, so release-asset counts would not measure uptake even
if we had them. Whatever unblocks this has to come from the store side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
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