rails-perftest 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +696 -0
- data/Rakefile +37 -0
- data/bin/perftest +9 -0
- data/lib/generators/rails/app/templates/test/performance/browsing_test.rb +12 -0
- data/lib/generators/rails/performance_test/USAGE +10 -0
- data/lib/generators/rails/performance_test/performance_test_generator.rb +14 -0
- data/lib/generators/rails/performance_test/templates/performance_test.rb +12 -0
- data/lib/rails-perftest.rb +2 -0
- data/lib/rails/performance_test_help.rb +3 -0
- data/lib/rails/perftest/action_controller.rb +3 -0
- data/lib/rails/perftest/action_controller/performance_test.rb +3 -0
- data/lib/rails/perftest/action_dispatch.rb +3 -0
- data/lib/rails/perftest/action_dispatch/performance_test.rb +10 -0
- data/lib/rails/perftest/active_support/testing/performance.rb +271 -0
- data/lib/rails/perftest/active_support/testing/performance/jruby.rb +115 -0
- data/lib/rails/perftest/active_support/testing/performance/rubinius.rb +113 -0
- data/lib/rails/perftest/active_support/testing/performance/ruby.rb +173 -0
- data/lib/rails/perftest/commands.rb +30 -0
- data/lib/rails/perftest/commands/benchmarker.rb +34 -0
- data/lib/rails/perftest/commands/profiler.rb +32 -0
- data/lib/rails/perftest/railtie.rb +20 -0
- data/lib/rails/perftest/railties/testing.tasks +12 -0
- data/lib/rails/perftest/version.rb +5 -0
- data/rails-perftest.gemspec +26 -0
- data/rails-perftest.gemspec.erb +26 -0
- data/test/generators/generators_test_helper.rb +16 -0
- data/test/generators/performance_test_generator_test.rb +18 -0
- data/test/helper.rb +8 -0
- data/test/performance_test.rb +58 -0
- metadata +171 -0
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
require 'rake/testtask'
|
5
|
+
|
6
|
+
namespace :test do
|
7
|
+
Rake::TestTask.new("regular") do |t|
|
8
|
+
t.libs = ["test"]
|
9
|
+
t.pattern = "test/*_test.rb"
|
10
|
+
t.ruby_opts = ['-w']
|
11
|
+
end
|
12
|
+
|
13
|
+
Rake::TestTask.new("generators") do |t|
|
14
|
+
t.libs = ["test"]
|
15
|
+
t.pattern = "test/generators/*_test.rb"
|
16
|
+
t.ruby_opts = ['-w']
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
task :test => ['test:regular', 'test:generators']
|
21
|
+
task :default => :test
|
22
|
+
|
23
|
+
specname = "rails-perftest.gemspec"
|
24
|
+
deps = `git ls-files`.split("\n") - [specname]
|
25
|
+
|
26
|
+
file specname => deps do
|
27
|
+
files = `git ls-files`.split("\n")
|
28
|
+
test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
29
|
+
executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
30
|
+
|
31
|
+
require 'erb'
|
32
|
+
|
33
|
+
File.open specname, 'w:utf-8' do |f|
|
34
|
+
f.write ERB.new(File.read("#{specname}.erb")).result(binding)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
task :build => specname
|
data/bin/perftest
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'rails/performance_test_help'
|
3
|
+
|
4
|
+
class BrowsingTest < ActionDispatch::PerformanceTest
|
5
|
+
# Refer to the documentation for all available options
|
6
|
+
# self.profile_options = { runs: 5, metrics: [:wall_time, :memory],
|
7
|
+
# output: 'tmp/performance', formats: [:flat] }
|
8
|
+
|
9
|
+
test "homepage" do
|
10
|
+
get '/'
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
Description:
|
2
|
+
Stubs out a new performance test. Pass the name of the test, either
|
3
|
+
CamelCased or under_scored, as an argument.
|
4
|
+
|
5
|
+
This generator invokes the current performance tool, which defaults to
|
6
|
+
TestUnit.
|
7
|
+
|
8
|
+
Example:
|
9
|
+
`rails generate performance_test GeneralStories` creates a GeneralStories
|
10
|
+
performance test in test/performance/general_stories_test.rb
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Rails
|
2
|
+
module Generators
|
3
|
+
class PerformanceTestGenerator < NamedBase # :nodoc:
|
4
|
+
check_class_collision suffix: "Test"
|
5
|
+
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
def create_performance_test_file
|
9
|
+
template 'performance_test.rb', File.join('test/performance', class_path, "#{file_name}_test.rb")
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'rails/performance_test_help'
|
3
|
+
|
4
|
+
class <%= class_name %>Test < ActionDispatch::PerformanceTest
|
5
|
+
# Refer to the documentation for all available options
|
6
|
+
# self.profile_options = { runs: 5, metrics: [:wall_time, :memory],
|
7
|
+
# output: 'tmp/performance', formats: [:flat] }
|
8
|
+
|
9
|
+
test "homepage" do
|
10
|
+
get '/'
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'rails/perftest/active_support/testing/performance'
|
2
|
+
|
3
|
+
module ActionDispatch
|
4
|
+
# An integration test that runs a code profiler on your test methods.
|
5
|
+
# Profiling output for combinations of each test method, measurement, and
|
6
|
+
# output format are written to your tmp/performance directory.
|
7
|
+
class PerformanceTest < ActionDispatch::IntegrationTest
|
8
|
+
include ActiveSupport::Testing::Performance
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'active_support/core_ext/class/delegating_attributes'
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
5
|
+
require 'active_support/core_ext/module/delegation'
|
6
|
+
require 'active_support/number_helper'
|
7
|
+
|
8
|
+
module ActiveSupport
|
9
|
+
module Testing
|
10
|
+
module Performance
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
superclass_delegating_accessor :profile_options
|
15
|
+
self.profile_options = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# each implementation should define metrics and freeze the defaults
|
19
|
+
DEFAULTS =
|
20
|
+
if ARGV.include?('--benchmark') # HAX for rake test
|
21
|
+
{ :runs => 4,
|
22
|
+
:output => 'tmp/performance',
|
23
|
+
:benchmark => true }
|
24
|
+
else
|
25
|
+
{ :runs => 1,
|
26
|
+
:output => 'tmp/performance',
|
27
|
+
:benchmark => false }
|
28
|
+
end
|
29
|
+
|
30
|
+
def full_profile_options
|
31
|
+
DEFAULTS.merge(profile_options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def full_test_name
|
35
|
+
"#{self.class.name}##{method_name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def run(runner)
|
39
|
+
@runner = runner
|
40
|
+
|
41
|
+
run_warmup
|
42
|
+
if full_profile_options && metrics = full_profile_options[:metrics]
|
43
|
+
metrics.each do |metric_name|
|
44
|
+
if klass = Metrics[metric_name.to_sym]
|
45
|
+
run_profile(klass.new)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
return
|
51
|
+
end
|
52
|
+
|
53
|
+
def run_test(metric, mode)
|
54
|
+
result = '.'
|
55
|
+
begin
|
56
|
+
run_callbacks :setup
|
57
|
+
setup
|
58
|
+
metric.send(mode) { __send__ method_name }
|
59
|
+
rescue Exception => e
|
60
|
+
result = @runner.puke(self.class, method_name, e)
|
61
|
+
ensure
|
62
|
+
begin
|
63
|
+
teardown
|
64
|
+
run_callbacks :teardown
|
65
|
+
rescue Exception => e
|
66
|
+
result = @runner.puke(self.class, method_name, e)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
result
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
# overridden by each implementation.
|
74
|
+
def run_gc; end
|
75
|
+
|
76
|
+
def run_warmup
|
77
|
+
run_gc
|
78
|
+
|
79
|
+
time = Metrics::Time.new
|
80
|
+
run_test(time, :benchmark)
|
81
|
+
puts "%s (%s warmup)" % [full_test_name, time.format(time.total)]
|
82
|
+
|
83
|
+
run_gc
|
84
|
+
end
|
85
|
+
|
86
|
+
def run_profile(metric)
|
87
|
+
klass = full_profile_options[:benchmark] ? Benchmarker : Profiler
|
88
|
+
performer = klass.new(self, metric)
|
89
|
+
|
90
|
+
performer.run
|
91
|
+
puts performer.report
|
92
|
+
performer.record
|
93
|
+
end
|
94
|
+
|
95
|
+
class Performer
|
96
|
+
delegate :run_test, :full_profile_options, :full_test_name, :to => :@harness
|
97
|
+
|
98
|
+
def initialize(harness, metric)
|
99
|
+
@harness, @metric, @supported = harness, metric, false
|
100
|
+
end
|
101
|
+
|
102
|
+
def report
|
103
|
+
if @supported
|
104
|
+
rate = @total / full_profile_options[:runs]
|
105
|
+
'%20s: %s' % [@metric.name, @metric.format(rate)]
|
106
|
+
else
|
107
|
+
'%20s: unsupported' % @metric.name
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
protected
|
112
|
+
def output_filename
|
113
|
+
"#{full_profile_options[:output]}/#{full_test_name}_#{@metric.name}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# overridden by each implementation.
|
118
|
+
class Profiler < Performer
|
119
|
+
def time_with_block
|
120
|
+
before = Time.now
|
121
|
+
yield
|
122
|
+
Time.now - before
|
123
|
+
end
|
124
|
+
|
125
|
+
def run; end
|
126
|
+
def record; end
|
127
|
+
end
|
128
|
+
|
129
|
+
class Benchmarker < Performer
|
130
|
+
def initialize(*args)
|
131
|
+
super
|
132
|
+
@supported = @metric.respond_to?('measure')
|
133
|
+
end
|
134
|
+
|
135
|
+
def run
|
136
|
+
return unless @supported
|
137
|
+
|
138
|
+
full_profile_options[:runs].to_i.times { run_test(@metric, :benchmark) }
|
139
|
+
@total = @metric.total
|
140
|
+
end
|
141
|
+
|
142
|
+
def record
|
143
|
+
avg = @metric.total / full_profile_options[:runs].to_i
|
144
|
+
now = Time.now.utc.xmlschema
|
145
|
+
with_output_file do |file|
|
146
|
+
file.puts "#{avg},#{now},#{environment}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def environment
|
151
|
+
@env ||= [].tap do |env|
|
152
|
+
env << "#{$1}.#{$2}" if File.directory?('.git') && `git branch -v` =~ /^\* (\S+)\s+(\S+)/
|
153
|
+
env << rails_version if defined?(Rails::VERSION::STRING)
|
154
|
+
env << "#{RUBY_ENGINE}-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}"
|
155
|
+
env << RUBY_PLATFORM
|
156
|
+
end.join(',')
|
157
|
+
end
|
158
|
+
|
159
|
+
protected
|
160
|
+
if defined?(Rails::VERSION::STRING)
|
161
|
+
HEADER = 'measurement,created_at,app,rails,ruby,platform'
|
162
|
+
else
|
163
|
+
HEADER = 'measurement,created_at,app,ruby,platform'
|
164
|
+
end
|
165
|
+
|
166
|
+
def with_output_file
|
167
|
+
fname = output_filename
|
168
|
+
|
169
|
+
if new = !File.exist?(fname)
|
170
|
+
FileUtils.mkdir_p(File.dirname(fname))
|
171
|
+
end
|
172
|
+
|
173
|
+
File.open(fname, 'ab') do |file|
|
174
|
+
file.puts(HEADER) if new
|
175
|
+
yield file
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def output_filename
|
180
|
+
"#{super}.csv"
|
181
|
+
end
|
182
|
+
|
183
|
+
def rails_version
|
184
|
+
"rails-#{Rails::VERSION::STRING}#{rails_branch}"
|
185
|
+
end
|
186
|
+
|
187
|
+
def rails_branch
|
188
|
+
if File.directory?('vendor/rails/.git')
|
189
|
+
Dir.chdir('vendor/rails') do
|
190
|
+
".#{$1}.#{$2}" if `git branch -v` =~ /^\* (\S+)\s+(\S+)/
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
module Metrics
|
197
|
+
def self.[](name)
|
198
|
+
const_get(name.to_s.camelize)
|
199
|
+
rescue NameError
|
200
|
+
nil
|
201
|
+
end
|
202
|
+
|
203
|
+
class Base
|
204
|
+
include ActiveSupport::NumberHelper
|
205
|
+
|
206
|
+
attr_reader :total
|
207
|
+
|
208
|
+
def initialize
|
209
|
+
@total = 0
|
210
|
+
end
|
211
|
+
|
212
|
+
def name
|
213
|
+
@name ||= self.class.name.demodulize.underscore
|
214
|
+
end
|
215
|
+
|
216
|
+
def benchmark
|
217
|
+
with_gc_stats do
|
218
|
+
before = measure
|
219
|
+
yield
|
220
|
+
@total += (measure - before)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# overridden by each implementation.
|
225
|
+
def profile; end
|
226
|
+
|
227
|
+
protected
|
228
|
+
# overridden by each implementation.
|
229
|
+
def with_gc_stats; end
|
230
|
+
end
|
231
|
+
|
232
|
+
class Time < Base
|
233
|
+
def measure
|
234
|
+
::Time.now.to_f
|
235
|
+
end
|
236
|
+
|
237
|
+
def format(measurement)
|
238
|
+
if measurement < 1
|
239
|
+
'%d ms' % (measurement * 1000)
|
240
|
+
else
|
241
|
+
'%.2f sec' % measurement
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class Amount < Base
|
247
|
+
def format(measurement)
|
248
|
+
number_to_delimited(measurement.floor)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
class DigitalInformationUnit < Base
|
253
|
+
def format(measurement)
|
254
|
+
number_to_human_size(measurement, :precision => 2)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# each implementation provides its own metrics like ProcessTime, Memory or GcRuns
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
case RUBY_ENGINE
|
265
|
+
when 'ruby' then require 'rails/perftest/active_support/testing/performance/ruby'
|
266
|
+
when 'rbx' then require 'rails/perftest/active_support/testing/performance/rubinius'
|
267
|
+
when 'jruby' then require 'rails/perftest/active_support/testing/performance/jruby'
|
268
|
+
else
|
269
|
+
$stderr.puts 'Your ruby interpreter is not supported for benchmarking.'
|
270
|
+
exit
|
271
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'jruby/profiler'
|
2
|
+
require 'java'
|
3
|
+
java_import java.lang.management.ManagementFactory
|
4
|
+
|
5
|
+
module ActiveSupport
|
6
|
+
module Testing
|
7
|
+
module Performance
|
8
|
+
DEFAULTS.merge!(
|
9
|
+
if ARGV.include?('--benchmark')
|
10
|
+
{:metrics => [:wall_time, :user_time, :memory, :gc_runs, :gc_time]}
|
11
|
+
else
|
12
|
+
{ :metrics => [:wall_time],
|
13
|
+
:formats => [:flat, :graph] }
|
14
|
+
end).freeze
|
15
|
+
|
16
|
+
protected
|
17
|
+
def run_gc
|
18
|
+
ManagementFactory.memory_mx_bean.gc
|
19
|
+
end
|
20
|
+
|
21
|
+
class Profiler < Performer
|
22
|
+
def initialize(*args)
|
23
|
+
super
|
24
|
+
@supported = @metric.is_a?(Metrics::WallTime)
|
25
|
+
end
|
26
|
+
|
27
|
+
def run
|
28
|
+
return unless @supported
|
29
|
+
|
30
|
+
@total = time_with_block do
|
31
|
+
@data = JRuby::Profiler.profile do
|
32
|
+
full_profile_options[:runs].to_i.times { run_test(@metric, :profile) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def record
|
38
|
+
return unless @supported
|
39
|
+
|
40
|
+
klasses = full_profile_options[:formats].map { |f| JRuby::Profiler.const_get("#{f.to_s.camelize}ProfilePrinter") }.compact
|
41
|
+
|
42
|
+
klasses.each do |klass|
|
43
|
+
fname = output_filename(klass)
|
44
|
+
FileUtils.mkdir_p(File.dirname(fname))
|
45
|
+
File.open(fname, 'wb') do |file|
|
46
|
+
klass.new(@data).printProfile(file)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
def output_filename(printer_class)
|
53
|
+
suffix =
|
54
|
+
case printer_class.name.demodulize
|
55
|
+
when 'FlatProfilePrinter'; 'flat.txt'
|
56
|
+
when 'GraphProfilePrinter'; 'graph.txt'
|
57
|
+
else printer_class.name.sub(/ProfilePrinter$/, '').underscore
|
58
|
+
end
|
59
|
+
|
60
|
+
"#{super()}_#{suffix}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Metrics
|
65
|
+
class Base
|
66
|
+
def profile
|
67
|
+
yield
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
def with_gc_stats
|
72
|
+
ManagementFactory.memory_mx_bean.gc
|
73
|
+
yield
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class WallTime < Time
|
78
|
+
def measure
|
79
|
+
super
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class CpuTime < Time
|
84
|
+
def measure
|
85
|
+
ManagementFactory.thread_mx_bean.get_current_thread_cpu_time / 1000 / 1000 / 1000.0 # seconds
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class UserTime < Time
|
90
|
+
def measure
|
91
|
+
ManagementFactory.thread_mx_bean.get_current_thread_user_time / 1000 / 1000 / 1000.0 # seconds
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class Memory < DigitalInformationUnit
|
96
|
+
def measure
|
97
|
+
ManagementFactory.memory_mx_bean.non_heap_memory_usage.used + ManagementFactory.memory_mx_bean.heap_memory_usage.used
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class GcRuns < Amount
|
102
|
+
def measure
|
103
|
+
ManagementFactory.garbage_collector_mx_beans.inject(0) { |total_runs, current_gc| total_runs += current_gc.collection_count }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class GcTime < Time
|
108
|
+
def measure
|
109
|
+
ManagementFactory.garbage_collector_mx_beans.inject(0) { |total_time, current_gc| total_time += current_gc.collection_time } / 1000.0 # seconds
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|