gitlab-derailed_benchmarks 1.6.1
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.
- checksums.yaml +7 -0
- data/.github/workflows/check_changelog.yml +10 -0
- data/.gitignore +8 -0
- data/.gitlab-ci.yml +56 -0
- data/.travis.yml +18 -0
- data/Appraisals +26 -0
- data/CHANGELOG.md +105 -0
- data/Gemfile +9 -0
- data/README.md +692 -0
- data/Rakefile +29 -0
- data/bin/derailed +93 -0
- data/derailed_benchmarks.gemspec +39 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_5_1.gemfile +15 -0
- data/gemfiles/rails_5_2.gemfile +15 -0
- data/gemfiles/rails_6_0.gemfile +15 -0
- data/gemfiles/rails_git.gemfile +19 -0
- data/lib/derailed_benchmarks.rb +51 -0
- data/lib/derailed_benchmarks/auth_helper.rb +34 -0
- data/lib/derailed_benchmarks/auth_helpers/devise.rb +41 -0
- data/lib/derailed_benchmarks/core_ext/kernel_require.rb +88 -0
- data/lib/derailed_benchmarks/load_tasks.rb +145 -0
- data/lib/derailed_benchmarks/require_tree.rb +65 -0
- data/lib/derailed_benchmarks/stats_from_dir.rb +128 -0
- data/lib/derailed_benchmarks/stats_in_file.rb +60 -0
- data/lib/derailed_benchmarks/tasks.rb +292 -0
- data/lib/derailed_benchmarks/version.rb +5 -0
- data/test/derailed_benchmarks/core_ext/kernel_require_test.rb +33 -0
- data/test/derailed_benchmarks/require_tree_test.rb +95 -0
- data/test/derailed_benchmarks/stats_from_dir_test.rb +125 -0
- data/test/derailed_test.rb +14 -0
- data/test/fixtures/require/child_one.rb +4 -0
- data/test/fixtures/require/child_two.rb +9 -0
- data/test/fixtures/require/parent_one.rb +8 -0
- data/test/fixtures/require/raise_child.rb +6 -0
- data/test/fixtures/require/relative_child.rb +4 -0
- data/test/fixtures/require/relative_child_two.rb +4 -0
- data/test/fixtures/stats/significant/loser.bench.txt +100 -0
- data/test/fixtures/stats/significant/winner.bench.txt +100 -0
- data/test/integration/tasks_test.rb +132 -0
- data/test/rails_app/Rakefile +9 -0
- data/test/rails_app/app/assets/config/manifest.js +0 -0
- data/test/rails_app/app/assets/javascripts/authenticated.js +2 -0
- data/test/rails_app/app/assets/stylesheets/authenticated.css +4 -0
- data/test/rails_app/app/controllers/application_controller.rb +17 -0
- data/test/rails_app/app/controllers/authenticated_controller.rb +8 -0
- data/test/rails_app/app/controllers/pages_controller.rb +14 -0
- data/test/rails_app/app/helpers/application_helper.rb +4 -0
- data/test/rails_app/app/helpers/authenticated_helper.rb +4 -0
- data/test/rails_app/app/models/user.rb +13 -0
- data/test/rails_app/app/views/authenticated/index.html.erb +1 -0
- data/test/rails_app/app/views/layouts/application.html.erb +14 -0
- data/test/rails_app/app/views/pages/index.html.erb +1 -0
- data/test/rails_app/config.ru +6 -0
- data/test/rails_app/config/application.rb +52 -0
- data/test/rails_app/config/boot.rb +12 -0
- data/test/rails_app/config/database.yml +22 -0
- data/test/rails_app/config/environment.rb +11 -0
- data/test/rails_app/config/environments/development.rb +27 -0
- data/test/rails_app/config/environments/production.rb +51 -0
- data/test/rails_app/config/environments/test.rb +37 -0
- data/test/rails_app/config/initializers/backtrace_silencers.rb +9 -0
- data/test/rails_app/config/initializers/devise.rb +258 -0
- data/test/rails_app/config/initializers/inflections.rb +12 -0
- data/test/rails_app/config/initializers/mime_types.rb +7 -0
- data/test/rails_app/config/initializers/secret_token.rb +13 -0
- data/test/rails_app/config/initializers/session_store.rb +10 -0
- data/test/rails_app/config/locales/devise.en.yml +59 -0
- data/test/rails_app/config/locales/en.yml +9 -0
- data/test/rails_app/config/locales/es.yml +10 -0
- data/test/rails_app/config/routes.rb +67 -0
- data/test/rails_app/db/migrate/20141210070547_devise_create_users.rb +45 -0
- data/test/rails_app/db/schema.rb +35 -0
- data/test/rails_app/perf.rake +10 -0
- data/test/rails_app/public/404.html +26 -0
- data/test/rails_app/public/422.html +26 -0
- data/test/rails_app/public/500.html +26 -0
- data/test/rails_app/public/favicon.ico +0 -0
- data/test/rails_app/public/javascripts/application.js +2 -0
- data/test/rails_app/public/javascripts/controls.js +965 -0
- data/test/rails_app/public/javascripts/dragdrop.js +974 -0
- data/test/rails_app/public/javascripts/effects.js +1123 -0
- data/test/rails_app/public/javascripts/prototype.js +6001 -0
- data/test/rails_app/public/javascripts/rails.js +202 -0
- data/test/rails_app/public/stylesheets/.gitkeep +0 -0
- data/test/rails_app/script/rails +8 -0
- data/test/support/integration_case.rb +7 -0
- data/test/test_helper.rb +65 -0
- metadata +398 -0
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :perf do
|
4
|
+
task :rails_load do
|
5
|
+
ENV["RAILS_ENV"] ||= "production"
|
6
|
+
ENV['RACK_ENV'] = ENV["RAILS_ENV"]
|
7
|
+
ENV["DISABLE_SPRING"] = "true"
|
8
|
+
|
9
|
+
ENV["SECRET_KEY_BASE"] ||= "foofoofoo"
|
10
|
+
|
11
|
+
ENV['LOG_LEVEL'] ||= "FATAL"
|
12
|
+
|
13
|
+
require 'rails'
|
14
|
+
|
15
|
+
puts "Booting: #{Rails.env}"
|
16
|
+
|
17
|
+
%W{ . lib test config }.each do |file|
|
18
|
+
$LOAD_PATH << File.expand_path(file)
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'application'
|
22
|
+
|
23
|
+
Rails.env = ENV["RAILS_ENV"]
|
24
|
+
|
25
|
+
DERAILED_APP = Rails.application
|
26
|
+
|
27
|
+
if DERAILED_APP.respond_to?(:initialized?)
|
28
|
+
DERAILED_APP.initialize! unless DERAILED_APP.initialized?
|
29
|
+
else
|
30
|
+
DERAILED_APP.initialize! unless DERAILED_APP.instance_variable_get(:@initialized)
|
31
|
+
end
|
32
|
+
|
33
|
+
if !ENV["DERAILED_SKIP_ACTIVE_RECORD"] && defined? ActiveRecord
|
34
|
+
if defined? ActiveRecord::Tasks::DatabaseTasks
|
35
|
+
ActiveRecord::Tasks::DatabaseTasks.create_current
|
36
|
+
else # Rails 3.2
|
37
|
+
raise "No valid database for #{ENV['RAILS_ENV']}, please create one" unless ActiveRecord::Base.connection.active?.inspect
|
38
|
+
end
|
39
|
+
|
40
|
+
ActiveRecord::Migrator.migrations_paths = DERAILED_APP.paths['db/migrate'].to_a
|
41
|
+
ActiveRecord::Migration.verbose = true
|
42
|
+
|
43
|
+
if Rails.version.start_with? '6'
|
44
|
+
ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths, ActiveRecord::SchemaMigration).migrate
|
45
|
+
elsif Rails.version.start_with? '5.2'
|
46
|
+
ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths).migrate
|
47
|
+
else
|
48
|
+
ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, nil)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
DERAILED_APP.config.consider_all_requests_local = true
|
53
|
+
end
|
54
|
+
|
55
|
+
task :rack_load do
|
56
|
+
puts "You're not using Rails"
|
57
|
+
puts "You need to tell derailed how to boot your app"
|
58
|
+
puts "In your perf.rake add:"
|
59
|
+
puts
|
60
|
+
puts "namespace :perf do"
|
61
|
+
puts " task :rack_load do"
|
62
|
+
puts " # DERAILED_APP = your code here"
|
63
|
+
puts " end"
|
64
|
+
puts "end"
|
65
|
+
end
|
66
|
+
|
67
|
+
task :setup do
|
68
|
+
if DerailedBenchmarks.gem_is_bundled?("railties")
|
69
|
+
Rake::Task["perf:rails_load"].invoke
|
70
|
+
else
|
71
|
+
Rake::Task["perf:rack_load"].invoke
|
72
|
+
end
|
73
|
+
|
74
|
+
WARM_COUNT = (ENV['WARM_COUNT'] || 0).to_i
|
75
|
+
TEST_COUNT = (ENV['TEST_COUNT'] || ENV['CNT'] || 1_000).to_i
|
76
|
+
PATH_TO_HIT = ENV["PATH_TO_HIT"] || ENV['ENDPOINT'] || "/"
|
77
|
+
puts "Endpoint: #{ PATH_TO_HIT.inspect }"
|
78
|
+
|
79
|
+
HTTP_HEADER_PREFIX = "HTTP_".freeze
|
80
|
+
RACK_HTTP_HEADERS = ENV.select { |key| key.start_with?(HTTP_HEADER_PREFIX) }
|
81
|
+
|
82
|
+
HTTP_HEADERS = RACK_HTTP_HEADERS.keys.inject({}) do |hash, rack_header_name|
|
83
|
+
# e.g. "HTTP_ACCEPT_CHARSET" -> "Accept-Charset"
|
84
|
+
header_name = rack_header_name[HTTP_HEADER_PREFIX.size..-1].split("_").map(&:downcase).map(&:capitalize).join("-")
|
85
|
+
hash[header_name] = RACK_HTTP_HEADERS[rack_header_name]
|
86
|
+
hash
|
87
|
+
end
|
88
|
+
puts "HTTP headers: #{HTTP_HEADERS}" unless HTTP_HEADERS.empty?
|
89
|
+
|
90
|
+
CURL_HTTP_HEADER_ARGS = HTTP_HEADERS.map { |http_header_name, value| "-H \"#{http_header_name}: #{value}\"" }.join(" ")
|
91
|
+
|
92
|
+
require 'rack/test'
|
93
|
+
require 'rack/file'
|
94
|
+
|
95
|
+
DERAILED_APP = DerailedBenchmarks.add_auth(Object.class_eval { remove_const(:DERAILED_APP) })
|
96
|
+
if server = ENV["USE_SERVER"]
|
97
|
+
@port = (3000..3900).to_a.sample
|
98
|
+
puts "Port: #{ @port.inspect }"
|
99
|
+
puts "Server: #{ server.inspect }"
|
100
|
+
thread = Thread.new do
|
101
|
+
Rack::Server.start(app: DERAILED_APP, :Port => @port, environment: "none", server: server)
|
102
|
+
end
|
103
|
+
sleep 1
|
104
|
+
|
105
|
+
def call_app(path = File.join("/", PATH_TO_HIT))
|
106
|
+
cmd = "curl #{CURL_HTTP_HEADER_ARGS} 'http://localhost:#{@port}#{path}' -s --fail 2>&1"
|
107
|
+
response = `#{cmd}`
|
108
|
+
unless $?.success?
|
109
|
+
STDERR.puts "Couldn't call app."
|
110
|
+
STDERR.puts "Bad request to #{cmd.inspect} \n\n***RESPONSE***:\n\n#{ response.inspect }"
|
111
|
+
|
112
|
+
FileUtils.mkdir_p("tmp")
|
113
|
+
File.open("tmp/fail.html", "w+") {|f| f.write response.body }
|
114
|
+
|
115
|
+
`open #{File.expand_path("tmp/fail.html")}` if ENV["DERAILED_DEBUG"]
|
116
|
+
|
117
|
+
exit(1)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
else
|
121
|
+
@app = Rack::MockRequest.new(DERAILED_APP)
|
122
|
+
|
123
|
+
def call_app
|
124
|
+
response = @app.get(PATH_TO_HIT, RACK_HTTP_HEADERS)
|
125
|
+
if response.status != 200
|
126
|
+
STDERR.puts "Couldn't call app. Bad request to #{PATH_TO_HIT}! Resulted in #{response.status} status."
|
127
|
+
STDERR.puts "\n\n***RESPONSE BODY***\n\n"
|
128
|
+
STDERR.puts response.body
|
129
|
+
|
130
|
+
FileUtils.mkdir_p("tmp")
|
131
|
+
File.open("tmp/fail.html", "w+") {|f| f.write response.body }
|
132
|
+
|
133
|
+
`open #{File.expand_path("tmp/fail.html")}` if ENV["DERAILED_DEBUG"]
|
134
|
+
|
135
|
+
exit(1)
|
136
|
+
end
|
137
|
+
response
|
138
|
+
end
|
139
|
+
end
|
140
|
+
if WARM_COUNT > 0
|
141
|
+
puts "Warming up app: #{WARM_COUNT} times"
|
142
|
+
WARM_COUNT.times { call_app }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Tree structure used to store and sort require memory costs
|
4
|
+
# RequireTree.new('get_process_mem')
|
5
|
+
module DerailedBenchmarks
|
6
|
+
class RequireTree
|
7
|
+
REQUIRED_BY = {}
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
attr_writer :cost
|
11
|
+
attr_accessor :parent
|
12
|
+
|
13
|
+
def initialize(name)
|
14
|
+
@name = name
|
15
|
+
@children = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def <<(tree)
|
19
|
+
@children[tree.name.to_s] = tree
|
20
|
+
tree.parent = self
|
21
|
+
(REQUIRED_BY[tree.name.to_s] ||= []) << self.name
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](name)
|
25
|
+
@children[name.to_s]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns array of child nodes
|
29
|
+
def children
|
30
|
+
@children.values
|
31
|
+
end
|
32
|
+
|
33
|
+
def cost
|
34
|
+
@cost || 0
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns sorted array of child nodes from Largest to Smallest
|
38
|
+
def sorted_children
|
39
|
+
children.sort { |c1, c2| c2.cost <=> c1.cost }
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_string
|
43
|
+
str = +"#{name}: #{cost.round(4)} MiB"
|
44
|
+
if parent && REQUIRED_BY[self.name.to_s]
|
45
|
+
names = REQUIRED_BY[self.name.to_s].uniq - [parent.name.to_s]
|
46
|
+
if names.any?
|
47
|
+
str << " (Also required by: #{ names.first(2).join(", ") }"
|
48
|
+
str << ", and #{names.count - 2} others" if names.count > 3
|
49
|
+
str << ")"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
str
|
53
|
+
end
|
54
|
+
|
55
|
+
# Recursively prints all child nodes
|
56
|
+
def print_sorted_children(level = 0, out = STDOUT)
|
57
|
+
return if cost < ENV['CUT_OFF'].to_f
|
58
|
+
out.puts " " * level + self.to_string
|
59
|
+
level += 1
|
60
|
+
sorted_children.each do |child|
|
61
|
+
child.print_sorted_children(level, out)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'statistics'
|
5
|
+
|
6
|
+
module DerailedBenchmarks
|
7
|
+
# A class used to read several benchmark files
|
8
|
+
# it will parse each file, then sort by average
|
9
|
+
# time of benchmarks. It can be used to find
|
10
|
+
# the fastest and slowest examples and give information
|
11
|
+
# about them such as what the percent difference is
|
12
|
+
# and if the results are statistically significant
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
#
|
16
|
+
# branch_info = {}
|
17
|
+
# branch_info["loser"] = { desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), name: "loser" }
|
18
|
+
# branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), name: "winner" }
|
19
|
+
# stats = DerailedBenchmarks::StatsFromDir.new(branch_info)
|
20
|
+
#
|
21
|
+
# stats.newest.average # => 10.5
|
22
|
+
# stats.oldest.average # => 11.0
|
23
|
+
# stats.significant? # => true
|
24
|
+
# stats.x_faster # => "1.0476"
|
25
|
+
class StatsFromDir
|
26
|
+
FORMAT = "%0.4f"
|
27
|
+
attr_reader :stats, :oldest, :newest
|
28
|
+
|
29
|
+
def initialize(hash)
|
30
|
+
@files = []
|
31
|
+
|
32
|
+
hash.each do |branch, info_hash|
|
33
|
+
file = info_hash.fetch(:file)
|
34
|
+
desc = info_hash.fetch(:desc)
|
35
|
+
time = info_hash.fetch(:time)
|
36
|
+
@files << StatsForFile.new(file: file, desc: desc, time: time, name: branch)
|
37
|
+
end
|
38
|
+
@files.sort_by! { |f| f.time }
|
39
|
+
@oldest = @files.first
|
40
|
+
@newest = @files.last
|
41
|
+
end
|
42
|
+
|
43
|
+
def call
|
44
|
+
@files.each(&:call)
|
45
|
+
|
46
|
+
stats_95 = statistical_test(confidence: 95)
|
47
|
+
|
48
|
+
# If default check is good, see if we also pass a more rigorous test
|
49
|
+
# if so, then use the more rigourous test
|
50
|
+
if stats_95[:alternative]
|
51
|
+
stats_99 = statistical_test(confidence: 99)
|
52
|
+
@stats = stats_99 if stats_99[:alternative]
|
53
|
+
end
|
54
|
+
@stats ||= stats_95
|
55
|
+
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
def statistical_test(series_1=oldest.values, series_2=newest.values, confidence: 95)
|
60
|
+
StatisticalTest::KSTest.two_samples(
|
61
|
+
group_one: series_1,
|
62
|
+
group_two: series_2,
|
63
|
+
alpha: (100 - confidence) / 100.0
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def significant?
|
68
|
+
@stats[:alternative]
|
69
|
+
end
|
70
|
+
|
71
|
+
def d_max
|
72
|
+
@stats[:d_max].to_f
|
73
|
+
end
|
74
|
+
|
75
|
+
def d_critical
|
76
|
+
@stats[:d_critical].to_f
|
77
|
+
end
|
78
|
+
|
79
|
+
def x_faster
|
80
|
+
(oldest.median/newest.median).to_f
|
81
|
+
end
|
82
|
+
|
83
|
+
def faster?
|
84
|
+
newest.median < oldest.median
|
85
|
+
end
|
86
|
+
|
87
|
+
def percent_faster
|
88
|
+
(((oldest.median - newest.median) / oldest.median).to_f * 100)
|
89
|
+
end
|
90
|
+
|
91
|
+
def change_direction
|
92
|
+
if faster?
|
93
|
+
"FASTER 🚀🚀🚀"
|
94
|
+
else
|
95
|
+
"SLOWER 🐢🐢🐢"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def align
|
100
|
+
" " * (percent_faster.to_s.index(".") - x_faster.to_s.index("."))
|
101
|
+
end
|
102
|
+
|
103
|
+
def banner(io = Kernel)
|
104
|
+
io.puts
|
105
|
+
if significant?
|
106
|
+
io.puts "❤️ ❤️ ❤️ (Statistically Significant) ❤️ ❤️ ❤️"
|
107
|
+
else
|
108
|
+
io.puts "👎👎👎(NOT Statistically Significant) 👎👎👎"
|
109
|
+
end
|
110
|
+
io.puts
|
111
|
+
io.puts "[#{newest.name}] #{newest.desc.inspect} - (#{newest.median} seconds)"
|
112
|
+
io.puts " #{change_direction} by:"
|
113
|
+
io.puts " #{align}#{FORMAT % x_faster}x [older/newer]"
|
114
|
+
io.puts " #{FORMAT % percent_faster}\% [(older - newer) / older * 100]"
|
115
|
+
io.puts "[#{oldest.name}] #{oldest.desc.inspect} - (#{oldest.median} seconds)"
|
116
|
+
io.puts
|
117
|
+
io.puts "Iterations per sample: #{ENV["TEST_COUNT"]}"
|
118
|
+
io.puts "Samples: #{newest.values.length}"
|
119
|
+
io.puts
|
120
|
+
io.puts "Test type: Kolmogorov Smirnov"
|
121
|
+
io.puts "Confidence level: #{@stats[:confidence_level] * 100} %"
|
122
|
+
io.puts "Is significant? (max > critical): #{significant?}"
|
123
|
+
io.puts "D critical: #{d_critical}"
|
124
|
+
io.puts "D max: #{d_max}"
|
125
|
+
io.puts
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DerailedBenchmarks
|
4
|
+
# A class for reading in benchmark results
|
5
|
+
# and converting them to numbers for comparison
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# puts `cat muhfile.bench.txt`
|
10
|
+
#
|
11
|
+
# 9.590142 0.831269 10.457801 ( 10.0)
|
12
|
+
# 9.836019 0.837319 10.728024 ( 11.0)
|
13
|
+
#
|
14
|
+
# x = StatsForFile.new(name: "muhcommit", file: "muhfile.bench.txt", desc: "I made it faster", time: Time.now)
|
15
|
+
# x.values #=> [11.437769, 11.792425]
|
16
|
+
# x.average # => 10.5
|
17
|
+
# x.name # => "muhfile"
|
18
|
+
class StatsForFile
|
19
|
+
attr_reader :name, :values, :desc, :time
|
20
|
+
|
21
|
+
def initialize(file:, name:, desc: "", time: )
|
22
|
+
@file = Pathname.new(file)
|
23
|
+
FileUtils.touch(@file)
|
24
|
+
|
25
|
+
@name = name
|
26
|
+
@desc = desc
|
27
|
+
@time = time
|
28
|
+
end
|
29
|
+
|
30
|
+
def call
|
31
|
+
load_file!
|
32
|
+
|
33
|
+
@median = (values[(values.length - 1) / 2] + values[values.length/ 2]) / 2.0
|
34
|
+
@average = values.inject(:+) / values.length
|
35
|
+
end
|
36
|
+
|
37
|
+
def median
|
38
|
+
@median.to_f
|
39
|
+
end
|
40
|
+
|
41
|
+
def average
|
42
|
+
@average.to_f
|
43
|
+
end
|
44
|
+
|
45
|
+
private def load_file!
|
46
|
+
@values = []
|
47
|
+
@file.each_line do |line|
|
48
|
+
line.match(/\( +(\d+\.\d+)\)/)
|
49
|
+
begin
|
50
|
+
values << BigDecimal($1)
|
51
|
+
rescue => e
|
52
|
+
raise e, "Problem with file #{@file.inspect}:\n#{@file.read}\n#{e.message}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
values.sort!
|
57
|
+
values.freeze
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,292 @@
|
|
1
|
+
require_relative 'load_tasks'
|
2
|
+
|
3
|
+
namespace :perf do
|
4
|
+
desc "runs the performance test against two most recent commits of the current app"
|
5
|
+
task :app do
|
6
|
+
ENV["DERAILED_PATH_TO_LIBRARY"] = '.'
|
7
|
+
Rake::Task["perf:library"].invoke
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "runs the same test against two different branches for statistical comparison"
|
11
|
+
task :library do
|
12
|
+
begin
|
13
|
+
DERAILED_SCRIPT_COUNT = (ENV["DERAILED_SCRIPT_COUNT"] ||= "200").to_i
|
14
|
+
ENV["TEST_COUNT"] ||= "200"
|
15
|
+
|
16
|
+
raise "test count must be at least 2, is set to #{DERAILED_SCRIPT_COUNT}" if DERAILED_SCRIPT_COUNT < 2
|
17
|
+
script = ENV["DERAILED_SCRIPT"] || "bundle exec derailed exec perf:test"
|
18
|
+
|
19
|
+
if ENV["DERAILED_PATH_TO_LIBRARY"]
|
20
|
+
library_dir = ENV["DERAILED_PATH_TO_LIBRARY"]
|
21
|
+
else
|
22
|
+
library_dir = DerailedBenchmarks.rails_path_on_disk
|
23
|
+
end
|
24
|
+
|
25
|
+
raise "Must be a path with a .git directory '#{library_dir}'" unless File.exist?(File.join(library_dir, ".git"))
|
26
|
+
|
27
|
+
# Use either the explicit SHAs when present or grab last two SHAs from commit history
|
28
|
+
# if only one SHA is given, then use it and the last SHA from commit history
|
29
|
+
branch_names = []
|
30
|
+
branch_names = ENV.fetch("SHAS_TO_TEST").split(",") if ENV["SHAS_TO_TEST"]
|
31
|
+
if branch_names.length < 2
|
32
|
+
Dir.chdir(library_dir) do
|
33
|
+
run!("git checkout '#{branch_names.first}'") unless branch_names.empty?
|
34
|
+
|
35
|
+
branches = run!('git log --format="%H" -n 2').chomp.split($/)
|
36
|
+
if branch_names.empty?
|
37
|
+
branch_names = branches
|
38
|
+
else
|
39
|
+
branches.shift
|
40
|
+
branch_names << branches.shift
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
current_library_branch = ""
|
46
|
+
Dir.chdir(library_dir) { current_library_branch = run!('git describe --contains --all HEAD').chomp }
|
47
|
+
|
48
|
+
out_dir = Pathname.new("tmp/compare_branches/#{Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')}")
|
49
|
+
out_dir.mkpath
|
50
|
+
|
51
|
+
branches_to_test = branch_names.each_with_object({}) {|elem, hash| hash[elem] = out_dir + "#{elem.gsub('/', ':')}.bench.txt" }
|
52
|
+
branch_info = {}
|
53
|
+
branch_to_sha = {}
|
54
|
+
|
55
|
+
branches_to_test.each do |branch, file|
|
56
|
+
Dir.chdir(library_dir) do
|
57
|
+
run!("git checkout '#{branch}'")
|
58
|
+
description = run!("git log --oneline --format=%B -n 1 HEAD | head -n 1").strip
|
59
|
+
time_stamp = run!("git log -n 1 --pretty=format:%ci").strip # https://stackoverflow.com/a/25921837/147390
|
60
|
+
short_sha = run!("git rev-parse --short HEAD").strip
|
61
|
+
branch_to_sha[branch] = short_sha
|
62
|
+
|
63
|
+
branch_info[short_sha] = { desc: description, time: DateTime.parse(time_stamp), file: file }
|
64
|
+
end
|
65
|
+
run!("#{script}")
|
66
|
+
end
|
67
|
+
|
68
|
+
puts
|
69
|
+
puts
|
70
|
+
branches_to_test.each.with_index do |(branch, _), i|
|
71
|
+
short_sha = branch_to_sha[branch]
|
72
|
+
desc = branch_info[short_sha][:desc]
|
73
|
+
puts "Testing #{i + 1}: #{short_sha}: #{desc}"
|
74
|
+
end
|
75
|
+
puts
|
76
|
+
puts
|
77
|
+
|
78
|
+
raise "SHAs to test must be different" if branch_info.length == 1
|
79
|
+
stats = DerailedBenchmarks::StatsFromDir.new(branch_info)
|
80
|
+
puts "Env var no longer has any affect DERAILED_STOP_VALID_COUNT" if ENV["DERAILED_STOP_VALID_COUNT"]
|
81
|
+
|
82
|
+
DERAILED_SCRIPT_COUNT.times do |i|
|
83
|
+
puts "Sample: #{i.next}/#{DERAILED_SCRIPT_COUNT} iterations per sample: #{ENV['TEST_COUNT']}"
|
84
|
+
branches_to_test.each do |branch, file|
|
85
|
+
Dir.chdir(library_dir) { run!("git checkout '#{branch}'") }
|
86
|
+
run!(" #{script} 2>&1 | tail -n 1 >> '#{file}'")
|
87
|
+
end
|
88
|
+
|
89
|
+
if (i % 50).zero?
|
90
|
+
puts "Intermediate result"
|
91
|
+
stats.call.banner
|
92
|
+
puts "Continuing execution"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
ensure
|
97
|
+
if library_dir && current_library_branch
|
98
|
+
puts "Resetting git dir of '#{library_dir.to_s}' to #{current_library_branch.inspect}"
|
99
|
+
Dir.chdir(library_dir) do
|
100
|
+
run!("git checkout '#{current_library_branch}'")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
if stats
|
105
|
+
stats.call.banner
|
106
|
+
|
107
|
+
result_file = out_dir + "results.txt"
|
108
|
+
File.open(result_file, "w") do |f|
|
109
|
+
stats.banner(f)
|
110
|
+
end
|
111
|
+
|
112
|
+
puts "Output: #{result_file.to_s}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
desc "hits the url TEST_COUNT times"
|
118
|
+
task :test => [:setup] do
|
119
|
+
require 'benchmark'
|
120
|
+
|
121
|
+
Benchmark.bm { |x|
|
122
|
+
x.report("#{TEST_COUNT} derailed requests") {
|
123
|
+
TEST_COUNT.times {
|
124
|
+
call_app
|
125
|
+
}
|
126
|
+
}
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
desc "stackprof"
|
131
|
+
task :stackprof => [:setup] do
|
132
|
+
# [:wall, :cpu, :object]
|
133
|
+
begin
|
134
|
+
require 'stackprof'
|
135
|
+
rescue LoadError
|
136
|
+
raise "Add stackprof to your gemfile to continue `gem 'stackprof', group: :development`"
|
137
|
+
end
|
138
|
+
TEST_COUNT = (ENV["TEST_COUNT"] ||= "100").to_i
|
139
|
+
file = "tmp/#{Time.now.iso8601}-stackprof-cpu-myapp.dump"
|
140
|
+
StackProf.run(mode: :cpu, out: file) do
|
141
|
+
Rake::Task["perf:test"].invoke
|
142
|
+
end
|
143
|
+
cmd = "stackprof #{file}"
|
144
|
+
puts "Running `#{cmd}`. Execute `stackprof --help` for more info"
|
145
|
+
puts `#{cmd}`
|
146
|
+
end
|
147
|
+
|
148
|
+
task :kernel_require_patch do
|
149
|
+
require 'derailed_benchmarks/core_ext/kernel_require.rb'
|
150
|
+
end
|
151
|
+
|
152
|
+
desc "show memory usage caused by invoking require per gem"
|
153
|
+
task :mem => [:kernel_require_patch, :setup] do
|
154
|
+
puts "## Impact of `require <file>` on RAM"
|
155
|
+
puts
|
156
|
+
puts "Showing all `require <file>` calls that consume #{ENV['CUT_OFF']} MiB or more of RSS"
|
157
|
+
puts "Configure with `CUT_OFF=0` for all entries or `CUT_OFF=5` for few entries"
|
158
|
+
|
159
|
+
puts "Note: Files only count against RAM on their first load."
|
160
|
+
puts " If multiple libraries require the same file, then"
|
161
|
+
puts " the 'cost' only shows up under the first library"
|
162
|
+
puts
|
163
|
+
|
164
|
+
call_app
|
165
|
+
|
166
|
+
TOP_REQUIRE.print_sorted_children
|
167
|
+
end
|
168
|
+
|
169
|
+
desc "outputs memory usage over time"
|
170
|
+
task :mem_over_time => [:setup] do
|
171
|
+
require 'get_process_mem'
|
172
|
+
puts "PID: #{Process.pid}"
|
173
|
+
ram = GetProcessMem.new
|
174
|
+
@keep_going = true
|
175
|
+
begin
|
176
|
+
unless ENV["SKIP_FILE_WRITE"]
|
177
|
+
ruby = `ruby -v`.chomp
|
178
|
+
FileUtils.mkdir_p("tmp")
|
179
|
+
file = File.open("tmp/#{Time.now.iso8601}-#{ruby}-memory-#{TEST_COUNT}-times.txt", 'w')
|
180
|
+
file.sync = true
|
181
|
+
end
|
182
|
+
|
183
|
+
ram_thread = Thread.new do
|
184
|
+
while @keep_going
|
185
|
+
mb = ram.mb
|
186
|
+
STDOUT.puts mb
|
187
|
+
file.puts mb unless ENV["SKIP_FILE_WRITE"]
|
188
|
+
sleep 5
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
TEST_COUNT.times {
|
193
|
+
call_app
|
194
|
+
}
|
195
|
+
ensure
|
196
|
+
@keep_going = false
|
197
|
+
ram_thread.join
|
198
|
+
file.close unless ENV["SKIP_FILE_WRITE"]
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
task :ram_over_time do
|
203
|
+
raise "Use mem_over_time"
|
204
|
+
end
|
205
|
+
|
206
|
+
desc "iterations per second"
|
207
|
+
task :ips => [:setup] do
|
208
|
+
require 'benchmark/ips'
|
209
|
+
|
210
|
+
Benchmark.ips do |x|
|
211
|
+
x.warmup = Float(ENV["IPS_WARMUP"] || 2)
|
212
|
+
x.time = Float(ENV["IPS_TIME"] || 5)
|
213
|
+
x.suite = ENV["IPS_SUITE"] if ENV["IPS_SUITE"]
|
214
|
+
x.iterations = Integer(ENV["IPS_ITERATIONS"] || 1)
|
215
|
+
|
216
|
+
x.report("ips") { call_app }
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
desc "outputs GC::Profiler.report data while app is called TEST_COUNT times"
|
221
|
+
task :gc => [:setup] do
|
222
|
+
GC::Profiler.enable
|
223
|
+
TEST_COUNT.times { call_app }
|
224
|
+
GC::Profiler.report
|
225
|
+
GC::Profiler.disable
|
226
|
+
end
|
227
|
+
|
228
|
+
desc "outputs allocated object diff after app is called TEST_COUNT times"
|
229
|
+
task :allocated_objects => [:setup] do
|
230
|
+
call_app
|
231
|
+
GC.start
|
232
|
+
GC.disable
|
233
|
+
start = ObjectSpace.count_objects
|
234
|
+
TEST_COUNT.times { call_app }
|
235
|
+
finish = ObjectSpace.count_objects
|
236
|
+
GC.enable
|
237
|
+
finish.each do |k,v|
|
238
|
+
puts k => (v - start[k]) / TEST_COUNT.to_f
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
desc "profiles ruby allocation"
|
244
|
+
task :objects => [:setup] do
|
245
|
+
require 'memory_profiler'
|
246
|
+
call_app
|
247
|
+
GC.start
|
248
|
+
|
249
|
+
num = Integer(ENV["TEST_COUNT"] || 1)
|
250
|
+
opts = {}
|
251
|
+
opts[:ignore_files] = /#{ENV['IGNORE_FILES_REGEXP']}/ if ENV['IGNORE_FILES_REGEXP']
|
252
|
+
opts[:allow_files] = "#{ENV['ALLOW_FILES']}" if ENV['ALLOW_FILES']
|
253
|
+
|
254
|
+
puts "Running #{num} times"
|
255
|
+
report = MemoryProfiler.report(opts) do
|
256
|
+
num.times { call_app }
|
257
|
+
end
|
258
|
+
report.pretty_print
|
259
|
+
end
|
260
|
+
|
261
|
+
desc "heap analyzer"
|
262
|
+
task :heap => [:setup] do
|
263
|
+
require 'objspace'
|
264
|
+
|
265
|
+
file_name = "tmp/#{Time.now.iso8601}-heap.dump"
|
266
|
+
FileUtils.mkdir_p("tmp")
|
267
|
+
ObjectSpace.trace_object_allocations_start
|
268
|
+
puts "Running #{ TEST_COUNT } times"
|
269
|
+
TEST_COUNT.times {
|
270
|
+
call_app
|
271
|
+
}
|
272
|
+
GC.start
|
273
|
+
|
274
|
+
puts "Heap file generated: #{ file_name.inspect }"
|
275
|
+
ObjectSpace.dump_all(output: File.open(file_name, 'w'))
|
276
|
+
|
277
|
+
require 'heapy'
|
278
|
+
|
279
|
+
Heapy::Analyzer.new(file_name).analyze
|
280
|
+
|
281
|
+
puts ""
|
282
|
+
puts "Run `$ heapy --help` for more options"
|
283
|
+
puts ""
|
284
|
+
puts "Also try uploading #{file_name.inspect} to http://tenderlove.github.io/heap-analyzer/"
|
285
|
+
end
|
286
|
+
|
287
|
+
def run!(cmd)
|
288
|
+
out = `#{cmd}`
|
289
|
+
raise "Error while running #{cmd.inspect}: #{out}" unless $?.success?
|
290
|
+
out
|
291
|
+
end
|
292
|
+
end
|