derailed_benchmarks 1.7.0 → 2.1.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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +75 -0
- data/.github/workflows/check_changelog.yml +11 -8
- data/CHANGELOG.md +24 -1
- data/README.md +73 -11
- data/derailed_benchmarks.gemspec +6 -5
- data/gemfiles/rails_5_1.gemfile +3 -1
- data/gemfiles/rails_5_2.gemfile +3 -3
- data/gemfiles/rails_6_1.gemfile +13 -0
- data/gemfiles/rails_git.gemfile +2 -2
- data/lib/derailed_benchmarks.rb +4 -2
- data/lib/derailed_benchmarks/core_ext/kernel_require.rb +20 -9
- data/lib/derailed_benchmarks/git/commit.rb +36 -0
- data/lib/derailed_benchmarks/git/in_path.rb +59 -0
- data/lib/derailed_benchmarks/git/switch_project.rb +128 -0
- data/lib/derailed_benchmarks/git_switch_project.rb +1 -0
- data/lib/derailed_benchmarks/load_tasks.rb +1 -1
- data/lib/derailed_benchmarks/require_tree.rb +11 -1
- data/lib/derailed_benchmarks/{stats_in_file.rb → stats_for_file.rb} +8 -2
- data/lib/derailed_benchmarks/stats_from_dir.rb +40 -21
- data/lib/derailed_benchmarks/tasks.rb +63 -66
- data/lib/derailed_benchmarks/version.rb +1 -1
- data/test/derailed_benchmarks/core_ext/kernel_require_test.rb +70 -11
- data/test/derailed_benchmarks/git_switch_project_test.rb +83 -0
- data/test/derailed_benchmarks/require_tree_test.rb +1 -1
- data/test/derailed_benchmarks/stats_from_dir_test.rb +29 -12
- data/test/derailed_test.rb +15 -0
- data/test/fixtures/require/autoload_child.rb +5 -0
- data/test/fixtures/require/autoload_parent.rb +8 -0
- data/test/fixtures/require/child_one.rb +1 -1
- data/test/fixtures/require/child_two.rb +1 -1
- data/test/fixtures/require/load_child.rb +3 -0
- data/test/fixtures/require/load_parent.rb +5 -0
- data/test/fixtures/require/parent_one.rb +1 -1
- data/test/integration/tasks_test.rb +36 -6
- data/test/rails_app/config/storage.yml +0 -0
- data/test/test_helper.rb +6 -1
- metadata +67 -37
- data/.travis.yml +0 -18
- data/Appraisals +0 -26
| @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            module DerailedBenchmarks
         | 
| 2 | 
            +
              # A class for running commands in a git directory
         | 
| 3 | 
            +
              #
         | 
| 4 | 
            +
              # It's faster to check if we're already in that directory instead
         | 
| 5 | 
            +
              # of having to `cd` into each time. https://twitter.com/schneems/status/1305196730170961920
         | 
| 6 | 
            +
              #
         | 
| 7 | 
            +
              # Example:
         | 
| 8 | 
            +
              #
         | 
| 9 | 
            +
              #   in_git_path = InGitPath.new(`bundle info heapy --path`.strip)
         | 
| 10 | 
            +
              #   in_git_path.checkout!("f0f92b06156f2274021aa42f15326da041ee9009")
         | 
| 11 | 
            +
              #   in_git_path.short_sha # => "f0f92b0"
         | 
| 12 | 
            +
              class Git::InPath
         | 
| 13 | 
            +
                attr_reader :path
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def initialize(path)
         | 
| 16 | 
            +
                  @path = path
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def description
         | 
| 20 | 
            +
                  run!("git log --oneline --format=%B -n 1 HEAD | head -n 1")
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def short_sha
         | 
| 24 | 
            +
                  run!("git rev-parse --short HEAD")
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def time_stamp_string
         | 
| 28 | 
            +
                  run!("git log -n 1 --pretty=format:%ci") # https://stackoverflow.com/a/25921837/147390
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def branch
         | 
| 32 | 
            +
                  branch = run!("git rev-parse --abbrev-ref HEAD")
         | 
| 33 | 
            +
                  branch == "HEAD" ? nil : branch
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def checkout!(ref)
         | 
| 37 | 
            +
                  run!("git checkout '#{ref}' 2>&1")
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def time
         | 
| 41 | 
            +
                  DateTime.parse(time_stamp_string)
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def run(cmd)
         | 
| 45 | 
            +
                  if Dir.pwd == path
         | 
| 46 | 
            +
                    out = `#{cmd}`.strip
         | 
| 47 | 
            +
                  else
         | 
| 48 | 
            +
                    out = `cd #{path} && #{cmd}`.strip
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                  out
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def run!(cmd)
         | 
| 54 | 
            +
                  out = run(cmd)
         | 
| 55 | 
            +
                  raise "Error while running #{cmd.inspect}: #{out}" unless $?.success?
         | 
| 56 | 
            +
                  out
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
            end
         | 
| @@ -0,0 +1,128 @@ | |
| 1 | 
            +
            module DerailedBenchmarks
         | 
| 2 | 
            +
              class Git
         | 
| 3 | 
            +
              end
         | 
| 4 | 
            +
            end
         | 
| 5 | 
            +
            require_relative "in_path.rb"
         | 
| 6 | 
            +
            require_relative "commit.rb"
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module DerailedBenchmarks
         | 
| 9 | 
            +
              # Wraps two or more git commits in a specific location
         | 
| 10 | 
            +
              #
         | 
| 11 | 
            +
              # Returns an array of GitCommit objects that can be used to manipulate
         | 
| 12 | 
            +
              # and checkout the repo
         | 
| 13 | 
            +
              #
         | 
| 14 | 
            +
              # Example:
         | 
| 15 | 
            +
              #
         | 
| 16 | 
            +
              #   `git clone https://sharpstone/default_ruby tmp/default_ruby`
         | 
| 17 | 
            +
              #
         | 
| 18 | 
            +
              #   project = GitSwitchProject.new(path: "tmp/default_ruby")
         | 
| 19 | 
            +
              #
         | 
| 20 | 
            +
              # By default it will represent the last two commits:
         | 
| 21 | 
            +
              #
         | 
| 22 | 
            +
              #   project.commits.length # => 2
         | 
| 23 | 
            +
              #
         | 
| 24 | 
            +
              # You can pass in explicit REFs in an array:
         | 
| 25 | 
            +
              #
         | 
| 26 | 
            +
              #   ref_array = ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "9b19275a592f148e2a53b87ead4ccd8c747539c9"]
         | 
| 27 | 
            +
              #   project = GitSwitchProject.new(path: "tmp/default_ruby", ref_array: ref_array)
         | 
| 28 | 
            +
              #
         | 
| 29 | 
            +
              #   puts project.commits.map(&:ref) == ref_array # => true
         | 
| 30 | 
            +
              #
         | 
| 31 | 
            +
              #
         | 
| 32 | 
            +
              # It knows the current branch or sha:
         | 
| 33 | 
            +
              #
         | 
| 34 | 
            +
              #    `cd tmp/ruby && git checkout -b mybranch`
         | 
| 35 | 
            +
              #    project.current_branch_or_sha #=> "mybranch"
         | 
| 36 | 
            +
              #
         | 
| 37 | 
            +
              # It can be used for safely wrapping checkouts to ensure the project returns to it's original branch:
         | 
| 38 | 
            +
              #
         | 
| 39 | 
            +
              #    project.restore_branch_on_return do
         | 
| 40 | 
            +
              #      project.commits.first.checkout!
         | 
| 41 | 
            +
              #      project.current_branch_or_sha # => "da748a593"
         | 
| 42 | 
            +
              #    end
         | 
| 43 | 
            +
              #
         | 
| 44 | 
            +
              #    project.current_branch_or_sha # => "mybranch"
         | 
| 45 | 
            +
              class Git::SwitchProject
         | 
| 46 | 
            +
                attr_reader :commits
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def initialize(path: , ref_array: [], io: STDOUT, log_dir: "/dev/null")
         | 
| 49 | 
            +
                  @path = Pathname.new(path)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  @in_git_path = Git::InPath.new(@path.expand_path)
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  raise "Must be a path with a .git directory '#{@path}'" if !@path.join(".git").exist?
         | 
| 54 | 
            +
                  @io = io
         | 
| 55 | 
            +
                  @commits = []
         | 
| 56 | 
            +
                  log_dir = Pathname(log_dir)
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  expand_refs(ref_array).each do |ref|
         | 
| 59 | 
            +
                    restore_branch_on_return(quiet: true) do
         | 
| 60 | 
            +
                      @commits << Git::Commit.new(path: @path, ref: ref, log_dir: log_dir)
         | 
| 61 | 
            +
                    end
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  if (duplicate = @commits.group_by(&:short_sha).detect {|(k, v)| v.length > 1})
         | 
| 65 | 
            +
                    raise "Duplicate SHA resolved #{duplicate[0].inspect}: #{duplicate[1].map {|c| "'#{c.ref}' => '#{c.short_sha}'"}.join(", ") } at #{@path}"
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def current_branch_or_sha
         | 
| 70 | 
            +
                  branch_or_sha = @in_git_path.branch
         | 
| 71 | 
            +
                  branch_or_sha ||= @in_git_path.short_sha
         | 
| 72 | 
            +
                  branch_or_sha
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def dirty?
         | 
| 76 | 
            +
                  !clean?
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                # https://stackoverflow.com/a/3879077/147390
         | 
| 80 | 
            +
                def clean?
         | 
| 81 | 
            +
                  @in_git_path.run("git diff-index --quiet HEAD --") && $?.success?
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                private def status(pattern: "*.gemspec")
         | 
| 85 | 
            +
                  @in_git_path.run("git status #{pattern}")
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def restore_branch_on_return(quiet: false)
         | 
| 89 | 
            +
                  if dirty? && status.include?("gemspec")
         | 
| 90 | 
            +
                    dirty_gemspec = true
         | 
| 91 | 
            +
                    unless quiet
         | 
| 92 | 
            +
                      @io.puts "Working tree at #{@path} is dirty, stashing. This will be popped on return"
         | 
| 93 | 
            +
                      @io.puts "Bundler modifies gemspec files on git install, this is normal"
         | 
| 94 | 
            +
                      @io.puts "Original status:\n#{status}"
         | 
| 95 | 
            +
                    end
         | 
| 96 | 
            +
                    @in_git_path.run!("git stash")
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
                  branch_or_sha = self.current_branch_or_sha
         | 
| 99 | 
            +
                  yield
         | 
| 100 | 
            +
                ensure
         | 
| 101 | 
            +
                  return unless branch_or_sha
         | 
| 102 | 
            +
                  @io.puts "Resetting git dir of '#{@path.to_s}' to #{branch_or_sha.inspect}" unless quiet
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  @in_git_path.checkout!(branch_or_sha)
         | 
| 105 | 
            +
                  if dirty_gemspec
         | 
| 106 | 
            +
                    out = @in_git_path.run!("git stash apply 2>&1")
         | 
| 107 | 
            +
                    @io.puts "Applying stash of '#{@path.to_s}':\n#{out}" unless quiet
         | 
| 108 | 
            +
                  end
         | 
| 109 | 
            +
                end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                # case ref_array.length
         | 
| 112 | 
            +
                # when >= 2
         | 
| 113 | 
            +
                #   returns original array
         | 
| 114 | 
            +
                # when 1
         | 
| 115 | 
            +
                #   returns the given ref plus the one before it
         | 
| 116 | 
            +
                # when 0
         | 
| 117 | 
            +
                #   returns the most recent 2 refs
         | 
| 118 | 
            +
                private def expand_refs(ref_array)
         | 
| 119 | 
            +
                  return ref_array if ref_array.length >= 2
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  @in_git_path.checkout!(ref_array.first) if ref_array.first
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  branches_string = @in_git_path.run!("git log --format='%H' -n 2")
         | 
| 124 | 
            +
                  ref_array = branches_string.split($/)
         | 
| 125 | 
            +
                  return ref_array
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
              end
         | 
| 128 | 
            +
            end
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
             | 
| @@ -110,7 +110,7 @@ namespace :perf do | |
| 110 110 | 
             
                      STDERR.puts "Bad request to #{cmd.inspect} \n\n***RESPONSE***:\n\n#{ response.inspect }"
         | 
| 111 111 |  | 
| 112 112 | 
             
                      FileUtils.mkdir_p("tmp")
         | 
| 113 | 
            -
                      File.open("tmp/fail.html", "w+") {|f| f.write response | 
| 113 | 
            +
                      File.open("tmp/fail.html", "w+") {|f| f.write response }
         | 
| 114 114 |  | 
| 115 115 | 
             
                      `open #{File.expand_path("tmp/fail.html")}` if ENV["DERAILED_DEBUG"]
         | 
| 116 116 |  | 
| @@ -13,6 +13,16 @@ module DerailedBenchmarks | |
| 13 13 | 
             
                def initialize(name)
         | 
| 14 14 | 
             
                  @name     = name
         | 
| 15 15 | 
             
                  @children = {}
         | 
| 16 | 
            +
                  @cost = 0
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def self.reset!
         | 
| 20 | 
            +
                  REQUIRED_BY.clear
         | 
| 21 | 
            +
                  if defined?(Kernel::REQUIRE_STACK)
         | 
| 22 | 
            +
                    Kernel::REQUIRE_STACK.clear
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    Kernel::REQUIRE_STACK.push(TOP_REQUIRE)
         | 
| 25 | 
            +
                  end
         | 
| 16 26 | 
             
                end
         | 
| 17 27 |  | 
| 18 28 | 
             
                def <<(tree)
         | 
| @@ -40,7 +50,7 @@ module DerailedBenchmarks | |
| 40 50 | 
             
                end
         | 
| 41 51 |  | 
| 42 52 | 
             
                def to_string
         | 
| 43 | 
            -
                  str =  | 
| 53 | 
            +
                  str = String.new("#{name}: #{cost.round(4)} MiB")
         | 
| 44 54 | 
             
                  if parent && REQUIRED_BY[self.name.to_s]
         | 
| 45 55 | 
             
                    names = REQUIRED_BY[self.name.to_s].uniq - [parent.name.to_s]
         | 
| 46 56 | 
             
                    if names.any?
         | 
| @@ -16,24 +16,30 @@ 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 |  | 
| 33 35 | 
             
                  @median = (values[(values.length - 1) / 2] + values[values.length/ 2]) / 2.0
         | 
| 34 36 | 
             
                  @average = values.inject(:+) / values.length
         | 
| 35 37 | 
             
                end
         | 
| 36 38 |  | 
| 39 | 
            +
                def empty?
         | 
| 40 | 
            +
                  values.empty?
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 37 43 | 
             
                def median
         | 
| 38 44 | 
             
                  @median.to_f
         | 
| 39 45 | 
             
                end
         | 
| @@ -2,9 +2,9 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            require 'bigdecimal'
         | 
| 4 4 | 
             
            require 'statistics'
         | 
| 5 | 
            -
            require 'unicode_plot'
         | 
| 6 5 | 
             
            require 'stringio'
         | 
| 7 6 | 
             
            require 'mini_histogram'
         | 
| 7 | 
            +
            require 'mini_histogram/plot'
         | 
| 8 8 |  | 
| 9 9 | 
             
            module DerailedBenchmarks
         | 
| 10 10 | 
             
              # A class used to read several benchmark files
         | 
| @@ -29,14 +29,28 @@ module DerailedBenchmarks | |
| 29 29 | 
             
                FORMAT = "%0.4f"
         | 
| 30 30 | 
             
                attr_reader :stats, :oldest, :newest
         | 
| 31 31 |  | 
| 32 | 
            -
                def initialize( | 
| 32 | 
            +
                def initialize(input)
         | 
| 33 33 | 
             
                  @files = []
         | 
| 34 34 |  | 
| 35 | 
            -
                   | 
| 36 | 
            -
                     | 
| 37 | 
            -
                     | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 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
         | 
| 40 54 | 
             
                  end
         | 
| 41 55 | 
             
                  @files.sort_by! { |f| f.time }
         | 
| 42 56 | 
             
                  @oldest = @files.first
         | 
| @@ -46,6 +60,8 @@ module DerailedBenchmarks | |
| 46 60 | 
             
                def call
         | 
| 47 61 | 
             
                  @files.each(&:call)
         | 
| 48 62 |  | 
| 63 | 
            +
                  return self if @files.detect(&:empty?)
         | 
| 64 | 
            +
             | 
| 49 65 | 
             
                  stats_95 = statistical_test(confidence: 95)
         | 
| 50 66 |  | 
| 51 67 | 
             
                  # If default check is good, see if we also pass a more rigorous test
         | 
| @@ -104,25 +120,28 @@ module DerailedBenchmarks | |
| 104 120 | 
             
                end
         | 
| 105 121 |  | 
| 106 122 | 
             
                def histogram(io = $stdout)
         | 
| 107 | 
            -
                   | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
                     | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
                       | 
| 123 | 
            +
                  dual_histogram = MiniHistogram.dual_plot do |a, b|
         | 
| 124 | 
            +
                    a.values = newest.values
         | 
| 125 | 
            +
                    a.options = {
         | 
| 126 | 
            +
                      title: "\n   [#{newest.short_sha || newest.name}] description:\n     #{newest.desc.inspect}",
         | 
| 127 | 
            +
                      xlabel: "# of runs in range"
         | 
| 128 | 
            +
                    }
         | 
| 129 | 
            +
                    b.values = oldest.values
         | 
| 130 | 
            +
                    b.options = {
         | 
| 131 | 
            +
                      title: "\n   [#{oldest.short_sha || oldest.name}] description:\n     #{oldest.desc.inspect}",
         | 
| 116 132 | 
             
                      xlabel: "# of runs in range"
         | 
| 117 | 
            -
                     | 
| 118 | 
            -
                    plot.render(io)
         | 
| 119 | 
            -
                    io.puts
         | 
| 133 | 
            +
                    }
         | 
| 120 134 | 
             
                  end
         | 
| 121 135 |  | 
| 122 136 | 
             
                  io.puts
         | 
| 137 | 
            +
                  io.puts "Histograms (time ranges are in seconds):"
         | 
| 138 | 
            +
                  io.puts(dual_histogram)
         | 
| 139 | 
            +
                  io.puts
         | 
| 123 140 | 
             
                end
         | 
| 124 141 |  | 
| 125 142 | 
             
                def banner(io = $stdout)
         | 
| 143 | 
            +
                  return if @files.detect(&:empty?)
         | 
| 144 | 
            +
             | 
| 126 145 | 
             
                  io.puts
         | 
| 127 146 | 
             
                  if significant?
         | 
| 128 147 | 
             
                    io.puts "❤️ ❤️ ❤️  (Statistically Significant) ❤️ ❤️ ❤️"
         | 
| @@ -130,11 +149,11 @@ module DerailedBenchmarks | |
| 130 149 | 
             
                    io.puts "👎👎👎(NOT Statistically Significant) 👎👎👎"
         | 
| 131 150 | 
             
                  end
         | 
| 132 151 | 
             
                  io.puts
         | 
| 133 | 
            -
                  io.puts "[#{newest.name}] #{newest.desc.inspect}  | 
| 152 | 
            +
                  io.puts "[#{newest.short_sha || newest.name}] (#{FORMAT % newest.median} seconds) #{newest.desc.inspect} ref: #{newest.name.inspect}"
         | 
| 134 153 | 
             
                  io.puts "  #{change_direction} by:"
         | 
| 135 154 | 
             
                  io.puts "    #{align}#{FORMAT % x_faster}x [older/newer]"
         | 
| 136 155 | 
             
                  io.puts "    #{FORMAT % percent_faster}\% [(older - newer) / older * 100]"
         | 
| 137 | 
            -
                  io.puts "[#{oldest.name}] #{oldest.desc.inspect}  | 
| 156 | 
            +
                  io.puts "[#{oldest.short_sha || oldest.name}] (#{FORMAT % oldest.median} seconds) #{oldest.desc.inspect} ref: #{oldest.name.inspect}"
         | 
| 138 157 | 
             
                  io.puts
         | 
| 139 158 | 
             
                  io.puts "Iterations per sample: #{ENV["TEST_COUNT"]}"
         | 
| 140 159 | 
             
                  io.puts "Samples: #{newest.values.length}"
         | 
| @@ -17,96 +17,57 @@ namespace :perf do | |
| 17 17 | 
             
                  script = ENV["DERAILED_SCRIPT"] || "bundle exec derailed exec perf:test"
         | 
| 18 18 |  | 
| 19 19 | 
             
                  if ENV["DERAILED_PATH_TO_LIBRARY"]
         | 
| 20 | 
            -
                    library_dir = ENV["DERAILED_PATH_TO_LIBRARY"]
         | 
| 20 | 
            +
                    library_dir = ENV["DERAILED_PATH_TO_LIBRARY"].chomp
         | 
| 21 21 | 
             
                  else
         | 
| 22 22 | 
             
                    library_dir = DerailedBenchmarks.rails_path_on_disk
         | 
| 23 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 }
         | 
| 24 | 
            +
                  library_dir = Pathname.new(library_dir)
         | 
| 47 25 |  | 
| 48 26 | 
             
                  out_dir = Pathname.new("tmp/compare_branches/#{Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')}")
         | 
| 49 27 | 
             
                  out_dir.mkpath
         | 
| 50 28 |  | 
| 51 | 
            -
                   | 
| 52 | 
            -
                  branch_info = {}
         | 
| 53 | 
            -
                  branch_to_sha = {}
         | 
| 29 | 
            +
                  ref_string = ENV["SHAS_TO_TEST"] || ENV["REFS_TO_TEST"] || ""
         | 
| 54 30 |  | 
| 55 | 
            -
                   | 
| 56 | 
            -
                     | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
                      short_sha   = run!("git rev-parse --short HEAD").strip
         | 
| 61 | 
            -
                      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 | 
            +
                  )
         | 
| 62 36 |  | 
| 63 | 
            -
             | 
| 64 | 
            -
                    end
         | 
| 65 | 
            -
                    run!("#{script}")
         | 
| 66 | 
            -
                  end
         | 
| 37 | 
            +
                  stats = DerailedBenchmarks::StatsFromDir.new(project.commits)
         | 
| 67 38 |  | 
| 39 | 
            +
                  # Advertise branch names early to make sure people know what they're testing
         | 
| 68 40 | 
             
                  puts
         | 
| 69 41 | 
             
                  puts
         | 
| 70 | 
            -
                   | 
| 71 | 
            -
                    short_sha  | 
| 72 | 
            -
                    desc      = branch_info[short_sha][:desc]
         | 
| 73 | 
            -
                    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}"
         | 
| 74 44 | 
             
                  end
         | 
| 75 45 | 
             
                  puts
         | 
| 76 46 | 
             
                  puts
         | 
| 77 47 |  | 
| 78 | 
            -
                   | 
| 79 | 
            -
             | 
| 80 | 
            -
             | 
| 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!
         | 
| 81 53 |  | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 84 | 
            -
             | 
| 85 | 
            -
                      Dir.chdir(library_dir) { run!("git checkout '#{branch}'") }
         | 
| 86 | 
            -
                      run!(" #{script} 2>&1 | tail -n 1 >> '#{file}'")
         | 
| 87 | 
            -
                    end
         | 
| 54 | 
            +
                        output = run!("#{script} 2>&1")
         | 
| 55 | 
            +
                        commit.log.open("a") {|f| f.puts output.lines.last }
         | 
| 56 | 
            +
                      end
         | 
| 88 57 |  | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 91 | 
            -
             | 
| 92 | 
            -
             | 
| 93 | 
            -
                       | 
| 58 | 
            +
                      if (i % 50).zero?
         | 
| 59 | 
            +
                        puts "Intermediate result"
         | 
| 60 | 
            +
                        stats.call.banner
         | 
| 61 | 
            +
                        puts "Continuing execution"
         | 
| 62 | 
            +
                      end
         | 
| 94 63 | 
             
                    end
         | 
| 95 64 | 
             
                  end
         | 
| 96 65 |  | 
| 97 66 | 
             
                ensure
         | 
| 98 | 
            -
                  if library_dir && current_library_branch
         | 
| 99 | 
            -
                    puts "Resetting git dir of '#{library_dir.to_s}' to #{current_library_branch.inspect}"
         | 
| 100 | 
            -
                    Dir.chdir(library_dir) do
         | 
| 101 | 
            -
                      run!("git checkout '#{current_library_branch}'")
         | 
| 102 | 
            -
                    end
         | 
| 103 | 
            -
                  end
         | 
| 104 | 
            -
             | 
| 105 67 | 
             
                  if stats
         | 
| 106 | 
            -
                    stats.call
         | 
| 107 | 
            -
                    stats.banner
         | 
| 68 | 
            +
                    stats.call.banner
         | 
| 108 69 |  | 
| 109 | 
            -
                    result_file = out_dir | 
| 70 | 
            +
                    result_file = out_dir.join("results.txt")
         | 
| 110 71 | 
             
                    File.open(result_file, "w") do |f|
         | 
| 111 72 | 
             
                      stats.banner(f)
         | 
| 112 73 | 
             
                    end
         | 
| @@ -286,6 +247,42 @@ namespace :perf do | |
| 286 247 | 
             
                puts "Also try uploading #{file_name.inspect} to http://tenderlove.github.io/heap-analyzer/"
         | 
| 287 248 | 
             
              end
         | 
| 288 249 |  | 
| 250 | 
            +
              desc "three heaps generation for comparison."
         | 
| 251 | 
            +
              task :heap_diff => [:setup] do
         | 
| 252 | 
            +
                require 'objspace'
         | 
| 253 | 
            +
             | 
| 254 | 
            +
                launch_time = Time.now.iso8601
         | 
| 255 | 
            +
                FileUtils.mkdir_p("tmp")
         | 
| 256 | 
            +
                ObjectSpace.trace_object_allocations_start
         | 
| 257 | 
            +
                3.times do |i|
         | 
| 258 | 
            +
                  file_name = "tmp/#{launch_time}-heap-#{i}.ndjson"
         | 
| 259 | 
            +
                  puts "Running #{ TEST_COUNT } times"
         | 
| 260 | 
            +
                  TEST_COUNT.times {
         | 
| 261 | 
            +
                    call_app
         | 
| 262 | 
            +
                  }
         | 
| 263 | 
            +
                  GC.start
         | 
| 264 | 
            +
             | 
| 265 | 
            +
                  puts "Heap file generated: #{ file_name.inspect }"
         | 
| 266 | 
            +
                  ObjectSpace.dump_all(output: File.open(file_name, 'w'))
         | 
| 267 | 
            +
                end
         | 
| 268 | 
            +
             | 
| 269 | 
            +
                require 'heapy'
         | 
| 270 | 
            +
             | 
| 271 | 
            +
                puts ""
         | 
| 272 | 
            +
                puts "Diff"
         | 
| 273 | 
            +
                puts "===="
         | 
| 274 | 
            +
                Heapy::Diff.new(
         | 
| 275 | 
            +
                  before: "tmp/#{launch_time}-heap-0.ndjson",
         | 
| 276 | 
            +
                  after: "tmp/#{launch_time}-heap-1.ndjson",
         | 
| 277 | 
            +
                  retained: "tmp/#{launch_time}-heap-2.ndjson"
         | 
| 278 | 
            +
                ).call
         | 
| 279 | 
            +
             | 
| 280 | 
            +
                puts ""
         | 
| 281 | 
            +
                puts "Run `$ heapy --help` for more options"
         | 
| 282 | 
            +
                puts ""
         | 
| 283 | 
            +
                puts "Also read https://speakerdeck.com/samsaffron/why-ruby-2-dot-1-excites-me?slide=27 to understand better what you are reading."
         | 
| 284 | 
            +
              end
         | 
| 285 | 
            +
             | 
| 289 286 | 
             
              def run!(cmd)
         | 
| 290 287 | 
             
                out = `#{cmd}`
         | 
| 291 288 | 
             
                raise "Error while running #{cmd.inspect}: #{out}" unless $?.success?
         |