From 2d53439a933aa17a37a90dafcdf16ce83aef0952 Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" Date: Mon, 13 Dec 2021 18:04:15 +0900 Subject: [PATCH] Improve performance of method call via `delegate_all` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, `delegatable?` call `private_methods` to check method is private or not. The `private_methods` generates an Array of methods per call. So a method call via `delegate_all` generates an extra Array every time. This patch use `private_method_defined?` instead of `private_methods`. This reduce the extra Array. The inherit argument for `private_method_defined?` is supported since Ruby 2.6. So this patch only works for >= Ruby 2.6. Rubies old than Ruby 2.6 keep using `private_methods`. Ref: https://bugs.ruby-lang.org/issues/14944 Benchmark is here. ```ruby require "bundler/inline" gemfile(true) do source "https://rubygems.org" gem "activerecord" gem "sqlite3" if ENV["USE_FORKED_GEM"] gem "draper", github: "y-yagi/draper", branch: "improve-delegate_all-performance" else gem "draper" end gem "benchmark-ips" end require "active_record" require "logger" ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Base.logger = nil ActiveRecord::Schema.define do create_table :users, force: true do |t| t.string :name, null: false t.timestamps end end ActiveRecord::Base.logger = Logger.new(STDOUT) class User < ActiveRecord::Base end class UserDecorator < Draper::Decorator delegate_all def decorated_name "'#{name}'" end end User.create!(name: "Dummy User") u = UserDecorator.decorate(User.first) Benchmark.ips do |x| x.report(ENV["USE_FORKED_GEM"] == "true" ? "forked draper" : "released draper") do 1000.times { u.decorated_name } end x.save! ENV["SAVE_FILE"] if ENV["SAVE_FILE"] x.compare! end ``` ```bash $ SAVE_FILE=result.out ruby draper.rb $ USE_FORKED_GEM=true SAVE_FILE=result.out ruby draper.rb ... Comparison: forked draper: 454.5 i/s released draper: 63.8 i/s - 7.12x (± 0.00) slower ``` --- lib/draper/automatic_delegation.rb | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/draper/automatic_delegation.rb b/lib/draper/automatic_delegation.rb index 81c45c42..0546f4c4 100644 --- a/lib/draper/automatic_delegation.rb +++ b/lib/draper/automatic_delegation.rb @@ -18,11 +18,21 @@ def respond_to_missing?(method, include_private = false) super || delegatable?(method) end - # @private - def delegatable?(method) - return if private_methods(false).include?(method) + # The inherit argument for `private_method_defined?` is supported since Ruby 2.6. + if RUBY_VERSION >= "2.6" + # @private + def delegatable?(method) + return if self.class.private_method_defined?(method, false) - object.respond_to?(method) + object.respond_to?(method) + end + else + # @private + def delegatable?(method) + return if private_methods(false).include?(method) + + object.respond_to?(method) + end end module ClassMethods