derailed_benchmarks 1.3.6 → 1.4.0

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/CHANGELOG.md +7 -1
  4. data/README.md +70 -2
  5. data/Rakefile +2 -0
  6. data/bin/derailed +1 -0
  7. data/derailed_benchmarks.gemspec +4 -1
  8. data/gemfiles/rails_5_1.gemfile +2 -0
  9. data/gemfiles/rails_5_2.gemfile +2 -0
  10. data/gemfiles/rails_6_0.gemfile +2 -0
  11. data/gemfiles/rails_git.gemfile +19 -0
  12. data/lib/derailed_benchmarks.rb +20 -0
  13. data/lib/derailed_benchmarks/auth_helper.rb +2 -0
  14. data/lib/derailed_benchmarks/auth_helpers/devise.rb +2 -0
  15. data/lib/derailed_benchmarks/core_ext/kernel_require.rb +7 -2
  16. data/lib/derailed_benchmarks/load_tasks.rb +138 -0
  17. data/lib/derailed_benchmarks/require_tree.rb +3 -1
  18. data/lib/derailed_benchmarks/stats_from_dir.rb +99 -0
  19. data/lib/derailed_benchmarks/stats_in_file.rb +53 -0
  20. data/lib/derailed_benchmarks/tasks.rb +70 -115
  21. data/lib/derailed_benchmarks/version.rb +3 -1
  22. data/test/derailed_benchmarks/core_ext/kernel_require_test.rb +3 -0
  23. data/test/derailed_benchmarks/require_tree_test.rb +2 -0
  24. data/test/derailed_benchmarks/stats_from_dir_test.rb +101 -0
  25. data/test/derailed_test.rb +2 -0
  26. data/test/fixtures/require/child_one.rb +1 -1
  27. data/test/fixtures/require/child_two.rb +1 -1
  28. data/test/fixtures/require/parent_one.rb +2 -1
  29. data/test/fixtures/require/raise_child.rb +2 -0
  30. data/test/fixtures/require/relative_child.rb +2 -0
  31. data/test/fixtures/require/relative_child_two.rb +4 -0
  32. data/test/fixtures/stats/significant/loser.bench.txt +100 -0
  33. data/test/fixtures/stats/significant/winner.bench.txt +100 -0
  34. data/test/integration/tasks_test.rb +9 -0
  35. data/test/rails_app/Rakefile +2 -0
  36. data/test/rails_app/app/controllers/application_controller.rb +2 -0
  37. data/test/rails_app/app/controllers/authenticated_controller.rb +2 -0
  38. data/test/rails_app/app/controllers/pages_controller.rb +2 -0
  39. data/test/rails_app/app/helpers/application_helper.rb +2 -0
  40. data/test/rails_app/app/helpers/authenticated_helper.rb +2 -0
  41. data/test/rails_app/app/models/user.rb +2 -0
  42. data/test/rails_app/config.ru +2 -0
  43. data/test/rails_app/config/application.rb +2 -0
  44. data/test/rails_app/config/boot.rb +3 -1
  45. data/test/rails_app/config/environment.rb +4 -0
  46. data/test/rails_app/config/environments/development.rb +2 -0
  47. data/test/rails_app/config/environments/production.rb +2 -0
  48. data/test/rails_app/config/environments/test.rb +2 -0
  49. data/test/rails_app/config/initializers/backtrace_silencers.rb +2 -0
  50. data/test/rails_app/config/initializers/devise.rb +2 -0
  51. data/test/rails_app/config/initializers/inflections.rb +2 -0
  52. data/test/rails_app/config/initializers/mime_types.rb +2 -0
  53. data/test/rails_app/config/initializers/secret_token.rb +2 -0
  54. data/test/rails_app/config/initializers/session_store.rb +2 -0
  55. data/test/rails_app/config/routes.rb +2 -0
  56. data/test/rails_app/db/migrate/20141210070547_devise_create_users.rb +2 -0
  57. data/test/rails_app/db/schema.rb +2 -0
  58. data/test/rails_app/perf.rake +2 -0
  59. data/test/rails_app/script/rails +2 -0
  60. data/test/support/integration_case.rb +2 -0
  61. data/test/test_helper.rb +6 -2
  62. metadata +30 -4
@@ -0,0 +1,53 @@
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
+ @average = values.inject(:+) / values.length
34
+ end
35
+
36
+ def average
37
+ @average.to_f
38
+ end
39
+
40
+ private def load_file!
41
+ @values = []
42
+ @file.each_line do |line|
43
+ line.match(/\( +(\d+\.\d+)\)/)
44
+ begin
45
+ values << BigDecimal($1)
46
+ rescue => e
47
+ raise e, "Problem with file #{@file.inspect}:\n#{@file.read}\n#{e.message}"
48
+ end
49
+ end
50
+ values.freeze
51
+ end
52
+ end
53
+ end
@@ -1,137 +1,81 @@
1
- namespace :perf do
2
- task :rails_load do
3
- ENV["RAILS_ENV"] ||= "production"
4
- ENV['RACK_ENV'] = ENV["RAILS_ENV"]
5
- ENV["DISABLE_SPRING"] = "true"
6
-
7
- ENV["SECRET_KEY_BASE"] ||= "foofoofoo"
8
-
9
- ENV['LOG_LEVEL'] ||= "FATAL"
10
-
11
- require 'rails'
12
-
13
- puts "Booting: #{Rails.env}"
1
+ require_relative 'load_tasks'
14
2
 
15
- %W{ . lib test config }.each do |file|
16
- $LOAD_PATH << File.expand_path(file)
17
- end
18
-
19
- require 'application'
20
-
21
- Rails.env = ENV["RAILS_ENV"]
3
+ namespace :perf do
4
+ desc "runs the same test against two different branches for statistical comparison"
5
+ task :library do
6
+ DERAILED_SCRIPT_COUNT = (ENV["DERAILED_SCRIPT_COUNT"] ||= "200").to_i
7
+ ENV["TEST_COUNT"] ||= "200"
22
8
 
23
- DERAILED_APP = Rails.application
9
+ raise "test count must be at least 2, is set to #{DERAILED_SCRIPT_COUNT}" if DERAILED_SCRIPT_COUNT < 2
10
+ script = ENV["DERAILED_SCRIPT"] || "bundle exec derailed exec perf:test"
24
11
 
25
- if DERAILED_APP.respond_to?(:initialized?)
26
- DERAILED_APP.initialize! unless DERAILED_APP.initialized?
12
+ if ENV["DERAILED_PATH_TO_LIBRARY"]
13
+ library_dir = ENV["DERAILED_PATH_TO_LIBRARY"]
27
14
  else
28
- DERAILED_APP.initialize! unless DERAILED_APP.instance_variable_get(:@initialized)
15
+ library_dir = DerailedBenchmarks.rails_path_on_disk
29
16
  end
30
17
 
31
- if ENV["DERAILED_SKIP_ACTIVE_RECORD"] && defined? ActiveRecord
32
- if defined? ActiveRecord::Tasks::DatabaseTasks
33
- ActiveRecord::Tasks::DatabaseTasks.create_current
34
- else # Rails 3.2
35
- raise "No valid database for #{ENV['RAILS_ENV']}, please create one" unless ActiveRecord::Base.connection.active?.inspect
18
+ raise "Must be a path with a .git directory '#{library_dir}'" unless File.exist?(File.join(library_dir, ".git"))
19
+
20
+ # Use either the explicit SHAs when present or grab last two SHAs from commit history
21
+ # if only one SHA is given, then use it and the last SHA from commit history
22
+ branch_names = []
23
+ branch_names = ENV.fetch("SHAS_TO_TEST").split(",") if ENV["SHAS_TO_TEST"]
24
+ if branch_names.length < 2
25
+ Dir.chdir(library_dir) do
26
+ branches = run!('git log --format="%H" -n 2').chomp.split($INPUT_RECORD_SEPARATOR)
27
+ if branch_names.empty?
28
+ branch_names = branches
29
+ else
30
+ branch_names << branches.shift
31
+ end
36
32
  end
37
-
38
- ActiveRecord::Migrator.migrations_paths = DERAILED_APP.paths['db/migrate'].to_a
39
- ActiveRecord::Migration.verbose = true
40
- ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, nil)
41
33
  end
42
34
 
43
- DERAILED_APP.config.consider_all_requests_local = true
44
- end
35
+ current_library_branch = ""
36
+ Dir.chdir(library_dir) { current_library_branch = run!('git describe --contains --all HEAD').chomp }
45
37
 
46
- task :rack_load do
47
- puts "You're not using Rails"
48
- puts "You need to tell derailed how to boot your app"
49
- puts "In your perf.rake add:"
50
- puts
51
- puts "namespace :perf do"
52
- puts " task :rack_load do"
53
- puts " # DERAILED_APP = your code here"
54
- puts " end"
55
- puts "end"
56
- end
38
+ out_dir = Pathname.new("tmp/library_branches/#{Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')}")
39
+ out_dir.mkpath
57
40
 
58
- task :setup do
59
- if DerailedBenchmarks.gem_is_bundled?("railties")
60
- Rake::Task["perf:rails_load"].invoke
61
- else
62
- Rake::Task["perf:rack_load"].invoke
63
- end
64
-
65
- WARM_COUNT = (ENV['WARM_COUNT'] || 0).to_i
66
- TEST_COUNT = (ENV['TEST_COUNT'] || ENV['CNT'] || 1_000).to_i
67
- PATH_TO_HIT = ENV["PATH_TO_HIT"] || ENV['ENDPOINT'] || "/"
68
- puts "Endpoint: #{ PATH_TO_HIT.inspect }"
41
+ branches_to_test = branch_names.each_with_object({}) {|elem, hash| hash[elem] = out_dir + "#{elem.gsub('/', ':')}.bench.txt" }
42
+ branch_info = {}
69
43
 
70
- HTTP_HEADER_PREFIX = "HTTP_".freeze
71
- RACK_HTTP_HEADERS = ENV.select { |key| key.start_with?(HTTP_HEADER_PREFIX) }
72
-
73
- HTTP_HEADERS = RACK_HTTP_HEADERS.keys.inject({}) do |hash, rack_header_name|
74
- # e.g. "HTTP_ACCEPT_CHARSET" -> "Accept-Charset"
75
- header_name = rack_header_name[HTTP_HEADER_PREFIX.size..-1].split("_").map(&:downcase).map(&:capitalize).join("-")
76
- hash[header_name] = RACK_HTTP_HEADERS[rack_header_name]
77
- hash
78
- end
79
- puts "HTTP headers: #{HTTP_HEADERS}" unless HTTP_HEADERS.empty?
80
-
81
- CURL_HTTP_HEADER_ARGS = HTTP_HEADERS.map { |http_header_name, value| "-H \"#{http_header_name}: #{value}\"" }.join(" ")
82
-
83
- require 'rack/test'
84
- require 'rack/file'
85
-
86
- DERAILED_APP = DerailedBenchmarks.add_auth(Object.class_eval { remove_const(:DERAILED_APP) })
87
- if server = ENV["USE_SERVER"]
88
- @port = (3000..3900).to_a.sample
89
- puts "Port: #{ @port.inspect }"
90
- puts "Server: #{ server.inspect }"
91
- thread = Thread.new do
92
- Rack::Server.start(app: DERAILED_APP, :Port => @port, environment: "none", server: server)
44
+ branches_to_test.each do |branch, file|
45
+ Dir.chdir(library_dir) do
46
+ run!("git checkout '#{branch}'")
47
+ description = run!("git log --oneline --format=%B -n 1 HEAD | head -n 1").strip
48
+ time_stamp = run!("git log -n 1 --pretty=format:%ci").strip # https://stackoverflow.com/a/25921837/147390
49
+ name = run!("git rev-parse --short HEAD").strip
50
+ branch_info[name] = { desc: description, time: DateTime.parse(time_stamp), file: file }
93
51
  end
94
- sleep 1
95
-
96
- def call_app(path = File.join("/", PATH_TO_HIT))
97
- cmd = "curl #{CURL_HTTP_HEADER_ARGS} 'http://localhost:#{@port}#{path}' -s --fail 2>&1"
98
- response = `#{cmd}`
99
- unless $?.success?
100
- STDERR.puts "Couldn't call app."
101
- STDERR.puts "Bad request to #{cmd.inspect} \n\n***RESPONSE***:\n\n#{ response.inspect }"
102
-
103
- FileUtils.mkdir_p("tmp")
104
- File.open("tmp/fail.html", "w+") {|f| f.write response.body }
52
+ run!("#{script}")
53
+ end
105
54
 
106
- `open #{File.expand_path("tmp/fail.html")}` if ENV["DERAILED_DEBUG"]
55
+ stats = DerailedBenchmarks::StatsFromDir.new(branch_info)
56
+ ENV["DERAILED_STOP_VALID_COUNT"] ||= "10"
57
+ stop_valid_count = Integer(ENV["DERAILED_STOP_VALID_COUNT"])
107
58
 
108
- exit(1)
109
- end
59
+ times_significant = 0
60
+ DERAILED_SCRIPT_COUNT.times do |i|
61
+ puts "Sample: #{i.next}/#{DERAILED_SCRIPT_COUNT} iterations per sample: #{ENV['TEST_COUNT']}"
62
+ branches_to_test.each do |branch, file|
63
+ Dir.chdir(library_dir) { run!("git checkout '#{branch}'") }
64
+ run!(" #{script} 2>&1 | tail -n 1 >> '#{file}'")
110
65
  end
111
- else
112
- @app = Rack::MockRequest.new(DERAILED_APP)
113
-
114
- def call_app
115
- response = @app.get(PATH_TO_HIT, RACK_HTTP_HEADERS)
116
- if response.status != 200
117
- STDERR.puts "Couldn't call app. Bad request to #{PATH_TO_HIT}! Resulted in #{response.status} status."
118
- STDERR.puts "\n\n***RESPONSE BODY***\n\n"
119
- STDERR.puts response.body
120
-
121
- FileUtils.mkdir_p("tmp")
122
- File.open("tmp/fail.html", "w+") {|f| f.write response.body }
123
-
124
- `open #{File.expand_path("tmp/fail.html")}` if ENV["DERAILED_DEBUG"]
66
+ times_significant += 1 if i >= 2 && stats.call.significant?
67
+ break if stop_valid_count != 0 && times_significant == stop_valid_count
68
+ end
125
69
 
126
- exit(1)
127
- end
128
- response
70
+ ensure
71
+ if library_dir && current_library_branch
72
+ puts "Resetting git dir of #{library_dir.inspect} to #{current_library_branch.inspect}"
73
+ Dir.chdir(library_dir) do
74
+ run!("git checkout '#{current_library_branch}'")
129
75
  end
130
76
  end
131
- if WARM_COUNT > 0
132
- puts "Warming up app: #{WARM_COUNT} times"
133
- WARM_COUNT.times { call_app }
134
- end
77
+
78
+ stats.call.banner if stats
135
79
  end
136
80
 
137
81
  desc "hits the url TEST_COUNT times"
@@ -228,6 +172,11 @@ namespace :perf do
228
172
  require 'benchmark/ips'
229
173
 
230
174
  Benchmark.ips do |x|
175
+ x.warmup = Float(ENV["IPS_WARMUP"] || 2)
176
+ x.time = Float(ENV["IPS_TIME"] || 5)
177
+ x.suite = ENV["IPS_SUITE"] if ENV["IPS_SUITE"]
178
+ x.iterations = Integer(ENV["IPS_ITERATIONS"] || 1)
179
+
231
180
  x.report("ips") { call_app }
232
181
  end
233
182
  end
@@ -298,4 +247,10 @@ namespace :perf do
298
247
  puts ""
299
248
  puts "Also try uploading #{file_name.inspect} to http://tenderlove.github.io/heap-analyzer/"
300
249
  end
250
+
251
+ def run!(cmd)
252
+ out = `#{cmd}`
253
+ raise "Error while running #{cmd.inspect}: #{out}" unless $?.success?
254
+ out
255
+ end
301
256
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DerailedBenchmarks
2
- VERSION = "1.3.6"
4
+ VERSION = "1.4.0"
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class KernelRequireTest < ActiveSupport::TestCase
@@ -25,6 +27,7 @@ class KernelRequireTest < ActiveSupport::TestCase
25
27
  assert_node_in_parent("child_one.rb", parent)
26
28
  child_two = assert_node_in_parent("child_two.rb", parent)
27
29
  assert_node_in_parent("relative_child", parent)
30
+ assert_node_in_parent("relative_child_two", parent)
28
31
  assert_node_in_parent("raise_child.rb", child_two)
29
32
  end
30
33
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class RequireTree < ActiveSupport::TestCase
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class StatsFromDirTest < ActiveSupport::TestCase
6
+ test "that it works" do
7
+ dir = fixtures_dir("stats/significant")
8
+ branch_info = {}
9
+ branch_info["loser"] = { desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), name: "loser" }
10
+ branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), name: "winner" }
11
+ stats = DerailedBenchmarks::StatsFromDir.new(branch_info).call
12
+
13
+ newest = stats.newest
14
+ oldest = stats.oldest
15
+
16
+ assert newest.average < oldest.average
17
+
18
+ assert_equal "winner", newest.name
19
+ assert_equal "loser", oldest.name
20
+
21
+ assert 3.6e-05 < stats.p_value
22
+ assert 3.8e-05 > stats.p_value
23
+ assert_equal true, stats.significant?
24
+
25
+ assert_equal "1.0062", stats.x_faster
26
+ assert_equal "0.6131", stats.percent_faster
27
+ end
28
+
29
+ test "banner faster" do
30
+ dir = fixtures_dir("stats/significant")
31
+ branch_info = {}
32
+ branch_info["loser"] = { desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), name: "loser" }
33
+ branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), name: "winner" }
34
+ stats = DerailedBenchmarks::StatsFromDir.new(branch_info).call
35
+ newest = stats.newest
36
+ oldest = stats.oldest
37
+
38
+ # Test fixture for banner
39
+ def stats.p_value
40
+ "0.000037"
41
+ end
42
+
43
+ def newest.average
44
+ 10.5
45
+ end
46
+
47
+ def oldest.average
48
+ 11.0
49
+ end
50
+
51
+ expected = <<-EOM
52
+ [winner] "I am the new commit" - (10.5 seconds)
53
+ FASTER by:
54
+ 1.0476x [older/newer]
55
+ 4.5455% [(older - newer) / older * 100]
56
+ [loser] "Old commit" - (11.0 seconds)
57
+ EOM
58
+
59
+ actual = StringIO.new
60
+ stats.banner(actual)
61
+
62
+ assert_match expected, actual.string
63
+ end
64
+
65
+ test "banner slower" do
66
+ dir = fixtures_dir("stats/significant")
67
+ branch_info = {}
68
+ branch_info["loser"] = { desc: "I am the new commit", time: Time.now, file: dir.join("loser.bench.txt"), name: "loser" }
69
+ branch_info["winner"] = { desc: "Old commit", time: Time.now - 10, file: dir.join("winner.bench.txt"), name: "winner" }
70
+ stats = DerailedBenchmarks::StatsFromDir.new(branch_info).call
71
+ newest = stats.newest
72
+ oldest = stats.oldest
73
+
74
+ def oldest.average
75
+ 10.5
76
+ end
77
+
78
+ def newest.average
79
+ 11.0
80
+ end
81
+
82
+ expected = <<-EOM
83
+ [loser] "I am the new commit" - (11.0 seconds)
84
+ SLOWER by:
85
+ 0.9545x [older/newer]
86
+ -4.7619% [(older - newer) / older * 100]
87
+ [winner] "Old commit" - (10.5 seconds)
88
+ EOM
89
+
90
+ actual = StringIO.new
91
+ stats.banner(actual)
92
+
93
+ assert_match expected, actual.string
94
+ end
95
+
96
+ test "stats from samples with slightly different sizes" do
97
+ stats = DerailedBenchmarks::StatsFromDir.new({})
98
+ out = stats.students_t_test([100,101,102], [1,3])
99
+ assert out[:alternative]
100
+ end
101
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class DerailedBenchmarksTest < ActiveSupport::TestCase
@@ -1,4 +1,4 @@
1
1
  class ChildOne
2
- @retained = ""
2
+ @retained = +""
3
3
  50_000.times.map { @retained << "A" }
4
4
  end
@@ -1,5 +1,5 @@
1
1
  class ChildTwo
2
- @retained = ""
2
+ @retained = +""
3
3
  200_000.times.map { @retained << "A" }
4
4
  end
5
5
 
@@ -1,7 +1,8 @@
1
1
  class ParentOne
2
- @retained = ""
2
+ @retained = +""
3
3
  1_000_000.times.map { @retained << "A" }
4
4
  end
5
5
  require File.expand_path('../child_one.rb', __FILE__)
6
6
  require File.expand_path('../child_two.rb', __FILE__)
7
7
  require_relative 'relative_child'
8
+ require_relative File.expand_path('relative_child_two', __dir__)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class RaiseChild
2
4
  end
3
5
 
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class RelativeChild
2
4
  end