@@ -0,0 +1,206 @@
require 'benchmark'
require 'yaml'
require 'set'
# This overrides 'require' to records the time it takes to require a file, and
# then generate a report. It's intelligent enough to figure out where files were
# required from and construct a hierarchy of the required files.
#
# To use, copy this file to lib/require_benchmarking.rb, then add this to the
# top of the Rails::Initializer block in environment.rb:
#
# # Benchmark requires
# require File.dirname(__FILE__) + '/../lib/require_benchmarking'
# RequireBenchmarking.hook(config)
#
# Then, start your Rails app using script/server. After the app has been initialized,
# the report will be generated and saved to RAILS_ROOT/boot.log. If you need
# to regenerate this report, simply run `ruby lib/require_benchmarking.rb`.
# By default, this will generate a flat report of only top-level requires, but
# pass `--all` to list all files in their respective hierarchy.
#
module RequireBenchmarking
class << self
def hook ( config )
Kernel . class_eval do
alias_method :__require_benchmarking_old_require , :require
def require ( path , *args )
RequireBenchmarking . benchmark_require ( path , caller ) { __require_benchmarking_old_require ( path , *args ) }
end
end
config . after_initialize { RequireBenchmarking . store_benchmark_data }
@hooked = true
end
def hooked?
@hooked
end
def benchmark_require ( path , full_backtrace , &block )
output = nil
backtrace = full_backtrace . reject { |x | x =~ /require|dependencies/ }
caller = File . expand_path ( backtrace [ 0 ] . split ( ":" ) [ 0 ] )
parent = required_files . find { |f | f [ :fullpath ] == caller }
unless parent
parent = {
:index => required_files . size ,
:fullpath => caller ,
:parent => nil ,
:is_root => true
}
required_files << parent
end
fullpath = find_file ( path )
expanded_path = path ; expanded_path = File . expand_path ( path ) if path =~ /^\/ /
new_file = {
:index => required_files . size ,
:path => expanded_path ,
:fullpath => fullpath ,
:backtrace => full_backtrace ,
:parent => parent ,
:is_root => false
}
# add this before the file is required so that anything that is required
# within the file that's about to be required already has a parent present
required_files << new_file
benchmark = Benchmark . measure do
output = yield # do the require here
end
new_file [ :time ] = benchmark . real
output
end
def store_benchmark_data
File . open ( data_file , "w" ) { |f | YAML . dump ( @required_files , f ) }
puts "Wrote data to #{ data_file } ."
generate_benchmark_report ( false )
exit
end
def generate_benchmark_report ( regenerating_report = true )
puts "Now generating benchmark report, please wait..."
if regenerating_report
@required_files = File . open ( data_file ) { |f | YAML . load ( f ) }
end
@report_fh = File . open ( report_file , "w" )
@indent_level = 0
root_files = @required_files . select { |file | file [ :is_root ] }
if ARGV . include? ( "--all" )
generate_benchmark_report_level ( root_files )
else
generate_benchmark_report_level ( @required_files . select { |file | !file [ :is_root ] && file [ :time ] } , true )
end
@report_fh . close
out = "Wrote report to #{ report_file } ."
out << " Run `ruby lib/require_benchmarking.rb` if you want to regenerate the report." unless regenerating_report
puts ( out )
end
private
def required_files
@required_files ||= [ ]
end
def printed_files
@printed_files ||= [ ]
end
def data_file
"#{ proj_dir } /boot.yml"
end
def report_file
"#{ proj_dir } /boot.log"
end
def proj_dir
@proj_dir ||= File . expand_path ( File . dirname ( __FILE__ ) + "/.." )
end
def find_file ( path )
return File . expand_path ( path ) if path =~ /^\/ /
expanded_path = nil
# Try to find the path in the ActiveSupport load paths and then the built-in load paths
catch :found_path do
%w( rb bundle so ) . each do |ext |
path_suffix = path ; path_suffix = "#{ path } .#{ ext } " unless path_suffix =~ /\. #{ ext } $/
( ActiveSupport ::Dependencies . load_paths + $:) . each do |path_prefix |
possible_path = File . join ( path_prefix , path_suffix )
if File . file? possible_path
expanded_path = File . expand_path ( possible_path )
throw :found_path
end
end
expanded_path
end
end
expanded_path
end
def generate_benchmark_report_level ( files , printing_all = false )
if printing_all
files = files . sort { |a , b | b [ :time ] <=> a [ :time ] }
else
files = files . sort_by { |f | [ ( f [ :parent ] ? 1 : 0 ) , -( f [ :time ] || 0 ) , f [ :index ] ] }
end
for file in files
already_printed = printed_files . include? ( file [ :fullpath ] )
# don't print this file if it's already been printed,
# or it will have been printed
next if already_printed
if file [ :parent ] && !printing_all
next if file [ :index ] < file [ :parent ] [ :index ]
end
path = file [ :fullpath ] ? format_path ( file [ :fullpath ] ) : file [ :path ]
out = "#{ file [ :index ] +1 } ) "
if file [ :time ] && !already_printed
#if file[:time] >= 0.5
# out << "%s: %.4f s" % [path, file[:time]]
#else
ms = file [ :time ] . to_f * 1000
out << "%s: %.1f ms" % [ path , ms ]
#end
else
out << path
end
if file [ :is_root ] && file [ :parent ]
out << " (required by #{ file [ :parent ] [ :fullpath ] } )"
end
unless file [ :parent ]
out << " (already loaded)"
end
if already_printed
out << " (already printed)"
end
indent ( out )
unless already_printed
printed_files << file [ :fullpath ]
unless printing_all
children = @required_files . select { |f | !f [ :is_root ] && f [ :parent ] && f [ :parent ] [ :fullpath ] == file [ :fullpath ] }
if children . any?
@indent_level += 1
generate_benchmark_report_level ( children )
@indent_level -= 1
end
end
end
end
end
def indent ( msg )
@report_fh . print ( " " * @indent_level )
@report_fh . puts ( msg )
end
def format_path ( path )
path . sub ( proj_dir , "*" )
end
end
end
at_exit do
RequireBenchmarking . generate_benchmark_report unless RequireBenchmarking . hooked?
end