aboutsummaryrefslogblamecommitdiffstats
path: root/actionpack/lib/action_controller/request_profiler.rb
blob: 88e25737a6692861c85ba35360f2ab0259faa9e4 (plain) (tree)
























































































                                                                                             
         





































                                                                                                         
                                                                                                                
                                                                                                                      
                                                                                                     

                                                                                                             

































                                                                                                                     
require 'optparse'

module ActionController
  class RequestProfiler
    # CGI with stubbed environment and standard input.
    class StubCGI < CGI
      attr_accessor :env_table, :stdinput

      def initialize(env_table, stdinput)
        @env_table = env_table
        super
        @stdinput = stdinput
      end
    end

    # Stripped-down dispatcher.
    class Sandbox
      attr_accessor :env, :body

      def self.benchmark(n, env, body)
        Benchmark.realtime { n.times { new(env, body).dispatch } }
      end

      def initialize(env, body)
        @env, @body = env, body
      end

      def dispatch
        cgi = StubCGI.new(env, StringIO.new(body))

        request = CgiRequest.new(cgi)
        response = CgiResponse.new(cgi)

        controller = Routing::Routes.recognize(request)
        controller.process(request, response)
      end
    end


    attr_reader :options

    def initialize(options = {})
      @options = default_options.merge(options)
    end


    def self.run(args = nil, options = {})
      profiler = new(options)
      profiler.parse_options(args) if args
      profiler.run
    end

    def run
      warmup
      options[:benchmark] ? benchmark : profile
    end

    def profile
      load_ruby_prof

      results = RubyProf.profile { benchmark }

      show_profile_results results
      results
    end

    def benchmark
      puts '%d req/sec' % (options[:n] / Sandbox.benchmark(options[:n], env, body))
    end

    def warmup
      puts "#{options[:benchmark] ? 'Benchmarking' : 'Profiling'} #{options[:n]}x"
      puts "\nrequest headers: #{env.to_yaml}"

      response = Sandbox.new(env, body).dispatch

      puts "\nresponse body: #{response.body[0...100]}#{'[...]' if response.body.size > 100}"
      puts "\nresponse headers: #{response.headers.to_yaml}"
      puts
    end


    def uri
      URI.parse(options[:uri])
    rescue URI::InvalidURIError
      URI.parse(default_uri)
    end

    def default_uri
      '/'
    end

    def env
      @env ||= default_env
    end

    def default_env
      defaults = {
        'HTTP_HOST'      => "#{uri.host || 'localhost'}:#{uri.port || 3000}",
        'REQUEST_URI'    => uri.path,
        'REQUEST_METHOD' => method,
        'CONTENT_LENGTH' => body.size }

      if fixture = options[:fixture]
        defaults['CONTENT_TYPE'] = "multipart/form-data; boundary=#{extract_multipart_boundary(fixture)}"
      end

      defaults
    end

    def method
      options[:method] || (options[:fixture] ? 'POST' : 'GET')
    end

    def body
      options[:fixture] ? File.read(options[:fixture]) : ''
    end


    def default_options
      { :n => 1000, :open => 'open %s &' }
    end

    # Parse command-line options
    def parse_options(args)
      OptionParser.new do |opt|
        opt.banner = "USAGE: #{$0} uri [options]"

        opt.on('-u', '--uri [URI]', 'Request URI. Defaults to http://localhost:3000/') { |v| options[:uri] = v }
        opt.on('-n', '--times [0000]', 'How many requests to process. Defaults to 1000.') { |v| options[:n] = v.to_i }
        opt.on('-b', '--benchmark', 'Benchmark instead of profiling') { |v| options[:benchmark] = v }
        opt.on('--method [GET]', 'HTTP request method. Defaults to GET.') { |v| options[:method] = v.upcase }
        opt.on('--fixture [FILE]', 'Path to POST fixture file') { |v| options[:fixture] = v }
        opt.on('--open [CMD]', 'Command to open profile results. Defaults to "open %s &"') { |v| options[:open] = v }
        opt.on('-h', '--help', 'Show this help') { puts opt; exit }

        opt.parse args
      end
    end

    protected
      def load_ruby_prof
        begin
          require 'ruby-prof'
          #RubyProf.measure_mode = RubyProf::ALLOCATED_OBJECTS
        rescue LoadError
          abort '`gem install ruby-prof` to use the profiler'
        end
      end

      def extract_multipart_boundary(path)
        File.open(path) { |f| f.readline }
      end

      def show_profile_results(results)
        File.open "#{RAILS_ROOT}/tmp/profile-graph.html", 'w' do |file|
          RubyProf::GraphHtmlPrinter.new(results).print(file)
          `#{options[:open] % file.path}` if options[:open]
        end

        File.open "#{RAILS_ROOT}/tmp/profile-flat.txt", 'w' do |file|
          RubyProf::FlatPrinter.new(results).print(file)
          `#{options[:open] % file.path}` if options[:open]
        end
      end
  end
end