mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
5bb1d4d288
I wrote a utility that helps find areas where you could optimize your program using a frozen string instead of a string literal, it's called [let_it_go](https://github.com/schneems/let_it_go). After going through the output and adding `.freeze` I was able to eliminate the creation of 1,114 string objects on EVERY request to [codetriage](codetriage.com). How does this impact execution? To look at memory: ```ruby require 'get_process_mem' mem = GetProcessMem.new GC.start GC.disable 1_114.times { " " } before = mem.mb after = mem.mb GC.enable puts "Diff: #{after - before} mb" ``` Creating 1,114 string objects results in `Diff: 0.03125 mb` of RAM allocated on every request. Or 1mb every 32 requests. To look at raw speed: ```ruby require 'benchmark/ips' number_of_objects_reduced = 1_114 Benchmark.ips do |x| x.report("freeze") { number_of_objects_reduced.times { " ".freeze } } x.report("no-freeze") { number_of_objects_reduced.times { " " } } end ``` We get the results ``` Calculating ------------------------------------- freeze 1.428k i/100ms no-freeze 609.000 i/100ms ------------------------------------------------- freeze 14.363k (± 8.5%) i/s - 71.400k no-freeze 6.084k (± 8.1%) i/s - 30.450k ``` Now we can do some maths: ```ruby ips = 6_226k # iterations / 1 second call_time_before = 1.0 / ips # seconds per iteration ips = 15_254 # iterations / 1 second call_time_after = 1.0 / ips # seconds per iteration diff = call_time_before - call_time_after number_of_objects_reduced * diff * 100 # => 0.4530373333993266 miliseconds saved per request ``` So we're shaving off 1 second of execution time for every 220 requests. Is this going to be an insane speed boost to any Rails app: nope. Should we merge it: yep. p.s. If you know of a method call that doesn't modify a string input such as [String#gsub](b0e2da69f0/lib/let_it_go/core_ext/string.rb (L37)
) please [give me a pull request to the appropriate file](b0e2da69f0/lib/let_it_go/core_ext/string.rb (L37)
), or open an issue in LetItGo so we can track and freeze more strings. Keep those strings Frozen ![](https://www.dropbox.com/s/z4dj9fdsv213r4v/let-it-go.gif?dl=1)
194 lines
6.8 KiB
Ruby
194 lines
6.8 KiB
Ruby
require 'active_support/dependencies'
|
|
|
|
module AbstractController
|
|
module Helpers
|
|
extend ActiveSupport::Concern
|
|
|
|
included do
|
|
class_attribute :_helpers
|
|
self._helpers = Module.new
|
|
|
|
class_attribute :_helper_methods
|
|
self._helper_methods = Array.new
|
|
end
|
|
|
|
class MissingHelperError < LoadError
|
|
def initialize(error, path)
|
|
@error = error
|
|
@path = "helpers/#{path}.rb"
|
|
set_backtrace error.backtrace
|
|
|
|
if error.path =~ /^#{path}(\.rb)?$/
|
|
super("Missing helper file helpers/%s.rb" % path)
|
|
else
|
|
raise error
|
|
end
|
|
end
|
|
end
|
|
|
|
module ClassMethods
|
|
# When a class is inherited, wrap its helper module in a new module.
|
|
# This ensures that the parent class's module can be changed
|
|
# independently of the child class's.
|
|
def inherited(klass)
|
|
helpers = _helpers
|
|
klass._helpers = Module.new { include helpers }
|
|
klass.class_eval { default_helper_module! } unless klass.anonymous?
|
|
super
|
|
end
|
|
|
|
# Declare a controller method as a helper. For example, the following
|
|
# makes the +current_user+ controller method available to the view:
|
|
# class ApplicationController < ActionController::Base
|
|
# helper_method :current_user, :logged_in?
|
|
#
|
|
# def current_user
|
|
# @current_user ||= User.find_by(id: session[:user])
|
|
# end
|
|
#
|
|
# def logged_in?
|
|
# current_user != nil
|
|
# end
|
|
# end
|
|
#
|
|
# In a view:
|
|
# <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>
|
|
#
|
|
# ==== Parameters
|
|
# * <tt>method[, method]</tt> - A name or names of a method on the controller
|
|
# to be made available on the view.
|
|
def helper_method(*meths)
|
|
meths.flatten!
|
|
self._helper_methods += meths
|
|
|
|
meths.each do |meth|
|
|
_helpers.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
|
def #{meth}(*args, &blk) # def current_user(*args, &blk)
|
|
controller.send(%(#{meth}), *args, &blk) # controller.send(:current_user, *args, &blk)
|
|
end # end
|
|
ruby_eval
|
|
end
|
|
end
|
|
|
|
# The +helper+ class method can take a series of helper module names, a block, or both.
|
|
#
|
|
# ==== Options
|
|
# * <tt>*args</tt> - Module, Symbol, String
|
|
# * <tt>block</tt> - A block defining helper methods
|
|
#
|
|
# When the argument is a module it will be included directly in the template class.
|
|
# helper FooHelper # => includes FooHelper
|
|
#
|
|
# When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file
|
|
# and include the module in the template class. The second form illustrates how to include custom helpers
|
|
# when working with namespaced controllers, or other cases where the file containing the helper definition is not
|
|
# in one of Rails' standard load paths:
|
|
# helper :foo # => requires 'foo_helper' and includes FooHelper
|
|
# helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper
|
|
#
|
|
# Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available
|
|
# to the template.
|
|
#
|
|
# # One line
|
|
# helper { def hello() "Hello, world!" end }
|
|
#
|
|
# # Multi-line
|
|
# helper do
|
|
# def foo(bar)
|
|
# "#{bar} is the very best"
|
|
# end
|
|
# end
|
|
#
|
|
# Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of
|
|
# +symbols+, +strings+, +modules+ and blocks.
|
|
#
|
|
# helper(:three, BlindHelper) { def mice() 'mice' end }
|
|
#
|
|
def helper(*args, &block)
|
|
modules_for_helpers(args).each do |mod|
|
|
add_template_helper(mod)
|
|
end
|
|
|
|
_helpers.module_eval(&block) if block_given?
|
|
end
|
|
|
|
# Clears up all existing helpers in this class, only keeping the helper
|
|
# with the same name as this class.
|
|
def clear_helpers
|
|
inherited_helper_methods = _helper_methods
|
|
self._helpers = Module.new
|
|
self._helper_methods = Array.new
|
|
|
|
inherited_helper_methods.each { |meth| helper_method meth }
|
|
default_helper_module! unless anonymous?
|
|
end
|
|
|
|
# Returns a list of modules, normalized from the acceptable kinds of
|
|
# helpers with the following behavior:
|
|
#
|
|
# String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper",
|
|
# and "foo_bar_helper.rb" is loaded using require_dependency.
|
|
#
|
|
# Module:: No further processing
|
|
#
|
|
# After loading the appropriate files, the corresponding modules
|
|
# are returned.
|
|
#
|
|
# ==== Parameters
|
|
# * <tt>args</tt> - An array of helpers
|
|
#
|
|
# ==== Returns
|
|
# * <tt>Array</tt> - A normalized list of modules for the list of
|
|
# helpers provided.
|
|
def modules_for_helpers(args)
|
|
args.flatten.map! do |arg|
|
|
case arg
|
|
when String, Symbol
|
|
file_name = "#{arg.to_s.underscore}_helper"
|
|
begin
|
|
require_dependency(file_name)
|
|
rescue LoadError => e
|
|
raise AbstractController::Helpers::MissingHelperError.new(e, file_name)
|
|
end
|
|
|
|
mod_name = file_name.camelize
|
|
begin
|
|
mod_name.constantize
|
|
rescue LoadError
|
|
# dependencies.rb gives a similar error message but its wording is
|
|
# not as clear because it mentions autoloading. To the user all it
|
|
# matters is that a helper module couldn't be loaded, autoloading
|
|
# is an internal mechanism that should not leak.
|
|
raise NameError, "Couldn't find #{mod_name}, expected it to be defined in helpers/#{file_name}.rb"
|
|
end
|
|
when Module
|
|
arg
|
|
else
|
|
raise ArgumentError, "helper must be a String, Symbol, or Module"
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
# Makes all the (instance) methods in the helper module available to templates
|
|
# rendered through this controller.
|
|
#
|
|
# ==== Parameters
|
|
# * <tt>module</tt> - The module to include into the current helper module
|
|
# for the class
|
|
def add_template_helper(mod)
|
|
_helpers.module_eval { include mod }
|
|
end
|
|
|
|
def default_helper_module!
|
|
module_name = name.sub(/Controller$/, ''.freeze)
|
|
module_path = module_name.underscore
|
|
helper module_path
|
|
rescue LoadError => e
|
|
raise e unless e.is_missing? "helpers/#{module_path}_helper"
|
|
rescue NameError => e
|
|
raise e unless e.missing_name? "#{module_name}Helper"
|
|
end
|
|
end
|
|
end
|
|
end
|