Table of Contents
- Useful blog posts
- Books
-
Tricks
- Preloading scopes and related tricks
- How to be a top 10% Ruby perf team
- Ensure all controller public methods have a corresponding route
- Install a gem and load it directly in the console without changing the Gemfile
- Ruby exception inheritance
- Form object concerns
- Scopeable Query Objects
- Service Object
- List gems with C extensions
- Ruby jemalloc
- Detecting thread unsafe code
- Test seeds
- ActiveRecord enable query logs in production rails console
- Using Object#tap to debug chained methods
- Gems
Ruby
- Polished Ruby Programming
- High Performance PostgreSQL for Rails
- Layered Design for Ruby on Rails Applications
- Next-Level Database Techniques For Developers
- https://literal.fun/
- https://www.phlex.fun/
- https://github.com/yippee-fun/goodcop
- https://github.com/yippee-fun/strict_ivars
- https://yippee.fun/
- https://github.com/soutaro/rbs-inline
- https://www.betterspecs.org/
- https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts
- https://github.com/ankane/production_rails
- https://github.com/ankane/secure_rails
- https://github.com/ankane/pgslice
- https://docs.newrelic.com/docs/apm/apm-ui-pages/events/thread-profiler-tool/
- https://railsdiff.org/
- https://github.com/speedshop/ids_must_be_indexed
- Ruby on Rails latency chart
- https://github.com/discourse/discourse
Useful blog posts 🔗
- How to debug Ruby performance problems in production
- The answer is in your heap: debugging a big memory increase in Ruby on Rails
- So We’ve Got a Memory Leak…
- https://evilmartians.com/chronicles/rubocoping-with-legacy-bring-your-ruby-code-up-to-standard
- https://evilmartians.com/chronicles/gemfile-of-dreams-libraries-we-use-to-build-rails-apps
- https://code.jeremyevans.net/2023-02-14-speeding-up-tests-in-applications-using-sequel-and-roda.html
Books 🔗
- Layered Design for Ruby on Rails Applications
- Gradual Modularization for Ruby and Rails
Tricks 🔗
Preloading scopes and related tricks 🔗
class Review < ActiveRecord::Base
belongs_to :restaurant
scope :positive, -> { where("rating > 3.0") }
end
class Restaurant < ActiveRecord::Base
has_many :reviews
has_many :positive_reviews, -> { positive }, class_name: "Review"
end
# Avoid returning record from a model's method
class User < ApplicationRecord
has_many :subscriptions
# Bad because a SQL query is triggered on each call
def active subscription
subscriptions.active.first
end
end
user = User.first
user active subscription # SELECT * FROM subscriptions ...
user active subscription # SELECT * FROM subscriptions ...
# Memoizing the result is not necessarily better since it won't be uncached when reload is called.
# Instead, switch that method to a relation:
class User < ApplicationRecord
has_many :subscriptions
# Good because the relation is cached until reload is called
has_one :active_subscription, -> { active }, class_name: "Subscription"
end
user = User.first
user.active subscription # SELECT * FROM subscriptions ...
user.active_subscription # Cached, no queries
user.reload
user.active_subscription # SELECT * FROM subscriptions ...
How to be a top 10% Ruby perf team 🔗
- SLO-based queues (within_6_hours, within_0_seconds, etc.)
- RMP in all environments
- RUM in browser
- Autoscale web and worker
- Instrument request queue time
- Automated alerts/SLOs in Terraform
- Prosopite/strictloading in tests
- Latest Ruby w jemalloc
- Turbo
https://x.com/nateberkopec/status/1844849691760214041
Ensure all controller public methods have a corresponding route 🔗
https://gist.github.com/fractaledmind/410e519ccd51445cc10c3408b5f24d77
class RoutingTest < ActionDispatch::IntegrationTest
IGNORED_CONTROLLERS = Set[
"Rails::MailersController"
]
test "no unrouted actions (public controller methods)" do
actions_by_controller.each do |controller_path, actions|
controller_name = "#{controller_path.camelize}Controller"
next if IGNORED_CONTROLLERS.include?(controller_name)
controller = Object.const_get(controller_name)
public_methods = controller.public_instance_methods(_include_super = false).map(&:to_s)
unrouted_actions = public_methods - actions
assert_empty(
unrouted_actions,
"#{controller_name} has unrouted actions (public methods). These should probably be private"
)
end
end
private
def actions_by_controller
{}.tap do |controllers|
@routes.routes.each do |route|
controller = route.requirements[:controller]
action = route.requirements[:action]
next unless controller && action
(controllers[controller] ||= []) << action
end
end
end
end
Install a gem and load it directly in the console without changing the Gemfile 🔗
$ gem install "ruby-progressbar"
$ gem which "ruby-progressbar"
...
$ bin/rails c
> $: << path.strip.toutf8.gsub("ruby-progressbar.rb", "")
Ruby exception inheritance 🔗
module Phlex
Error = Module.new
NameError = Class.new(NameError) { include Error }
ArgumentError = Class.new(ArgumentError) { include Error }
StandardError = Class.new(StandardError) { include Error }
end
Form object concerns 🔗
https://speakerdeck.com/palkan/kaigi-on-rails-2024-rails-way-or-the-highway
https://speakerdeck.com/palkan/kaigi-on-rails-2024-rails-way-or-the-highway
class ApplicationForm
include ActiveModel::API
include ActiveModel::Attributes
define_callbacks :save, only: :after
define_callbacks :commit, only: :after
class << self
def after_save(...)
set_callback(:save, :after, ...)
end
def after_commit(...)
set_callback(:commit, :after, ...)
end
def from(params)
new(params.permit(attribute_names.map(&:to_sym)))
end
end
def save
return false unless valid?
with_transaction do
AfterCommitEverywhere.after_commit { run_callbacks(:commit) }
run_callbacks(:save) { submit! }
end
end
def model_name
ActiveModel::Name.new(nil, nil, self.class.name.sub(/Form$/, ""))
end
private
def with_transaction(&) = ApplicationRecord.transaction(&)
def submit!
raise NotImplementedError
end
end
class ApplicationWorkflow
include Workflow
end
class Cable
class CreateForm < ApplicationForm
class Wizard < ApplicationWorkflow
workflow do
state :name do
event :submit, transitions_to: :framework
end
state :framework do
event :submit, transitions_to: :rpc, if: :needs_rpc?
event :submit, transitions_to: :secrets
event :back, transitions_to: :name
end
state :rpc do
event :submit, transitions_to: :secrets
event :back, transitions_to: :framework
end
state :secrets do
event :submit, transitions_to: :region
event :back, transitions_to: :rpc, if: :needs_rpc?
event :back, transitions_to: :framework
end
state :complete
end
end
self.model_name = "Cable"
attribute :name
attribute :region, default: -> { "sea" }
attribute :framework, default: -> { "rails" }
attributes :secret, :rpc_host, :rpc_secret, :turbo_secret, :jwt_secret
attribute :wizard_state, default: -> { "name" }
attribute :wizard_action
validates :cable_is_valid
validates :rpc_host, format: %r{\\Ahttps?://}, allow_blank: true
after_commit :enqueue_provisioning
attr_reader :cable
def initialize(...)
super
@cable = Cable.new(
name:, region:,
metadata: {framework:},
configuration: {
secret:, rpc_host:, rpc_secret:,
turbo_secret:, jwt_secret:
}
)
end
def submit!
if wizard_action == "back"
wizard.back!
else
wizard.submit!
end
return false unless wizard.complete?
cable.save!
end
def wizard = @wizard ||= Wizard.new(self)
private
def cable_is_valid
return if cable.valid?
merge_errors!(cable)
end
def enqueue_provisioning = cable.provision_later
end
end
Scopeable Query Objects 🔗
https://speakerdeck.com/palkan/kaigi-on-rails-2024-rails-way-or-the-highway
class ApplicationQuery
private attr_reader :relation
def initialize(relation = self.class.query_model.all) = @relation = relation
def resolve(...) = relation
class << self
# To pin `query_model_name` you may:
# 1. Set `self.query_model_name = "MyModel"` in the query object
# 2. Pass the model name whenever you use the query object MyQueryObject[MyModel]
# 3. Let the fallback (`ApplicationQuery.query_model_name`) do the work
#
# I personally don't like 3 and prefer to always declare it in the query object or pass it explicitly.
attr_writer :query_model_name
def query_model_name
@query_model_name ||= name.sub(/::[^\:]+$/, "")
end
def query_model
query_model_name.constantize
end
# Useful for when the query object can be reused by two base model relation
def [](model)
klass_name = "Generated__#{model.name}#{self.name}"
return klass_name.constantize if Object.const_defined?(klass_name)
klass = Class.new(self).tap {
_1.query_model_name = model.name
}
Object.const_set(klass_name, klass)
end
def resolve(...) = new.resolve(...)
alias_method :call, :resolve
end
end
class LeadListEntry::WithBlacklistedEmailsQuery < ApplicationQuery
self.query_model_name = "LeadListEntry"
def resolve(user)
excluded = ExcludedEmail.where("excluded_emails.email = lead_list_entries.email")
.where(account_id: user.account_id)
.select("1")
.arel.exists
relation.where(excluded)
end
end
class LeadListEntry < ApplicationRecord
scope :with_blacklisted_emails, WithBlacklistedEmailsQuery[self]
# Or:
# scope :with_blacklisted_emails, WithBlacklistedEmailsQuery
# Instead of:
# scope :with_blacklisted_emails, ->(user) { ... }
end
Service Object 🔗
class ApplicationCallable
def initialize(...) = raise NoMethodError
def call = raise NoMethodError
def to_proc = method(:call).to_proc
class << self
def call(...) = new(...).()
end
end
class IntegerIncrementer < ApplicationCallable
def initialize(i)
@i = i
end
def call
@i + 1
end
end
# Or:
module Callable
def call = raise NoMethodError
def to_proc = method(:call).to_proc
end
module CallableOperation
include Callable
module ClassMethods
include Callable
def call(...) = new(...).()
end
def self.included(klass)
klass.extend(ClassMethods)
end
end
class IntegerIncrementer
include CallableOperation
def initialize(i)
@i = i
end
def call
@i + 1
end
end
def call_inside_block(&block)
block.(1)
end
IntegerIncrementer.(1)
IntegerIncrementer.new(1).()
call_inside_block(&IntegerIncrementer)
List gems with C extensions 🔗
require "rubygems"
gems = Gem::Specification.each.select { |spec| spec.extensions.any? }
puts gems.map(&:full_name).map {|x| x.split("-", 2).first }.uniq
Ruby jemalloc 🔗
Adding jemalloc via LD_PRELOAD 🔗
FROM ruby:3.2-slim
RUN apt-get update; \\
apt-get install -y --no-install-recommends libjemalloc2; \\
rm -rf /var/lib/apt/lists/*
# TODO: figure out how to LD_PRELOAD on arbitrary architectures dynamically
# /usr/lib/x86_64-linux-gnu/libjemalloc.so.2
# /usr/lib/aarch64-linux-gnu/libjemalloc.so.2
# /usr/lib/arm-linux-gnueabihf/libjemalloc.so.2
# /usr/lib/i386-linux-gnu/libjemalloc.so.2
# /usr/lib/powerpc64le-linux-gnu/libjemalloc.so.2
# /usr/lib/s390x-linux-gnu/libjemalloc.so.2
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
Adding jemalloc at ruby compile time 🔗
$ apt-get install -y --no-install-recommends libjemalloc-dev libjemalloc2
$ RUBY_CONFIGURE_OPTS="--with-jemalloc" rbenv install 3.2
Patching Ruby binary to use jemalloc 🔗
FROM ruby:3.2-slim
RUN apt-get update && \\\\
apt-get upgrade -y && \\\\
apt-get install libjemalloc2 patchelf && \\\\
patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby && \\\\
apt-get purge patchelf -y
Detecting if jemalloc is being used 🔗
$ MALLOC_CONF=stats_print:true /opt/rbenv/shims/ruby -e "exit" |& grep "jemalloc statistics"
Detecting thread unsafe code 🔗
require "concurrent-ruby"
def run_forever(pool_size: 10)
pool = Concurrent::FixedThreadPool.new(pool_size)
i = Concurrent::AtomicFixnum.new
loop do
pool.post do
yield i.increment
end
end
end
class Repeater
class << self
attr_accessor :result
def repeat_by(content, n)
self.result = [content] * n
end
end
end
run_forever do |iteration|
n = rand(100)
Repeater.repeat_by("hello", n)
array_size = Repeater.result.size
if array_size != n
raise "[#{array_size}] should be [#{n}]?"
end
rescue StandardError => e
puts "Iteration[#{iteration}] #{e.message}"
end
Test seeds 🔗
# frozen_string_literal: true
require "test_helper"
class SeedTest < ActiveSupport::TestCase
def total_count
ActiveRecord::Base.connection.execute(
"SELECT SUM((SELECT COUNT(*) FROM #{ActiveRecord::Base.connection.quote_table_name(t)})) FROM (SELECT tablename as t FROM pg_tables WHERE schemaname = 'public') tabs"
).first["sum"].to_i
end
def test_seeding_successfully
ActiveRecord::Base.connection.execute(
"TRUNCATE TABLE #{ActiveRecord::Base.connection.tables.join(',')} RESTART IDENTITY CASCADE"
)
assert_difference "total_count", 1000..Float::INFINITY do
Rails.application.load_seed
end
end
def test_idempotent
Rails.application.load_seed
assert_no_difference "total_count" do
Rails.application.load_seed
end
end
end
ActiveRecord enable query logs in production rails console 🔗
Rails.logger.level = :debug
# Or:
ActiveRecord::Base.logger.level = Logger::DEBUG
Using Object#tap to debug chained methods 🔗
require "debug"
def process(arr)
arr
.map { |n| n * 2}
.tap { |arr| debugger(pre: "arr") }
.select { |n| n > 5 }
end
Gems 🔗
- yippee-fun/goodcop: An agreeable RuboCop configuration
- yippee-fun/strict_ivars: Make Ruby raise a NameError if you read an undefined instance variable
- oldmoe/tinybits-rb: tinybits binary encoding for Ruby
- https://evilmartians.com/chronicles/gemfile-of-dreams-libraries-we-use-to-build-rails-apps
- overcommit: A fully configurable and extendable Git hook manager
- roaring-ruby: Roaring compressed bitmaps for Ruby
- parallel_tests: 2 CPUs = 2x Testing Speed for RSpec, Test::Unit and Cucumber
- charkost/prosopite: Rails N+1 queries auto-detection with zero false positives / false negatives
- robotdana/leftovers: Find unused ruby methods and constants and etc
- zip_kit: Compact ZIP file writing/reading for Ruby, for streaming applications
- litestack: All your data infrastructure, in a gem!
- diogob/postgres-copy: Simple PostgreSQL’s COPY command support in ActiveRecord models
- geekq/workflow: Ruby finite-state-machine-inspired API for modeling workflow
- julienbourdeau/debugbar: Powerful devtools for Ruby on Rails. Inspired by the Laravel Debugbar
- ohbarye/pbt: Property-Based Testing tool for Ruby, supporting multiple concurrency methods (Ractor, multiprocesses, multithreads)
- BetterErrors/better_errors: Better error page for Rack apps
- excid3/madmin: A robust Admin Interface for Ruby on Rails apps
- ioquatix/rack-freeze: A policy framework for implementing thread-safe rack middleware
- drwl/annotaterb: A Ruby Gem that adds annotations to your Rails models and route files
- DamirSvrtan/fasterer: ⚡ Don’t make your Rubies go fast. Make them go fasterer™ ⚡
- standardrb/standard: Ruby’s bikeshed-proof linter and formatter 🚲
- Shopify/ruby_memcheck: Use Valgrind memcheck on your native gem without going crazy
- jeremyevans/autoforme:Web Administrative Console for Roda/Sinatra/Rails and Sequel::Model
- https://github.com/kamui/retriable
- https://github.com/palkan/anyway_config
- ivoanjo/gvl-tracing: Get a timeline view of Global VM Lock usage in your Ruby app
- https://github.com/evanphx/benchmark-ips
- stevegeek/awfy: CLI tool to help run suites of benchmarks , and compare results between control implementations, across branches and with or without YJIT
- https://github.com/MiniProfiler/rack-mini-profiler
-
https://github.com/jhawthorn/vernier
-
https://www.youtube.com/watch?v=QSjN-H4hGsM
$ bundle exec vernier run --hooks=rails --output=/tmp/puma.profile.json --signal CONT --start-paused -- bin/rails s -p 3000 $ pkill -CONT -f puma
-
https://www.youtube.com/watch?v=QSjN-H4hGsM
- https://github.com/tmm1/stackprof
- https://github.com/ruby-prof/ruby-prof
- https://github.com/tmm1/rbtrace
- https://github.com/rbspy/rbspy
- jhawthorn/sheap: Interactive Ruby Heap Snapshot analyzer
- oxidize-rb/reap: Tooling for Ruby heap dumps