Reduce allocation churn of events#past - #2973
Merged
Merged
Conversation
The past/upcoming event listings eager-load associations the card render path never reads: root-level :permissions on Workshop, Meeting and Event (organisers is a through-permissions association, so its join already covers permissions; the chapter-organisers fallback reads chapter.permissions, which stays eager-loaded via the chapter subtree), and :sponsorships on Event (sponsors goes through sponsorships). Dropping them shrinks the cartesian join fan-out per workshop row. Rendered HTML is unchanged: organisers, sponsors, venue and the chapter-organisers fallback render identically, verified by rendering /hosts/past with the old and new eager lists in one process over the same fixtures. Cuts GC allocations per cache-miss render of GET /events/past by ~15% (87,196 -> 74,089 objects); steady-state warm renders drop ~6%.
The past and upcoming event actions run four separate MAX(updated_at) queries (Workshop, Meeting, Event, Member) on every request to build the ETag source. One UNION ALL subquery with a single MAX returns the same value in one round trip, with the same nil-when-empty semantics as the compact.max form it replaces. Cuts SQL statements per request from 9 to 6 and saves ~1.5% of GC allocations on both cache-miss and warm renders of GET /events/past.
fresh_when renders a 304 response without halting, so fetch_past_events and fetch_upcoming_events still ran on top of the already-performed response. Return early instead, mirroring the workshops#show guard. Production conditional-GET share is negligible (2 of 1,805 requests), so this is correctness hygiene rather than a measured win.
KimberleyCook
approved these changes
Oct 1, 2026
mroderick
marked this pull request as ready for review
October 1, 2026 09:48
olleolleolle
reviewed
Oct 1, 2026
olleolleolle
reviewed
Oct 1, 2026
olleolleolle
approved these changes
Oct 1, 2026
Co-authored-by: Olle Jonsson <olle.jonsson@gmail.com>
Co-authored-by: Olle Jonsson <olle.jonsson@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reduce per-request allocation churn of
GET /events/pastby 15.8% (87,196 -> 73,401 GC objects on the cache-miss render) and 7.3% on the steady-state warm render (46,830 -> 43,426), with SQL statements per request down from 9 to 6. Rendered output is unchanged. Three changes inEventsController:load_events: root-level:permissionson Workshop/Meeting/Event and:sponsorshipson Event are never read on the render path.organisersis a through-permissions association (its join already covers permissions),sponsorsgoes through sponsorships, and the chapter-organisers fallback readschapter.permissions, which stays eager-loaded. Dropping them shrinks the cartesian join fan-out per workshop row.latest_model_updatedfrom fourMAX(updated_at)queries (Workshop, Meeting, Event, Member) into one UNION ALL subquery with a single MAX. Same nil-when-empty semantics, one round trip instead of four; this also feeds the ETag.fresh_whenrenders a 304 without halting, so the fetch methods previously ran on top of the already-performed response. Mirrors the workshops#show guard.Measurements
GC.stat(:total_allocated_objects)deltas through the full request stack, median over 25 requests per phase, anonymous GET over a seeded 20-card page backed by ~1,850 records. Run-to-run noise within a process is ±2 objects; the benchmark is sensitive to Postgres plan changes in the eager queries, so each comparison below was taken from back-to-back runs inside a single plan window.Production drain logs (2026-09-24..30) put the warm path at the site's heaviest controller endpoint per request, so the warm column is the steady-state win.
Verification
app/Review notes
workshop.permissionsorevent.sponsorshipsdirectly would now lazy-load. The render path reads only organisers, sponsors, venue, chapter and the chapter-organisers fallback, all still eager-loaded.fresh_when; semantics (including nil when all tables are empty) match the previous four-query form.