Skip to content
Browse files

Added all files

  • Loading branch information...
1 parent 9125eed commit bfc0db9aef8ad1c04ad862d52375af197beabeb9 Ben Brinckerhoff committed Apr 19, 2010
View
2 LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2009 Ben Brinckerhoff
+Copyright (c) 2010 Ben Brinckerhoff
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
View
96 README.rdoc
@@ -1,6 +1,98 @@
-= rack-perftools-profiler
+= Rack::PerftoolsProfiler
-Description goes here.
+Middleware for profiling Rack-compatible apps using perftools.rb (http://github.com/tmm1/perftools.rb)
+
+== Requirements
+
+You'll need graphviz to generate call graphs using dot (for the GIF printer):
+
+ sudo port install graphviz # OS X
+ brew install graphviz # Homebrew
+ sudo apt-get install graphviz # Debian/Ubuntu
+
+You'll need ps2pdf to generate PDFs (On OS X, ps2pdf comes is installed as part of Ghostscript)
+
+ sudo port install ghostscript # OSX
+ brew install ghostscript # Homebrew
+ sudo apt-get install ps2pdf # Debian/Ubuntu
+
+== Configuration
+
+Install the gem
+
+ gem install rack-perftools_profiler
+
+Include the middleware
+
+ require 'rack/perftools_profiler'
+
+For Rails, add the following to config/environment.rb
+
+ config.middleware.use Rack::PerftoolsProfiler, :default_printer => 'gif'
+
+For Sinatra, call 'use' inside a configure block, like so:
+
+ configure :profiling do
+ use Rack::PerftoolsProfiler, :default_printer => 'gif'
+ end
+
+For Rack::Builder, call 'use' inside the Builder constructor block
+
+ Rack::Builder.new do
+ use Rack::PerftoolsProfiler, :default_printer => 'gif'
+ end
+
+== Options
+
+* :default_printer - can be set to 'text', 'gif', or 'pdf'. Default is :text
+* :mode - can be set to 'cputime' or 'walltime'. Default is :cputime
+* :frequency - in :cputime mode, the number of times per second the app will be sampled. Default is 100 (times/sec)
+
+== Usage
+
+There are two modes for the profiler
+
+First, you can run in 'simple' mode. Just visit the url you want to profile, but
+add the 'profile' and (optionally) the 'times' GET params (which will rerun the action
+the specified number of times).
+
+Example:
+ curl http://localhost:8080/foobar?profile=true&times=3
+
+Note that this will change the status, body, and headers of the response (you'll get
+back the profiling data, NOT the original response).
+
+The other mode is start/stop mode.
+
+Example:
+ curl http://localhost:8080/__start__
+ curl http://localhost:8080/foobar
+ curl http://localhost:8080/foobaz
+ curl http://localhost:8080/__stop__
+ curl http://localhost:8080/__data__
+
+In this mode, all responses are normal. You must visit `__stop__` to complete profiling and
+then you can view the profiling data by visiting `__data__`
+
+== Profiling Data Options
+
+In both simple and start/stop modes, you can add additional params to change how the data
+is displayed. In simple mode, these params are just added to the URL being profiled. In
+start/stop mode, they are added to the `__data__` URL
+
+* printer - overrides the default_printer option (see above)
+* ignore - a regular expression of the area of code to ignore
+* focus - a regular expression of the area of code to solely focus on.
+
+(for 'ignore' and 'focus', please see http://google-perftools.googlecode.com/svn/trunk/doc/cpuprofile.html#pprof
+for more details)
+
+== Acknowledgments
+
+A huge thanks to Aman Gupta for the awesome perftools.rb gem.
+
+The basic idea and initial implementation of the middleware was heavily influenced by
+Rack::Profiler from rack-contrib.
== Note on Patches/Pull Requests
View
20 Rakefile
@@ -4,13 +4,17 @@ require 'rake'
begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
- gem.name = "rack-perftools-profiler"
- gem.summary = %Q{TODO: one-line summary of your gem}
- gem.description = %Q{TODO: longer description of your gem}
- gem.email = "ben@devver.net"
- gem.homepage = "http://github.com/devver/rack-perftools-profiler"
- gem.authors = ["Ben Brinckerhoff"]
- gem.add_development_dependency "thoughtbot-shoulda"
+ gem.name = 'rack-perftools_profiler'
+ gem.summary = %Q{Middleware for profiling Rack-compatible apps using perftools.rb}
+ gem.description = %Q{Middleware for profiling Rack-compatible apps using perftools.rb}
+ gem.email = 'ben@bbrinck.com'
+ gem.homepage = 'http://github.com/bhb/rack-perftools_profiler'
+ gem.authors = ['Ben Brinckerhoff']
+ gem.add_dependency 'perftools.rb', '~> 0.4.0'
+ gem.add_dependency 'rack', '~> 1.1.0'
+ gem.add_dependency('open4', '~> 1.0.1')
+ gem.add_development_dependency 'shoulda', '~> 2.10.2'
+ gem.add_development_dependency 'mocha', '~> 0.9.8'
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
@@ -51,7 +55,7 @@ Rake::RDocTask.new do |rdoc|
end
rdoc.rdoc_dir = 'rdoc'
- rdoc.title = "rack-perftools-profiler #{version}"
+ rdoc.title = "rack-perftools_profiler #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
View
2 VERSION
@@ -1 +1 @@
-0.0.0
+0.0.1
View
0 lib/rack-perftools-profiler.rb
No changes.
View
120 lib/rack/perftools_profiler.rb
@@ -0,0 +1,120 @@
+# REQUIREMENTS
+#
+# You'll need graphviz to generate call graphs using dot (for the GIF printer):
+#
+# sudo port install graphviz # osx
+# sudo apt-get install graphviz # debian/ubuntu
+
+# You'll need ps2pdf to generate PDFs
+# On OS X, ps2pdf comes is installed as part of Ghostscript
+#
+# sudo port install ghostscript # osx
+# brew install ghostscript # homebrew
+# sudo apt-get install ps2pdf # debian/ubuntu
+
+# CONFIGURATION
+#
+# Include the middleware
+#
+# require 'rack/perftools_profiler'
+#
+# For Rails, add the following to config/environment.rb
+#
+# config.middleware.use Rack::PerftoolsProfiler, :default_printer => 'gif'
+#
+# For Sinatra, call 'use' inside a configure block, like so:
+#
+# configure :profiling do
+# use Rack::PerftoolsProfiler, :default_printer => 'gif'
+# end
+#
+# For Rack::Builder, call 'use' inside the Builder constructor block
+#
+# Rack::Builder.new do
+# use Rack::PerftoolsProfiler, :default_printer => 'gif'
+# end
+#
+#
+# OPTIONS
+#
+# :default_printer - can be set to 'text', 'gif', or 'pdf'. Default is :text
+# :mode - can be set to 'cputime' or 'walltime'. Default is :cputime
+# :frequency - in :cputime mode, the number of times per second the app will be sampled.
+# Default is 100 (times/sec)
+#
+# USAGE
+#
+# There are two modes for the profiler
+#
+# First, you can run in 'simple' mode. Just visit the url you want to profile, but
+# add the 'profile' and (optionally) the 'times' GET params
+#
+# example:
+# curl http://localhost:8080/foobar?profile=true&times=3
+#
+# Note that this will change the status, body, and headers of the response (you'll get
+# back the profiling data, NOT the original response.
+#
+#
+# The other mode is start/stop mode.
+#
+# example:
+# curl http://localhost:8080/__start__
+# curl http://localhost:8080/foobar
+# curl http://localhost:8080/foobaz
+# curl http://localhost:8080/__stop__
+# curl http://localhost:8080/__data__
+#
+# In this mode, all responses are normal. You must visit __stop__ to complete profiling and
+# then you can view the profiling data by visiting __data__
+
+# PROFILING DATA OPTIONS
+#
+# In both simple and start/stop modes, you can add additional params to change how the data
+# is displayed. In simple mode, these params are just added to the URL being profiled. In
+# start/stop mode, they are added to the __data__ URL
+
+# printer - overrides the default_printer option (see above)
+# ignore - a regular expression of the area of code to ignore
+# focus - a regular expression of the area of code to solely focus on.
+
+# (for ignore and focus, please see http://google-perftools.googlecode.com/svn/trunk/doc/cpuprofile.html#pprof
+# for more details)
+#
+# ACKNOWLEDGMENTS
+#
+# The basic idea and initial implementation was heavily influenced by Rack::Profiler from rack-contrib.
+
+require 'rack'
+require 'perftools'
+require 'pstore'
+require 'open4'
+
+require 'rack/perftools_profiler/profiler_middleware'
+require 'rack/perftools_profiler/action'
+require 'rack/perftools_profiler/profiler'
+require 'rack/perftools_profiler/start_profiling'
+require 'rack/perftools_profiler/stop_profiling'
+require 'rack/perftools_profiler/profile_data_action'
+require 'rack/perftools_profiler/profile_once'
+require 'rack/perftools_profiler/return_data'
+require 'rack/perftools_profiler/call_app_directly'
+
+module Rack::PerftoolsProfiler
+
+ def self.new(app, options={})
+ ProfilerMiddleware.new(app, options)
+ end
+
+ # helpers for testing
+ def self.clear_data
+ Profiler.clear_data
+ end
+
+ def self.with_profiling_off(app, options = {})
+ instance = ProfilerMiddleware.new(app, options)
+ instance.force_stop
+ instance
+ end
+
+end
View
39 lib/rack/perftools_profiler/action.rb
@@ -0,0 +1,39 @@
+module Rack::PerftoolsProfiler
+
+ class Action
+
+ def initialize(env, profiler, middleware)
+ @env = env
+ @request = Rack::Request.new(env)
+ @data_params = @request.params.clone
+ @profiler = profiler
+ @middleware = middleware
+ end
+
+ def act
+ # do nothing
+ end
+
+ def self.for_env(env, profiler, middleware)
+ request = Rack::Request.new(env)
+ klass =
+ case request.path
+ when '/__start__'
+ StartProfiling
+ when '/__stop__'
+ StopProfiling
+ when '/__data__'
+ ReturnData
+ else
+ if ProfileOnce.has_special_param?(request)
+ ProfileOnce
+ else
+ CallAppDirectly
+ end
+ end
+ klass.new(env, profiler, middleware)
+ end
+
+ end
+
+end
View
15 lib/rack/perftools_profiler/call_app_directly.rb
@@ -0,0 +1,15 @@
+module Rack::PerftoolsProfiler
+
+ class CallAppDirectly < Action
+
+ def act
+ @result = @middleware.call_app(@env)
+ end
+
+ def response
+ @result
+ end
+
+ end
+
+end
View
20 lib/rack/perftools_profiler/profile_data_action.rb
@@ -0,0 +1,20 @@
+module Rack::PerftoolsProfiler
+
+ class ProfileDataAction < Action
+
+ def check_printer_arg
+ request = Rack::Request.new(@env)
+ printer = request.params['printer']
+ self.class.check_printer(printer, @env)
+ end
+
+ def self.check_printer(printer, env=nil)
+ if printer != nil && !ProfilerMiddleware::PRINTERS.member?(printer.to_sym)
+ message = "Invalid printer type: #{printer}. Valid printer values are #{ProfilerMiddleware::PRINTERS.join(", ")}"
+ raise ProfilerArgumentError, message
+ end
+ end
+
+ end
+
+end
View
47 lib/rack/perftools_profiler/profile_once.rb
@@ -0,0 +1,47 @@
+module Rack::PerftoolsProfiler
+
+ class ProfileOnce < ProfileDataAction
+ include Rack::Utils
+
+ def self.has_special_param?(request)
+ request.params['profile'] != nil
+ end
+
+ def initialize(*args)
+ super
+ request = Rack::Request.new(@env)
+ @times = (request.params.fetch('times') {1}).to_i
+ check_printer_arg
+ @new_env = delete_custom_params(@env)
+ end
+
+ def act
+ @profiler.profile do
+ @times.times { @middleware.call_app(@new_env) }
+ end
+ end
+
+ def response
+ @middleware.profiler_data_response(@profiler.data(@data_params))
+ end
+
+ def delete_custom_params(env)
+ new_env = env.clone
+
+ params = Rack::Request.new(new_env).params
+ params.delete('profile')
+ params.delete('times')
+ params.delete('printer')
+ params.delete('ignore')
+ params.delete('focus')
+
+ new_env.delete('rack.request.query_string')
+ new_env.delete('rack.request.query_hash')
+
+ new_env['QUERY_STRING'] = build_query(params)
+ new_env
+ end
+
+ end
+
+end
View
123 lib/rack/perftools_profiler/profiler.rb
@@ -0,0 +1,123 @@
+module Rack::PerftoolsProfiler
+
+ class ProfilingError < RuntimeError
+
+ attr_reader :stderr
+
+ def initialize(message, stderr)
+ super(message)
+ @stderr = stderr
+ end
+
+ end
+
+ class Profiler
+
+ def self.tmpdir
+ dir = nil
+ Dir.chdir Dir.tmpdir do dir = Dir.pwd end # HACK FOR OSX
+ dir
+ end
+
+ PROFILING_DATA_FILE = ::File.join(self.tmpdir, 'rack_perftools_profiler.prof')
+ PROFILING_SETTINGS_FILE = ::File.join(self.tmpdir, 'rack_perftools_profiler.config')
+ DEFAULT_PRINTER = :text
+ DEFAULT_MODE = :cputime
+ UNSET_FREQUENCY = -1
+
+ def initialize(app, options)
+ @printer = (options.delete(:default_printer) { DEFAULT_PRINTER }).to_sym
+ ProfileDataAction.check_printer(@printer)
+ @frequency = (options.delete(:frequency) { UNSET_FREQUENCY }).to_s
+ @mode = (options.delete(:mode) { DEFAULT_MODE }).to_sym
+ raise ProfilerArgumentError, "Invalid option(s): #{options.keys.join(' ')}" unless options.empty?
+ end
+
+ def profile
+ start
+ yield
+ ensure
+ stop
+ end
+
+ def self.clear_data
+ ::File.delete(PROFILING_DATA_FILE) if ::File.exists?(PROFILING_DATA_FILE)
+ end
+
+ def start
+ set_env_vars
+ PerfTools::CpuProfiler.start(PROFILING_DATA_FILE)
+ self.profiling = true
+ end
+
+ def stop
+ PerfTools::CpuProfiler.stop
+ self.profiling = false
+ unset_env_vars
+ end
+
+ def profiling?
+ pstore_transaction(true) do |store|
+ store[:profiling?]
+ end
+ end
+
+ def data(options = {})
+ printer = (options.fetch('printer') {@printer}).to_sym
+ ignore = options.fetch('ignore') { nil }
+ focus = options.fetch('focus') { nil }
+ if ::File.exists?(PROFILING_DATA_FILE)
+ args = "--#{printer}"
+ args += " --ignore=#{ignore}" if ignore
+ args += " --focus=#{focus}" if focus
+ cmd = "pprof.rb #{args} #{PROFILING_DATA_FILE}"
+ stdout, stderr, status = run(cmd)
+ if(status == 0)
+ [printer, stdout]
+ else
+ raise ProfilingError.new("Running the command '#{cmd}' exited with status #{status}", stderr)
+ end
+ else
+ [:none, nil]
+ end
+ end
+
+ private
+
+ def run(command)
+ out = err = pid = nil
+ status = Open4.popen4(command) do |pid, stdin, stdout, stderr|
+ stdin.close
+ pid = pid
+ out = stdout.read
+ err = stderr.read
+ end
+ [out,err,status.exitstatus]
+ end
+
+ def set_env_vars
+ ENV['CPUPROFILE_REALTIME'] = '1' if @mode == :walltime
+ ENV['CPUPROFILE_FREQUENCY'] = @frequency if @frequency != UNSET_FREQUENCY
+ end
+
+ def unset_env_vars
+ ENV.delete('CPUPROFILE_REALTIME')
+ ENV.delete('CPUPROFILE_FREQUENCY')
+ end
+
+ def profiling=(value)
+ pstore_transaction(false) do |store|
+ store[:profiling?] = value
+ end
+ end
+
+ def pstore_transaction(read_only)
+ pstore = PStore.new(PROFILING_SETTINGS_FILE)
+ pstore.transaction(read_only) do
+ yield pstore if block_given?
+ end
+ end
+
+ end
+
+end
View
74 lib/rack/perftools_profiler/profiler_middleware.rb
@@ -0,0 +1,74 @@
+module Rack::PerftoolsProfiler
+
+ class ProfilerArgumentError < RuntimeError; end;
+
+ class ProfilerMiddleware
+ include Rack::Utils
+
+ PRINTER_CONTENT_TYPE = {
+ :text => 'text/plain',
+ :gif => 'image/gif',
+ :pdf => 'application/pdf'
+ }
+
+ PRINTERS = PRINTER_CONTENT_TYPE.keys
+
+ def initialize(app, options = {})
+ @app = app
+ @profiler = Profiler.new(@app, options.clone)
+ end
+
+ def call(env)
+ @env = env.clone
+ action = Action.for_env(@env, @profiler, self)
+ action.act
+ action.response
+ rescue ProfilerArgumentError => err
+ @env['rack.errors'].write(err.message)
+ [400, {'Content-Type' => 'text/plain'}, [err.message]]
+ rescue ProfilingError => err
+ @env['rack.errors'].write(err.message + "\n" + err.stderr)
+ [500, {'Content-Type' => 'text/plain'}, [err.message+"\n\n", "Standard error:\n"+err.stderr+"\n"]]
+ end
+
+ def call_app(env)
+ @app.call(env)
+ end
+
+ def force_stop
+ @profiler.stop
+ end
+
+ def profiler_data_response(profiling_data)
+ format, body = profiling_data
+ body = Array(body)
+ if format==:none
+ message = 'No profiling data available. Visit /__stop__ and then visit /__data__'
+ [404, {'Content-Type' => 'text/plain'}, [message]]
+ else
+ [200, headers(format, body), Array(body)]
+ end
+ end
+
+ private
+
+ def headers(printer, body)
+ headers = {
+ 'Content-Type' => PRINTER_CONTENT_TYPE[printer],
+ 'Content-Length' => content_length(body)
+ }
+ if printer==:pdf
+ filetype = printer
+ filename='profile_data'
+ headers['Content-Disposition'] = %(attachment; filename="#{filename}.#{filetype}")
+ end
+ headers
+ end
+
+ def content_length(body)
+ body.inject(0) { |len, part| len + bytesize(part) }.to_s
+ end
+
+ end
+
+end
View
20 lib/rack/perftools_profiler/return_data.rb
@@ -0,0 +1,20 @@
+module Rack::PerftoolsProfiler
+
+ class ReturnData < ProfileDataAction
+
+ def initialize(*args)
+ super
+ check_printer_arg
+ end
+
+ def response
+ if @profiler.profiling?
+ [400, {'Content-Type' => 'text/plain'}, ['No profiling data available.']]
+ else
+ @middleware.profiler_data_response(@profiler.data(@data_params))
+ end
+ end
+
+ end
+
+end
View
21 lib/rack/perftools_profiler/start_profiling.rb
@@ -0,0 +1,21 @@
+module Rack::PerftoolsProfiler
+
+ class StartProfiling < Action
+
+ def act
+ @profiler.start
+ end
+
+ def response
+ [200, {'Content-Type' => 'text/plain'},
+ [<<-EOS
+Profiling is now enabled.
+Visit the URLS that should be profiled.
+When you are finished, visit /__stop__, then visit /__data__ to view the results.
+EOS
+ ]]
+ end
+
+ end
+
+end
View
20 lib/rack/perftools_profiler/stop_profiling.rb
@@ -0,0 +1,20 @@
+module Rack::PerftoolsProfiler
+
+ class StopProfiling < Action
+
+ def act
+ @profiler.stop
+ end
+
+ def response
+ [200, {'Content-Type' => 'text/plain'},
+ [<<-EOS
+Profiling is now disabled.
+Visit /__data__ to view the results.
+EOS
+ ]]
+ end
+
+ end
+
+end
View
404 test/rack-perftools-profiler_test.rb
@@ -1,7 +1,407 @@
require 'test_helper'
+ITERATIONS = case RUBY_VERSION
+ when /1\.9\.1/
+ 350_000 # Ruby 1.9.1 is that we need to add extra iterations to get profiling data
+ else
+ 35_000
+ end
+
+# From the Rack spec (http://rack.rubyforge.org/doc/files/SPEC.html) :
+# The Body must respond to each and must only yield String values. The Body should not be an instance of String.
+# ... The Body commonly is an Array of Strings, the application instance itself, or a File-like object.
+
+class RackResponseBody
+ include Test::Unit::Assertions
+
+ def initialize(body)
+ assert !body.instance_of?(String)
+ @body = body
+ end
+
+ def to_s
+ str = ""
+ @body.each do |part|
+ str << part
+ end
+ str
+ end
+
+end
+
+class TestApp
+
+ def call(env)
+ case env['PATH_INFO']
+ when /method1/
+ ITERATIONS.times do
+ self.class.new.method1
+ end
+ GC.start
+ when /method2/
+ ITERATIONS.times do
+ self.class.new.method2
+ end
+ GC.start
+ end
+ [200, {}, ['Done']]
+ end
+
+ def method1
+ 100.times do
+ 1+2+3+4+5
+ end
+ end
+
+ def method2
+ 100.times do
+ 1+2+3+4+5
+ end
+ end
+
+end
+
class RackPerftoolsProfilerTest < Test::Unit::TestCase
- should "probably rename this file and start testing for real" do
- flunk "hey buddy, you should probably rename this file and start testing for real"
+ include Rack::PerftoolsProfiler
+
+ context "testing Rack::PerftoolsProfiler" do
+
+ setup do
+ @app = lambda { |env| ITERATIONS.times {1+2+3+4+5}; [200, {'Content-Type' => 'text/plain'}, ['Oh hai der']] }
+ @slow_app = lambda { |env| ITERATIONS.times {1+2+3+4+5}; [200, {'Content-Type' => 'text/plain'}, ['slow app']] }
+ @start_env = Rack::MockRequest.env_for('/__start__')
+ @stop_env = Rack::MockRequest.env_for('/__stop__')
+ @data_env = Rack::MockRequest.env_for('/__data__')
+ @root_request_env = Rack::MockRequest.env_for("/")
+ @profiled_request_env = Rack::MockRequest.env_for("/", :params => "profile=true")
+ @profiled_request_env_with_times = Rack::MockRequest.env_for("/", :params => "profile=true&times=2")
+ end
+
+ context 'Rack::Lint checks' do
+
+ should 'pass all Lint checks with text printer' do
+ app = Rack::Lint.new(Rack::PerftoolsProfiler.with_profiling_off(@slow_app, :default_printer => 'text'))
+ app.call(@root_request_env)
+ app.call(@profiled_request_env)
+ app.call(@profiled_request_env_with_times)
+ app.call(@start_env)
+ app.call(@stop_env)
+ app.call(@data_env)
+ end
+
+ should 'pass all Lint checks with text printer' do
+ app = Rack::Lint.new(Rack::PerftoolsProfiler.with_profiling_off(@slow_app, :default_printer => 'gif'))
+ app.call(@root_request_env)
+ app.call(@profiled_request_env)
+ app.call(@profiled_request_env_with_times)
+ app.call(@start_env)
+ app.call(@stop_env)
+ app.call(@data_env)
+ end
+
+ end
+
+ should 'raise error if options contains invalid key' do
+ error = assert_raises ProfilerArgumentError do
+ Rack::PerftoolsProfiler.with_profiling_off(@app, :mode => 'walltime', :default_printer => 'gif', :foobar => 'baz')
+ end
+ assert_match(/Invalid option\(s\)\: foobar/, error.message)
+ end
+
+ should 'raise error if printer is invalid' do
+ error = assert_raises ProfilerArgumentError do
+ Rack::PerftoolsProfiler.with_profiling_off(@app, :mode => 'walltime', :default_printer => 'badprinter')
+ end
+ assert_match(/Invalid printer type\: badprinter/, error.message)
+ end
+
+ should 'not modify options hash' do
+ options = {:mode => 'walltime', :default_printer => 'gif'}
+ old_options = options.clone
+ Rack::PerftoolsProfiler.with_profiling_off(@app, options)
+ assert_equal old_options, options
+ end
+
+ context 'without profiling' do
+
+ should 'call app directly' do
+ status, headers, body = Rack::PerftoolsProfiler.with_profiling_off(@app).call(@root_request_env)
+ assert_equal 200, status
+ assert_equal 'text/plain', headers['Content-Type']
+ assert_equal 'Oh hai der', RackResponseBody.new(body).to_s
+ end
+
+ should 'provide no data by default when __data__ is called' do
+ Rack::PerftoolsProfiler.clear_data
+ status, headers, body = Rack::PerftoolsProfiler.with_profiling_off(@app, :default_printer => 'text').call(@data_env)
+ assert_equal 404, status
+ assert_equal 'text/plain', headers['Content-Type']
+ assert_match(/No profiling data available./, RackResponseBody.new(body).to_s)
+ end
+
+ end
+
+ context 'simple profiling mode' do
+
+ should 'default to text printer' do
+ _, headers, _ = Rack::PerftoolsProfiler.new(@app).call(@profiled_request_env)
+ assert_equal "text/plain", headers['Content-Type']
+ end
+
+ should "set CPUPROFILE_REALTIME to 1 if mode is 'walltime'" do
+ realtime = ENV['CPUPROFILE_REALTIME']
+ assert_nil realtime
+ app = lambda do |env|
+ realtime = ENV['CPUPROFILE_REALTIME']
+ [200, {}, ["hi"]]
+ end
+ Rack::PerftoolsProfiler.new(app, :mode => 'walltime').call(@profiled_request_env)
+ assert_equal '1', realtime
+ end
+
+ should 'alter CPUPROFILE_FREQUENCY if frequency is set' do
+ frequency = ENV['CPUPROFILE_FREQUENCY']
+ assert_nil frequency
+ app = lambda do |env|
+ frequency = ENV['CPUPROFILE_FREQUENCY']
+ [200, {}, ["hi"]]
+ end
+ Rack::PerftoolsProfiler.new(app, :frequency => 500).call(@profiled_request_env)
+ assert_equal '500', frequency
+ end
+
+ context 'text printer' do
+
+ should 'return profiling data' do
+ _, _, body = Rack::PerftoolsProfiler.new(@slow_app, :default_printer => 'text').call(@profiled_request_env)
+ assert_match(/Total: \d+ samples/, RackResponseBody.new(body).to_s)
+ end
+
+ should 'have Content-Type text/plain' do
+ _, headers, _ = Rack::PerftoolsProfiler.new(@app, :default_printer => 'text').call(@profiled_request_env)
+ assert_equal "text/plain", headers['Content-Type']
+ end
+
+ should 'have Content-Length' do
+ _, headers, _ = Rack::PerftoolsProfiler.new(@slow_app, :default_printer => 'text').call(@profiled_request_env)
+ assert (headers.fetch('Content-Length').to_i > 500)
+ end
+
+ end
+
+ context 'gif printer' do
+
+ should 'gif printer has Content-Type image/gif' do
+ _, headers, _ = Rack::PerftoolsProfiler.new(@app, :default_printer => 'gif').call(@profiled_request_env)
+ assert_equal "image/gif", headers['Content-Type']
+ end
+
+ should 'gif printer has Content-Length' do
+ _, headers, _ = Rack::PerftoolsProfiler.new(@slow_app, :default_printer => 'gif').call(@profiled_request_env)
+ assert headers.fetch('Content-Length').to_i > 25_000
+ end
+
+ should 'pdf printer has Content-Type application/pdf' do
+ _, headers, _ = Rack::PerftoolsProfiler.new(@app, :default_printer => 'pdf').call(@profiled_request_env)
+ assert_equal "application/pdf", headers['Content-Type']
+ end
+
+ end
+
+ context 'pdf printer' do
+
+ should 'have default filename' do
+ _, headers, _ = Rack::PerftoolsProfiler.new(@app, :default_printer => 'pdf').call(@profiled_request_env)
+ assert_equal %q{attachment; filename="profile_data.pdf"}, headers['Content-Disposition']
+ end
+
+ end
+
+ should 'be able to call app multiple times' do
+ env = Rack::MockRequest.env_for('/', :params => 'profile=true&times=3')
+ app = @app.clone
+ app.expects(:call).times(3)
+ Rack::PerftoolsProfiler.new(app, :default_printer => 'text').call(env)
+ end
+
+ should "allow 'printer' param override :default_printer option'" do
+ env = Rack::MockRequest.env_for('/', :params => 'profile=true&printer=gif')
+ _, headers, _ = Rack::PerftoolsProfiler.new(@app, :default_printer => 'pdf').call(env)
+ assert_equal 'image/gif', headers['Content-Type']
+ end
+
+ should 'give 400 if printer is invalid' do
+ env = Rack::MockRequest.env_for('/', :params => 'profile=true&printer=badprinter')
+ status, _, _ = Rack::PerftoolsProfiler.new(@app).call(env)
+ assert_equal 400, status
+ end
+
+ should 'send Rack environment to underlying application (minus special profiling GET params)' do
+ env = Rack::MockRequest.env_for('/', :params => 'profile=true&times=1&param=value&printer=gif&focus=foo&ignore=bar')
+ old_env = env.clone
+ expected_env = env.clone
+ expected_env["QUERY_STRING"] = 'param=value'
+ app = @app.clone
+ app.expects(:call).with(expected_env)
+ Rack::PerftoolsProfiler.new(app, :default_printer => 'gif').call(env)
+ assert_equal env, old_env
+ end
+
+ should "accept 'focus' param" do
+ profiled_app = Rack::PerftoolsProfiler.with_profiling_off(TestApp.new, :default_printer => 'text', :mode => 'walltime')
+ custom_env = Rack::MockRequest.env_for('/method1', :params => 'profile=true&focus=method1')
+ status, headers, body = profiled_app.call(custom_env)
+ assert_no_match(/garbage/, RackResponseBody.new(body).to_s)
+ end
+
+ should "accept 'ignore' param" do
+ profiled_app = Rack::PerftoolsProfiler.with_profiling_off(TestApp.new, :default_printer => 'text', :mode => 'walltime')
+ custom_env = Rack::MockRequest.env_for('/method1', :params => 'profile=true&ignore=method1')
+ status, headers, body = profiled_app.call(custom_env)
+ assert_match(/garbage/, RackResponseBody.new(body).to_s)
+ assert_no_match(/method1/, RackResponseBody.new(body).to_s)
+ end
+
+ end
+
+ context 'start/stop profiling' do
+
+ should "set CPUPROFILE_REALTIME to 1 if mode is 'walltime' " do
+ realtime = ENV['CPUPROFILE_REALTIME']
+ assert_nil realtime
+ app = lambda do |env|
+ realtime = ENV['CPUPROFILE_REALTIME']
+ [200, {}, ["hi"]]
+ end
+ profiled_app = Rack::PerftoolsProfiler.new(app, :mode => 'walltime')
+ profiled_app.call(@start_env)
+ profiled_app.call(@root_request_env)
+ profiled_app.call(@stop_env)
+ assert_equal '1', realtime
+ end
+
+ should 'alter CPUPROFILE_FREQUENCY if frequency is set' do
+ frequency = ENV['CPUPROFILE_FREQUENCY']
+ assert_nil frequency
+ app = lambda do |env|
+ frequency = ENV['CPUPROFILE_FREQUENCY']
+ [200, {}, ["hi"]]
+ end
+ profiled_app = Rack::PerftoolsProfiler.new(app, :frequency => 250)
+ profiled_app.call(@start_env)
+ profiled_app.call(@root_request_env)
+ assert_equal '250', frequency
+ end
+
+ context 'when profiling is on' do
+
+ should 'not provide profiling data when __data__ is called' do
+ Rack::PerftoolsProfiler.clear_data
+ profiled_app = Rack::PerftoolsProfiler.with_profiling_off(@app, :default_printer => 'text')
+ profiled_app.call(@start_env)
+ profiled_app.call(@root_request_env)
+ status, _, body = profiled_app.call(@data_env)
+ assert_equal 400, status
+ assert_match(/No profiling data available./, RackResponseBody.new(body).to_s)
+ end
+
+ should 'pass on profiling params in environment' do
+ env = Rack::MockRequest.env_for('/', :params => 'times=2')
+ old_env = env.clone
+ app = @app.clone
+ expected_env = env.clone
+ expected_env['rack.request.query_string'] = 'times=2'
+ expected_env['rack.request.query_hash'] = {'times' => '2'}
+ app.expects(:call).with(expected_env)
+ profiled_app = Rack::PerftoolsProfiler.new(app, :default_printer => 'text')
+ profiled_app.call(@start_env)
+ profiled_app.call(env)
+ assert_equal env, old_env
+ end
+
+ should 'pass on non-profiling params in environment' do
+ env = Rack::MockRequest.env_for('/', :params => 'param=value')
+ old_env = env.clone
+ app = @app.clone
+ expected_env = env.clone
+ expected_env['rack.request.query_string'] = 'param=value'
+ expected_env['rack.request.query_hash'] = {'param' => 'value'}
+ app.expects(:call).with(expected_env)
+ profiled_app = Rack::PerftoolsProfiler.new(app, :default_printer => 'text')
+ profiled_app.call(@start_env)
+ profiled_app.call(env)
+ assert_equal env, old_env
+ end
+
+ should 'not alter regular calls' do
+ profiled_app = Rack::PerftoolsProfiler.with_profiling_off(@app, :default_printer => 'gif')
+ profiled_app.call(@start_env)
+ status, headers, body = profiled_app.call(@root_request_env)
+ assert_equal 200, status
+ assert_equal 'text/plain', headers['Content-Type']
+ assert_equal 'Oh hai der', RackResponseBody.new(body).to_s
+ end
+
+ end
+
+ context 'after profiling is finished' do
+
+ should 'return profiling data when __data__ is called' do
+ profiled_app = Rack::PerftoolsProfiler.with_profiling_off(@app, :default_printer => 'gif')
+ profiled_app.call(@start_env)
+ profiled_app.call(@root_request_env)
+ profiled_app.call(@stop_env)
+ status, headers, body = profiled_app.call(@data_env)
+ assert_equal 200, status
+ assert_equal "image/gif", headers['Content-Type']
+ end
+
+ end
+
+ should 'keeps data from multiple calls' do
+ profiled_app = Rack::PerftoolsProfiler.with_profiling_off(TestApp.new, :default_printer => 'text', :mode => 'walltime')
+ profiled_app.call(@start_env)
+ profiled_app.call(Rack::MockRequest.env_for('/method1'))
+ profiled_app.call(Rack::MockRequest.env_for('/method2'))
+ profiled_app.call(@stop_env)
+ status, headers, body = profiled_app.call(@data_env)
+ assert_match(/method1/, RackResponseBody.new(body).to_s)
+ assert_match(/method2/, RackResponseBody.new(body).to_s)
+ end
+
+ should "allow 'printer' param to override :default_printer option'" do
+ profiled_app = Rack::PerftoolsProfiler.new(@app, :default_printer => 'pdf')
+ profiled_app.call(@start_env)
+ profiled_app.call(@root_request_env)
+ profiled_app.call(@stop_env)
+ custom_data_env = Rack::MockRequest.env_for('__data__', :params => 'printer=gif')
+ _, headers, _ = profiled_app.call(custom_data_env)
+ assert_equal 'image/gif', headers['Content-Type']
+ end
+
+ should "accept 'focus' param" do
+ profiled_app = Rack::PerftoolsProfiler.with_profiling_off(TestApp.new, :default_printer => 'text', :mode => 'walltime')
+ profiled_app.call(@start_env)
+ profiled_app.call(Rack::MockRequest.env_for('/method1'))
+ profiled_app.call(Rack::MockRequest.env_for('/method2'))
+ profiled_app.call(@stop_env)
+ custom_data_env = Rack::MockRequest.env_for('__data__', :params => 'focus=method1')
+ status, headers, body = profiled_app.call(custom_data_env)
+ assert_no_match(/method2/, RackResponseBody.new(body).to_s)
+ end
+
+ should "accept 'ignore' param" do
+ profiled_app = Rack::PerftoolsProfiler.with_profiling_off(TestApp.new, :default_printer => 'text', :mode => 'walltime')
+ profiled_app.call(@start_env)
+ profiled_app.call(Rack::MockRequest.env_for('/method1'))
+ profiled_app.call(Rack::MockRequest.env_for('/method2'))
+ profiled_app.call(@stop_env)
+ custom_data_env = Rack::MockRequest.env_for('__data__', :params => 'ignore=method1')
+ status, headers, body = profiled_app.call(custom_data_env)
+ assert_no_match(/method1/, RackResponseBody.new(body).to_s)
+ end
+
+ end
end
+
end
View
3 test/test_helper.rb
@@ -1,10 +1,11 @@
require 'rubygems'
require 'test/unit'
require 'shoulda'
+require 'mocha'
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.dirname(__FILE__))
-require 'rack-perftools-profiler'
+require 'rack/perftools_profiler'
class Test::Unit::TestCase
end

0 comments on commit bfc0db9

Please sign in to comment.
Something went wrong with that request. Please try again.