Cache the rendered /sponsors page body — recurring 1.3s render stalls dyno threads
Nobody has claimed this yet.
- Dominant language
- Ruby
- Stars
- 104
- Forks
- 205
- Avg merge
- 1d 5h
- Merged PRs (30d)
- 63
Description
Summary
/sponsors is the public sponsors page: SponsorsController#index loads every active sponsor, groups them by level, and the view renders a logo grid per level from shared/sponsors → a per-sponsor partial with cached: true. It re-renders 721 sponsor fragments on every request, even when those fragments are cache hits. That costs ~1.3s of CPU-bound Ruby per render (~123 requests in a 3h window). On production dynos (puma, 1 worker × 3 threads) a CPU-bound render holds the GVL and stalls unrelated requests on the same dyno. The slowest renders reach 17–21s.
Proposal: cache the rendered page body in Solid Cache, keyed on the latest sponsors.updated_at, so a warm request costs one cache read.
Measured
From production canonical logs (3h window, 123 requests) and a local reproduction against a production dump:
| Metric | Value |
|---|---|
| median view_runtime (warm, fragments all hits) | 1,337ms |
| allocations per render | ~2.5M (17.7M worst) |
| db_runtime / queries | 41ms / 5 queries |
| cold render after dyno restart | 17.8s local repro, 21.3s in production |
| page size | ~210KB HTML, 721 logos |
Local reproduction: cold 17.8s, warm ~1.1s — matching production's max and median.
Why the per-sponsor fragments don't help
#2803 fragment-cached each sponsor partial (cached: true) and removed the CarrierWave allocation hot spot. The fragments do live in Solid Cache in production (config.cache_store = :solid_cache_store, which Rails also uses for collection caching — not the default per-process MemoryStore as hypothesised in #2883), so restarts don't wipe them. But the per-request cost is still dominated by the collection machinery: building 721 fragment keys, reading 721 entries back from the DB-backed cache store, and assembling a 210KB page — all per render, scaling with sponsor count.
The 17–21s cold renders are not explained by a wiped in-process store. The likely mechanism is Solid Cache eviction or expiry forcing a full re-render plus 721 fragment writes on the first request after a deploy, but that needs verification before we lean on it.
Proposed fix
Cache the whole rendered body, keyed on the most recent sponsor change:
def index
key = "sponsors/index/#{Sponsor.active.maximum(:updated_at)&.to_fs(:usec)}"
body = Rails.cache.fetch(key) do
@sponsor_levels = Sponsor.active.group_by(&:level)
render_to_string
end
render html: body.html_safe
end
Properties:
- Warm requests cost one
maximumquery plus one Solid Cache read — the 721-fragment read and the sponsor load only run on a miss - Solid Cache is DB-backed, so the body survives dyno restarts and deploys
- Any sponsor edit bumps
updated_at→ new key → one re-render, then warm again - Stale window: none (key changes the moment a sponsor is saved)
Expected impact
Median /sponsors view drops from ~1,337ms to ~10–20ms (one cache read; a full re-render only after a sponsor edit), and the recurring GVL stall disappears from both dynos.
Related
- #2883 — the original diagnosis; its "per-dyno memory store" hypothesis is corrected above
- #2803 — the merged fragment caching + indexes this builds on
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at SponsorsController#index and follow the shared/sponsors view and per-sponsor partial described in the issue. Check the existing Rails cache configuration and the /sponsors request path, then implement and verify a whole-body cache keyed by the latest active sponsor update. Done means warm requests avoid the 721-fragment render work and sponsor edits produce fresh output without a stale window.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rails, ruby
- Domain
- backend, performance
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100