diff options
author | zedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9> | 2006-03-26 20:01:50 +0000 |
---|---|---|
committer | zedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9> | 2006-03-26 20:01:50 +0000 |
commit | f2b53a3a4b1ddacac4fc18ccbe2b016194a50777 (patch) | |
tree | 10489b9db362c71558d16d85af8bcec4139c9266 /lib/mongrel | |
parent | f4a5c938d461d9c5dc17f521c9efaaf352b931fa (diff) | |
download | unicorn-f2b53a3a4b1ddacac4fc18ccbe2b016194a50777.tar.gz |
git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@122 19e92222-5c0b-0410-8929-a290d50e31e9
Diffstat (limited to 'lib/mongrel')
-rw-r--r-- | lib/mongrel/command.rb | 2 | ||||
-rw-r--r-- | lib/mongrel/debug.rb | 233 | ||||
-rw-r--r-- | lib/mongrel/rails.rb | 313 | ||||
-rw-r--r-- | lib/mongrel/stats.rb | 71 |
4 files changed, 411 insertions, 208 deletions
diff --git a/lib/mongrel/command.rb b/lib/mongrel/command.rb index 2b21684..0341e15 100644 --- a/lib/mongrel/command.rb +++ b/lib/mongrel/command.rb @@ -15,7 +15,7 @@ module Mongrel # user's bidding. module Base - attr_reader :valid, :done_validating + attr_reader :valid, :done_validating, :original_args # Called by the implemented command to set the options for that command. # Every option has a short and long version, a description, a variable to diff --git a/lib/mongrel/debug.rb b/lib/mongrel/debug.rb index 8b2dd5c..6178872 100644 --- a/lib/mongrel/debug.rb +++ b/lib/mongrel/debug.rb @@ -1,6 +1,6 @@ require 'logger' require 'set' - +require 'socket' $mongrel_debugging=true @@ -8,14 +8,14 @@ module MongrelDbg SETTINGS = { :tracing => {}} LOGGING = { } - def MongrelDbg::configure(log_dir = "mongrel_debug") + def MongrelDbg::configure(log_dir = "log/mongrel_debug") Dir.mkdir(log_dir) if not File.exist?(log_dir) @log_dir = log_dir end def MongrelDbg::trace(target, message) - if SETTINGS[:tracing][target] + if SETTINGS[:tracing][target] and LOGGING[target] LOGGING[target].log(Logger::DEBUG, message) end end @@ -34,32 +34,28 @@ module MongrelDbg LOGGING[target].close LOGGING[target] = nil end + + def MongrelDbg::tracing?(target) + SETTINGS[:tracing][target] + end end module ObjectTracker @active_objects = nil - @live_object_tracking = false + @live_object_tracking = true def ObjectTracker.configure @active_objects = Set.new + ObjectSpace.each_object do |obj| @active_objects << obj.object_id end - srand @active_objects.object_id - @sample_thread = Thread.new do - loop do - sleep(rand(3) + (rand(100)/100.0)) - ObjectTracker.sample - end - end - @sample_thread.priority = 20 end + def ObjectTracker.start - @stopit = true @live_object_tracking = true - @stopit = false end def ObjectTracker.stop @@ -67,78 +63,197 @@ module ObjectTracker end def ObjectTracker.sample - ospace = Set.new - ObjectSpace.each_object do |obj| - ospace << obj.object_id + Class.stopit do + ospace = Set.new + counts = {} + + # Strings can't be tracked easily and are so numerous that they drown out all else + # so we just ignore them in the counts. + ObjectSpace.each_object do |obj| + if not obj.kind_of? String + ospace << obj.object_id + counts[obj.class] ||= 0 + counts[obj.class] += 1 + end + end + + dead_objects = @active_objects - ospace + new_objects = ospace - @active_objects + live_objects = ospace & @active_objects + + MongrelDbg::trace(:objects, "COUNTS: #{dead_objects.length},#{new_objects.length},#{live_objects.length}") + + if MongrelDbg::tracing? :objects + top_20 = counts.sort{|a,b| b[1] <=> a[1]}[0..20] + MongrelDbg::trace(:objects,"TOP 20: #{top_20.inspect}") + end + + @active_objects = live_objects + new_objects + + [@active_objects, top_20] end - - dead_objects = @active_objects - ospace - new_objects = ospace - @active_objects - live_objects = ospace & @active_objects - - STDERR.puts "#{dead_objects.length},#{new_objects.length},#{live_objects.length}" + end + +end + +$open_files = {} - @active_objects = live_objects + new_objects +class IO + alias_method :orig_open, :open + alias_method :orig_close, :close + + def open(*arg, &blk) + $open_files[self] = args.inspect + orig_open(*arg,&blk) end + def close(*arg,&blk) + $open_files.delete self + orig_close(*arg,&blk) + end end + +module Kernel + alias_method :orig_open, :open + + def open(*arg, &blk) + $open_files[self] = arg[0] + orig_open(*arg,&blk) + end + + def log_open_files + Class.stopit do + open_counts = {} + $open_files.each do |f,args| + open_counts[args] ||= 0 + open_counts[args] += 1 + end + MongrelDbg::trace(:files, open_counts.to_yaml) + end + end +end + + + class Class alias_method :orig_new, :new @@count = 0 - @@stoppit = false + @@stopit = false @@class_caller_count = Hash.new{|hash,key| hash[key] = Hash.new(0)} def new(*arg,&blk) - unless @@stoppit - @@stoppit = true + unless @@stopit + @@stopit = true @@count += 1 - @@class_caller_count[self][caller[0]] += 1 - @@stoppit = false + @@class_caller_count[self][caller.join("\n\t")] += 1 + @@stopit = false end orig_new(*arg,&blk) end - def Class.report_object_creations - @@stoppit = true - puts "Number of objects created = #{@@count}" - - total = Hash.new(0) - - @@class_caller_count.each_key do |klass| - caller_count = @@class_caller_count[klass] - caller_count.each_value do |count| - total[klass] += count - end - end - - klass_list = total.keys.sort{|klass_a, klass_b| - a = total[klass_a] - b = total[klass_b] - if a != b - -1* (a <=> b) - else - klass_a.to_s <=> klass_b.to_s + def Class.report_object_creations(out=$stderr, more_than=20) + Class.stopit do + out.puts "Number of objects created = #{@@count}" + + total = Hash.new(0) + + @@class_caller_count.each_key do |klass| + caller_count = @@class_caller_count[klass] + caller_count.each_value do |count| + total[klass] += count + end end - } - klass_list.each do |klass| - puts "#{total[klass]}\t#{klass} objects created." - caller_count = @@class_caller_count[ klass] - caller_count.keys.sort_by{|call| -1*caller_count[call]}.each do |call| - puts "\t#{call}\tCreated #{caller_count[call]} #{klass} objects." + + klass_list = total.keys.sort{|klass_a, klass_b| + a = total[klass_a] + b = total[klass_b] + if a != b + -1* (a <=> b) + else + klass_a.to_s <=> klass_b.to_s + end + } + + below_count = 0 + + klass_list.each do |klass| + below_calls = 0 + if total[klass] > more_than + out.puts "#{total[klass]}\t#{klass} objects created." + caller_count = @@class_caller_count[ klass] + caller_count.keys.sort_by{|call| -1*caller_count[call]}.each do |call| + if caller_count[call] > more_than + out.puts "\t** #{caller_count[call]} #{klass} objects AT:" + out.puts "\t#{call}\n\n" + else + below_calls += 1 + end + end + out.puts "\t#{below_calls} more objects had calls less that #{more_than} limit.\n\n" if below_calls > 0 + else + below_count += 1 + end end - puts + + out.puts "\t** #{below_count} More objects were created but the count was below the #{more_than} limit." if below_count > 0 end end def Class.reset_object_creations + Class.stopit do + @@count = 0 + @@class_caller_count = Hash.new{|hash,key| hash[key] = Hash.new(0)} + end + end + + def Class.stopit @@stopit = true - @@count = 0 - @@class_caller_count = Hash.new{|hash,key| hash[key] = Hash.new(0)} - @@stoppit = false + yield + @@stopit = false + end + +end + + +module RequestLog + class Files < GemPlugin::Plugin "/handlers" + include Mongrel::HttpHandlerPlugin + + def process(request, response) + MongrelDbg::trace(:files, "#{Time.now} FILES OPEN BEFORE REQUEST #{request.params['PATH_INFO']}") + log_open_files + end + + end + + class Objects < GemPlugin::Plugin "/handlers" + include Mongrel::HttpHandlerPlugin + + def process(request, response) + MongrelDbg::trace(:objects, "#{Time.now} OBJECT STATS BEFORE REQUEST #{request.params['PATH_INFO']}") + ObjectTracker.sample + end + + end + + + class Params < GemPlugin::Plugin "/handlers" + include Mongrel::HttpHandlerPlugin + + def process(request, response) + MongrelDbg::trace(:rails, "#{Time.now} REQUEST #{request.params['PATH_INFO']}") + MongrelDbg::trace(:rails, request.params.to_yaml) + end + end end +END { +open("log/mongrel_debug/object_tracking.log", "w") {|f| Class.report_object_creations(f) } +MongrelDbg::trace(:files, "FILES OPEN AT EXIT") +log_open_files +} diff --git a/lib/mongrel/rails.rb b/lib/mongrel/rails.rb index 808d7d3..8dc42a0 100644 --- a/lib/mongrel/rails.rb +++ b/lib/mongrel/rails.rb @@ -1,159 +1,176 @@ require 'mongrel' require 'cgi' -# Creates Rails specific configuration options for people to use -# instead of the base Configurator. -class RailsConfigurator < Mongrel::Configurator +module Mongrel + module Rails - # Used instead of Mongrel::Configurator.uri to setup - # a rails application at a particular URI. Requires - # the following options: - # - # * :docroot => The public dir to serve from. - # * :environment => Rails environment to use. - # - # And understands the following optional settings: - # - # * :mime => A map of mime types. - # - # Because of how Rails is designed you can only have - # one installed per Ruby interpreter (talk to them - # about thread safety). This function will abort - # with an exception if called more than once. - def rails(location, options={}) - ops = resolve_defaults(options) - - # fix up some defaults - ops[:environment] ||= "development" - ops[:docroot] ||= "public" - ops[:mime] ||= {} - - if @rails_handler - raise "You can only register one RailsHandler for the whole Ruby interpreter. Complain to the ordained Rails core about thread safety." - end - - $orig_dollar_quote = $".clone - ENV['RAILS_ENV'] = ops[:environment] - require 'config/environment' - require 'dispatcher' - require 'mongrel/rails' - - @rails_handler = RailsHandler.new(ops[:docroot], ops[:mime]) - end - - - # Reloads rails. This isn't too reliable really, but - # should work for most minimal reload purposes. Only reliable - # way it so stop then start the process. - def reload! - if not @rails_handler - raise "Rails was not configured. Read the docs for RailsConfigurator." - end - - STDERR.puts "Reloading rails..." - @rails_handler.reload! - STDERR.puts "Done reloading rails." - - end -end - -# Implements a handler that can run Rails and serve files out of the -# Rails application's public directory. This lets you run your Rails -# application with Mongrel during development and testing, then use it -# also in production behind a server that's better at serving the -# static files. -# -# The RailsHandler takes a mime_map parameter which is a simple suffix=mimetype -# mapping that it should add to the list of valid mime types. -# -# It also supports page caching directly and will try to resolve a request -# in the following order: -# -# * If the requested exact PATH_INFO exists as a file then serve it. -# * If it exists at PATH_INFO+".html" exists then serve that. -# * Finally, construct a Mongrel::CGIWrapper and run Dispatcher.dispath to have Rails go. -# -# This means that if you are using page caching it will actually work with Mongrel -# and you should see a decent speed boost (but not as fast as if you use lighttpd). -# -# An additional feature you can use is -class RailsHandler < Mongrel::HttpHandler - attr_reader :files - attr_reader :guard - - def initialize(dir, mime_map = {}) - @files = Mongrel::DirHandler.new(dir,false) - @guard = Mutex.new - - # register the requested mime types - mime_map.each {|k,v| Mongrel::DirHandler::add_mime_type(k,v) } - end - - # Attempts to resolve the request as follows: - # - # - # * If the requested exact PATH_INFO exists as a file then serve it. - # * If it exists at PATH_INFO+".html" exists then serve that. - # * Finally, construct a Mongrel::CGIWrapper and run Dispatcher.dispath to have Rails go. - def process(request, response) - return if response.socket.closed? - - path_info = request.params[Mongrel::Const::PATH_INFO] - page_cached = request.params[Mongrel::Const::PATH_INFO] + ".html" - - if @files.can_serve(path_info) - # File exists as-is so serve it up - @files.process(request,response) - elsif @files.can_serve(page_cached) - # possible cached page, serve it up - request.params[Mongrel::Const::PATH_INFO] = page_cached - @files.process(request,response) - else - begin - cgi = Mongrel::CGIWrapper.new(request, response) - cgi.handler = self - - @guard.synchronize do - # Rails is not thread safe so must be run entirely within synchronize - Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, response.body) + # Creates Rails specific configuration options for people to use + # instead of the base Configurator. + class RailsConfigurator < Mongrel::Configurator + + # Creates a single rails handler and returns it so you + # can add it to a uri. You can actually attach it to + # as many URIs as you want, but this returns the + # same RailsHandler for each call. + # + # Requires the following options: + # + # * :docroot => The public dir to serve from. + # * :environment => Rails environment to use. + # + # And understands the following optional settings: + # + # * :mime => A map of mime types. + # + # Because of how Rails is designed you can only have + # one installed per Ruby interpreter (talk to them + # about thread safety). Because of this the first + # time you call this function it does all the config + # needed to get your rails working. After that + # it returns the one handler you've configured. + # This lets you attach Rails to any URI (and mulitple) + # you want, but still protects you from threads destroying + # your handler. + def rails(options={}) + + return @rails_handler if @rails_handler + + ops = resolve_defaults(options) + + # fix up some defaults + ops[:environment] ||= "development" + ops[:docroot] ||= "public" + ops[:mime] ||= {} + + + $orig_dollar_quote = $".clone + ENV['RAILS_ENV'] = ops[:environment] + require 'config/environment' + require 'dispatcher' + require 'mongrel/rails' + + @rails_handler = RailsHandler.new(ops[:docroot], ops[:mime]) + end + + + # Reloads rails. This isn't too reliable really, but + # should work for most minimal reload purposes. Only reliable + # way it so stop then start the process. + def reload! + if not @rails_handler + raise "Rails was not configured. Read the docs for RailsConfigurator." end - - # This finalizes the output using the proper HttpResponse way - cgi.out {""} - rescue Errno::EPIPE - # ignored - rescue Object => rails_error - STDERR.puts "Error calling Dispatcher.dispatch #{rails_error.inspect}" - STDERR.puts rails_error.backtrace.join("\n") + + log "Reloading rails..." + @rails_handler.reload! + log "Done reloading rails." + end - end - end - - - def reload! - @guard.synchronize do - $".replace $orig_dollar_quote - GC.start - Dispatcher.reset_application! - ActionController::Routing::Routes.reload - end - end -end - - -if $mongrel_debugging - - # Tweak the rails handler to allow for tracing - class RailsHandler - alias :real_process :process - - def process(request, response) - MongrelDbg::trace(:rails, "REQUEST #{Time.now}\n" + request.params.to_yaml) - real_process(request, response) + # Takes the exact same configuration as Mongrel::Configurator (and actually calls that) + # but sets up the additional HUP handler to call reload!. + def setup_rails_signals(options={}) + ops = resolve_defaults(options) + + if RUBY_PLATFORM !~ /mswin/ + setup_signals(options) + + # rails reload + trap("HUP") { + log "HUP signal received." + reload! + } + + log "Rails signals registered. HUP => reload (without restart). It might not work well." + else + log "WARNING: Rails does not support signals on Win32." + end + end - MongrelDbg::trace(:rails, "REQUEST #{Time.now}\n" + request.params.to_yaml) + # Implements a handler that can run Rails and serve files out of the + # Rails application's public directory. This lets you run your Rails + # application with Mongrel during development and testing, then use it + # also in production behind a server that's better at serving the + # static files. + # + # The RailsHandler takes a mime_map parameter which is a simple suffix=mimetype + # mapping that it should add to the list of valid mime types. + # + # It also supports page caching directly and will try to resolve a request + # in the following order: + # + # * If the requested exact PATH_INFO exists as a file then serve it. + # * If it exists at PATH_INFO+".html" exists then serve that. + # * Finally, construct a Mongrel::CGIWrapper and run Dispatcher.dispath to have Rails go. + # + # This means that if you are using page caching it will actually work with Mongrel + # and you should see a decent speed boost (but not as fast as if you use lighttpd). + # + # An additional feature you can use is + class RailsHandler < Mongrel::HttpHandler + attr_reader :files + attr_reader :guard + + def initialize(dir, mime_map = {}) + @files = Mongrel::DirHandler.new(dir,false) + @guard = Mutex.new + + # register the requested mime types + mime_map.each {|k,v| Mongrel::DirHandler::add_mime_type(k,v) } + end + + # Attempts to resolve the request as follows: + # + # + # * If the requested exact PATH_INFO exists as a file then serve it. + # * If it exists at PATH_INFO+".html" exists then serve that. + # * Finally, construct a Mongrel::CGIWrapper and run Dispatcher.dispath to have Rails go. + def process(request, response) + return if response.socket.closed? + + path_info = request.params[Mongrel::Const::PATH_INFO] + page_cached = request.params[Mongrel::Const::PATH_INFO] + ".html" + + if @files.can_serve(path_info) + # File exists as-is so serve it up + @files.process(request,response) + elsif @files.can_serve(page_cached) + # possible cached page, serve it up + request.params[Mongrel::Const::PATH_INFO] = page_cached + @files.process(request,response) + else + begin + cgi = Mongrel::CGIWrapper.new(request, response) + cgi.handler = self + + @guard.synchronize do + # Rails is not thread safe so must be run entirely within synchronize + Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, response.body) + end + + # This finalizes the output using the proper HttpResponse way + cgi.out {""} + rescue Errno::EPIPE + # ignored + rescue Object => rails_error + log "Error calling Dispatcher.dispatch #{rails_error.inspect}" + log rails_error.backtrace.join("\n") + end + end + end + + + # Does the internal reload for Rails. It might work for most cases, but + # sometimes you get exceptions. In that case just do a real restart. + def reload! + @guard.synchronize do + $".replace $orig_dollar_quote + GC.start + Dispatcher.reset_application! + ActionController::Routing::Routes.reload + end + end + end end end - end diff --git a/lib/mongrel/stats.rb b/lib/mongrel/stats.rb new file mode 100644 index 0000000..a29a018 --- /dev/null +++ b/lib/mongrel/stats.rb @@ -0,0 +1,71 @@ +# A very simple little class for doing some basic fast statistics sampling. +# You feed it either samples of numeric data you want measured or you call +# Stats.tick to get it to add a time delta between the last time you called it. +# When you're done either call sum, sumsq, n, min, max, mean or sd to get +# the information. The other option is to just call dump and see everything. +# +# It does all of this very fast and doesn't take up any memory since the samples +# are not stored but instead all the values are calculated on the fly. +class Stats + attr_reader :sum, :sumsq, :n, :min, :max + + def initialize(name) + @name = name + reset + end + + # Resets the internal counters so you can start sampling again. + def reset + @sum = 0.0 + @sumsq = 0.0 + @last_time = Time.new + @n = 0.0 + @min = 0.0 + @max = 0.0 + end + + # Adds a sampling to the calculations. + def sample(s) + @sum += s + @sumsq += s * s + if @n == 0 + @min = @max = s + else + @min = s if @min > s + @max = s if @max < s + end + @n+=1 + end + + # Dump this Stats object with an optional additional message. + def dump(msg = "") + STDERR.puts "[#{@name}] #{msg} : SUM=#@sum, SUMSQ=#@sumsq, N=#@n, MEAN=#{mean}, SD=#{sd}, MIN=#@min, MAX=#@max" + end + + # Calculates and returns the mean for the data passed so far. + def mean + @sum / @n + end + + # Calculates the standard deviation of the data so far. + def sd + # (sqrt( ((s).sumsq - ( (s).sum * (s).sum / (s).n)) / ((s).n-1) )) + Math.sqrt( (@sumsq - ( @sum * @sum / @n)) / (@n-1) ) + end + + + # Adds a time delta between now and the last time you called this. This + # will give you the average time between two activities. + # + # An example is: + # + # t = Stats.new("do_stuff") + # 10000.times { do_stuff(); t.tick } + # t.dump("time") + # + def tick + now = Time.now + sample(now - @last_time) + @last_time = now + end +end |