codebar / codebar/planner

Cache the rendered /sponsors page body — recurring 1.3s render stalls dyno threads

Open
#2,885 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

performance
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 maximum query 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.