derailed_benchmarks 1.4.2 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -30,7 +30,7 @@ namespace :perf do
30
30
  DERAILED_APP.initialize! unless DERAILED_APP.instance_variable_get(:@initialized)
31
31
  end
32
32
 
33
- if ENV["DERAILED_SKIP_ACTIVE_RECORD"] && defined? ActiveRecord
33
+ if !ENV["DERAILED_SKIP_ACTIVE_RECORD"] && defined? ActiveRecord
34
34
  if defined? ActiveRecord::Tasks::DatabaseTasks
35
35
  ActiveRecord::Tasks::DatabaseTasks.create_current
36
36
  else # Rails 3.2
@@ -39,7 +39,14 @@ namespace :perf do
39
39
 
40
40
  ActiveRecord::Migrator.migrations_paths = DERAILED_APP.paths['db/migrate'].to_a
41
41
  ActiveRecord::Migration.verbose = true
42
- ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, nil)
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
43
50
  end
44
51
 
45
52
  DERAILED_APP.config.consider_all_requests_local = true
@@ -135,4 +142,4 @@ namespace :perf do
135
142
  WARM_COUNT.times { call_app }
136
143
  end
137
144
  end
138
- end
145
+ end
@@ -40,7 +40,7 @@ module DerailedBenchmarks
40
40
  end
41
41
 
42
42
  def to_string
43
- str = +"#{name}: #{cost.round(4)} MiB"
43
+ str = String.new("#{name}: #{cost.round(4)} MiB")
44
44
  if parent && REQUIRED_BY[self.name.to_s]
45
45
  names = REQUIRED_BY[self.name.to_s].uniq - [parent.name.to_s]
46
46
  if names.any?
@@ -16,23 +16,34 @@ module DerailedBenchmarks
16
16
  # x.average # => 10.5
17
17
  # x.name # => "muhfile"
18
18
  class StatsForFile
19
- attr_reader :name, :values, :desc, :time
19
+ attr_reader :name, :values, :desc, :time, :short_sha
20
20
 
21
- def initialize(file:, name:, desc: "", time: )
21
+ def initialize(file:, name:, desc: "", time: , short_sha: nil)
22
22
  @file = Pathname.new(file)
23
23
  FileUtils.touch(@file)
24
24
 
25
25
  @name = name
26
26
  @desc = desc
27
27
  @time = time
28
+ @short_sha = short_sha
28
29
  end
29
30
 
30
31
  def call
31
32
  load_file!
33
+ return if values.empty?
32
34
 
35
+ @median = (values[(values.length - 1) / 2] + values[values.length/ 2]) / 2.0
33
36
  @average = values.inject(:+) / values.length
34
37
  end
35
38
 
39
+ def empty?
40
+ values.empty?
41
+ end
42
+
43
+ def median
44
+ @median.to_f
45
+ end
46
+
36
47
  def average
37
48
  @average.to_f
38
49
  end
@@ -47,6 +58,8 @@ module DerailedBenchmarks
47
58
  raise e, "Problem with file #{@file.inspect}:\n#{@file.read}\n#{e.message}"
48
59
  end
49
60
  end
61
+
62
+ values.sort!
50
63
  values.freeze
51
64
  end
52
65
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  require 'bigdecimal'
4
4
  require 'statistics'
5
+ require 'stringio'
6
+ require 'mini_histogram'
7
+ require 'mini_histogram/plot'
5
8
 
6
9
  module DerailedBenchmarks
7
10
  # A class used to read several benchmark files
@@ -26,14 +29,28 @@ module DerailedBenchmarks
26
29
  FORMAT = "%0.4f"
27
30
  attr_reader :stats, :oldest, :newest
28
31
 
29
- def initialize(hash)
32
+ def initialize(input)
30
33
  @files = []
31
34
 
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)
35
+ if input.is_a?(Hash)
36
+ hash = input
37
+ hash.each do |branch, info_hash|
38
+ file = info_hash.fetch(:file)
39
+ desc = info_hash.fetch(:desc)
40
+ time = info_hash.fetch(:time)
41
+ short_sha = info_hash["short_sha"]
42
+ @files << StatsForFile.new(file: file, desc: desc, time: time, name: branch, short_sha: short_sha)
43
+ end
44
+ else
45
+ input.each do |commit|
46
+ @files << StatsForFile.new(
47
+ file: commit.file,
48
+ desc: commit.desc,
49
+ time: commit.time,
50
+ name: commit.ref,
51
+ short_sha: commit.short_sha
52
+ )
53
+ end
37
54
  end
38
55
  @files.sort_by! { |f| f.time }
39
56
  @oldest = @files.first
@@ -42,14 +59,27 @@ module DerailedBenchmarks
42
59
 
43
60
  def call
44
61
  @files.each(&:call)
45
- @stats = statistical_test
62
+
63
+ return self if @files.detect(&:empty?)
64
+
65
+ stats_95 = statistical_test(confidence: 95)
66
+
67
+ # If default check is good, see if we also pass a more rigorous test
68
+ # if so, then use the more rigourous test
69
+ if stats_95[:alternative]
70
+ stats_99 = statistical_test(confidence: 99)
71
+ @stats = stats_99 if stats_99[:alternative]
72
+ end
73
+ @stats ||= stats_95
74
+
46
75
  self
47
76
  end
48
77
 
49
- def statistical_test(series_1=oldest.values, series_2=newest.values)
78
+ def statistical_test(series_1=oldest.values, series_2=newest.values, confidence: 95)
50
79
  StatisticalTest::KSTest.two_samples(
51
80
  group_one: series_1,
52
- group_two: series_2
81
+ group_two: series_2,
82
+ alpha: (100 - confidence) / 100.0
53
83
  )
54
84
  end
55
85
 
@@ -66,18 +96,51 @@ module DerailedBenchmarks
66
96
  end
67
97
 
68
98
  def x_faster
69
- FORMAT % (oldest.average/newest.average).to_f
99
+ (oldest.median/newest.median).to_f
100
+ end
101
+
102
+ def faster?
103
+ newest.median < oldest.median
70
104
  end
71
105
 
72
106
  def percent_faster
73
- FORMAT % (((oldest.average - newest.average) / oldest.average).to_f * 100)
107
+ (((oldest.median - newest.median) / oldest.median).to_f * 100)
74
108
  end
75
109
 
76
110
  def change_direction
77
- newest.average < oldest.average ? "FASTER" : "SLOWER"
111
+ if faster?
112
+ "FASTER 🚀🚀🚀"
113
+ else
114
+ "SLOWER 🐢🐢🐢"
115
+ end
116
+ end
117
+
118
+ def align
119
+ " " * (percent_faster.to_s.index(".") - x_faster.to_s.index("."))
78
120
  end
79
121
 
80
- def banner(io = Kernel)
122
+ def histogram(io = $stdout)
123
+ newest_histogram = MiniHistogram.new(newest.values)
124
+ oldest_histogram = MiniHistogram.new(oldest.values)
125
+ MiniHistogram.set_average_edges!(newest_histogram, oldest_histogram)
126
+
127
+ {newest => newest_histogram, oldest => oldest_histogram}.each do |report, histogram|
128
+ plot = histogram.plot(
129
+ title: "\n#{' ' * 18 }Histogram - [#{report.short_sha || report.name}] #{report.desc.inspect}",
130
+ ylabel: "Time (s)",
131
+ xlabel: "# of runs in range"
132
+ )
133
+
134
+ plot.render(io)
135
+ io.puts
136
+ end
137
+
138
+ io.puts
139
+ end
140
+
141
+ def banner(io = $stdout)
142
+ return if @files.detect(&:empty?)
143
+
81
144
  io.puts
82
145
  if significant?
83
146
  io.puts "❤️ ❤️ ❤️ (Statistically Significant) ❤️ ❤️ ❤️"
@@ -85,19 +148,23 @@ module DerailedBenchmarks
85
148
  io.puts "👎👎👎(NOT Statistically Significant) 👎👎👎"
86
149
  end
87
150
  io.puts
88
- io.puts "[#{newest.name}] #{newest.desc.inspect} - (#{newest.average} seconds)"
151
+ io.puts "[#{newest.short_sha || newest.name}] (#{FORMAT % newest.median} seconds) #{newest.desc.inspect} ref: #{newest.name.inspect}"
89
152
  io.puts " #{change_direction} by:"
90
- io.puts " #{x_faster}x [older/newer]"
91
- io.puts " #{percent_faster}\% [(older - newer) / older * 100]"
92
- io.puts "[#{oldest.name}] #{oldest.desc.inspect} - (#{oldest.average} seconds)"
153
+ io.puts " #{align}#{FORMAT % x_faster}x [older/newer]"
154
+ io.puts " #{FORMAT % percent_faster}\% [(older - newer) / older * 100]"
155
+ io.puts "[#{oldest.short_sha || oldest.name}] (#{FORMAT % oldest.median} seconds) #{oldest.desc.inspect} ref: #{oldest.name.inspect}"
93
156
  io.puts
94
157
  io.puts "Iterations per sample: #{ENV["TEST_COUNT"]}"
95
158
  io.puts "Samples: #{newest.values.length}"
96
159
  io.puts
97
160
  io.puts "Test type: Kolmogorov Smirnov"
161
+ io.puts "Confidence level: #{@stats[:confidence_level] * 100} %"
98
162
  io.puts "Is significant? (max > critical): #{significant?}"
99
163
  io.puts "D critical: #{d_critical}"
100
164
  io.puts "D max: #{d_max}"
165
+
166
+ histogram(io)
167
+
101
168
  io.puts
102
169
  end
103
170
  end
@@ -1,6 +1,12 @@
1
1
  require_relative 'load_tasks'
2
2
 
3
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
+
4
10
  desc "runs the same test against two different branches for statistical comparison"
5
11
  task :library do
6
12
  begin
@@ -11,92 +17,65 @@ namespace :perf do
11
17
  script = ENV["DERAILED_SCRIPT"] || "bundle exec derailed exec perf:test"
12
18
 
13
19
  if ENV["DERAILED_PATH_TO_LIBRARY"]
14
- library_dir = ENV["DERAILED_PATH_TO_LIBRARY"]
20
+ library_dir = ENV["DERAILED_PATH_TO_LIBRARY"].chomp
15
21
  else
16
22
  library_dir = DerailedBenchmarks.rails_path_on_disk
17
23
  end
24
+ library_dir = Pathname.new(library_dir)
18
25
 
19
- raise "Must be a path with a .git directory '#{library_dir}'" unless File.exist?(File.join(library_dir, ".git"))
20
-
21
- # Use either the explicit SHAs when present or grab last two SHAs from commit history
22
- # if only one SHA is given, then use it and the last SHA from commit history
23
- branch_names = []
24
- branch_names = ENV.fetch("SHAS_TO_TEST").split(",") if ENV["SHAS_TO_TEST"]
25
- if branch_names.length < 2
26
- Dir.chdir(library_dir) do
27
- run!("git checkout '#{branch_names.first}'") unless branch_names.empty?
28
-
29
- branches = run!('git log --format="%H" -n 2').chomp.split($/)
30
- if branch_names.empty?
31
- branch_names = branches
32
- else
33
- branches.shift
34
- branch_names << branches.shift
35
- end
36
- end
37
- end
38
-
39
- current_library_branch = ""
40
- Dir.chdir(library_dir) { current_library_branch = run!('git describe --contains --all HEAD').chomp }
41
-
42
- out_dir = Pathname.new("tmp/library_branches/#{Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')}")
26
+ out_dir = Pathname.new("tmp/compare_branches/#{Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')}")
43
27
  out_dir.mkpath
44
28
 
45
- branches_to_test = branch_names.each_with_object({}) {|elem, hash| hash[elem] = out_dir + "#{elem.gsub('/', ':')}.bench.txt" }
46
- branch_info = {}
47
- branch_to_sha = {}
29
+ ref_string = ENV["SHAS_TO_TEST"] || ENV["REFS_TO_TEST"] || ""
48
30
 
49
- branches_to_test.each do |branch, file|
50
- Dir.chdir(library_dir) do
51
- run!("git checkout '#{branch}'")
52
- description = run!("git log --oneline --format=%B -n 1 HEAD | head -n 1").strip
53
- time_stamp = run!("git log -n 1 --pretty=format:%ci").strip # https://stackoverflow.com/a/25921837/147390
54
- short_sha = run!("git rev-parse --short HEAD").strip
55
- branch_to_sha[branch] = short_sha
31
+ project = DerailedBenchmarks::Git::SwitchProject.new(
32
+ path: library_dir,
33
+ ref_array: ref_string.split(","),
34
+ log_dir: out_dir
35
+ )
56
36
 
57
- branch_info[short_sha] = { desc: description, time: DateTime.parse(time_stamp), file: file }
58
- end
59
- run!("#{script}")
60
- end
37
+ stats = DerailedBenchmarks::StatsFromDir.new(project.commits)
61
38
 
39
+ # Advertise branch names early to make sure people know what they're testing
62
40
  puts
63
41
  puts
64
- branches_to_test.each.with_index do |(branch, _), i|
65
- short_sha = branch_to_sha[branch]
66
- desc = branch_info[short_sha][:desc]
67
- puts "Testing #{i + 1}: #{short_sha}: #{desc}"
42
+ project.commits.each_with_index do |commit, i|
43
+ puts "Testing #{i + 1}: #{commit.short_sha}: #{commit.description}"
68
44
  end
69
45
  puts
70
46
  puts
71
47
 
72
- raise "SHAs to test must be different" if branch_info.length == 1
73
- stats = DerailedBenchmarks::StatsFromDir.new(branch_info)
74
- ENV["DERAILED_STOP_VALID_COUNT"] ||= "50"
75
- stop_valid_count = Integer(ENV["DERAILED_STOP_VALID_COUNT"])
76
-
77
- times_significant = 0
78
- DERAILED_SCRIPT_COUNT.times do |i|
79
- puts "Sample: #{i.next}/#{DERAILED_SCRIPT_COUNT} iterations per sample: #{ENV['TEST_COUNT']}"
80
- branches_to_test.each do |branch, file|
81
- Dir.chdir(library_dir) { run!("git checkout '#{branch}'") }
82
- run!(" #{script} 2>&1 | tail -n 1 >> '#{file}'")
48
+ project.restore_branch_on_return do
49
+ DERAILED_SCRIPT_COUNT.times do |i|
50
+ puts "Sample: #{i.next}/#{DERAILED_SCRIPT_COUNT} iterations per sample: #{ENV['TEST_COUNT']}"
51
+ project.commits.each do |commit|
52
+ commit.checkout!
53
+
54
+ output = run!("#{script} 2>&1")
55
+ commit.log.open("a") {|f| f.puts output.lines.last }
56
+ end
57
+
58
+ if (i % 50).zero?
59
+ puts "Intermediate result"
60
+ stats.call.banner
61
+ puts "Continuing execution"
62
+ end
83
63
  end
84
- times_significant += 1 if i >= 2 && stats.call.significant?
85
- break if stop_valid_count != 0 && times_significant == stop_valid_count
86
64
  end
87
65
 
88
66
  ensure
89
- if library_dir && current_library_branch
90
- puts "Resetting git dir of '#{library_dir.to_s}' to #{current_library_branch.inspect}"
91
- Dir.chdir(library_dir) do
92
- run!("git checkout '#{current_library_branch}'")
67
+ if stats
68
+ stats.call.banner
69
+
70
+ result_file = out_dir.join("results.txt")
71
+ File.open(result_file, "w") do |f|
72
+ stats.banner(f)
93
73
  end
94
- end
95
74
 
96
- stats.call.banner if stats
75
+ puts "Output: #{result_file.to_s}"
76
+ end
97
77
  end
98
- end
99
-
78
+ end
100
79
 
101
80
  desc "hits the url TEST_COUNT times"
102
81
  task :test => [:setup] do
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DerailedBenchmarks
4
- VERSION = "1.4.2"
4
+ VERSION = "1.8.0"
5
5
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class GitSwitchProjectTest < ActiveSupport::TestCase
6
+ test "tells me when it's not pointing at a git project" do
7
+ exception = assert_raises {
8
+ DerailedBenchmarks::Git::SwitchProject.new(path: "/dev/null")
9
+ }
10
+ assert_includes(exception.message, '.git directory')
11
+ end
12
+
13
+ test "dirty gemspec cleaning" do
14
+ Dir.mktmpdir do |dir|
15
+ run!("git clone https://github.com/sharpstone/default_ruby #{dir} 2>&1 && cd #{dir} && git checkout 6e642963acec0ff64af51bd6fba8db3c4176ed6e 2>&1 && git checkout -b mybranch 2>&1")
16
+ run!("cd #{dir} && echo lol > foo.gemspec && git add .")
17
+
18
+ io = StringIO.new
19
+ project = DerailedBenchmarks::Git::SwitchProject.new(path: dir, io: io)
20
+
21
+ assert project.dirty?
22
+ refute project.clean?
23
+
24
+ project.restore_branch_on_return do
25
+ project.commits.map(&:checkout!)
26
+ end
27
+
28
+ assert_includes io.string, "Bundler modifies gemspec files"
29
+ assert_includes io.string, "Applying stash"
30
+ end
31
+ end
32
+
33
+ test "works on a git repo" do
34
+ Dir.mktmpdir do |dir|
35
+ run!("git clone https://github.com/sharpstone/default_ruby #{dir} 2>&1 && cd #{dir} && git checkout 6e642963acec0ff64af51bd6fba8db3c4176ed6e 2>&1 && git checkout -b mybranch 2>&1")
36
+
37
+ # finds shas when none given
38
+ project = DerailedBenchmarks::Git::SwitchProject.new(path: dir)
39
+
40
+ assert_equal ["6e642963acec0ff64af51bd6fba8db3c4176ed6e", "da748a59340be8b950e7bbbfb32077eb67d70c3c"], project.commits.map(&:ref)
41
+ first_commit = project.commits.first
42
+
43
+ assert_equal "CI test support", first_commit.description
44
+ assert_equal "6e64296", first_commit.short_sha
45
+ assert_equal "/dev/null/6e642963acec0ff64af51bd6fba8db3c4176ed6e.bench.txt", first_commit.log.to_s
46
+ assert_equal DateTime.parse("Tue, 14 Apr 2020 13:26:03 -0500"), first_commit.time
47
+
48
+ assert_equal "mybranch", project.current_branch_or_sha
49
+
50
+ # Finds shas when 1 is given
51
+ project = DerailedBenchmarks::Git::SwitchProject.new(path: dir, ref_array: ["da748a59340be8b950e7bbbfb32077eb67d70c3c"])
52
+
53
+ assert_equal ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "5c09f748957d2098182762004adee27d1ff83160"], project.commits.map(&:ref)
54
+
55
+
56
+ # Returns correct refs if given
57
+ project = DerailedBenchmarks::Git::SwitchProject.new(path: dir, ref_array: ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "9b19275a592f148e2a53b87ead4ccd8c747539c9"])
58
+
59
+ assert_equal ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "9b19275a592f148e2a53b87ead4ccd8c747539c9"], project.commits.map(&:ref)
60
+
61
+ first_commit = project.commits.first
62
+
63
+ first_commit.checkout!
64
+
65
+ assert_equal first_commit.short_sha, project.current_branch_or_sha
66
+
67
+ # Test restore_branch_on_return
68
+ project.restore_branch_on_return(quiet: true) do
69
+ project.commits.last.checkout!
70
+
71
+ assert_equal project.commits.last.short_sha, project.current_branch_or_sha
72
+ end
73
+
74
+ assert_equal project.commits.first.short_sha, project.current_branch_or_sha
75
+
76
+ exception = assert_raise {
77
+ DerailedBenchmarks::Git::SwitchProject.new(path: dir, ref_array: ["6e642963acec0ff64af51bd6fba8db3c4176ed6e", "mybranch"])
78
+ }
79
+
80
+ assert_includes(exception.message, 'Duplicate SHA resolved "6e64296"')
81
+ end
82
+ end
83
+ end