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
'/benchmarks/hello'
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/benchmarks/hello') { |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('--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('--benchmark', 'Benchmark instead of profiling') { |v| options[:benchmark] = 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