Skip to content

Reduce Puma memory usage by fixing N+1s and Ruby-side data processing#764

Merged
andrew merged 2 commits intomainfrom
fix/memory-optimizations
Apr 4, 2026
Merged

Reduce Puma memory usage by fixing N+1s and Ruby-side data processing#764
andrew merged 2 commits intomainfrom
fix/memory-optimizations

Conversation

@andrew
Copy link
Copy Markdown
Member

@andrew andrew commented Apr 4, 2026

Puma web process is using ~25GB RSS. AppSignal metrics show AuthorsController#show consuming 64% of total request time, with RepositoriesController#show at 18.5%.

Root causes and fixes:

Host.find_by_domain was loading all 2,038 hosts and parsing each URL with Addressable on every request. Now memoized as a class-level hash, invalidated via after_commit.

AuthorsController#show had several problems:

  • pluck(:labels).flatten.compact.group_by materialized every label array into Ruby -- replaced with SQL unnest()
  • group(:repository).count loaded AR objects as hash keys for 35M repos -- replaced with joins(:repository).group("repositories.full_name")
  • .empty? instead of .exists? for existence check
  • 10 separate queries with identical filters -- consolidated into reused scopes

RepositoriesController#show, charts, chart_data loaded all issues with includes(:owner) then .map(&:owner) to find 3 hidden users. Replaced with a direct Owner.hidden.where(login: subquery).

Repository model *_labels_count methods plucked all label arrays into Ruby. Replaced with SQL unnest().

IssuesController#index had N+1 from issue.html_url loading host and repository per row. Added includes(:host, :repository).

andrew added 2 commits April 4, 2026 14:49
…ssing

- Memoize Host.find_by_domain with a class-level domain map, invalidated
  on commit. Was loading all 2K hosts and parsing each URL with
  Addressable on every API lookup request.

- AuthorsController#show: replace .empty? with .exists?, consolidate
  duplicate query scopes, move label counting from
  pluck/flatten/group_by in Ruby to SQL unnest(), replace
  group(:repository) with a join that avoids loading AR objects as hash
  keys.

- RepositoriesController: replace issues.includes(:owner).map(&:owner)
  with a direct Owner subquery to find hidden users. Was loading every
  issue record for the repository into memory.

- Repository model: rewrite label counting methods to use SQL unnest()
  instead of plucking all label arrays into Ruby.

- IssuesController#index: add includes(:host, :repository) to fix N+1
  from issue.html_url in the partial.
…x to domain map

Move labels_with_counts to Issue model as a class method instead of
duplicating it in AuthorsController and Repository. Extract
hidden_users_for helper in RepositoriesController to replace three
identical blocks. Add Mutex around Host.domain_map for thread safety
under multi-threaded Puma.
@andrew andrew merged commit f7e6f96 into main Apr 4, 2026
1 check passed
@andrew andrew deleted the fix/memory-optimizations branch April 4, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant