From b2a726298159b4d84bf8573665b2b4afd1ab67ca Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Sat, 19 Sep 2026 14:39:41 +0200 Subject: [PATCH] Eager-load host and chapter-organiser chains for event listings /events/upcoming issued ~31 queries per render (production median 467ms). Three per-card query sources, all association hops the existing eager loads didn't cover: - WorkshopPresenter#venue -> model.host -> workshop_host.sponsor: the belongs_to sponsor behind the has_one :host through-chain now loads with the workshop (13 single-sponsor queries on the production page). - WorkshopPresenter#organisers falls back to the chapter's organiser permission members; that hop now loads with the chapter, and chapter_organisers reads the loaded collections instead of find_by (which would bypass the association cache). - latest_model_updated ran its 4 MAX queries twice (once for fresh_when's Last-Modified, once for the etag); compute it once. Against codebar_production_dump: 26 queries -> 9, warm render ~115ms. Fixes #2888 --- app/controllers/events_controller.rb | 10 +++++++--- app/presenters/workshop_presenter.rb | 2 +- .../event_card_render_query_cost_spec.rb | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index ded3436e5..c7d7d6100 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -8,13 +8,15 @@ def index end def upcoming - fresh_when(latest_model_updated, etag: latest_model_updated) + latest = latest_model_updated + fresh_when(latest, etag: latest) @events, @pagy = fetch_upcoming_events end def past - fresh_when(latest_model_updated, etag: latest_model_updated) + latest = latest_model_updated + fresh_when(latest, etag: latest) @past_events, @pagy = fetch_past_events end @@ -166,7 +168,9 @@ def load_events(rows) (hash[row['event_type']] ||= []) << row['id'].to_i end - workshops = Workshop.eager_load(:chapter, :sponsors, :organisers, :permissions, :workshop_host) + workshops = Workshop.eager_load(:sponsors, :organisers, :permissions, + workshop_host: :sponsor, + chapter: { permissions: :members }) .where(id: grouped['Workshop']) .to_a.index_by(&:id) meetings = Meeting.eager_load(:venue, :organisers, :permissions).where(id: grouped['Meeting']) diff --git a/app/presenters/workshop_presenter.rb b/app/presenters/workshop_presenter.rb index bd57e7be8..e26252a4d 100644 --- a/app/presenters/workshop_presenter.rb +++ b/app/presenters/workshop_presenter.rb @@ -127,6 +127,6 @@ def map_member_details(organisers, member, role) end def chapter_organisers - model.chapter.permissions.find_by(name: 'organiser')&.members || [] + model.chapter.permissions.to_a.find { |permission| permission.name == 'organiser' }&.members || [] end end diff --git a/spec/requests/event_card_render_query_cost_spec.rb b/spec/requests/event_card_render_query_cost_spec.rb index f11028ffe..a3bc835c2 100644 --- a/spec/requests/event_card_render_query_cost_spec.rb +++ b/spec/requests/event_card_render_query_cost_spec.rb @@ -35,6 +35,25 @@ def count_queries expect(four_cards).to be <= one_card + 8 end + it 'does not add per-card queries for hosted workshops on /events/upcoming' do + chapter = Fabricate(:chapter, active: true) + host_sponsor = Fabricate(:sponsor) + first_workshop = Fabricate(:workshop_no_sponsor, chapter:) + Fabricate(:workshop_sponsor, workshop: first_workshop, sponsor: host_sponsor, host: true) + + get '/events/upcoming' + one_card = count_queries { get '/events/upcoming' } + + 3.times do + workshop = Fabricate(:workshop_no_sponsor, chapter:) + Fabricate(:workshop_sponsor, workshop:, sponsor: host_sponsor, host: true) + end + + four_cards = count_queries { get '/events/upcoming' } + + expect(four_cards).to eq(one_card) + end + it 'does not add per-card queries on the chapter page as cards grow' do chapter = Fabricate(:chapter, active: true) organiser = Fabricate(:member)