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.
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