skaes-railsbench 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. data/BUGS +2 -0
  2. data/CHANGELOG +2124 -0
  3. data/GCPATCH +73 -0
  4. data/INSTALL +75 -0
  5. data/LICENSE +222 -0
  6. data/Manifest.txt +53 -0
  7. data/PROBLEMS +56 -0
  8. data/README +330 -0
  9. data/Rakefile +51 -0
  10. data/bin/railsbench +80 -0
  11. data/config/benchmarking.rb +21 -0
  12. data/config/benchmarks.rb +21 -0
  13. data/config/benchmarks.yml +2 -0
  14. data/images/empty.png +0 -0
  15. data/images/minus.png +0 -0
  16. data/images/plus.png +0 -0
  17. data/install.rb +70 -0
  18. data/latest_changes.txt +18 -0
  19. data/lib/benchmark.rb +576 -0
  20. data/lib/railsbench/benchmark.rb +576 -0
  21. data/lib/railsbench/benchmark_specs.rb +63 -0
  22. data/lib/railsbench/gc_info.rb +157 -0
  23. data/lib/railsbench/perf_info.rb +146 -0
  24. data/lib/railsbench/perf_utils.rb +198 -0
  25. data/lib/railsbench/railsbenchmark.rb +519 -0
  26. data/lib/railsbench/version.rb +9 -0
  27. data/lib/railsbench/write_headers_only.rb +15 -0
  28. data/postinstall.rb +12 -0
  29. data/ruby184gc.patch +516 -0
  30. data/ruby185gc.patch +562 -0
  31. data/ruby186gc.patch +564 -0
  32. data/ruby19gc.patch +2425 -0
  33. data/script/convert_raw_data_files +49 -0
  34. data/script/generate_benchmarks +171 -0
  35. data/script/perf_bench +74 -0
  36. data/script/perf_comp +151 -0
  37. data/script/perf_comp_gc +113 -0
  38. data/script/perf_diff +48 -0
  39. data/script/perf_diff_gc +53 -0
  40. data/script/perf_html +103 -0
  41. data/script/perf_plot +225 -0
  42. data/script/perf_plot_gc +254 -0
  43. data/script/perf_prof +87 -0
  44. data/script/perf_run +39 -0
  45. data/script/perf_run_gc +40 -0
  46. data/script/perf_table +104 -0
  47. data/script/perf_tex +58 -0
  48. data/script/perf_times +66 -0
  49. data/script/perf_times_gc +94 -0
  50. data/script/run_urls +56 -0
  51. data/setup.rb +1585 -0
  52. data/test/railsbench_test.rb +11 -0
  53. data/test/test_helper.rb +2 -0
  54. metadata +115 -0
@@ -0,0 +1,519 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/benchmark_specs')
2
+
3
+ class RailsBenchmark
4
+
5
+ attr_accessor :gc_frequency, :iterations
6
+ attr_accessor :http_host, :remote_addr, :server_port
7
+ attr_accessor :relative_url_root
8
+ attr_accessor :perform_caching, :cache_template_loading
9
+ attr_accessor :session_data, :session_key, :cookie_data
10
+
11
+ def error_exit(msg)
12
+ STDERR.puts msg
13
+ raise msg
14
+ end
15
+
16
+ def patched_gc?
17
+ @patched_gc
18
+ end
19
+
20
+ def relative_url_root=(value)
21
+ ActionController::AbstractRequest.relative_url_root = value
22
+ @relative_url_root = value
23
+ end
24
+
25
+ def initialize(options={})
26
+ unless @gc_frequency = options[:gc_frequency]
27
+ @gc_frequency = 0
28
+ ARGV.each{|arg| @gc_frequency = $1.to_i if arg =~ /-gc(\d+)/ }
29
+ end
30
+
31
+ @iterations = (options[:iterations] || 100).to_i
32
+
33
+ @remote_addr = options[:remote_addr] || '127.0.0.1'
34
+ @http_host = options[:http_host] || '127.0.0.1'
35
+ @server_port = options[:server_port] || '80'
36
+
37
+ @session_data = options[:session_data] || {}
38
+ @session_key = options[:session_key] || '_session_id'
39
+
40
+ ENV['RAILS_ENV'] = 'benchmarking'
41
+
42
+ begin
43
+ require ENV['RAILS_ROOT'] + "/config/environment"
44
+ require 'dispatcher' # make edge rails happy
45
+ rescue => e
46
+ $stderr.puts "failed to load application environment"
47
+ e.backtrace.each{|line| $stderr.puts line}
48
+ $stderr.puts "benchmarking aborted"
49
+ exit(-1)
50
+ end
51
+
52
+ # we don't want local error template output, which crashes anyway, when run under railsbench
53
+ ActionController::Rescue.class_eval "def local_request?; false; end"
54
+
55
+ # print backtrace and exit if action execution raises an exception
56
+ ActionController::Rescue.class_eval <<-"end_eval"
57
+ def rescue_action_in_public(exception)
58
+ $stderr.puts "benchmarking aborted due to application error: " + exception.message
59
+ exception.backtrace.each{|line| $stderr.puts line}
60
+ $stderr.print "clearing database connections ..."
61
+ ActiveRecord::Base.send :clear_all_cached_connections! if ActiveRecord::Base.respond_to?(:clear_all_cached_connections)
62
+ ActiveRecord::Base.clear_all_connections! if ActiveRecord::Base.respond_to?(:clear_all_connections)
63
+ $stderr.puts
64
+ exit!(-1)
65
+ end
66
+ end_eval
67
+
68
+ # override rails ActiveRecord::Base#inspect to make profiles more readable
69
+ ActiveRecord::Base.class_eval <<-"end_eval"
70
+ def self.inspect
71
+ super
72
+ end
73
+ end_eval
74
+
75
+ # make sure Rails doesn't try to read post data from stdin
76
+ CGI::QueryExtension.module_eval <<-end_eval
77
+ def read_body(content_length)
78
+ ENV['RAW_POST_DATA']
79
+ end
80
+ end_eval
81
+
82
+ if ARGV.include?('-path')
83
+ $:.each{|f| STDERR.puts f}
84
+ exit
85
+ end
86
+
87
+ logger_module = Logger
88
+ if defined?(Log4r) && RAILS_DEFAULT_LOGGER.is_a?(Log4r::Logger)
89
+ logger_module = Logger
90
+ end
91
+ default_log_level = logger_module.const_get("ERROR")
92
+ log_level = options[:log] || default_log_level
93
+ ARGV.each do |arg|
94
+ case arg
95
+ when '-log'
96
+ log_level = default_log_level
97
+ when '-log=(nil|none)'
98
+ log_level = nil
99
+ when /-log=([a-zA-Z]*)/
100
+ log_level = logger_module.const_get($1.upcase) rescue default_log_level
101
+ end
102
+ end
103
+
104
+ if log_level
105
+ RAILS_DEFAULT_LOGGER.level = log_level
106
+ ActiveRecord::Base.logger.level = log_level
107
+ ActionController::Base.logger.level = log_level
108
+ ActionMailer::Base.logger = level = log_level if defined?(ActionMailer)
109
+ else
110
+ RAILS_DEFAULT_LOGGER.level = logger_module.const_get "FATAL"
111
+ ActiveRecord::Base.logger = nil
112
+ ActionController::Base.logger = nil
113
+ ActionMailer::Base.logger = nil if defined?(ActionMailer)
114
+ end
115
+
116
+ if options.has_key?(:perform_caching)
117
+ ActionController::Base.perform_caching = options[:perform_caching]
118
+ else
119
+ ActionController::Base.perform_caching = false if ARGV.include?('-nocache')
120
+ ActionController::Base.perform_caching = true if ARGV.include?('-cache')
121
+ end
122
+
123
+ if ActionView::Base.respond_to?(:cache_template_loading)
124
+ if options.has_key?(:cache_template_loading)
125
+ ActionView::Base.cache_template_loading = options[:cache_template_loading]
126
+ else
127
+ ActionView::Base.cache_template_loading = true
128
+ end
129
+ end
130
+
131
+ self.relative_url_root = options[:relative_url_root] || ''
132
+
133
+ @patched_gc = GC.collections.is_a?(Numeric) rescue false
134
+
135
+ if ARGV.include? '-headers_only'
136
+ require File.dirname(__FILE__) + '/write_headers_only'
137
+ end
138
+
139
+ end
140
+
141
+ def establish_test_session
142
+ session_options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.stringify_keys
143
+ session_options = session_options.merge('new_session' => true)
144
+ @session = CGI::Session.new(Hash.new, session_options)
145
+ @session_data.each{ |k,v| @session[k] = v }
146
+ @session.update
147
+ @session_id = @session.session_id
148
+ end
149
+
150
+ def update_test_session_data(session_data)
151
+ dbman = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager]
152
+ old_session_data = dbman.new(@session).restore
153
+ # $stderr.puts old_session_data.inspect
154
+ new_session_data = old_session_data.merge(session_data || {})
155
+ new_session_data.each{ |k,v| @session[k] = v }
156
+ @session.update
157
+ end
158
+
159
+ def delete_test_session
160
+ @session.delete
161
+ @session = nil
162
+ end
163
+
164
+ # can be redefined in subclasses to clean out test sessions
165
+ def delete_new_test_sessions
166
+ end
167
+
168
+ def setup_test_urls(name)
169
+ @benchmark = name
170
+ @urls = BenchmarkSpec.load(name)
171
+ end
172
+
173
+ def setup_initial_env
174
+ ENV['REMOTE_ADDR'] = remote_addr
175
+ ENV['HTTP_HOST'] = http_host
176
+ ENV['SERVER_PORT'] = server_port.to_s
177
+ end
178
+
179
+ def setup_request_env(entry)
180
+ # $stderr.puts entry.inspect
181
+ ENV['REQUEST_URI'] = @relative_url_root + entry.uri
182
+ ENV.delete 'RAW_POST_DATA'
183
+ ENV.delete 'QUERY_STRING'
184
+ case ENV['REQUEST_METHOD'] = (entry.method || 'get').upcase
185
+ when 'GET'
186
+ query_data = entry.query_string || ''
187
+ query_data = escape_data(query_data) unless entry.raw_data
188
+ ENV['QUERY_STRING'] = query_data
189
+ when 'POST'
190
+ query_data = entry.post_data || ''
191
+ query_data = escape_data(query_data) unless entry.raw_data
192
+ ENV['RAW_POST_DATA'] = query_data
193
+ end
194
+ ENV['CONTENT_LENGTH'] = query_data.length.to_s
195
+ ENV['HTTP_COOKIE'] = entry.new_session ? '' : cookie
196
+ ENV['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' if entry.xhr
197
+ # $stderr.puts entry.session_data.inspect
198
+ update_test_session_data(entry.session_data) unless entry.new_session
199
+ end
200
+
201
+ def before_dispatch_hook(entry)
202
+ end
203
+
204
+ def cookie
205
+ "#{@session_key}=#{@session_id}#{cookie_data}"
206
+ end
207
+
208
+ def escape_data(str)
209
+ str.split('&').map{|e| e.split('=').map{|e| CGI::escape e}.join('=')}.join('&')
210
+ end
211
+
212
+ def warmup
213
+ error_exit "No urls given for performance test" unless @urls && @urls.size>0
214
+ setup_initial_env
215
+ @urls.each do |entry|
216
+ error_exit "No uri given for benchmark entry: #{entry.inspect}" unless entry.uri
217
+ setup_request_env(entry)
218
+ Dispatcher.dispatch
219
+ end
220
+ end
221
+
222
+ def run_urls_without_benchmark(gc_stats)
223
+ # support for running Ruby Performance Validator
224
+ # or Ruby Memory Validator
225
+ svl = nil
226
+ begin
227
+ if ARGV.include?('-svlPV')
228
+ require 'svlRubyPV'
229
+ svl = SvlRubyPV.new
230
+ elsif ARGV.include?('-svlMV')
231
+ require 'svlRubyMV'
232
+ svl = SvlRubyMV
233
+ end
234
+ rescue LoadError
235
+ # SVL dll not available, do nothing
236
+ end
237
+
238
+ # support ruby-prof
239
+ ruby_prof = nil
240
+ ARGV.each{|arg| ruby_prof=$1 if arg =~ /-ruby_prof=([^ ]*)/ }
241
+ begin
242
+ if ruby_prof
243
+ # redirect stderr (TODO: I can't remember why we don't do this later)
244
+ if benchmark_file = ENV['RAILS_BENCHMARK_FILE']
245
+ $stderr = File.open(benchmark_file, "w")
246
+ end
247
+ require 'ruby-prof'
248
+ RubyProf.measure_mode = RubyProf::WALL_TIME
249
+ RubyProf.start
250
+ end
251
+ rescue LoadError
252
+ # ruby-prof not available, do nothing
253
+ $stderr = STDERR
254
+ $stderr.puts "ruby-prof not available: giving up"
255
+ exit(-1)
256
+ end
257
+
258
+ # start profiler and trigger data collection if required
259
+ if svl
260
+ svl.startProfiler
261
+ svl.startDataCollection
262
+ end
263
+
264
+ setup_initial_env
265
+ GC.enable_stats if gc_stats
266
+ if gc_frequency==0
267
+ run_urls_without_benchmark_and_without_gc_control(@urls, iterations)
268
+ else
269
+ run_urls_without_benchmark_but_with_gc_control(@urls, iterations, gc_frequency)
270
+ end
271
+ if gc_stats
272
+ GC.enable if gc_frequency
273
+ GC.start
274
+ GC.dump
275
+ GC.disable_stats
276
+ GC.log "number of requests processed: #{@urls.size * iterations}"
277
+ end
278
+
279
+ # try to detect Ruby interpreter memory leaks (OS X)
280
+ if ARGV.include?('-leaks')
281
+ leaks_log = "#{ENV['RAILS_PERF_DATA']}/leaks.log"
282
+ leaks_command = "leaks -nocontext #{$$} >#{leaks_log}"
283
+ ENV.delete 'MallocStackLogging'
284
+ # $stderr.puts "executing '#{leaks_command}'"
285
+ raise "could not execute leaks command" unless system(leaks_command)
286
+ mallocs, leaks = *`head -n 2 #{leaks_log}`.split("\n").map{|l| l.gsub(/Process #{$$}: /, '')}
287
+ if mem_leaks = (leaks =~ /(\d+) leaks for (\d+) total leaked bytes/)
288
+ $stderr.puts "\n!!!!! memory leaks detected !!!!! (#{leaks_log})"
289
+ $stderr.puts "=" * leaks.length
290
+ end
291
+ if gc_stats
292
+ GC.log mallocs
293
+ GC.log leaks
294
+ end
295
+ $stderr.puts mallocs, leaks
296
+ $stderr.puts "=" * leaks.length if mem_leaks
297
+ end
298
+
299
+ # stop data collection if necessary
300
+ svl.stopDataCollection if svl
301
+
302
+ if defined? RubyProf
303
+ GC.disable #ruby-pof 0.7.x crash workaround
304
+ result = RubyProf.stop
305
+ GC.enable #ruby-pof 0.7.x crash workaround
306
+ min_percent = ruby_prof.split('/')[0].to_f rescue 0.1
307
+ threshold = ruby_prof.split('/')[1].to_f rescue 1.0
308
+ profile_type = nil
309
+ ARGV.each{|arg| profile_type=$1 if arg =~ /-profile_type=([^ ]*)/ }
310
+ profile_type ||= 'stack'
311
+ printer =
312
+ case profile_type
313
+ when 'stack' then RubyProf::CallStackPrinter
314
+ when 'grind' then RubyProf::CallTreePrinter
315
+ when 'flat' then RubyProf::FlatPrinter
316
+ when 'graph' then RubyProf::GraphHtmlPrinter
317
+ when 'multi' then RubyProf::MultiPrinter
318
+ else raise "unknown profile type: #{profile_type}"
319
+ end.new(result)
320
+ if profile_type == 'multi'
321
+ raise "you must specify a benchmark file when using multi printer" unless $stderr.is_a?(File)
322
+ $stderr.close
323
+ $stderr = STDERR
324
+ file_name = ENV['RAILS_BENCHMARK_FILE']
325
+ profile_name = File.basename(file_name).sub('.html','').sub(".#{profile_type}",'')
326
+ printer.print(:path => File.dirname(file_name),
327
+ :profile => profile_name,
328
+ :min_percent => min_percent, :threshold => threshold,
329
+ :title => "call tree/graph for benchmark #{@benchmark}")
330
+ else
331
+ printer.print($stderr, :min_percent => min_percent, :threshold => threshold,
332
+ :title => "call tree for benchmark #{@benchmark}")
333
+ end
334
+ end
335
+
336
+ delete_test_session
337
+ delete_new_test_sessions
338
+ end
339
+
340
+ def run_urls(test)
341
+ setup_initial_env
342
+ if gc_frequency>0
343
+ run_urls_with_gc_control(test, @urls, iterations, gc_frequency)
344
+ else
345
+ run_urls_without_gc_control(test, @urls, iterations)
346
+ end
347
+ delete_test_session
348
+ delete_new_test_sessions
349
+ end
350
+
351
+ def run_url_mix(test)
352
+ if gc_frequency>0
353
+ run_url_mix_with_gc_control(test, @urls, iterations, gc_frequency)
354
+ else
355
+ run_url_mix_without_gc_control(test, @urls, iterations)
356
+ end
357
+ delete_test_session
358
+ delete_new_test_sessions
359
+ end
360
+
361
+ private
362
+
363
+ def run_urls_without_benchmark_but_with_gc_control(urls, n, gc_frequency)
364
+ urls.each do |entry|
365
+ setup_request_env(entry)
366
+ GC.enable; GC.start; GC.disable
367
+ request_count = 0
368
+ n.times do
369
+ before_dispatch_hook(entry)
370
+ Dispatcher.dispatch
371
+ if (request_count += 1) == gc_frequency
372
+ GC.enable; GC.start; GC.disable
373
+ request_count = 0
374
+ end
375
+ end
376
+ end
377
+ end
378
+
379
+ def run_urls_without_benchmark_and_without_gc_control(urls, n)
380
+ urls.each do |entry|
381
+ setup_request_env(entry)
382
+ n.times do
383
+ before_dispatch_hook(entry)
384
+ Dispatcher.dispatch
385
+ end
386
+ end
387
+ end
388
+
389
+ def run_urls_with_gc_control(test, urls, n, gc_freq)
390
+ gc_stats = patched_gc?
391
+ GC.clear_stats if gc_stats
392
+ urls.each do |entry|
393
+ request_count = 0
394
+ setup_request_env(entry)
395
+ test.report(entry.name) do
396
+ GC.disable_stats if gc_stats
397
+ GC.enable; GC.start; GC.disable
398
+ GC.enable_stats if gc_stats
399
+ n.times do
400
+ before_dispatch_hook(entry)
401
+ Dispatcher.dispatch
402
+ if (request_count += 1) == gc_freq
403
+ GC.enable; GC.start; GC.disable
404
+ request_count = 0
405
+ end
406
+ end
407
+ end
408
+ end
409
+ if gc_stats
410
+ GC.disable_stats
411
+ Benchmark::OUTPUT.puts "GC.collections=#{GC.collections}, GC.time=#{GC.time/1E6}"
412
+ GC.clear_stats
413
+ end
414
+ end
415
+
416
+ def run_urls_without_gc_control(test, urls, n)
417
+ gc_stats = patched_gc?
418
+ GC.clear_stats if gc_stats
419
+ urls.each do |entry|
420
+ setup_request_env(entry)
421
+ GC.disable_stats if gc_stats
422
+ GC.start
423
+ GC.enable_stats if gc_stats
424
+ test.report(entry.name) do
425
+ n.times do
426
+ before_dispatch_hook(entry)
427
+ Dispatcher.dispatch
428
+ end
429
+ end
430
+ end
431
+ if gc_stats
432
+ GC.disable_stats
433
+ Benchmark::OUTPUT.puts "GC.collections=#{GC.collections}, GC.time=#{GC.time/1E6}"
434
+ GC.clear_stats
435
+ end
436
+ end
437
+
438
+ def run_url_mix_without_gc_control(test, urls, n)
439
+ gc_stats = patched_gc?
440
+ GC.start
441
+ if gc_stats
442
+ GC.clear_stats; GC.enable_stats
443
+ end
444
+ test.report("url_mix (#{urls.length} urls)") do
445
+ n.times do
446
+ urls.each do |entry|
447
+ setup_request_env(entry)
448
+ before_dispatch_hook(entry)
449
+ Dispatcher.dispatch
450
+ end
451
+ end
452
+ end
453
+ if gc_stats
454
+ GC.disable_stats
455
+ Benchmark::OUTPUT.puts "GC.collections=#{GC.collections}, GC.time=#{GC.time/1E6}"
456
+ GC.clear_stats
457
+ end
458
+ end
459
+
460
+ def run_url_mix_with_gc_control(test, urls, n, gc_frequency)
461
+ gc_stats = patched_gc?
462
+ GC.enable; GC.start; GC.disable
463
+ if gc_stats
464
+ GC.clear_stats; GC.enable_stats
465
+ end
466
+ test.report("url_mix (#{urls.length} urls)") do
467
+ request_count = 0
468
+ n.times do
469
+ urls.each do |entry|
470
+ setup_request_env(entry)
471
+ before_dispatch_hook(entry)
472
+ Dispatcher.dispatch
473
+ if (request_count += 1) == gc_frequency
474
+ GC.enable; GC.start; GC.disable
475
+ request_count = 0
476
+ end
477
+ end
478
+ end
479
+ end
480
+ if gc_stats
481
+ GC.disable_stats
482
+ Benchmark::OUTPUT.puts "GC.collections=#{GC.collections}, GC.time=#{GC.time/1E6}"
483
+ GC.clear_stats
484
+ end
485
+ end
486
+ end
487
+
488
+
489
+ class RailsBenchmarkWithActiveRecordStore < RailsBenchmark
490
+
491
+ def initialize(options={})
492
+ super(options)
493
+ @session_class = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager].session_class rescue CGI::Session::ActiveRecordStore
494
+ end
495
+
496
+ def delete_new_test_sessions
497
+ @session_class.delete_all if @session_class.respond_to?(:delete_all)
498
+ end
499
+
500
+ end
501
+
502
+
503
+ __END__
504
+
505
+ # Copyright (C) 2005-2008 Stefan Kaes
506
+ #
507
+ # This program is free software; you can redistribute it and/or modify
508
+ # it under the terms of the GNU General Public License as published by
509
+ # the Free Software Foundation; either version 2 of the License, or
510
+ # (at your option) any later version.
511
+ #
512
+ # This program is distributed in the hope that it will be useful,
513
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
514
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
515
+ # GNU General Public License for more details.
516
+ #
517
+ # You should have received a copy of the GNU General Public License
518
+ # along with this program; if not, write to the Free Software
519
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA