gitlab-derailed_benchmarks 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/check_changelog.yml +10 -0
  3. data/.gitignore +8 -0
  4. data/.gitlab-ci.yml +56 -0
  5. data/.travis.yml +18 -0
  6. data/Appraisals +26 -0
  7. data/CHANGELOG.md +105 -0
  8. data/Gemfile +9 -0
  9. data/README.md +692 -0
  10. data/Rakefile +29 -0
  11. data/bin/derailed +93 -0
  12. data/derailed_benchmarks.gemspec +39 -0
  13. data/gemfiles/.bundle/config +2 -0
  14. data/gemfiles/rails_5_1.gemfile +15 -0
  15. data/gemfiles/rails_5_2.gemfile +15 -0
  16. data/gemfiles/rails_6_0.gemfile +15 -0
  17. data/gemfiles/rails_git.gemfile +19 -0
  18. data/lib/derailed_benchmarks.rb +51 -0
  19. data/lib/derailed_benchmarks/auth_helper.rb +34 -0
  20. data/lib/derailed_benchmarks/auth_helpers/devise.rb +41 -0
  21. data/lib/derailed_benchmarks/core_ext/kernel_require.rb +88 -0
  22. data/lib/derailed_benchmarks/load_tasks.rb +145 -0
  23. data/lib/derailed_benchmarks/require_tree.rb +65 -0
  24. data/lib/derailed_benchmarks/stats_from_dir.rb +128 -0
  25. data/lib/derailed_benchmarks/stats_in_file.rb +60 -0
  26. data/lib/derailed_benchmarks/tasks.rb +292 -0
  27. data/lib/derailed_benchmarks/version.rb +5 -0
  28. data/test/derailed_benchmarks/core_ext/kernel_require_test.rb +33 -0
  29. data/test/derailed_benchmarks/require_tree_test.rb +95 -0
  30. data/test/derailed_benchmarks/stats_from_dir_test.rb +125 -0
  31. data/test/derailed_test.rb +14 -0
  32. data/test/fixtures/require/child_one.rb +4 -0
  33. data/test/fixtures/require/child_two.rb +9 -0
  34. data/test/fixtures/require/parent_one.rb +8 -0
  35. data/test/fixtures/require/raise_child.rb +6 -0
  36. data/test/fixtures/require/relative_child.rb +4 -0
  37. data/test/fixtures/require/relative_child_two.rb +4 -0
  38. data/test/fixtures/stats/significant/loser.bench.txt +100 -0
  39. data/test/fixtures/stats/significant/winner.bench.txt +100 -0
  40. data/test/integration/tasks_test.rb +132 -0
  41. data/test/rails_app/Rakefile +9 -0
  42. data/test/rails_app/app/assets/config/manifest.js +0 -0
  43. data/test/rails_app/app/assets/javascripts/authenticated.js +2 -0
  44. data/test/rails_app/app/assets/stylesheets/authenticated.css +4 -0
  45. data/test/rails_app/app/controllers/application_controller.rb +17 -0
  46. data/test/rails_app/app/controllers/authenticated_controller.rb +8 -0
  47. data/test/rails_app/app/controllers/pages_controller.rb +14 -0
  48. data/test/rails_app/app/helpers/application_helper.rb +4 -0
  49. data/test/rails_app/app/helpers/authenticated_helper.rb +4 -0
  50. data/test/rails_app/app/models/user.rb +13 -0
  51. data/test/rails_app/app/views/authenticated/index.html.erb +1 -0
  52. data/test/rails_app/app/views/layouts/application.html.erb +14 -0
  53. data/test/rails_app/app/views/pages/index.html.erb +1 -0
  54. data/test/rails_app/config.ru +6 -0
  55. data/test/rails_app/config/application.rb +52 -0
  56. data/test/rails_app/config/boot.rb +12 -0
  57. data/test/rails_app/config/database.yml +22 -0
  58. data/test/rails_app/config/environment.rb +11 -0
  59. data/test/rails_app/config/environments/development.rb +27 -0
  60. data/test/rails_app/config/environments/production.rb +51 -0
  61. data/test/rails_app/config/environments/test.rb +37 -0
  62. data/test/rails_app/config/initializers/backtrace_silencers.rb +9 -0
  63. data/test/rails_app/config/initializers/devise.rb +258 -0
  64. data/test/rails_app/config/initializers/inflections.rb +12 -0
  65. data/test/rails_app/config/initializers/mime_types.rb +7 -0
  66. data/test/rails_app/config/initializers/secret_token.rb +13 -0
  67. data/test/rails_app/config/initializers/session_store.rb +10 -0
  68. data/test/rails_app/config/locales/devise.en.yml +59 -0
  69. data/test/rails_app/config/locales/en.yml +9 -0
  70. data/test/rails_app/config/locales/es.yml +10 -0
  71. data/test/rails_app/config/routes.rb +67 -0
  72. data/test/rails_app/db/migrate/20141210070547_devise_create_users.rb +45 -0
  73. data/test/rails_app/db/schema.rb +35 -0
  74. data/test/rails_app/perf.rake +10 -0
  75. data/test/rails_app/public/404.html +26 -0
  76. data/test/rails_app/public/422.html +26 -0
  77. data/test/rails_app/public/500.html +26 -0
  78. data/test/rails_app/public/favicon.ico +0 -0
  79. data/test/rails_app/public/javascripts/application.js +2 -0
  80. data/test/rails_app/public/javascripts/controls.js +965 -0
  81. data/test/rails_app/public/javascripts/dragdrop.js +974 -0
  82. data/test/rails_app/public/javascripts/effects.js +1123 -0
  83. data/test/rails_app/public/javascripts/prototype.js +6001 -0
  84. data/test/rails_app/public/javascripts/rails.js +202 -0
  85. data/test/rails_app/public/stylesheets/.gitkeep +0 -0
  86. data/test/rails_app/script/rails +8 -0
  87. data/test/support/integration_case.rb +7 -0
  88. data/test/test_helper.rb +65 -0
  89. 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