diff options
Diffstat (limited to 'activesupport/lib')
12 files changed, 761 insertions, 405 deletions
diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb index a14f008be5..b937d4c50d 100644 --- a/activesupport/lib/active_support/buffered_logger.rb +++ b/activesupport/lib/active_support/buffered_logger.rb @@ -48,14 +48,17 @@ module ActiveSupport if log.respond_to?(:write) @log = log elsif File.exist?(log) - @log = open(log, (File::WRONLY | File::APPEND)) - @log.binmode - @log.sync = true + @log = open_log(log, (File::WRONLY | File::APPEND)) else FileUtils.mkdir_p(File.dirname(log)) - @log = open(log, (File::WRONLY | File::APPEND | File::CREAT)) - @log.binmode - @log.sync = true + @log = open_log(log, (File::WRONLY | File::APPEND | File::CREAT)) + end + end + + def open_log(log, mode) + open(log, mode).tap do |log| + log.set_encoding(Encoding::BINARY) if log.respond_to?(:set_encoding) + log.sync = true end end diff --git a/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb b/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb index ca3db2349e..ec475134ef 100644 --- a/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb @@ -130,7 +130,6 @@ class Class # :nodoc: end def write_inheritable_attribute(key, value) - ActiveSupport::Deprecation.warn ClassInheritableAttributes::DEPRECATION_WARNING_MESSAGE if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES) @inheritable_attributes = {} end @@ -148,7 +147,6 @@ class Class # :nodoc: end def read_inheritable_attribute(key) - ActiveSupport::Deprecation.warn ClassInheritableAttributes::DEPRECATION_WARNING_MESSAGE inheritable_attributes[key] end diff --git a/activesupport/lib/active_support/core_ext/module/attr_accessor_with_default.rb b/activesupport/lib/active_support/core_ext/module/attr_accessor_with_default.rb index e3259a0a84..984f6fb957 100644 --- a/activesupport/lib/active_support/core_ext/module/attr_accessor_with_default.rb +++ b/activesupport/lib/active_support/core_ext/module/attr_accessor_with_default.rb @@ -19,6 +19,7 @@ class Module # attr_accessor_with_default(:element_name) { name.underscore } # def attr_accessor_with_default(sym, default = Proc.new) + ActiveSupport::Deprecation.warn "attr_accessor_with_default is deprecated. Use Ruby instead!" define_method(sym, block_given? ? default : Proc.new { default }) module_eval(<<-EVAL, __FILE__, __LINE__ + 1) def #{sym}=(value) # def age=(value) diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index e73915ffcf..58a03d508e 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -1,4 +1,6 @@ require 'active_support/duration' +require 'active_support/core_ext/time/calculations' +require 'active_support/core_ext/time/acts_like' class Numeric # Enables the use of time calculations and declarations, like 45.minutes + 2.hours + 4.years. diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index dcac17536a..00fda2b370 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -1,4 +1,5 @@ require 'active_support/duration' +require 'active_support/core_ext/time/zones' class Time COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 8ec4f6e09a..39ebc1ec82 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -10,6 +10,10 @@ module ActiveSupport true end + def with_indifferent_access + self + end + def initialize(constructor = {}) if constructor.is_a?(Hash) super() @@ -58,8 +62,12 @@ module ActiveSupport # hash_1.update(hash_2) # => {"key"=>"New Value!"} # def update(other_hash) - other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) } - self + if other_hash.is_a? HashWithIndifferentAccess + super(other_hash) + else + other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) } + self + end end alias_method :merge!, :update diff --git a/activesupport/lib/active_support/testing/performance.rb b/activesupport/lib/active_support/testing/performance.rb index 7cd9bfa947..02c19448fd 100644 --- a/activesupport/lib/active_support/testing/performance.rb +++ b/activesupport/lib/active_support/testing/performance.rb @@ -1,470 +1,317 @@ -begin - require 'ruby-prof' - - require 'fileutils' - require 'rails/version' - require 'active_support/concern' - require 'active_support/core_ext/class/delegating_attributes' - require 'active_support/core_ext/string/inflections' - - module ActiveSupport - module Testing - module Performance - extend ActiveSupport::Concern - - included do - superclass_delegating_accessor :profile_options - self.profile_options = DEFAULTS - - if defined?(MiniTest::Assertions) && TestCase < MiniTest::Assertions - include ForMiniTest - else - include ForClassicTestUnit - end +require 'fileutils' +require 'rails/version' +require 'active_support/concern' +require 'active_support/core_ext/class/delegating_attributes' +require 'active_support/core_ext/string/inflections' +require 'action_view/helpers/number_helper' + +module ActiveSupport + module Testing + module Performance + extend ActiveSupport::Concern + + included do + superclass_delegating_accessor :profile_options + self.profile_options = {} + + if defined?(MiniTest::Assertions) && TestCase < MiniTest::Assertions + include ForMiniTest + else + include ForClassicTestUnit end + end + + # each implementation should define metrics and freeze the defaults + DEFAULTS = + if ARGV.include?('--benchmark') # HAX for rake test + { :runs => 4, + :output => 'tmp/performance', + :benchmark => true } + else + { :runs => 1, + :output => 'tmp/performance', + :benchmark => false } + end + + def full_profile_options + DEFAULTS.merge(profile_options) + end - module ForMiniTest - def run(runner) - @runner = runner - - run_warmup - if profile_options && metrics = profile_options[:metrics] - metrics.each do |metric_name| - if klass = Metrics[metric_name.to_sym] - run_profile(klass.new) - end + def full_test_name + "#{self.class.name}##{method_name}" + end + + module ForMiniTest + def run(runner) + @runner = runner + + run_warmup + if full_profile_options && metrics = full_profile_options[:metrics] + metrics.each do |metric_name| + if klass = Metrics[metric_name.to_sym] + run_profile(klass.new) end end end + + return + end - def run_test(metric, mode) - result = '.' + def run_test(metric, mode) + result = '.' + begin + run_callbacks :setup + setup + metric.send(mode) { __send__ method_name } + rescue Exception => e + result = @runner.puke(self.class, method_name, e) + ensure begin - run_callbacks :setup - setup - metric.send(mode) { __send__ method_name } + teardown + run_callbacks :teardown, :enumerator => :reverse_each rescue Exception => e result = @runner.puke(self.class, method_name, e) - ensure - begin - teardown - run_callbacks :teardown, :enumerator => :reverse_each - rescue Exception => e - result = @runner.puke(self.class, method_name, e) - end end - result end + result end + end - module ForClassicTestUnit - def run(result) - return if method_name =~ /^default_test$/ + module ForClassicTestUnit + def run(result) + return if method_name =~ /^default_test$/ - yield(self.class::STARTED, name) - @_result = result + yield(self.class::STARTED, name) + @_result = result - run_warmup - if profile_options && metrics = profile_options[:metrics] - metrics.each do |metric_name| - if klass = Metrics[metric_name.to_sym] - run_profile(klass.new) - result.add_run - end + run_warmup + if full_profile_options && metrics = full_profile_options[:metrics] + metrics.each do |metric_name| + if klass = Metrics[metric_name.to_sym] + run_profile(klass.new) + result.add_run + else + puts '%20s: unsupported' % metric_name end end - - yield(self.class::FINISHED, name) end - def run_test(metric, mode) - run_callbacks :setup - setup - metric.send(mode) { __send__ @method_name } + yield(self.class::FINISHED, name) + end + + def run_test(metric, mode) + run_callbacks :setup + setup + metric.send(mode) { __send__ @method_name } + rescue ::Test::Unit::AssertionFailedError => e + add_failure(e.message, e.backtrace) + rescue StandardError, ScriptError => e + add_error(e) + ensure + begin + teardown + run_callbacks :teardown, :enumerator => :reverse_each rescue ::Test::Unit::AssertionFailedError => e - add_failure(e.message, e.backtrace) + add_failure(e.message, e.backtrace) rescue StandardError, ScriptError => e add_error(e) - ensure - begin - teardown - run_callbacks :teardown, :enumerator => :reverse_each - rescue ::Test::Unit::AssertionFailedError => e - add_failure(e.message, e.backtrace) - rescue StandardError, ScriptError => e - add_error(e) - end end end + end - DEFAULTS = - if benchmark = ARGV.include?('--benchmark') # HAX for rake test - { :benchmark => true, - :runs => 4, - :metrics => [:wall_time, :memory, :objects, :gc_runs, :gc_time], - :output => 'tmp/performance' } - else - { :benchmark => false, - :runs => 1, - :min_percent => 0.01, - :metrics => [:process_time, :memory, :objects], - :formats => [:flat, :graph_html, :call_tree], - :output => 'tmp/performance' } - end.freeze - - def full_test_name - "#{self.class.name}##{method_name}" - end - - protected - def run_warmup - GC.start - - time = Metrics::Time.new - run_test(time, :benchmark) - puts "%s (%s warmup)" % [full_test_name, time.format(time.total)] + protected + # overridden by each implementation + def run_gc; end + + def run_warmup + run_gc - GC.start - end + time = Metrics::Time.new + run_test(time, :benchmark) + puts "%s (%s warmup)" % [full_test_name, time.format(time.total)] - def run_profile(metric) - klass = profile_options[:benchmark] ? Benchmarker : Profiler - performer = klass.new(self, metric) - - performer.run - puts performer.report - performer.record - end + run_gc + end + + def run_profile(metric) + klass = full_profile_options[:benchmark] ? Benchmarker : Profiler + performer = klass.new(self, metric) + + performer.run + puts performer.report + performer.record + end - class Performer - delegate :run_test, :profile_options, :full_test_name, :to => :@harness + class Performer + delegate :run_test, :full_profile_options, :full_test_name, :to => :@harness - def initialize(harness, metric) - @harness, @metric = harness, metric - end + def initialize(harness, metric) + @harness, @metric, @supported = harness, metric, false + end - def report - rate = @total / profile_options[:runs] + def report + if @supported + rate = @total / full_profile_options[:runs] '%20s: %s' % [@metric.name, @metric.format(rate)] + else + '%20s: unsupported' % @metric.name end - - protected - def output_filename - "#{profile_options[:output]}/#{full_test_name}_#{@metric.name}" - end end - class Benchmarker < Performer - def run - profile_options[:runs].to_i.times { run_test(@metric, :benchmark) } - @total = @metric.total - end - - def record - avg = @metric.total / profile_options[:runs].to_i - now = Time.now.utc.xmlschema - with_output_file do |file| - file.puts "#{avg},#{now},#{environment}" - end - end - - def environment - unless defined? @env - app = "#{$1}.#{$2}" if File.directory?('.git') && `git branch -v` =~ /^\* (\S+)\s+(\S+)/ - - rails = Rails::VERSION::STRING - if File.directory?('vendor/rails/.git') - Dir.chdir('vendor/rails') do - rails += ".#{$1}.#{$2}" if `git branch -v` =~ /^\* (\S+)\s+(\S+)/ - end - end - - ruby = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby' - ruby += "-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}" - - @env = [app, rails, ruby, RUBY_PLATFORM] * ',' - end - - @env + protected + def output_filename + "#{full_profile_options[:output]}/#{full_test_name}_#{@metric.name}" end - - protected - HEADER = 'measurement,created_at,app,rails,ruby,platform' - - def with_output_file - fname = output_filename - - if new = !File.exist?(fname) - FileUtils.mkdir_p(File.dirname(fname)) - end - - File.open(fname, 'ab') do |file| - file.puts(HEADER) if new - yield file - end - end - - def output_filename - "#{super}.csv" - end + end + + # overridden by each implementation + class Profiler < Performer + def time_with_block + before = Time.now + yield + Time.now - before end + + def run; end + def record; end + end - class Profiler < Performer - def initialize(*args) - super - @supported = @metric.measure_mode rescue false - end - - def run - return unless @supported - - RubyProf.measure_mode = @metric.measure_mode - RubyProf.start - RubyProf.pause - profile_options[:runs].to_i.times { run_test(@metric, :profile) } - @data = RubyProf.stop - @total = @data.threads.values.sum(0) { |method_infos| method_infos.max.total_time } - end + class Benchmarker < Performer + def initialize(*args) + super + @supported = @metric.respond_to?('measure') + end + + def run + return unless @supported + + full_profile_options[:runs].to_i.times { run_test(@metric, :benchmark) } + @total = @metric.total + end - def report - if @supported - super - else - '%20s: unsupported' % @metric.name - end + def record + avg = @metric.total / full_profile_options[:runs].to_i + now = Time.now.utc.xmlschema + with_output_file do |file| + file.puts "#{avg},#{now},#{environment}" end + end - def record - return unless @supported - - klasses = profile_options[:formats].map { |f| RubyProf.const_get("#{f.to_s.camelize}Printer") }.compact + def environment + unless defined? @env + app = "#{$1}.#{$2}" if File.directory?('.git') && `git branch -v` =~ /^\* (\S+)\s+(\S+)/ - klasses.each do |klass| - fname = output_filename(klass) - FileUtils.mkdir_p(File.dirname(fname)) - File.open(fname, 'wb') do |file| - klass.new(@data).print(file, profile_options.slice(:min_percent)) + rails = Rails::VERSION::STRING + if File.directory?('vendor/rails/.git') + Dir.chdir('vendor/rails') do + rails += ".#{$1}.#{$2}" if `git branch -v` =~ /^\* (\S+)\s+(\S+)/ end end - end - protected - def output_filename(printer_class) - suffix = - case printer_class.name.demodulize - when 'FlatPrinter'; 'flat.txt' - when 'FlatPrinterWithLineNumbers'; 'flat_line_numbers.txt' - when 'GraphPrinter'; 'graph.txt' - when 'GraphHtmlPrinter'; 'graph.html' - when 'GraphYamlPrinter'; 'graph.yml' - when 'CallTreePrinter'; 'tree.txt' - when 'CallStackPrinter'; 'stack.html' - when 'DotPrinter'; 'graph.dot' - else printer_class.name.sub(/Printer$/, '').underscore - end - - "#{super()}_#{suffix}" - end - end + ruby = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby' + ruby += "-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}" - module Metrics - def self.[](name) - const_get(name.to_s.camelize) - rescue NameError - nil + @env = [app, rails, ruby, RUBY_PLATFORM] * ',' end - class Base - attr_reader :total - - def initialize - @total = 0 - end + @env + end - def name - @name ||= self.class.name.demodulize.underscore - end + protected + HEADER = 'measurement,created_at,app,rails,ruby,platform' - def measure_mode - self.class::Mode - end + def with_output_file + fname = output_filename - def measure - 0 - end - - def benchmark - with_gc_stats do - before = measure - yield - @total += (measure - before) - end + if new = !File.exist?(fname) + FileUtils.mkdir_p(File.dirname(fname)) end - def profile - RubyProf.resume - yield - ensure - RubyProf.pause + File.open(fname, 'ab') do |file| + file.puts(HEADER) if new + yield file end - - protected - # Ruby 1.9 with GC::Profiler - if defined?(GC::Profiler) - def with_gc_stats - GC::Profiler.enable - GC.start - yield - ensure - GC::Profiler.disable - end - - # Ruby 1.8 + ruby-prof wrapper (enable/disable stats for Benchmarker) - elsif GC.respond_to?(:enable_stats) - def with_gc_stats - GC.enable_stats - yield - ensure - GC.disable_stats - end - - else - def with_gc_stats - yield - end - end end - class Time < Base - def measure - ::Time.now.to_f - end - - def format(measurement) - if measurement < 1 - '%d ms' % (measurement * 1000) - else - '%.2f sec' % measurement - end - end + def output_filename + "#{super}.csv" end + end + + module Metrics + def self.[](name) + const_get(name.to_s.camelize) + rescue NameError + nil + end - class ProcessTime < Time - Mode = RubyProf::PROCESS_TIME + class Base + include ActionView::Helpers::NumberHelper + + attr_reader :total - def measure - RubyProf.measure_process_time - end + def initialize + @total = 0 end - class WallTime < Time - Mode = RubyProf::WALL_TIME - - def measure - RubyProf.measure_wall_time - end + def name + @name ||= self.class.name.demodulize.underscore end - class CpuTime < Time - Mode = RubyProf::CPU_TIME if RubyProf.const_defined?(:CPU_TIME) - - def initialize(*args) - # FIXME: yeah my CPU is 2.33 GHz - RubyProf.cpu_frequency = 2.33e9 unless RubyProf.cpu_frequency > 0 - super - end - - def measure - RubyProf.measure_cpu_time + def benchmark + with_gc_stats do + before = measure + yield + @total += (measure - before) end end - - class Memory < Base - Mode = RubyProf::MEMORY if RubyProf.const_defined?(:MEMORY) - - # Ruby 1.9 + GCdata patch - if GC.respond_to?(:malloc_allocated_size) - def measure - GC.malloc_allocated_size / 1024.0 - end - - # Ruby 1.8 + ruby-prof wrapper - elsif RubyProf.respond_to?(:measure_memory) - def measure - RubyProf.measure_memory / 1024.0 - end - end - - def format(measurement) - '%.2f KB' % measurement - end + + # overridden by each implementation + def profile; end + + protected + # overridden by each implementation + def with_gc_stats; end + end + + class Time < Base + def measure + ::Time.now.to_f end - class Objects < Base - Mode = RubyProf::ALLOCATIONS if RubyProf.const_defined?(:ALLOCATIONS) - - # Ruby 1.9 + GCdata patch - if GC.respond_to?(:malloc_allocations) - def measure - GC.malloc_allocations - end - - # Ruby 1.8 + ruby-prof wrapper - elsif RubyProf.respond_to?(:measure_allocations) - def measure - RubyProf.measure_allocations - end - end - - def format(measurement) - measurement.to_i.to_s + def format(measurement) + if measurement < 1 + '%d ms' % (measurement * 1000) + else + '%.2f sec' % measurement end end - - class GcRuns < Base - Mode = RubyProf::GC_RUNS if RubyProf.const_defined?(:GC_RUNS) - - # Ruby 1.9 - if GC.respond_to?(:count) - def measure - GC.count - end - - # Ruby 1.8 + ruby-prof wrapper - elsif RubyProf.respond_to?(:measure_gc_runs) - def measure - RubyProf.measure_gc_runs - end - end - - def format(measurement) - measurement.to_i.to_s - end + end + + class Amount < Base + def format(measurement) + number_with_delimiter(measurement.floor) end - - class GcTime < Base - Mode = RubyProf::GC_TIME if RubyProf.const_defined?(:GC_TIME) - - # Ruby 1.9 with GC::Profiler - if defined?(GC::Profiler) && GC::Profiler.respond_to?(:total_time) - def measure - GC::Profiler.total_time - end - - # Ruby 1.8 + ruby-prof wrapper - elsif RubyProf.respond_to?(:measure_gc_time) - def measure - RubyProf.measure_gc_time / 1000 - end - end - - def format(measurement) - '%.2f ms' % measurement - end + end + + class DigitalInformationUnit < Base + def format(measurement) + number_to_human_size(measurement, :precision => 2) end end + + # each implementation provides its own metrics like ProcessTime, Memory or GcRuns end end end -rescue LoadError +end + +RUBY_ENGINE = 'ruby' unless defined?(RUBY_ENGINE) # mri 1.8 +case RUBY_ENGINE + when 'ruby' then require 'active_support/testing/performance/ruby' + when 'rbx' then require 'active_support/testing/performance/rubinius' + when 'jruby' then require 'active_support/testing/performance/jruby' + else + $stderr.puts 'Your ruby interpreter is not supported for benchmarking.' + exit end diff --git a/activesupport/lib/active_support/testing/performance/jruby.rb b/activesupport/lib/active_support/testing/performance/jruby.rb new file mode 100644 index 0000000000..6b27959840 --- /dev/null +++ b/activesupport/lib/active_support/testing/performance/jruby.rb @@ -0,0 +1,115 @@ +require 'jruby/profiler' +require 'java' +import java.lang.management.ManagementFactory + +module ActiveSupport + module Testing + module Performance + DEFAULTS.merge!( + if ARGV.include?('--benchmark') + {:metrics => [:wall_time, :user_time, :memory, :gc_runs, :gc_time]} + else + { :metrics => [:wall_time], + :formats => [:flat, :graph] } + end).freeze + + protected + def run_gc + ManagementFactory.memory_mx_bean.gc + end + + class Profiler < Performer + def initialize(*args) + super + @supported = @metric.is_a?(Metrics::WallTime) + end + + def run + return unless @supported + + @total = time_with_block do + @data = JRuby::Profiler.profile do + full_profile_options[:runs].to_i.times { run_test(@metric, :profile) } + end + end + end + + def record + return unless @supported + + klasses = full_profile_options[:formats].map { |f| JRuby::Profiler.const_get("#{f.to_s.camelize}ProfilePrinter") }.compact + + klasses.each do |klass| + fname = output_filename(klass) + FileUtils.mkdir_p(File.dirname(fname)) + file = File.open(fname, 'wb') do |file| + klass.new(@data).printProfile(file) + end + end + end + + protected + def output_filename(printer_class) + suffix = + case printer_class.name.demodulize + when 'FlatProfilePrinter'; 'flat.txt' + when 'GraphProfilePrinter'; 'graph.txt' + else printer_class.name.sub(/ProfilePrinter$/, '').underscore + end + + "#{super()}_#{suffix}" + end + end + + module Metrics + class Base + def profile + yield + end + + protected + def with_gc_stats + ManagementFactory.memory_mx_bean.gc + yield + end + end + + class WallTime < Time + def measure + super + end + end + + class CpuTime < Time + def measure + ManagementFactory.thread_mx_bean.get_current_thread_cpu_time / 1000 / 1000 / 1000.0 # seconds + end + end + + class UserTime < Time + def measure + ManagementFactory.thread_mx_bean.get_current_thread_user_time / 1000 / 1000 / 1000.0 # seconds + end + end + + class Memory < DigitalInformationUnit + def measure + ManagementFactory.memory_mx_bean.non_heap_memory_usage.used + ManagementFactory.memory_mx_bean.heap_memory_usage.used + end + end + + class GcRuns < Amount + def measure + ManagementFactory.garbage_collector_mx_beans.inject(0) { |total_runs, current_gc| total_runs += current_gc.collection_count } + end + end + + class GcTime < Time + def measure + ManagementFactory.garbage_collector_mx_beans.inject(0) { |total_time, current_gc| total_time += current_gc.collection_time } / 1000.0 # seconds + end + end + end + end + end +end diff --git a/activesupport/lib/active_support/testing/performance/rubinius.rb b/activesupport/lib/active_support/testing/performance/rubinius.rb new file mode 100644 index 0000000000..198d235548 --- /dev/null +++ b/activesupport/lib/active_support/testing/performance/rubinius.rb @@ -0,0 +1,113 @@ +require 'rubinius/agent' + +module ActiveSupport + module Testing + module Performance + DEFAULTS.merge!( + if ARGV.include?('--benchmark') + {:metrics => [:wall_time, :memory, :objects, :gc_runs, :gc_time]} + else + { :metrics => [:wall_time], + :formats => [:flat, :graph] } + end).freeze + + protected + def run_gc + GC.run(true) + end + + class Performer; end + + class Profiler < Performer + def initialize(*args) + super + @supported = @metric.is_a?(Metrics::WallTime) + end + + def run + return unless @supported + + @profiler = Rubinius::Profiler::Instrumenter.new + + @total = time_with_block do + @profiler.profile(false) do + full_profile_options[:runs].to_i.times { run_test(@metric, :profile) } + end + end + end + + def record + return unless @supported + + if(full_profile_options[:formats].include?(:flat)) + create_path_and_open_file(:flat) do |file| + @profiler.show(file) + end + end + + if(full_profile_options[:formats].include?(:graph)) + create_path_and_open_file(:graph) do |file| + @profiler.show(file) + end + end + end + + protected + def create_path_and_open_file(printer_name) + fname = "#{output_filename}_#{printer_name}.txt" + FileUtils.mkdir_p(File.dirname(fname)) + File.open(fname, 'wb') do |file| + yield(file) + end + end + end + + module Metrics + class Base + attr_reader :loopback + + def profile + yield + end + + protected + def with_gc_stats + @loopback = Rubinius::Agent.loopback + GC.run(true) + yield + end + end + + class WallTime < Time + def measure + super + end + end + + class Memory < DigitalInformationUnit + def measure + loopback.get("system.memory.counter.bytes").last + end + end + + class Objects < Amount + def measure + loopback.get("system.memory.counter.objects").last + end + end + + class GcRuns < Amount + def measure + loopback.get("system.gc.full.count").last + loopback.get("system.gc.young.count").last + end + end + + class GcTime < Time + def measure + (loopback.get("system.gc.full.wallclock").last + loopback.get("system.gc.young.wallclock").last) / 1000.0 + end + end + end + end + end +end diff --git a/activesupport/lib/active_support/testing/performance/ruby.rb b/activesupport/lib/active_support/testing/performance/ruby.rb new file mode 100644 index 0000000000..b29ec6719c --- /dev/null +++ b/activesupport/lib/active_support/testing/performance/ruby.rb @@ -0,0 +1,152 @@ +begin + require 'ruby-prof' +rescue LoadError + $stderr.puts 'Specify ruby-prof as application\'s dependency in Gemfile to run benchmarks.' + exit +end + +module ActiveSupport + module Testing + module Performance + DEFAULTS.merge!( + if ARGV.include?('--benchmark') + { :metrics => [:wall_time, :memory, :objects, :gc_runs, :gc_time] } + else + { :min_percent => 0.01, + :metrics => [:process_time, :memory, :objects], + :formats => [:flat, :graph_html, :call_tree, :call_stack] } + end).freeze + + protected + def run_gc + GC.start + end + + class Profiler < Performer + def initialize(*args) + super + @supported = @metric.measure_mode rescue false + end + + def run + return unless @supported + + RubyProf.measure_mode = @metric.measure_mode + RubyProf.start + RubyProf.pause + full_profile_options[:runs].to_i.times { run_test(@metric, :profile) } + @data = RubyProf.stop + @total = @data.threads.values.sum(0) { |method_infos| method_infos.max.total_time } + end + + def record + return unless @supported + + klasses = full_profile_options[:formats].map { |f| RubyProf.const_get("#{f.to_s.camelize}Printer") }.compact + + klasses.each do |klass| + fname = output_filename(klass) + FileUtils.mkdir_p(File.dirname(fname)) + File.open(fname, 'wb') do |file| + klass.new(@data).print(file, full_profile_options.slice(:min_percent)) + end + end + end + + protected + def output_filename(printer_class) + suffix = + case printer_class.name.demodulize + when 'FlatPrinter'; 'flat.txt' + when 'FlatPrinterWithLineNumbers'; 'flat_line_numbers.txt' + when 'GraphPrinter'; 'graph.txt' + when 'GraphHtmlPrinter'; 'graph.html' + when 'GraphYamlPrinter'; 'graph.yml' + when 'CallTreePrinter'; 'tree.txt' + when 'CallStackPrinter'; 'stack.html' + when 'DotPrinter'; 'graph.dot' + else printer_class.name.sub(/Printer$/, '').underscore + end + + "#{super()}_#{suffix}" + end + end + + module Metrics + class Base + def measure_mode + self.class::Mode + end + + def profile + RubyProf.resume + yield + ensure + RubyProf.pause + end + + protected + # overridden by each implementation + def with_gc_stats + yield + end + end + + class ProcessTime < Time + Mode = RubyProf::PROCESS_TIME if RubyProf.const_defined?(:PROCESS_TIME) + + def measure + RubyProf.measure_process_time + end + end + + class WallTime < Time + Mode = RubyProf::WALL_TIME if RubyProf.const_defined?(:WALL_TIME) + + def measure + RubyProf.measure_wall_time + end + end + + class CpuTime < Time + Mode = RubyProf::CPU_TIME if RubyProf.const_defined?(:CPU_TIME) + + def initialize(*args) + # FIXME: yeah my CPU is 2.33 GHz + RubyProf.cpu_frequency = 2.33e9 unless RubyProf.cpu_frequency > 0 + super + end + + def measure + RubyProf.measure_cpu_time + end + end + + class Memory < DigitalInformationUnit + Mode = RubyProf::MEMORY if RubyProf.const_defined?(:MEMORY) + end + + class Objects < Amount + Mode = RubyProf::ALLOCATIONS if RubyProf.const_defined?(:ALLOCATIONS) + end + + class GcRuns < Amount + Mode = RubyProf::GC_RUNS if RubyProf.const_defined?(:GC_RUNS) + end + + class GcTime < Time + Mode = RubyProf::GC_TIME if RubyProf.const_defined?(:GC_TIME) + end + end + end + end +end + +if RUBY_VERSION.between?('1.9.2', '2.0') + require 'active_support/testing/performance/ruby/yarv' +elsif RUBY_VERSION.between?('1.8.6', '1.9') + require 'active_support/testing/performance/ruby/mri' +else + $stderr.puts 'Update your ruby interpreter to be able to run benchmarks.' + exit +end diff --git a/activesupport/lib/active_support/testing/performance/ruby/mri.rb b/activesupport/lib/active_support/testing/performance/ruby/mri.rb new file mode 100644 index 0000000000..86e650050b --- /dev/null +++ b/activesupport/lib/active_support/testing/performance/ruby/mri.rb @@ -0,0 +1,59 @@ +module ActiveSupport + module Testing + module Performance + module Metrics + class Base + protected + # Ruby 1.8 + ruby-prof wrapper (enable/disable stats for Benchmarker) + if GC.respond_to?(:enable_stats) + def with_gc_stats + GC.enable_stats + GC.start + yield + ensure + GC.disable_stats + end + end + end + + class Memory < DigitalInformationUnit + # Ruby 1.8 + ruby-prof wrapper + if RubyProf.respond_to?(:measure_memory) + def measure + RubyProf.measure_memory + end + end + end + + class Objects < Amount + # Ruby 1.8 + ruby-prof wrapper + if RubyProf.respond_to?(:measure_allocations) + def measure + RubyProf.measure_allocations + end + end + end + + class GcRuns < Amount + # Ruby 1.8 + ruby-prof wrapper + if RubyProf.respond_to?(:measure_gc_runs) + def measure + RubyProf.measure_gc_runs + end + end + end + + class GcTime < Time + # Ruby 1.8 + ruby-prof wrapper + if RubyProf.respond_to?(:measure_gc_time) + def measure + RubyProf.measure_gc_time / 1000.0 / 1000.0 + end + end + end + end + end + end +end + + diff --git a/activesupport/lib/active_support/testing/performance/ruby/yarv.rb b/activesupport/lib/active_support/testing/performance/ruby/yarv.rb new file mode 100644 index 0000000000..62095a8fe4 --- /dev/null +++ b/activesupport/lib/active_support/testing/performance/ruby/yarv.rb @@ -0,0 +1,57 @@ +module ActiveSupport + module Testing + module Performance + module Metrics + class Base + protected + # Ruby 1.9 with GC::Profiler + if defined?(GC::Profiler) + def with_gc_stats + GC::Profiler.enable + GC.start + yield + ensure + GC::Profiler.disable + end + end + end + + class Memory < DigitalInformationUnit + # Ruby 1.9 + GCdata patch + if GC.respond_to?(:malloc_allocated_size) + def measure + GC.malloc_allocated_size + end + end + end + + class Objects < Amount + # Ruby 1.9 + GCdata patch + if GC.respond_to?(:malloc_allocations) + def measure + GC.malloc_allocations + end + end + end + + class GcRuns < Amount + # Ruby 1.9 + if GC.respond_to?(:count) + def measure + GC.count + end + end + end + + class GcTime < Time + # Ruby 1.9 with GC::Profiler + if defined?(GC::Profiler) && GC::Profiler.respond_to?(:total_time) + def measure + GC::Profiler.total_time + end + end + end + end + end + end +end |