Skip to content

perf(appstore): optimize apps.json processing and reuse stale cache on refresh failure#59891

Draft
joshtrichards wants to merge 12 commits intomasterfrom
jtr/perf-appstore-AppFetcher-releases
Draft

perf(appstore): optimize apps.json processing and reuse stale cache on refresh failure#59891
joshtrichards wants to merge 12 commits intomasterfrom
jtr/perf-appstore-AppFetcher-releases

Conversation

@joshtrichards
Copy link
Copy Markdown
Member

@joshtrichards joshtrichards commented Apr 23, 2026

Summary

Improve appstore refresh performance and resilience by reducing work in the hot path, avoiding unnecessary cache I/O, and reusing stale cached data when refresh fails.

Changes

  • refactor AppFetcher::fetch() to:
    • reuse a single VersionParser
    • memoize parsed platform/PHP version specs
    • select the best compatible release in a single pass
    • avoid temporary arrays and per-app sorting
  • clean up Fetcher::get() to:
    • return processed data directly after writing cache content
    • avoid immediately re-reading and decoding the just-written cache file
    • return stale cached data instead of [] when a refresh fails and a previous cache exists
  • tests
    • adjust to anticipate stale caching response behavior change
    • adjust to no longer expect reading data back from storage immediately after writing it
    • expand appfetcher coverage to include "highest compatible version wins"

Why

The appstore feed is large (~27 MB raw in local benchmarking), while the processed cache is much smaller (~3.4 MB). Most of the hot-path CPU cost comes from filtering and selecting compatible releases for each app. On refresh failures, serving the last known good cache is also more resilient than returning an empty app list.

Benchmarks

Local benchmarks on a real apps.json showed:

  • AppFetcher processing: ~1.4x faster
  • end-to-end local refresh path: ~1.09x–1.11x faster
  • Note: the end-to-end number is dominated by HTTP + JSON decode, which this PR doesn't change.

Notes

This change is intended to be behavior-preserving on successful refreshes and does not change the persisted cache format.

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

Signed-off-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Josh <josh.t.richards@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant