Class: RailsBenchmark

Inherits:
Object
  • Object
show all
Defined in:
lib/railsbench/railsbenchmark.rb

Direct Known Subclasses

RailsBenchmarkWithActiveRecordStore

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ RailsBenchmark

Returns a new instance of RailsBenchmark.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/railsbench/railsbenchmark.rb', line 31

def initialize(options={})
  unless @gc_frequency = options[:gc_frequency]
    @gc_frequency = 0
    ARGV.each{|arg| @gc_frequency = $1.to_i if arg =~ /-gc(\d+)/ }
  end

  @iterations = (options[:iterations] || 100).to_i

  @remote_addr = options[:remote_addr] || '127.0.0.1'
  @http_host =  options[:http_host] || '127.0.0.1'
  @server_port = options[:server_port] || '80'

  @session_data = options[:session_data] || {}
  @session_key = options[:session_key] || '_session_id'

  ENV['RAILS_ENV'] = 'benchmarking'

  begin
    require ENV['RAILS_ROOT'] + "/config/environment"
    require 'dispatcher' # make edge rails happy

    if Rails::VERSION::STRING >= "2.3"
      @rack_middleware = true
      require 'cgi/session'
      CGI.class_eval <<-"end_eval"
        def env_table
          @env_table ||= ENV.to_hash
        end
      end_eval
    else
       @rack_middleware = false
    end

  rescue => e
    $stderr.puts "failed to load application environment"
    e.backtrace.each{|line| $stderr.puts line}
    $stderr.puts "benchmarking aborted"
    exit(-1)
  end

  # we don't want local error template output, which crashes anyway, when run under railsbench
  ActionController::Rescue.class_eval "def local_request?; false; end"

  # print backtrace and exit if action execution raises an exception
  ActionController::Rescue.class_eval <<-"end_eval"
    def rescue_action_in_public(exception)
      $stderr.puts "benchmarking aborted due to application error: " + exception.message
      exception.backtrace.each{|line| $stderr.puts line}
      $stderr.print "clearing database connections ..."
      ActiveRecord::Base.send :clear_all_cached_connections! if ActiveRecord::Base.respond_to?(:clear_all_cached_connections)
      ActiveRecord::Base.clear_all_connections! if ActiveRecord::Base.respond_to?(:clear_all_connections)
      $stderr.puts
      exit!(-1)
    end
  end_eval

  # override rails ActiveRecord::Base#inspect to make profiles more readable
  ActiveRecord::Base.class_eval <<-"end_eval"
    def self.inspect
      super
    end
  end_eval

  # make sure Rails doesn't try to read post data from stdin
  CGI::QueryExtension.module_eval <<-end_eval
    def read_body(content_length)
      ENV['RAW_POST_DATA']
    end
  end_eval

  if ARGV.include?('-path')
    $:.each{|f| STDERR.puts f}
    exit
  end

  logger_module = Logger
  if defined?(Log4r) && RAILS_DEFAULT_LOGGER.is_a?(Log4r::Logger)
    logger_module = Logger
  end
  default_log_level = logger_module.const_get("ERROR")
  log_level = options[:log] || default_log_level
  ARGV.each do |arg|
      case arg
      when '-log'
        log_level = default_log_level
      when '-log=(nil|none)'
        log_level = nil
      when /-log=([a-zA-Z]*)/
        log_level = logger_module.const_get($1.upcase) rescue default_log_level
      end
  end

  if log_level
    RAILS_DEFAULT_LOGGER.level = log_level
    ActiveRecord::Base.logger.level = log_level
    ActionController::Base.logger.level = log_level
    ActionMailer::Base.logger = level = log_level if defined?(ActionMailer)
  else
    RAILS_DEFAULT_LOGGER.level = logger_module.const_get "FATAL"
    ActiveRecord::Base.logger = nil
    ActionController::Base.logger = nil
    ActionMailer::Base.logger = nil if defined?(ActionMailer)
  end

  if options.has_key?(:perform_caching)
    ActionController::Base.perform_caching = options[:perform_caching]
  else
    ActionController::Base.perform_caching = false if ARGV.include?('-nocache')
    ActionController::Base.perform_caching = true if ARGV.include?('-cache')
  end

  if ActionView::Base.respond_to?(:cache_template_loading)
    if options.has_key?(:cache_template_loading)
      ActionView::Base.cache_template_loading = options[:cache_template_loading]
    else
      ActionView::Base.cache_template_loading = true
    end
  end

  self.relative_url_root = options[:relative_url_root] || ''

  @patched_gc = GC.collections.is_a?(Numeric) rescue false

  if ARGV.include? '-headers_only'
    require File.dirname(__FILE__) + '/write_headers_only'
  end

end

Instance Attribute Details

#cache_template_loadingObject

Returns the value of attribute cache_template_loading.



8
9
10
# File 'lib/railsbench/railsbenchmark.rb', line 8

def cache_template_loading
  @cache_template_loading
end

Returns the value of attribute cookie_data.



9
10
11
# File 'lib/railsbench/railsbenchmark.rb', line 9

def cookie_data
  @cookie_data
end

#gc_frequencyObject

Returns the value of attribute gc_frequency.



5
6
7
# File 'lib/railsbench/railsbenchmark.rb', line 5

def gc_frequency
  @gc_frequency
end

#http_hostObject

Returns the value of attribute http_host.



6
7
8
# File 'lib/railsbench/railsbenchmark.rb', line 6

def http_host
  @http_host
end

#iterationsObject

Returns the value of attribute iterations.



5
6
7
# File 'lib/railsbench/railsbenchmark.rb', line 5

def iterations
  @iterations
end

#perform_cachingObject

Returns the value of attribute perform_caching.



8
9
10
# File 'lib/railsbench/railsbenchmark.rb', line 8

def perform_caching
  @perform_caching
end

#relative_url_rootObject

Returns the value of attribute relative_url_root.



7
8
9
# File 'lib/railsbench/railsbenchmark.rb', line 7

def relative_url_root
  @relative_url_root
end

#remote_addrObject

Returns the value of attribute remote_addr.



6
7
8
# File 'lib/railsbench/railsbenchmark.rb', line 6

def remote_addr
  @remote_addr
end

#server_portObject

Returns the value of attribute server_port.



6
7
8
# File 'lib/railsbench/railsbenchmark.rb', line 6

def server_port
  @server_port
end

#session_dataObject

Returns the value of attribute session_data.



9
10
11
# File 'lib/railsbench/railsbenchmark.rb', line 9

def session_data
  @session_data
end

#session_keyObject

Returns the value of attribute session_key.



9
10
11
# File 'lib/railsbench/railsbenchmark.rb', line 9

def session_key
  @session_key
end

Instance Method Details

#before_dispatch_hook(entry) ⇒ Object



252
253
# File 'lib/railsbench/railsbenchmark.rb', line 252

def before_dispatch_hook(entry)
end


255
256
257
# File 'lib/railsbench/railsbenchmark.rb', line 255

def cookie
  "#{@session_key}=#{@session_id}#{cookie_data}"
end

#delete_new_test_sessionsObject

can be redefined in subclasses to clean out test sessions



216
217
# File 'lib/railsbench/railsbenchmark.rb', line 216

def delete_new_test_sessions
end

#delete_test_sessionObject



207
208
209
210
211
212
213
# File 'lib/railsbench/railsbenchmark.rb', line 207

def delete_test_session
  # no way to delete a session by going through the session adpater in rails 2.3
  if @session
    @session.delete
    @session = nil
  end
end

#error_exit(msg) ⇒ Object



11
12
13
14
# File 'lib/railsbench/railsbenchmark.rb', line 11

def error_exit(msg)
  STDERR.puts msg
  raise msg
end

#escape_data(str) ⇒ Object



259
260
261
# File 'lib/railsbench/railsbenchmark.rb', line 259

def escape_data(str)
  str.split('&').map{|e| e.split('=').map{|e| CGI::escape e}.join('=')}.join('&')
end

#establish_test_sessionObject



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/railsbench/railsbenchmark.rb', line 160

def establish_test_session
  if @rack_middleware
    session_options = ActionController::Base.session_options
    @session_id = ActiveSupport::SecureRandom.hex(16)
    do_not_do_much = lambda do |env|
      env["rack.session"] = @session_data
      env["rack.session.options"] = {:id => @session_id}
      [200, {}, ""]
    end
    @session_store = ActionController::Base.session_store.new(do_not_do_much, session_options)
    @session_store.call({})
  else
    session_options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.stringify_keys
    session_options = session_options.merge('new_session' => true)
    @session = CGI::Session.new(Hash.new, session_options)
    @session_data.each{ |k,v| @session[k] = v }
    @session.update
    @session_id = @session.session_id
  end
end

#patched_gc?Boolean

Returns:

  • (Boolean)


16
17
18
# File 'lib/railsbench/railsbenchmark.rb', line 16

def patched_gc?
  @patched_gc
end

#run_url_mix(test) ⇒ Object



410
411
412
413
414
415
416
417
418
# File 'lib/railsbench/railsbenchmark.rb', line 410

def run_url_mix(test)
  if gc_frequency>0
    run_url_mix_with_gc_control(test, @urls, iterations, gc_frequency)
  else
    run_url_mix_without_gc_control(test, @urls, iterations)
  end
  delete_test_session
  delete_new_test_sessions
end

#run_urls(test) ⇒ Object



399
400
401
402
403
404
405
406
407
408
# File 'lib/railsbench/railsbenchmark.rb', line 399

def run_urls(test)
  setup_initial_env
  if gc_frequency>0
    run_urls_with_gc_control(test, @urls, iterations, gc_frequency)
  else
    run_urls_without_gc_control(test, @urls, iterations)
  end
  delete_test_session
  delete_new_test_sessions
end

#run_urls_without_benchmark(gc_stats) ⇒ Object



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/railsbench/railsbenchmark.rb', line 273

def run_urls_without_benchmark(gc_stats)
  # support for running Ruby Performance Validator
  # or Ruby Memory Validator
  svl = nil
  begin
    if ARGV.include?('-svlPV')
      require 'svlRubyPV'
      svl = SvlRubyPV.new
    elsif ARGV.include?('-svlMV')
      require 'svlRubyMV'
      svl = SvlRubyMV
    end
  rescue LoadError
    # SVL dll not available, do nothing
  end

  # support ruby-prof
  ruby_prof = nil
  ARGV.each{|arg| ruby_prof=$1 if arg =~ /-ruby_prof=([^ ]*)/ }
  begin
    if ruby_prof
      # redirect stderr (TODO: I can't remember why we don't do this later)
      if benchmark_file = ENV['RAILS_BENCHMARK_FILE']
        $stderr = File.open(benchmark_file, "w")
      end
      require 'ruby-prof'
      measure_mode = "WALL_TIME"
      ARGV.each{|arg| measure_mode=$1.upcase if arg =~ /-measure_mode=([^ ]*)/ }
      if %w(PROCESS_TIME WALL_TIME CPU_TIME ALLOCATIONS MEMORY).include?(measure_mode)
        RubyProf.measure_mode = RubyProf.const_get measure_mode
      else
        $stderr = STDERR
        $stderr.puts "unsupported ruby_prof measure mode: #{measure_mode}"
        exit(-1)
      end
      RubyProf.start
    end
  rescue LoadError
    # ruby-prof not available, do nothing
    $stderr = STDERR
    $stderr.puts "ruby-prof not available: giving up"
    exit(-1)
  end

  # start profiler and trigger data collection if required
  if svl
    svl.startProfiler
    svl.startDataCollection
  end

  setup_initial_env
  GC.enable_stats if gc_stats
  if gc_frequency==0
    run_urls_without_benchmark_and_without_gc_control(@urls, iterations)
  else
    run_urls_without_benchmark_but_with_gc_control(@urls, iterations, gc_frequency)
  end
  if gc_stats
    GC.enable if gc_frequency
    GC.start
    GC.dump
    GC.disable_stats
    GC.log "number of requests processed: #{@urls.size * iterations}"
  end

  # try to detect Ruby interpreter memory leaks (OS X)
  if ARGV.include?('-leaks')
    leaks_log = "#{ENV['RAILS_PERF_DATA']}/leaks.log"
    leaks_command = "leaks -nocontext #{$$} >#{leaks_log}"
    ENV.delete 'MallocStackLogging'
    # $stderr.puts "executing '#{leaks_command}'"
    raise "could not execute leaks command" unless system(leaks_command)
    mallocs, leaks = *`head -n 2 #{leaks_log}`.split("\n").map{|l| l.gsub(/Process #{$$}: /, '')}
    if mem_leaks = (leaks =~ /(\d+) leaks for (\d+) total leaked bytes/)
      $stderr.puts "\n!!!!! memory leaks detected !!!!! (#{leaks_log})"
      $stderr.puts "=" * leaks.length
    end
    if gc_stats
      GC.log mallocs
      GC.log leaks
    end
    $stderr.puts mallocs, leaks
    $stderr.puts "=" * leaks.length if mem_leaks
  end

  # stop data collection if necessary
  svl.stopDataCollection if svl

  if defined? RubyProf
    GC.disable #ruby-pof 0.7.x crash workaround
    result = RubyProf.stop
    GC.enable  #ruby-pof 0.7.x crash workaround
    min_percent = ruby_prof.split('/')[0].to_f rescue 0.1
    threshold = ruby_prof.split('/')[1].to_f rescue 1.0
    profile_type = nil
    ARGV.each{|arg| profile_type=$1 if arg =~ /-profile_type=([^ ]*)/ }
    profile_type ||= 'stack'
    printer =
      case profile_type
      when 'stack' then RubyProf::CallStackPrinter
      when 'grind' then RubyProf::CallTreePrinter
      when 'flat'  then RubyProf::FlatPrinter
      when 'graph' then RubyProf::GraphHtmlPrinter
      when 'multi' then RubyProf::MultiPrinter
      else raise "unknown profile type: #{profile_type}"
      end.new(result)
    if profile_type == 'multi'
      raise "you must specify a benchmark file when using multi printer" unless $stderr.is_a?(File)
      $stderr.close
      $stderr = STDERR
      file_name = ENV['RAILS_BENCHMARK_FILE']
      profile_name = File.basename(file_name).sub('.html','').sub(".#{profile_type}",'')
      printer.print(:path => File.dirname(file_name),
                    :profile => profile_name,
                    :min_percent => min_percent, :threshold => threshold,
                    :title => "call tree/graph for benchmark #{@benchmark}")
    else
      printer.print($stderr, :min_percent => min_percent, :threshold => threshold,
                    :title => "call tree for benchmark #{@benchmark}")
    end
  end

  delete_test_session
  delete_new_test_sessions
end

#setup_initial_envObject



224
225
226
227
228
# File 'lib/railsbench/railsbenchmark.rb', line 224

def setup_initial_env
  ENV['REMOTE_ADDR'] = remote_addr
  ENV['HTTP_HOST'] = http_host
  ENV['SERVER_PORT'] = server_port.to_s
end

#setup_request_env(entry) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/railsbench/railsbenchmark.rb', line 230

def setup_request_env(entry)
  # $stderr.puts entry.inspect
  ENV['REQUEST_URI'] = @relative_url_root + entry.uri
  ENV.delete 'RAW_POST_DATA'
  ENV.delete 'QUERY_STRING'
  case ENV['REQUEST_METHOD'] = (entry.method || 'get').upcase
  when 'GET'
    query_data = entry.query_string || ''
    query_data = escape_data(query_data) unless entry.raw_data
    ENV['QUERY_STRING'] = query_data
  when 'POST'
    query_data = entry.post_data || ''
    query_data = escape_data(query_data) unless entry.raw_data
    ENV['RAW_POST_DATA'] = query_data
  end
  ENV['CONTENT_LENGTH'] = query_data.length.to_s
  ENV['HTTP_COOKIE'] = entry.new_session ? '' : cookie
  ENV['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' if entry.xhr
  # $stderr.puts entry.session_data.inspect
  update_test_session_data(entry.session_data) unless entry.new_session
end

#setup_test_urls(name) ⇒ Object



219
220
221
222
# File 'lib/railsbench/railsbenchmark.rb', line 219

def setup_test_urls(name)
  @benchmark = name
  @urls = BenchmarkSpec.load(name)
end

#update_test_session_data(session_data) ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/railsbench/railsbenchmark.rb', line 181

def update_test_session_data(session_data)
  if @rack_middleware
    session_options = ActionController::Base.session_options
    merge_url_specific_session_data = lambda do |env|
      old_session_data = env["rack.session"]
      # $stderr.puts "data in old session: #{old_session_data.inspect}"
      new_session_data = old_session_data.merge(session_data || {})
      # $stderr.puts "data in new session: #{new_session_data.inspect}"
      env["rack.session"] = new_session_data
      [200, {}, ""]
    end
    @session_store.instance_eval { @app = merge_url_specific_session_data }
    env = {}
    env["HTTP_COOKIE"] = cookie
    # debugger
    @session_store.call(env)
  else
    dbman = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager]
    old_session_data = dbman.new(@session).restore
    # $stderr.puts old_session_data.inspect
    new_session_data = old_session_data.merge(session_data || {})
    new_session_data.each{ |k,v| @session[k] = v }
    @session.update
  end
end

#warmupObject



263
264
265
266
267
268
269
270
271
# File 'lib/railsbench/railsbenchmark.rb', line 263

def warmup
  error_exit "No urls given for performance test" unless @urls && @urls.size>0
  setup_initial_env
  @urls.each do |entry|
    error_exit "No uri given for benchmark entry: #{entry.inspect}" unless entry.uri
    setup_request_env(entry)
    Dispatcher.dispatch(CGI.new)
  end
end