gitlab-derailed_benchmarks 1.6.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|