derailed_benchmarks 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +75 -0
  3. data/.github/workflows/check_changelog.yml +11 -8
  4. data/CHANGELOG.md +26 -1
  5. data/README.md +44 -11
  6. data/derailed_benchmarks.gemspec +7 -5
  7. data/gemfiles/rails_5_1.gemfile +3 -1
  8. data/gemfiles/rails_5_2.gemfile +3 -3
  9. data/gemfiles/rails_6_1.gemfile +13 -0
  10. data/gemfiles/rails_git.gemfile +2 -2
  11. data/lib/derailed_benchmarks.rb +4 -2
  12. data/lib/derailed_benchmarks/core_ext/kernel_require.rb +29 -24
  13. data/lib/derailed_benchmarks/git/commit.rb +36 -0
  14. data/lib/derailed_benchmarks/git/in_path.rb +59 -0
  15. data/lib/derailed_benchmarks/git/switch_project.rb +128 -0
  16. data/lib/derailed_benchmarks/git_switch_project.rb +1 -0
  17. data/lib/derailed_benchmarks/load_tasks.rb +11 -4
  18. data/lib/derailed_benchmarks/require_tree.rb +11 -1
  19. data/lib/derailed_benchmarks/{stats_in_file.rb → stats_for_file.rb} +8 -2
  20. data/lib/derailed_benchmarks/stats_from_dir.rb +53 -9
  21. data/lib/derailed_benchmarks/tasks.rb +32 -63
  22. data/lib/derailed_benchmarks/version.rb +1 -1
  23. data/test/derailed_benchmarks/core_ext/kernel_require_test.rb +70 -11
  24. data/test/derailed_benchmarks/git_switch_project_test.rb +83 -0
  25. data/test/derailed_benchmarks/require_tree_test.rb +1 -1
  26. data/test/derailed_benchmarks/stats_from_dir_test.rb +43 -10
  27. data/test/fixtures/require/autoload_child.rb +5 -0
  28. data/test/fixtures/require/autoload_parent.rb +8 -0
  29. data/test/fixtures/require/child_one.rb +1 -1
  30. data/test/fixtures/require/child_two.rb +1 -1
  31. data/test/fixtures/require/load_child.rb +3 -0
  32. data/test/fixtures/require/load_parent.rb +5 -0
  33. data/test/fixtures/require/parent_one.rb +1 -1
  34. data/test/integration/tasks_test.rb +45 -5
  35. data/test/rails_app/config/application.rb +2 -0
  36. data/test/rails_app/config/storage.yml +0 -0
  37. data/test/test_helper.rb +6 -1
  38. metadata +86 -30
  39. data/.travis.yml +0 -18
  40. data/Appraisals +0 -26
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DerailedBenchmarks
4
- VERSION = "1.5.0"
4
+ VERSION = "2.0.0"
5
5
  end
@@ -3,7 +3,6 @@
3
3
  require 'test_helper'
4
4
 
5
5
  class KernelRequireTest < ActiveSupport::TestCase
6
-
7
6
  setup do
8
7
  require 'derailed_benchmarks/core_ext/kernel_require'
9
8
  GC.disable
@@ -11,23 +10,83 @@ class KernelRequireTest < ActiveSupport::TestCase
11
10
 
12
11
  teardown do
13
12
  GC.enable
13
+ DerailedBenchmarks::RequireTree.reset!
14
+ end
15
+
16
+ test "profiles load" do
17
+ in_fork do
18
+ require fixtures_dir("require/load_parent.rb")
19
+
20
+ parent = assert_node_in_parent("load_parent.rb", TOP_REQUIRE)
21
+
22
+ assert_node_in_parent("load_child.rb", parent)
23
+ end
24
+ end
25
+
26
+ test "profiles autoload" do
27
+ skip if RUBY_VERSION.start_with?("2.2") # Fails on CI, I can't install Ruby 2.2 locally to debug https://stackoverflow.com/questions/63926460/install-ruby-2-2-on-mac-osx-catalina-with-ruby-install, https://github.com/postmodern/ruby-install/issues/375
28
+
29
+ in_fork do
30
+ require fixtures_dir("require/autoload_parent.rb")
31
+ parent = assert_node_in_parent("autoload_parent.rb", TOP_REQUIRE)
32
+
33
+ assert_node_in_parent("autoload_child.rb", parent)
34
+ end
14
35
  end
15
36
 
37
+ test "core extension profiles useage" do
38
+ in_fork do
39
+ require fixtures_dir("require/parent_one.rb")
40
+ parent = assert_node_in_parent("parent_one.rb", TOP_REQUIRE)
41
+ assert_node_in_parent("child_one.rb", parent)
42
+ child_two = assert_node_in_parent("child_two.rb", parent)
43
+ assert_node_in_parent("relative_child", parent)
44
+ assert_node_in_parent("relative_child_two", parent)
45
+ assert_node_in_parent("raise_child.rb", child_two)
46
+ end
47
+ end
48
+
49
+ # Checks to see that the given file name is present in the
50
+ # parent tree node and that the memory of that file
51
+ # is less than the parent (since the parent should include itself
52
+ # plus its children)
53
+ #
54
+ # Returns the child node
16
55
  def assert_node_in_parent(file_name, parent)
17
56
  file = fixtures_dir(File.join("require", file_name))
18
57
  node = parent[file]
19
- assert node, "Expected:\n#{parent.children}\nto include:\n#{file.inspect}"
20
- assert node.cost < parent.cost, "Expected:\n#{node.inspect}\nto cost less than:\n#{parent.inspect}" unless parent == TOP_REQUIRE
58
+ assert node, "Expected: #{parent.name} to include: #{file.to_s} but it did not.\nChildren: #{parent.children.map(&:name).map(&:to_s)}"
59
+ unless parent == TOP_REQUIRE
60
+ assert node.cost < parent.cost, "Expected: #{node.name.inspect} (#{node.cost}) to cost less than: #{parent.name.inspect} (#{parent.cost})"
61
+ end
21
62
  node
22
63
  end
23
64
 
24
- test "core extension profiles useage" do
25
- require fixtures_dir("require/parent_one.rb")
26
- parent = assert_node_in_parent("parent_one.rb", TOP_REQUIRE)
27
- assert_node_in_parent("child_one.rb", parent)
28
- child_two = assert_node_in_parent("child_two.rb", parent)
29
- assert_node_in_parent("relative_child", parent)
30
- assert_node_in_parent("relative_child_two", parent)
31
- assert_node_in_parent("raise_child.rb", child_two)
65
+ # Used to get semi-clean process memory
66
+ # It would be better to run the requires in a totally different process
67
+ # but...that would take engineering
68
+ #
69
+ # If I was going to do that, I would find a way to serialize RequireTree
70
+ # into a json structure with file names and costs, run the script
71
+ # dump the json to a file, then in this process read the file and
72
+ # run assertions
73
+ def in_fork
74
+ Tempfile.create("stdout") do |tmp_file|
75
+ pid = fork do
76
+ $stdout.reopen(tmp_file, "w")
77
+ $stderr.reopen(tmp_file, "w")
78
+ $stdout.sync = true
79
+ $stderr.sync = true
80
+ yield
81
+ Kernel.exit!(0) # needed for https://github.com/seattlerb/minitest/pull/683
82
+ end
83
+ Process.waitpid(pid)
84
+
85
+ if $?.success?
86
+ print File.read(tmp_file)
87
+ else
88
+ raise File.read(tmp_file)
89
+ end
90
+ end
32
91
  end
33
92
  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
@@ -9,7 +9,7 @@ class RequireTree < ActiveSupport::TestCase
9
9
  end
10
10
 
11
11
  def teardown
12
- DerailedBenchmarks::RequireTree.const_set("REQUIRED_BY", {})
12
+ DerailedBenchmarks::RequireTree.reset!
13
13
  end
14
14
 
15
15
  test "default_cost" do
@@ -3,6 +3,19 @@
3
3
  require 'test_helper'
4
4
 
5
5
  class StatsFromDirTest < ActiveSupport::TestCase
6
+ test "empty files" do
7
+ Dir.mktmpdir do |dir|
8
+ dir = Pathname.new(dir)
9
+ branch_info = {}
10
+ branch_info["loser"] = { desc: "Old commit", time: Time.now, file: dir.join("loser"), name: "loser" }
11
+ branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner"), name: "winner" }
12
+ stats = DerailedBenchmarks::StatsFromDir.new(branch_info).call
13
+ io = StringIO.new
14
+ stats.call.banner(io: io) # Doesn't error
15
+ assert_equal "", io.read
16
+ end
17
+ end
18
+
6
19
  test "that it works" do
7
20
  dir = fixtures_dir("stats/significant")
8
21
  branch_info = {}
@@ -29,6 +42,23 @@ class StatsFromDirTest < ActiveSupport::TestCase
29
42
  assert_equal "11.3844", format % newest.median
30
43
  end
31
44
 
45
+ test "histogram output" do
46
+ dir = fixtures_dir("stats/significant")
47
+ branch_info = {}
48
+ branch_info["loser"] = { desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), short_sha: "5594a2d" }
49
+ branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), short_sha: "f1ab117" }
50
+ stats = DerailedBenchmarks::StatsFromDir.new(branch_info).call
51
+
52
+ io = StringIO.new
53
+ stats.call.banner(io)
54
+
55
+ puts io.string
56
+
57
+ assert_match(/11\.2 , 11\.28/, io.string)
58
+ assert_match(/11\.8 , 11\.88/, io.string)
59
+ end
60
+
61
+
32
62
  test "alignment" do
33
63
  dir = fixtures_dir("stats/significant")
34
64
  branch_info = {}
@@ -48,10 +78,13 @@ class StatsFromDirTest < ActiveSupport::TestCase
48
78
 
49
79
  test "banner faster" do
50
80
  dir = fixtures_dir("stats/significant")
51
- branch_info = {}
52
- branch_info["loser"] = { desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), name: "loser" }
53
- branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), name: "winner" }
54
- stats = DerailedBenchmarks::StatsFromDir.new(branch_info).call
81
+ Branch_info = {}
82
+
83
+ require 'ostruct'
84
+ commits = []
85
+ commits << OpenStruct.new({ desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), ref: "loser", short_sha: "aaaaa" })
86
+ commits << OpenStruct.new({ desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), ref: "winner", short_sha: "bbbbb" })
87
+ stats = DerailedBenchmarks::StatsFromDir.new(commits).call
55
88
  newest = stats.newest
56
89
  oldest = stats.oldest
57
90
 
@@ -72,12 +105,12 @@ class StatsFromDirTest < ActiveSupport::TestCase
72
105
  11.0
73
106
  end
74
107
 
75
- expected = <<~EOM
76
- [winner] "I am the new commit" - (10.5 seconds)
108
+ expected = <<-EOM
109
+ [bbbbb] (10.5000 seconds) "I am the new commit" ref: "winner"
77
110
  FASTER 🚀🚀🚀 by:
78
111
  1.0476x [older/newer]
79
112
  4.5455% [(older - newer) / older * 100]
80
- [loser] "Old commit" - (11.0 seconds)
113
+ [aaaaa] (11.0000 seconds) "Old commit" ref: "loser"
81
114
  EOM
82
115
 
83
116
  actual = StringIO.new
@@ -103,12 +136,12 @@ EOM
103
136
  11.0
104
137
  end
105
138
 
106
- expected = <<~EOM
107
- [loser] "I am the new commit" - (11.0 seconds)
139
+ expected = <<-EOM
140
+ [loser] (11.0000 seconds) "I am the new commit" ref: "loser"
108
141
  SLOWER 🐢🐢🐢 by:
109
142
  0.9545x [older/newer]
110
143
  -4.7619% [(older - newer) / older * 100]
111
- [winner] "Old commit" - (10.5 seconds)
144
+ [winner] (10.5000 seconds) "Old commit" ref: "winner"
112
145
  EOM
113
146
 
114
147
  actual = StringIO.new
@@ -0,0 +1,5 @@
1
+ @retained = String.new("")
2
+ 1_000_000.times.map { @retained << "A" }
3
+
4
+ module AutoLoadChild
5
+ end
@@ -0,0 +1,8 @@
1
+ @retained = String.new("")
2
+ 1_000_000.times.map { @retained << "A" }
3
+
4
+ autoload :AutoLoadChild, File.join(__dir__, 'autoload_child.rb')
5
+
6
+ if AutoLoadChild
7
+ # yay
8
+ end
@@ -1,4 +1,4 @@
1
1
  class ChildOne
2
- @retained = +""
2
+ @retained = String.new("")
3
3
  50_000.times.map { @retained << "A" }
4
4
  end
@@ -1,5 +1,5 @@
1
1
  class ChildTwo
2
- @retained = +""
2
+ @retained = String.new("")
3
3
  200_000.times.map { @retained << "A" }
4
4
  end
5
5
 
@@ -0,0 +1,3 @@
1
+ @retained = String.new("")
2
+ 1_000_000.times.map { @retained << "A" }
3
+
@@ -0,0 +1,5 @@
1
+ @retained = String.new("")
2
+ 1_000_000.times.map { @retained << "A" }
3
+
4
+ load File.join(__dir__, "load_child.rb")
5
+
@@ -1,5 +1,5 @@
1
1
  class ParentOne
2
- @retained = +""
2
+ @retained = String.new("")
3
3
  1_000_000.times.map { @retained << "A" }
4
4
  end
5
5
  require File.expand_path('../child_one.rb', __FILE__)
@@ -13,27 +13,60 @@ class TasksTest < ActiveSupport::TestCase
13
13
  FileUtils.remove_entry_secure(rails_app_path('tmp'))
14
14
  end
15
15
 
16
+ def run!(cmd)
17
+ puts "Running: #{cmd}"
18
+ out = `#{cmd}`
19
+ raise "Could not run #{cmd}, output: #{out}" unless $?.success?
20
+ out
21
+ end
22
+
16
23
  def rake(cmd, options = {})
17
24
  assert_success = options.key?(:assert_success) ? options[:assert_success] : true
18
25
  env = options[:env] || {}
19
26
  env_string = env.map {|key, value| "#{key.shellescape}=#{value.to_s.shellescape}" }.join(" ")
20
27
  cmd = "env #{env_string} bundle exec rake -f perf.rake #{cmd} --trace"
21
28
  puts "Running: #{cmd}"
22
- result = `cd '#{rails_app_path}' && #{cmd}`
23
- if assert_success
24
- assert $?.success?, "Expected '#{cmd}' to return a success status.\nOutput: #{result}"
29
+ result = Bundler.with_original_env { `cd '#{rails_app_path}' && #{cmd} 2>&1` }
30
+ if assert_success && !$?.success?
31
+ puts result
32
+ raise "Expected '#{cmd}' to return a success status.\nOutput: #{result}"
25
33
  end
26
34
 
27
35
  result
28
36
  end
29
37
 
30
- test 'library_branches' do
38
+ test 'non-rails library with branch specified' do
39
+ skip unless ENV['USING_RAILS_WICKED_BRANCH']
40
+
41
+ gem_path = run!("bundle info wicked --path")
42
+ env = { "TEST_COUNT" => 10, "DERAILED_SCRIPT_COUNT" => 2, "DERAILED_PATH_TO_LIBRARY" => gem_path}
43
+ puts rake "perf:library", { env: env }
44
+ end
45
+
46
+ test 'rails perf:library from git' do
47
+ # BUNDLE_GEMFILE="$(pwd)/gemfiles/rails_git.gemfile" bundle exec m test/integration/tasks_test.rb:<linenumber>
48
+
31
49
  skip unless ENV['USING_RAILS_GIT']
32
50
 
33
- env = { "TEST_COUNT" => 10, "DERAILED_SCRIPT_COUNT" => 2, "SHAS_TO_TEST" => "3054e1d584e7eca110c69a1f8423f2e0866abbf9,80f989aecece1a2b1830e9c953e5887421b52d3c"}
51
+ env = { "TEST_COUNT" => 2, "DERAILED_SCRIPT_COUNT" => 2,
52
+ "SHAS_TO_TEST" => "acb6631cd99cdfe7db356773ef74cad7cbb570ed,12bb9d32f56883914abcd98fd72e3c68c444808d"}
34
53
  puts rake "perf:library", { env: env }
35
54
  end
36
55
 
56
+ test "rails perf:library with bad script" do
57
+ # BUNDLE_GEMFILE="$(pwd)/gemfiles/rails_git.gemfile" bundle exec m test/integration/tasks_test.rb:<linenumber>
58
+
59
+ skip unless ENV['USING_RAILS_GIT']
60
+
61
+ error = assert_raises {
62
+ env = { "DERAILED_SCRIPT" => "nopenopenop", "TEST_COUNT" => 2, "DERAILED_SCRIPT_COUNT" => 2,
63
+ "SHAS_TO_TEST" => "acb6631cd99cdfe7db356773ef74cad7cbb570ed,12bb9d32f56883914abcd98fd72e3c68c444808d"}
64
+ puts rake "perf:library", { env: env }
65
+ }
66
+
67
+ assert error.message =~ /nopenopenop:( command)? not found/, "Expected #{error.message} to include /nopenopenop: (command)? not found/ but it did not"
68
+ end
69
+
37
70
  test 'hitting authenticated devise apps' do
38
71
  env = { "PATH_TO_HIT" => "authenticated", "USE_AUTH" => "true", "TEST_COUNT" => "2" }
39
72
  result = rake 'perf:test', env: env
@@ -55,6 +88,13 @@ class TasksTest < ActiveSupport::TestCase
55
88
  rake "perf:test"
56
89
  end
57
90
 
91
+ test 'app' do
92
+ skip unless ENV['USING_RAILS_GIT']
93
+ run!("cd #{rails_app_path} && git init . && git add . && git commit -m first && git commit --allow-empty -m second")
94
+ env = { "TEST_COUNT" => 10, "DERAILED_SCRIPT_COUNT" => 2 }
95
+ puts rake "perf:app", { env: env }
96
+ end
97
+
58
98
  test 'TEST_COUNT' do
59
99
  result = rake "perf:test", env: { "TEST_COUNT" => 1 }
60
100
  assert_match "1 derailed requests", result
@@ -13,6 +13,8 @@ require 'devise'
13
13
 
14
14
  module Dummy
15
15
  class Application < Rails::Application
16
+ config.load_defaults Rails.version.to_f
17
+
16
18
  config.action_mailer.default_url_options = { host: 'localhost:3000' }
17
19
 
18
20
  # Settings in config/environments/* take precedence over those specified here.
File without changes
@@ -51,7 +51,6 @@ class ActiveSupport::IntegrationCase
51
51
  end
52
52
  end
53
53
 
54
-
55
54
  def fixtures_dir(name = "")
56
55
  root_path("test/fixtures").join(name)
57
56
  end
@@ -63,3 +62,9 @@ end
63
62
  def rails_app_path(name = "")
64
63
  root_path("test/rails_app").join(name)
65
64
  end
65
+
66
+ def run!(cmd)
67
+ output = `#{cmd}`
68
+ raise "Cmd #{cmd} failed:\n#{output}" unless $?.success?
69
+ output
70
+ end