derailed_benchmarks 1.4.2 → 1.8.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.
@@ -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