derailed_benchmarks 1.3.6 → 1.4.0

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