From d69be7df6dd1d358b019a2b8fe2c7cd10050063e Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Thu, 25 Oct 2007 20:33:28 +0000 Subject: [PATCH] Request profiler git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8016 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- actionpack/CHANGELOG | 2 + .../lib/action_controller/request_profiler.rb | 167 ++++++++++++++++++ railties/CHANGELOG | 2 + railties/bin/performance/request | 3 + railties/lib/commands/performance/request.rb | 7 + .../applications/app/app_generator.rb | 2 +- 6 files changed, 182 insertions(+), 1 deletion(-) create mode 100755 actionpack/lib/action_controller/request_profiler.rb create mode 100644 railties/bin/performance/request create mode 100755 railties/lib/commands/performance/request.rb diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 7adad721fc..297a8aa1f1 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Request profiler. [Jeremy Kemper] + * Disabled checkboxes don't submit a form value. #9301 [vladr, robinjfisher] * Added tests for options to ActiveRecordHelper#form. Closes #7213 [richcollins, mikong, mislav] diff --git a/actionpack/lib/action_controller/request_profiler.rb b/actionpack/lib/action_controller/request_profiler.rb new file mode 100755 index 0000000000..1f82fde5e7 --- /dev/null +++ b/actionpack/lib/action_controller/request_profiler.rb @@ -0,0 +1,167 @@ +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 diff --git a/railties/CHANGELOG b/railties/CHANGELOG index b09cbb3a59..99dc2c54d6 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Request profiler. [Jeremy Kemper] + * config/boot.rb correctly detects RAILS_GEM_VERSION. #9834 [alexch, thewoolleyman] * Fixed incorrect migration number if script/generate executed outside of Rails root #7080 [jeremymcanally] diff --git a/railties/bin/performance/request b/railties/bin/performance/request new file mode 100644 index 0000000000..ae3f38c74b --- /dev/null +++ b/railties/bin/performance/request @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/performance/request' diff --git a/railties/lib/commands/performance/request.rb b/railties/lib/commands/performance/request.rb new file mode 100755 index 0000000000..8e19c9d6ed --- /dev/null +++ b/railties/lib/commands/performance/request.rb @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +RAILS_DEFAULT_LOGGER = nil +require 'config/environment' +require 'application' +require 'action_controller/request_profiler' + +ActionController::RequestProfiler.run(ARGV) diff --git a/railties/lib/rails_generator/generators/applications/app/app_generator.rb b/railties/lib/rails_generator/generators/applications/app/app_generator.rb index e78286495c..55c8bf3239 100644 --- a/railties/lib/rails_generator/generators/applications/app/app_generator.rb +++ b/railties/lib/rails_generator/generators/applications/app/app_generator.rb @@ -67,7 +67,7 @@ def manifest m.file "environments/test.rb", "config/environments/test.rb" # Scripts - %w( about console destroy generate performance/benchmarker performance/profiler process/reaper process/spawner process/inspector runner server plugin ).each do |file| + %w( about console destroy generate performance/benchmarker performance/profiler performance/request process/reaper process/spawner process/inspector runner server plugin ).each do |file| m.file "bin/#{file}", "script/#{file}", script_options end -- GitLab