aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/lib/action_dispatch/testing/performance_test.rb19
-rw-r--r--activesupport/lib/active_support/testing/performance.rb637
-rw-r--r--activesupport/lib/active_support/testing/performance/jruby.rb115
-rw-r--r--activesupport/lib/active_support/testing/performance/rubinius.rb113
-rw-r--r--activesupport/lib/active_support/testing/performance/ruby.rb152
-rw-r--r--activesupport/lib/active_support/testing/performance/ruby/mri.rb59
-rw-r--r--activesupport/lib/active_support/testing/performance/ruby/yarv.rb57
-rw-r--r--railties/guides/source/performance_testing.textile243
-rw-r--r--railties/lib/rails/commands/benchmarker.rb47
-rw-r--r--railties/lib/rails/commands/profiler.rb70
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb5
-rw-r--r--railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb5
12 files changed, 966 insertions, 556 deletions
diff --git a/actionpack/lib/action_dispatch/testing/performance_test.rb b/actionpack/lib/action_dispatch/testing/performance_test.rb
index e7aeb45fb3..13fe693c32 100644
--- a/actionpack/lib/action_dispatch/testing/performance_test.rb
+++ b/actionpack/lib/action_dispatch/testing/performance_test.rb
@@ -1,17 +1,10 @@
require 'active_support/testing/performance'
-begin
- module ActionDispatch
- # An integration test that runs a code profiler on your test methods.
- # Profiling output for combinations of each test method, measurement, and
- # output format are written to your tmp/performance directory.
- #
- # By default, process_time is measured and both flat and graph_html output
- # formats are written, so you'll have two output files per test method.
- class PerformanceTest < ActionDispatch::IntegrationTest
- include ActiveSupport::Testing::Performance
- end
+module ActionDispatch
+ # An integration test that runs a code profiler on your test methods.
+ # Profiling output for combinations of each test method, measurement, and
+ # output format are written to your tmp/performance directory.
+ class PerformanceTest < ActionDispatch::IntegrationTest
+ include ActiveSupport::Testing::Performance
end
-rescue NameError
- $stderr.puts "Specify ruby-prof as application's dependency in Gemfile to run benchmarks."
end
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
diff --git a/railties/guides/source/performance_testing.textile b/railties/guides/source/performance_testing.textile
index 5679bae531..2b79237c59 100644
--- a/railties/guides/source/performance_testing.textile
+++ b/railties/guides/source/performance_testing.textile
@@ -4,7 +4,7 @@ This guide covers the various ways of performance testing a Ruby on Rails applic
* Understand the various types of benchmarking and profiling metrics
* Generate performance and benchmarking tests
-* Use a GC-patched Ruby binary to measure memory usage and object allocation
+* Install and use a GC-patched Ruby binary to measure memory usage and object allocation
* Understand the benchmarking information provided by Rails inside the log files
* Learn about various tools facilitating benchmarking and profiling
@@ -23,7 +23,7 @@ require 'test_helper'
require 'rails/performance_test_help'
# Profiling results for each test method are written to tmp/performance.
-class BrowsingTest < ActionController::PerformanceTest
+class BrowsingTest < ActionDispatch::PerformanceTest
def test_homepage
get '/'
end
@@ -34,10 +34,10 @@ This example is a simple performance test case for profiling a GET request to th
h4. Generating Performance Tests
-Rails provides a generator called +test_unit:performance+ for creating new performance tests:
+Rails provides a generator called +performance_test+ for creating new performance tests:
<shell>
-$ rails generate test_unit:performance homepage
+$ rails generate performance_test homepage
</shell>
This generates +homepage_test.rb+ in the +test/performance+ directory:
@@ -46,7 +46,7 @@ This generates +homepage_test.rb+ in the +test/performance+ directory:
require 'test_helper'
require 'rails/performance_test_help'
-class HomepageTest < ActionController::PerformanceTest
+class HomepageTest < ActionDispatch::PerformanceTest
# Replace this with your real tests.
def test_homepage
get '/'
@@ -105,7 +105,7 @@ Here's the performance test for +HomeController#dashboard+ and +PostsController#
require 'test_helper'
require 'rails/performance_test_help'
-class PostPerformanceTest < ActionController::PerformanceTest
+class PostPerformanceTest < ActionDispatch::PerformanceTest
def setup
# Application requires logged-in user
login_as(:lifo)
@@ -133,7 +133,7 @@ Performance test for +Post+ model:
require 'test_helper'
require 'rails/performance_test_help'
-class PostModelTest < ActionController::PerformanceTest
+class PostModelTest < ActionDispatch::PerformanceTest
def test_creation
Post.create :body => 'still fooling you', :cost => '100'
end
@@ -151,7 +151,7 @@ Performance tests can be run in two modes: Benchmarking and Profiling.
h5. Benchmarking
-Benchmarking helps find out how fast each performance test runs. Each test case is run +4 times+ in benchmarking mode.
+Benchmarking makes it easy to quickly gather a few metrics about each test tun. By default, each test case is run +4 times+ in benchmarking mode.
To run performance tests in benchmarking mode:
@@ -161,7 +161,7 @@ $ rake test:benchmark
h5. Profiling
-Profiling helps you see the details of a performance test and provide an in-depth picture of the slow and memory hungry parts. Each test case is run +1 time+ in profiling mode.
+Profiling allows you to make an in-depth analysis of each of your tests by using an external profiler. Depending on your Ruby interpreter, this profiler can be native (Rubinius, JRuby) or not (MRI, which uses RubyProf). By default, each test case is run +1 time+ in profiling mode.
To run performance tests in profiling mode:
@@ -171,43 +171,59 @@ $ rake test:profile
h4. Metrics
-Benchmarking and profiling run performance tests in various modes described below.
+Benchmarking and profiling run performance tests and give you multiple metrics. The availability of each metric is determined by the interpreter being used—none of them support all metrics—and by the mode in use. A brief description of each metric and their availability across interpreters/modes is given below.
h5. Wall Time
Wall time measures the real world time elapsed during the test run. It is affected by any other processes concurrently running on the system.
-Mode: Benchmarking
-
h5. Process Time
Process time measures the time taken by the process. It is unaffected by any other processes running concurrently on the same system. Hence, process time is likely to be constant for any given performance test, irrespective of the machine load.
-Mode: Profiling
+h5. CPU Time
+
+Similar to process time, but leverages the more accurate CPU clock counter available on the Pentium and PowerPC platforms.
+
+h5. User Time
+
+User time measures the amount of time the CPU spent in user-mode, i.e. within the process. This is not affected by other processes and by the time it possibly spends blocked.
h5. Memory
Memory measures the amount of memory used for the performance test case.
-Mode: Benchmarking, Profiling "Requires GC Patched Ruby":#installing-gc-patched-ruby
-
h5. Objects
Objects measures the number of objects allocated for the performance test case.
-Mode: Benchmarking, Profiling "Requires GC Patched Ruby":#installing-gc-patched-ruby
-
h5. GC Runs
GC Runs measures the number of times GC was invoked for the performance test case.
-Mode: Benchmarking "Requires GC Patched Ruby":#installing-gc-patched-ruby
-
h5. GC Time
GC Time measures the amount of time spent in GC for the performance test case.
-Mode: Benchmarking "Requires GC Patched Ruby":#installing-gc-patched-ruby
+h5. Metric Availability
+
+h6. Benchmarking
+
+|_.Interpreter|_.Wall Time|_.Process Time|_.CPU Time|_.User Time|_.Memory|_.Objects|_.GC Runs|_.GC Time|
+|_.MRI | yes | yes | yes | no | yes | yes | yes | yes |
+|_.REE | yes | yes | yes | no | yes | yes | yes | yes |
+|_.Rubinius | yes | no | no | no | yes | yes | yes | yes |
+|_.JRuby | yes | no | no | yes | yes | yes | yes | yes |
+
+h6. Profiling
+
+|_.Interpreter|_.Wall Time|_.Process Time|_.CPU Time|_.User Time|_.Memory|_.Objects|_.GC Runs|_.GC Time|
+|_.MRI | yes | yes | no | no | yes | yes | yes | yes |
+|_.REE | yes | yes | no | no | yes | yes | yes | yes |
+|_.Rubinius | yes | no | no | no | no | no | no | no |
+|_.JRuby | yes | no | no | no | no | no | no | no |
+
+NOTE: To profile under JRuby you'll need to run +export JRUBY_OPTS="-Xlaunch.inproc=false --profile.api"+ *before* the performance tests.
h4. Understanding the Output
@@ -215,7 +231,7 @@ Performance tests generate different outputs inside +tmp/performance+ directory
h5(#output-benchmarking). Benchmarking
-In benchmarking mode, performance tests generate two types of outputs:
+In benchmarking mode, performance tests generate two types of outputs.
h6(#output-command-line). Command Line
@@ -225,7 +241,7 @@ This is the primary form of output in benchmarking mode. Example:
BrowsingTest#test_homepage (31 ms warmup)
wall_time: 6 ms
memory: 437.27 KB
- objects: 5514
+ objects: 5,514
gc_runs: 0
gc_time: 19 ms
</shell>
@@ -260,7 +276,7 @@ measurement,created_at,app,rails,ruby,platform
h5(#output-profiling). Profiling
-In profiling mode, you can choose from four types of output.
+In profiling mode, performance tests can generate multiple types of outputs. The command line output is always presented but support for the others is dependant on the interpreter in use. A brief description of each type and their availability across interpreters is given below.
h6. Command Line
@@ -270,26 +286,67 @@ This is a very basic form of output in profiling mode:
BrowsingTest#test_homepage (58 ms warmup)
process_time: 63 ms
memory: 832.13 KB
- objects: 7882
+ objects: 7,882
</shell>
h6. Flat
-Flat output shows the total amount of time spent in each method. "Check ruby prof documentation for a better explanation":http://ruby-prof.rubyforge.org/files/examples/flat_txt.html.
+Flat output shows the metric—time, memory, etc—measure in each method. "Check Ruby-Prof documentation for a better explanation":http://ruby-prof.rubyforge.org/files/examples/flat_txt.html.
h6. Graph
-Graph output shows how long each method takes to run, which methods call it and which methods it calls. "Check ruby prof documentation for a better explanation":http://ruby-prof.rubyforge.org/files/examples/graph_txt.html.
+Graph output shows the metric measure in each method, which methods call it and which methods it calls. "Check Ruby-Prof documentation for a better explanation":http://ruby-prof.rubyforge.org/files/examples/graph_txt.html.
h6. Tree
Tree output is profiling information in calltree format for use by "kcachegrind":http://kcachegrind.sourceforge.net/html/Home.html and similar tools.
+h6. Output Availability
+
+|_. |_.Flat|_.Graph|_.Tree|
+|_.MRI | yes | yes | yes |
+|_.REE | yes | yes | yes |
+|_.Rubinius | yes | yes | no |
+|_.JRuby | yes | yes | no |
+
h4. Tuning Test Runs
-By default, each performance test is run +4 times+ in benchmarking mode and +1 time+ in profiling. However, test runs can easily be configured.
+Test runs can be tuned by setting the +profile_options+ class variable on your test class.
-WARNING: Performance test configurability is not yet enabled in Rails. But it will be soon.
+<ruby>
+require 'test_helper'
+require 'rails/performance_test_help'
+
+# Profiling results for each test method are written to tmp/performance.
+class BrowsingTest < ActionDispatch::PerformanceTest
+ self.profile_options = { :runs => 5,
+ :metrics => [:wall_time, :memory] }
+
+ def test_homepage
+ get '/'
+ end
+end
+</ruby>
+
+In this example, the test would run 5 times and measure wall time and memory. There are a few configurable options:
+
+|_.Option |_.Description|_.Default|_.Mode|
+|+:runs+ |Number of runs.|Benchmarking: 4, Profiling: 1|Both|
+|+:output+ |Directory to use when writing the results.|+tmp/performance+|Both|
+|+:metrics+ |Metrics to use.|See below.|Both|
+|+:formats+ |Formats to output to.|See below.|Profiling|
+
+Metrics and formats have different defaults depending on the interpreter in use.
+
+|_.Interpreter|_.Mode|_.Default metrics|_.Default formats|
+|/2.MRI/REE |Benchmarking|+[:wall_time, :memory, :objects, :gc_runs, :gc_time]+|N/A|
+|Profiling |+[:process_time, :memory, :objects]+|+[:flat, :graph_html, :call_tree, :call_stack]+|
+|/2.Rubinius|Benchmarking|+[:wall_time, :memory, :objects, :gc_runs, :gc_time]+|N/A|
+|Profiling |+[:wall_time]+|+[:flat, :graph]+|
+|/2.JRuby |Benchmarking|+[:wall_time, :user_time, :memory, :gc_runs, :gc_time]+|N/A|
+|Profiling |+[:wall_time]+|+[:flat, :graph]+|
+
+As you've probably noticed by now, metrics and formats are specified using a symbol array with each name "underscored.":http://api.rubyonrails.org/classes/String.html#method-i-underscore
h4. Performance Test Environment
@@ -303,41 +360,71 @@ Rails.logger.level = ActiveSupport::BufferedLogger::INFO
As +ActionController::Base.perform_caching+ is set to +true+, performance tests will behave much as they do in the +production+ environment.
-h4. Installing GC-Patched Ruby
+h4. Installing GC-Patched MRI
+
+To get the best from Rails' performance tests under MRI, you'll need to build a special Ruby binary with some super powers.
-To get the best from Rails performance tests, you need to build a special Ruby binary with some super powers - "GC patch":http://rubyforge.org/tracker/download.php/1814/7062/17676/3291/ruby186gc.patch for measuring GC Runs/Time and memory/object allocation.
+The recommended patches for each MRI version are:
-The process is fairly straightforward. If you've never compiled a Ruby binary before, follow these steps to build a ruby binary inside your home directory:
+|_.Version|_.Patch|
+|1.8.6|ruby186gc|
+|1.8.7|ruby187gc|
+|1.9.2 and above|gcdata|
-h5. Installation
+All of these can be found on "RVM's _patches_ directory":https://github.com/wayneeseguin/rvm/tree/master/patches/ruby under each specific interpreter version.
-Compile Ruby and apply this "GC Patch":http://rubyforge.org/tracker/download.php/1814/7062/17676/3291/ruby186gc.patch.
+Concerning the installation itself, you can either do this easily by using "RVM":http://rvm.beginrescueend.com or you can build everything from source, which is a little bit harder.
-h5. Download and Extract
+h5. Install Using RVM
+
+The process of installing a patched Ruby interpreter is very easy if you let RVM do the hard work. All of the following RVM commands will provide you with a patched Ruby interpreter:
+
+<shell>
+$ rvm install 1.9.2-p180 --patch gcdata
+$ rvm install 1.8.7 --patch ruby187gc
+$ rvm install 1.9.2-p180 --patch ~/Downloads/downloaded_gcdata_patch.patch
+</shell>
+
+You can even keep your regular interpreter by assigning a name to the patched one:
+
+<shell>
+$ rvm install 1.9.2-p180 --patch gcdata --name gcdata
+$ rvm use 1.9.2-p180 # your regular ruby
+$ rvm use 1.9.2-p180-gcdata # your patched ruby
+</shell>
+
+And it's done! You have installed a patched Ruby interpreter.
+
+h5. Install From Source
+
+This process is a bit more complicated, but straightforward nonetheless. If you've never compiled a Ruby binary before, follow these steps to build a Ruby binary inside your home directory.
+
+h6. Download and Extract
<shell>
$ mkdir rubygc
-$ wget <download the latest stable ruby from ftp://ftp.ruby-lang.org/pub/ruby>
+$ wget <the version you want from ftp://ftp.ruby-lang.org/pub/ruby>
$ tar -xzvf <ruby-version.tar.gz>
$ cd <ruby-version>
</shell>
-h5. Apply the Patch
+h6. Apply the Patch
<shell>
-$ curl http://rubyforge.org/tracker/download.php/1814/7062/17676/3291/ruby186gc.patch | patch -p0
+$ curl http://github.com/wayneeseguin/rvm/raw/master/patches/ruby/1.9.2/p180/gcdata.patch | patch -p0 # if you're on 1.9.2!
+$ curl http://github.com/wayneeseguin/rvm/raw/master/patches/ruby/1.8.7/ruby187gc.patch | patch -p0 # if you're on 1.8.7!
</shell>
-h5. Configure and Install
+h6. Configure and Install
-The following will install ruby in your home directory's +/rubygc+ directory. Make sure to replace +&lt;homedir&gt;+ with a full patch to your actual home directory.
+The following will install Ruby in your home directory's +/rubygc+ directory. Make sure to replace +&lt;homedir&gt;+ with a full patch to your actual home directory.
<shell>
$ ./configure --prefix=/<homedir>/rubygc
$ make && make install
</shell>
-h5. Prepare Aliases
+h6. Prepare Aliases
For convenience, add the following lines in your +~/.profile+:
@@ -349,26 +436,21 @@ alias gcirb='~/rubygc/bin/irb'
alias gcrails='~/rubygc/bin/rails'
</shell>
-h5. Install Rubygems and Dependency Gems
+Don't forget to use your aliases from now on.
-Download "Rubygems":http://rubyforge.org/projects/rubygems and install it from source. Rubygem's README file should have necessary installation instructions.
+h6. Install Rubygems (1.8 only!)
-Additionally, install the following gems:
+Download "Rubygems":http://rubyforge.org/projects/rubygems and install it from source. Rubygem's README file should have necessary installation instructions. Please note that this step isn't necessary if you've installed Ruby 1.9 and above.
-* +rake+
-* +rails+
-* +ruby-prof+
-* +rack+
-* +mysql+
+h4. Using Ruby-Prof on MRI and REE
-If installing +mysql+ fails, you can try to install it manually:
+Add Ruby-Prof to your applications' Gemfile if you want to benchmark/profile under MRI or REE:
-<shell>
-$ gcruby extconf.rb --with-mysql-config
-$ make && make install
-</shell>
+<ruby>
+gem 'ruby-prof', :path => 'git://github.com/wycats/ruby-prof.git'
+</ruby>
-And you're ready to go. Don't forget to use +gcruby+ and +gcrake+ aliases when running the performance tests.
+Now run +bundle install+ and you're ready to go.
h3. Command Line Tools
@@ -376,55 +458,47 @@ Writing performance test cases could be an overkill when you are looking for one
h4. +benchmarker+
-+benchmarker+ is a wrapper around Ruby's "Benchmark":http://ruby-doc.org/core/classes/Benchmark.html standard library.
-
Usage:
<shell>
-$ rails benchmarker [times] 'Person.expensive_way' 'Person.another_expensive_way' ...
-</shell>
-
-Examples:
-
-<shell>
-$ rails benchmarker 10 'Item.all' 'CouchItem.all'
+Usage: rails benchmarker 'Ruby.code' 'Ruby.more_code' ... [OPTS]
+ -r, --runs N Number of runs.
+ Default: 4
+ -o, --output PATH Directory to use when writing the results.
+ Default: tmp/performance
+ -m, --metrics a,b,c Metrics to use.
+ Default: wall_time,memory,objects,gc_runs,gc_time
</shell>
-If the +[times]+ argument is omitted, supplied methods are run just once:
+Example:
<shell>
-$ rails benchmarker 'Item.first' 'Item.last'
+$ rails benchmarker 'Item.all' 'CouchItem.all' --runs 3 --metrics wall_time,memory
</shell>
h4. +profiler+
-+profiler+ is a wrapper around the "ruby-prof":http://ruby-prof.rubyforge.org gem.
-
Usage:
<shell>
-$ rails profiler 'Person.expensive_method(10)' [times] [flat|graph|graph_html]
+Usage: rails benchmarker 'Ruby.code' 'Ruby.more_code' ... [OPTS]
+ -r, --runs N Number of runs.
+ Default: 1
+ -o, --output PATH Directory to use when writing the results.
+ Default: tmp/performance
+ --metrics a,b,c Metrics to use.
+ Default: process_time,memory,objects
+ -m, --formats x,y,z Formats to output to.
+ Default: flat,graph_html,call_tree
</shell>
-Examples:
+Example:
<shell>
-$ rails profiler 'Item.all'
+$ rails profiler 'Item.all' 'CouchItem.all' --runs 2 --metrics process_time --formats flat
</shell>
-This will profile +Item.all+ in +RubyProf::WALL_TIME+ measure mode. By default, it prints flat output to the shell.
-
-<shell>
-$ rails profiler 'Item.all' 10 graph
-</shell>
-
-This will profile +10.times { Item.all }+ with +RubyProf::WALL_TIME+ measure mode and print graph output to the shell.
-
-If you want to store the output in a file:
-
-<shell>
-$ rails profiler 'Item.all' 10 graph 2> graph.txt
-</shell>
+NOTE: Metrics and formats vary from interpreter to interpreter. Pass +--help+ to each tool to see the defaults for your interpreter.
h3. Helper Methods
@@ -517,12 +591,13 @@ h4. Tutorials and Documentation
h3. Commercial Products
-Rails has been lucky to have two startups dedicated to Rails specific performance tools:
+Rails has been lucky to have a few companies dedicated to Rails-specific performance tools. A couple of those are:
* "New Relic":http://www.newrelic.com
* "Scout":http://scoutapp.com
h3. Changelog
+* March 30, 2011: Documented the recent improvements (multiple interpreters, options, etc) and necessary adjustments. Other minor improvements. "Gonçalo Silva":http://goncalossilva.com.
* January 9, 2009: Complete rewrite by "Pratik":credits.html#lifo
* September 6, 2008: Initial version by Matthew Bergman
diff --git a/railties/lib/rails/commands/benchmarker.rb b/railties/lib/rails/commands/benchmarker.rb
index f230f405c0..b06c915ac3 100644
--- a/railties/lib/rails/commands/benchmarker.rb
+++ b/railties/lib/rails/commands/benchmarker.rb
@@ -1,25 +1,34 @@
-require 'active_support/core_ext/object/inclusion'
+require 'optparse'
+require 'rails/test_help'
+require 'rails/performance_test_help'
-if ARGV.first.in?([nil, "-h", "--help"])
- puts "Usage: rails benchmarker [times] 'Person.expensive_way' 'Person.another_expensive_way' ..."
- exit 1
-end
+ARGV.push('--benchmark') # HAX
+require 'active_support/testing/performance'
+ARGV.pop
-begin
- N = Integer(ARGV.first)
- ARGV.shift
-rescue ArgumentError
- N = 1
+def options
+ options = {}
+ defaults = ActiveSupport::Testing::Performance::DEFAULTS
+
+ OptionParser.new do |opt|
+ opt.banner = "Usage: rails benchmarker 'Ruby.code' 'Ruby.more_code' ... [OPTS]"
+ opt.on('-r', '--runs N', Numeric, 'Number of runs.', "Default: #{defaults[:runs]}") { |r| options[:runs] = r }
+ opt.on('-o', '--output PATH', String, 'Directory to use when writing the results.', "Default: #{defaults[:output]}") { |o| options[:output] = o }
+ opt.on('-m', '--metrics a,b,c', Array, 'Metrics to use.', "Default: #{defaults[:metrics].join(",")}") { |m| options[:metrics] = m.map(&:to_sym) }
+ opt.parse!(ARGV)
+ end
+
+ options
end
-require 'benchmark'
-include Benchmark
-
-# Don't include compilation in the benchmark
-ARGV.each { |expression| eval(expression) }
-
-bm(6) do |x|
- ARGV.each_with_index do |expression, idx|
- x.report("##{idx + 1}") { N.times { eval(expression) } }
+class BenchmarkerTest < ActionDispatch::PerformanceTest
+ self.profile_options = options
+
+ ARGV.each do |expression|
+ eval <<-RUBY
+ def test_#{expression.parameterize('_')}
+ #{expression}
+ end
+ RUBY
end
end
diff --git a/railties/lib/rails/commands/profiler.rb b/railties/lib/rails/commands/profiler.rb
index 7959d8a981..94cf32d32d 100644
--- a/railties/lib/rails/commands/profiler.rb
+++ b/railties/lib/rails/commands/profiler.rb
@@ -1,48 +1,32 @@
-require 'active_support/core_ext/object/inclusion'
+require 'optparse'
+require 'rails/test_help'
+require 'rails/performance_test_help'
+require 'active_support/testing/performance'
-if ARGV.first.in?([nil, "-h", "--help"])
- $stderr.puts "Usage: rails profiler 'Person.expensive_method(10)' [times] [flat|graph|graph_html]"
- exit(1)
-end
-
-# Define a method to profile.
-if ARGV[1] and ARGV[1].to_i > 1
- eval "def profile_me() #{ARGV[1]}.times { #{ARGV[0]} } end"
-else
- eval "def profile_me() #{ARGV[0]} end"
+def options
+ options = {}
+ defaults = ActiveSupport::Testing::Performance::DEFAULTS
+
+ OptionParser.new do |opt|
+ opt.banner = "Usage: rails benchmarker 'Ruby.code' 'Ruby.more_code' ... [OPTS]"
+ opt.on('-r', '--runs N', Numeric, 'Number of runs.', "Default: #{defaults[:runs]}") { |r| options[:runs] = r }
+ opt.on('-o', '--output PATH', String, 'Directory to use when writing the results.', "Default: #{defaults[:output]}") { |o| options[:output] = o }
+ opt.on('-m', '--metrics a,b,c', Array, 'Metrics to use.', "Default: #{defaults[:metrics].join(",")}") { |m| options[:metrics] = m.map(&:to_sym) }
+ opt.on('-f', '--formats x,y,z', Array, 'Formats to output to.', "Default: #{defaults[:formats].join(",")}") { |m| options[:formats] = m.map(&:to_sym) }
+ opt.parse!(ARGV)
+ end
+
+ options
end
-# Use the ruby-prof extension if available. Fall back to stdlib profiler.
-begin
- begin
- require "ruby-prof"
- $stderr.puts 'Using the ruby-prof extension.'
- RubyProf.measure_mode = RubyProf::WALL_TIME
- RubyProf.start
- profile_me
- results = RubyProf.stop
- if ARGV[2]
- printer_class = RubyProf.const_get((ARGV[2] + "_printer").classify)
- else
- printer_class = RubyProf::FlatPrinter
- end
- printer = printer_class.new(results)
- printer.print($stderr)
- rescue LoadError
- require "prof"
- $stderr.puts 'Using the old ruby-prof extension.'
- Prof.clock_mode = Prof::GETTIMEOFDAY
- Prof.start
- profile_me
- results = Prof.stop
- require 'rubyprof_ext'
- Prof.print_profile(results, $stderr)
+class ProfilerTest < ActionDispatch::PerformanceTest
+ self.profile_options = options
+
+ ARGV.each do |expression|
+ eval <<-RUBY
+ def test_#{expression.parameterize('_')}
+ #{expression}
+ end
+ RUBY
end
-rescue LoadError
- require 'profiler'
- $stderr.puts 'Using the standard Ruby profiler.'
- Profiler__.start_profile
- profile_me
- Profiler__.stop_profile
- Profiler__.print_profile($stderr)
end
diff --git a/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb b/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb
index 867fc8c985..5d1be041a5 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb
+++ b/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb
@@ -1,8 +1,11 @@
require 'test_helper'
require 'rails/performance_test_help'
-# Profiling results for each test method are written to tmp/performance.
class BrowsingTest < ActionDispatch::PerformanceTest
+ # Refer to the documentation for all available options
+ # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory]
+ # :output => 'tmp/performance', :formats => [:flat] }
+
def test_homepage
get '/'
end
diff --git a/railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb b/railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb
index e827aa918f..14a878328b 100644
--- a/railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb
+++ b/railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb
@@ -2,7 +2,10 @@ require 'test_helper'
require 'rails/performance_test_help'
class <%= class_name %>Test < ActionDispatch::PerformanceTest
- # Replace this with your real tests.
+ # Refer to the documentation for all available options
+ # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory]
+ # :output => 'tmp/performance', :formats => [:flat] }
+
def test_homepage
get '/'
end