aboutsummaryrefslogblamecommitdiffstats
path: root/activesupport/lib/active_support/testing/performance.rb
blob: 24eea1e40bef6d63a578991e38c5c5c05c370d23 (plain) (tree)
1
2
3
4
5
6
7





                                                               
                                                      






















                                                                              
 


                                             
 

                                                   
 

                                          
 






                                                                   
               
             
 

                                           
 



                                                     



                                                      








                                                                 

           


                        
 


                                                                             
 

                    
 


                                                                        
 



                                 
 

                                                                                 
 


                                               
 


                                                             
             
 



                                                                             

           



                                                                               
             
 




                                                             
               

             


                                                                                                      
 





                                                                                   
 

                                                                 
 
                                                            
               
 
                
             
 

                                                                     
 





                                                      
 




                                              
 


                               
           
 

                                  
                 
                                                          
             
 

                                    
 






                                                                                                     
 




                                                


               



                                                                                                                   
 






                                                                                 
             
 












                                                                        

           




                                         

             

                              
 


                          
 


                                                             
 

                              
               
 


                       
 


                                
                     
                                            

                 
 




                             
               
 






















                                              
             
 



                             
 






                                              
             
 

                                         
 
                       
                                           
               
             
 

                                      
 
                       
                                        
               
             
 

                                                                           
 



                                              
               
 
                       
                                       
               

             

                                                                       
 




                                                    
 




                                                 
 


























                                                                    
               
 

                                     
               

             

                                                                                 
 



                                                         
 















                                                                           
               
 


                                   
             
 

                                                                         
 











                                                     
               


                                   


               















                                                                         

             


         

                
begin
  require 'ruby-prof'

  require 'fileutils'
  require 'rails/version'
  require 'active_support/core_ext/class/delegating_attributes'
  require 'active_support/core_ext/string/inflections'

  module ActiveSupport
    module Testing
      module Performance
        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 self.included(base)
          base.superclass_delegating_accessor :profile_options
          base.profile_options = DEFAULTS
        end

        def full_test_name
          "#{self.class.name}##{method_name}"
        end

        def run(result)
          return if method_name =~ /^default_test$/

          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
            end
          end

          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
          add_error($!)
        ensure
          begin
            teardown
            run_callbacks :teardown, :enumerator => :reverse_each
          rescue ::Test::Unit::AssertionFailedError => e
            add_failure(e.message, e.backtrace)
          rescue StandardError, ScriptError
            add_error($!)
          end
        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)]

            GC.start
          end

          def run_profile(metric)
            klass = 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

          def initialize(harness, metric)
            @harness, @metric = harness, metric
          end

          def report
            rate = @total / profile_options[:runs]
            '%20s: %s' % [@metric.name, @metric.format(rate)]
          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
          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

        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.sort.last.total_time }
          end

          def report
            if @supported
              super
            else
              '%20s: unsupported' % @metric.name
            end
          end

          def record
            return unless @supported

            klasses = 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, 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 'GraphPrinter'; 'graph.txt'
                  when 'GraphHtmlPrinter'; 'graph.html'
                  when 'CallTreePrinter'; 'tree.txt'
                  else printer_class.name.sub(/Printer$/, '').underscore
                end

              "#{super()}_#{suffix}"
            end
        end

        module Metrics
          def self.[](name)
            const_get(name.to_s.camelize)
          rescue NameError
            nil
          end

          class Base
            attr_reader :total

            def initialize
              @total = 0
            end

            def name
              @name ||= self.class.name.demodulize.underscore
            end

            def measure_mode
              self.class::Mode
            end

            def measure
              0
            end

            def benchmark
              with_gc_stats do
                before = measure
                yield
                @total += (measure - before)
              end
            end

            def profile
              RubyProf.resume
              yield
            ensure
              RubyProf.pause
            end

            protected
              if GC.respond_to?(:enable_stats)
                def with_gc_stats
                  GC.enable_stats
                  yield
                ensure
                  GC.disable_stats
                end
              elsif defined?(GC::Profiler)
                def with_gc_stats
                  GC.start
                  GC.disable
                  GC::Profiler.enable
                  yield
                ensure
                  GC::Profiler.disable
                  GC.enable
                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 < 2
                '%d ms' % (measurement * 1000)
              else
                '%.2f sec' % measurement
              end
            end
          end

          class ProcessTime < Time
            Mode = RubyProf::PROCESS_TIME

            def measure
              RubyProf.measure_process_time
            end
          end

          class WallTime < Time
            Mode = RubyProf::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
              super
            end

            def measure
              RubyProf.measure_cpu_time
            end
          end

          class Memory < Base
            Mode = RubyProf::MEMORY if RubyProf.const_defined?(:MEMORY)

            # ruby-prof wrapper
            if RubyProf.respond_to?(:measure_memory)
              def measure
                RubyProf.measure_memory / 1024.0
              end

            # Ruby 1.8 + railsbench patch
            elsif GC.respond_to?(:allocated_size)
              def measure
                GC.allocated_size / 1024.0
              end

            # Ruby 1.8 + lloyd patch
            elsif GC.respond_to?(:heap_info)
              def measure
                GC.heap_info['heap_current_memory'] / 1024.0
              end

            # Ruby 1.9 with total_malloc_allocated_size patch
            elsif GC.respond_to?(:malloc_total_allocated_size)
              def measure
                GC.total_malloc_allocated_size / 1024.0
              end

            # Ruby 1.9 unpatched
            elsif GC.respond_to?(:malloc_allocated_size)
              def measure
                GC.malloc_allocated_size / 1024.0
              end

            # Ruby 1.9 + GC profiler patch
            elsif defined?(GC::Profiler)
              def measure
                GC.enable
                GC.start
                kb = GC::Profiler.data.last[:HEAP_USE_SIZE] / 1024.0
                GC.disable
                kb
              end
            end

            def format(measurement)
              '%.2f KB' % measurement
            end
          end

          class Objects < Base
            Mode = RubyProf::ALLOCATIONS if RubyProf.const_defined?(:ALLOCATIONS)

            if RubyProf.respond_to?(:measure_allocations)
              def measure
                RubyProf.measure_allocations
              end

            # Ruby 1.8 + railsbench patch
            elsif ObjectSpace.respond_to?(:allocated_objects)
              def measure
                ObjectSpace.allocated_objects
              end

            # Ruby 1.9 + GC profiler patch
            elsif defined?(GC::Profiler)
              def measure
                GC.enable
                GC.start
                last = GC::Profiler.data.last
                count = last[:HEAP_LIVE_OBJECTS] + last[:HEAP_FREE_OBJECTS]
                GC.disable
                count
              end
            end

            def format(measurement)
              measurement.to_i.to_s
            end
          end

          class GcRuns < Base
            Mode = RubyProf::GC_RUNS if RubyProf.const_defined?(:GC_RUNS)

            if RubyProf.respond_to?(:measure_gc_runs)
              def measure
                RubyProf.measure_gc_runs
              end
            elsif GC.respond_to?(:collections)
              def measure
                GC.collections
              end
            elsif GC.respond_to?(:heap_info)
              def measure
                GC.heap_info['num_gc_passes']
              end
            end

            def format(measurement)
              measurement.to_i.to_s
            end
          end

          class GcTime < Base
            Mode = RubyProf::GC_TIME if RubyProf.const_defined?(:GC_TIME)

            if RubyProf.respond_to?(:measure_gc_time)
              def measure
                RubyProf.measure_gc_time
              end
            elsif GC.respond_to?(:time)
              def measure
                GC.time
              end
            end

            def format(measurement)
              '%d ms' % (measurement / 1000)
            end
          end
        end
      end
    end
  end
rescue LoadError
end