gitlab-derailed_benchmarks 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/check_changelog.yml +10 -0
- data/.gitignore +8 -0
- data/.gitlab-ci.yml +56 -0
- data/.travis.yml +18 -0
- data/Appraisals +26 -0
- data/CHANGELOG.md +105 -0
- data/Gemfile +9 -0
- data/README.md +692 -0
- data/Rakefile +29 -0
- data/bin/derailed +93 -0
- data/derailed_benchmarks.gemspec +39 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_5_1.gemfile +15 -0
- data/gemfiles/rails_5_2.gemfile +15 -0
- data/gemfiles/rails_6_0.gemfile +15 -0
- data/gemfiles/rails_git.gemfile +19 -0
- data/lib/derailed_benchmarks.rb +51 -0
- data/lib/derailed_benchmarks/auth_helper.rb +34 -0
- data/lib/derailed_benchmarks/auth_helpers/devise.rb +41 -0
- data/lib/derailed_benchmarks/core_ext/kernel_require.rb +88 -0
- data/lib/derailed_benchmarks/load_tasks.rb +145 -0
- data/lib/derailed_benchmarks/require_tree.rb +65 -0
- data/lib/derailed_benchmarks/stats_from_dir.rb +128 -0
- data/lib/derailed_benchmarks/stats_in_file.rb +60 -0
- data/lib/derailed_benchmarks/tasks.rb +292 -0
- data/lib/derailed_benchmarks/version.rb +5 -0
- data/test/derailed_benchmarks/core_ext/kernel_require_test.rb +33 -0
- data/test/derailed_benchmarks/require_tree_test.rb +95 -0
- data/test/derailed_benchmarks/stats_from_dir_test.rb +125 -0
- data/test/derailed_test.rb +14 -0
- data/test/fixtures/require/child_one.rb +4 -0
- data/test/fixtures/require/child_two.rb +9 -0
- data/test/fixtures/require/parent_one.rb +8 -0
- data/test/fixtures/require/raise_child.rb +6 -0
- data/test/fixtures/require/relative_child.rb +4 -0
- data/test/fixtures/require/relative_child_two.rb +4 -0
- data/test/fixtures/stats/significant/loser.bench.txt +100 -0
- data/test/fixtures/stats/significant/winner.bench.txt +100 -0
- data/test/integration/tasks_test.rb +132 -0
- data/test/rails_app/Rakefile +9 -0
- data/test/rails_app/app/assets/config/manifest.js +0 -0
- data/test/rails_app/app/assets/javascripts/authenticated.js +2 -0
- data/test/rails_app/app/assets/stylesheets/authenticated.css +4 -0
- data/test/rails_app/app/controllers/application_controller.rb +17 -0
- data/test/rails_app/app/controllers/authenticated_controller.rb +8 -0
- data/test/rails_app/app/controllers/pages_controller.rb +14 -0
- data/test/rails_app/app/helpers/application_helper.rb +4 -0
- data/test/rails_app/app/helpers/authenticated_helper.rb +4 -0
- data/test/rails_app/app/models/user.rb +13 -0
- data/test/rails_app/app/views/authenticated/index.html.erb +1 -0
- data/test/rails_app/app/views/layouts/application.html.erb +14 -0
- data/test/rails_app/app/views/pages/index.html.erb +1 -0
- data/test/rails_app/config.ru +6 -0
- data/test/rails_app/config/application.rb +52 -0
- data/test/rails_app/config/boot.rb +12 -0
- data/test/rails_app/config/database.yml +22 -0
- data/test/rails_app/config/environment.rb +11 -0
- data/test/rails_app/config/environments/development.rb +27 -0
- data/test/rails_app/config/environments/production.rb +51 -0
- data/test/rails_app/config/environments/test.rb +37 -0
- data/test/rails_app/config/initializers/backtrace_silencers.rb +9 -0
- data/test/rails_app/config/initializers/devise.rb +258 -0
- data/test/rails_app/config/initializers/inflections.rb +12 -0
- data/test/rails_app/config/initializers/mime_types.rb +7 -0
- data/test/rails_app/config/initializers/secret_token.rb +13 -0
- data/test/rails_app/config/initializers/session_store.rb +10 -0
- data/test/rails_app/config/locales/devise.en.yml +59 -0
- data/test/rails_app/config/locales/en.yml +9 -0
- data/test/rails_app/config/locales/es.yml +10 -0
- data/test/rails_app/config/routes.rb +67 -0
- data/test/rails_app/db/migrate/20141210070547_devise_create_users.rb +45 -0
- data/test/rails_app/db/schema.rb +35 -0
- data/test/rails_app/perf.rake +10 -0
- data/test/rails_app/public/404.html +26 -0
- data/test/rails_app/public/422.html +26 -0
- data/test/rails_app/public/500.html +26 -0
- data/test/rails_app/public/favicon.ico +0 -0
- data/test/rails_app/public/javascripts/application.js +2 -0
- data/test/rails_app/public/javascripts/controls.js +965 -0
- data/test/rails_app/public/javascripts/dragdrop.js +974 -0
- data/test/rails_app/public/javascripts/effects.js +1123 -0
- data/test/rails_app/public/javascripts/prototype.js +6001 -0
- data/test/rails_app/public/javascripts/rails.js +202 -0
- data/test/rails_app/public/stylesheets/.gitkeep +0 -0
- data/test/rails_app/script/rails +8 -0
- data/test/support/integration_case.rb +7 -0
- data/test/test_helper.rb +65 -0
- metadata +398 -0
| @@ -0,0 +1,145 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            namespace :perf do
         | 
| 4 | 
            +
              task :rails_load do
         | 
| 5 | 
            +
                ENV["RAILS_ENV"] ||= "production"
         | 
| 6 | 
            +
                ENV['RACK_ENV']  = ENV["RAILS_ENV"]
         | 
| 7 | 
            +
                ENV["DISABLE_SPRING"] = "true"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                ENV["SECRET_KEY_BASE"] ||= "foofoofoo"
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                ENV['LOG_LEVEL'] ||= "FATAL"
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                require 'rails'
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                puts "Booting: #{Rails.env}"
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                %W{ . lib test config }.each do |file|
         | 
| 18 | 
            +
                  $LOAD_PATH << File.expand_path(file)
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                require 'application'
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                Rails.env = ENV["RAILS_ENV"]
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                DERAILED_APP = Rails.application
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                if DERAILED_APP.respond_to?(:initialized?)
         | 
| 28 | 
            +
                  DERAILED_APP.initialize! unless DERAILED_APP.initialized?
         | 
| 29 | 
            +
                else
         | 
| 30 | 
            +
                  DERAILED_APP.initialize! unless DERAILED_APP.instance_variable_get(:@initialized)
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                if !ENV["DERAILED_SKIP_ACTIVE_RECORD"] && defined? ActiveRecord
         | 
| 34 | 
            +
                  if defined? ActiveRecord::Tasks::DatabaseTasks
         | 
| 35 | 
            +
                    ActiveRecord::Tasks::DatabaseTasks.create_current
         | 
| 36 | 
            +
                  else # Rails 3.2
         | 
| 37 | 
            +
                    raise "No valid database for #{ENV['RAILS_ENV']}, please create one" unless ActiveRecord::Base.connection.active?.inspect
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  ActiveRecord::Migrator.migrations_paths = DERAILED_APP.paths['db/migrate'].to_a
         | 
| 41 | 
            +
                  ActiveRecord::Migration.verbose         = true
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  if Rails.version.start_with? '6'
         | 
| 44 | 
            +
                    ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths, ActiveRecord::SchemaMigration).migrate
         | 
| 45 | 
            +
                  elsif Rails.version.start_with? '5.2'
         | 
| 46 | 
            +
                    ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths).migrate
         | 
| 47 | 
            +
                  else
         | 
| 48 | 
            +
                    ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, nil)
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                DERAILED_APP.config.consider_all_requests_local = true
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              task :rack_load do
         | 
| 56 | 
            +
                puts "You're not using Rails"
         | 
| 57 | 
            +
                puts "You need to tell derailed how to boot your app"
         | 
| 58 | 
            +
                puts "In your perf.rake add:"
         | 
| 59 | 
            +
                puts
         | 
| 60 | 
            +
                puts "namespace :perf do"
         | 
| 61 | 
            +
                puts "  task :rack_load do"
         | 
| 62 | 
            +
                puts "    # DERAILED_APP = your code here"
         | 
| 63 | 
            +
                puts "  end"
         | 
| 64 | 
            +
                puts "end"
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
              task :setup do
         | 
| 68 | 
            +
                if DerailedBenchmarks.gem_is_bundled?("railties")
         | 
| 69 | 
            +
                  Rake::Task["perf:rails_load"].invoke
         | 
| 70 | 
            +
                else
         | 
| 71 | 
            +
                  Rake::Task["perf:rack_load"].invoke
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                WARM_COUNT  = (ENV['WARM_COUNT'] || 0).to_i
         | 
| 75 | 
            +
                TEST_COUNT  = (ENV['TEST_COUNT'] || ENV['CNT'] || 1_000).to_i
         | 
| 76 | 
            +
                PATH_TO_HIT = ENV["PATH_TO_HIT"] || ENV['ENDPOINT'] || "/"
         | 
| 77 | 
            +
                puts "Endpoint: #{ PATH_TO_HIT.inspect }"
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                HTTP_HEADER_PREFIX = "HTTP_".freeze
         | 
| 80 | 
            +
                RACK_HTTP_HEADERS = ENV.select { |key| key.start_with?(HTTP_HEADER_PREFIX) }
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                HTTP_HEADERS = RACK_HTTP_HEADERS.keys.inject({}) do |hash, rack_header_name|
         | 
| 83 | 
            +
                  # e.g. "HTTP_ACCEPT_CHARSET" -> "Accept-Charset"
         | 
| 84 | 
            +
                  header_name = rack_header_name[HTTP_HEADER_PREFIX.size..-1].split("_").map(&:downcase).map(&:capitalize).join("-")
         | 
| 85 | 
            +
                  hash[header_name] = RACK_HTTP_HEADERS[rack_header_name]
         | 
| 86 | 
            +
                  hash
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
                puts "HTTP headers: #{HTTP_HEADERS}" unless HTTP_HEADERS.empty?
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                CURL_HTTP_HEADER_ARGS = HTTP_HEADERS.map { |http_header_name, value| "-H \"#{http_header_name}: #{value}\"" }.join(" ")
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                require 'rack/test'
         | 
| 93 | 
            +
                require 'rack/file'
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                DERAILED_APP = DerailedBenchmarks.add_auth(Object.class_eval { remove_const(:DERAILED_APP) })
         | 
| 96 | 
            +
                if server = ENV["USE_SERVER"]
         | 
| 97 | 
            +
                  @port = (3000..3900).to_a.sample
         | 
| 98 | 
            +
                  puts "Port: #{ @port.inspect }"
         | 
| 99 | 
            +
                  puts "Server: #{ server.inspect }"
         | 
| 100 | 
            +
                  thread = Thread.new do
         | 
| 101 | 
            +
                    Rack::Server.start(app: DERAILED_APP, :Port => @port, environment: "none", server: server)
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
                  sleep 1
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                  def call_app(path = File.join("/", PATH_TO_HIT))
         | 
| 106 | 
            +
                    cmd = "curl #{CURL_HTTP_HEADER_ARGS} 'http://localhost:#{@port}#{path}' -s --fail 2>&1"
         | 
| 107 | 
            +
                    response = `#{cmd}`
         | 
| 108 | 
            +
                    unless $?.success?
         | 
| 109 | 
            +
                      STDERR.puts "Couldn't call app."
         | 
| 110 | 
            +
                      STDERR.puts "Bad request to #{cmd.inspect} \n\n***RESPONSE***:\n\n#{ response.inspect }"
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                      FileUtils.mkdir_p("tmp")
         | 
| 113 | 
            +
                      File.open("tmp/fail.html", "w+") {|f| f.write response.body }
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                      `open #{File.expand_path("tmp/fail.html")}` if ENV["DERAILED_DEBUG"]
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                      exit(1)
         | 
| 118 | 
            +
                    end
         | 
| 119 | 
            +
                  end
         | 
| 120 | 
            +
                else
         | 
| 121 | 
            +
                  @app = Rack::MockRequest.new(DERAILED_APP)
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  def call_app
         | 
| 124 | 
            +
                    response = @app.get(PATH_TO_HIT, RACK_HTTP_HEADERS)
         | 
| 125 | 
            +
                    if response.status != 200
         | 
| 126 | 
            +
                      STDERR.puts "Couldn't call app. Bad request to #{PATH_TO_HIT}! Resulted in #{response.status} status."
         | 
| 127 | 
            +
                      STDERR.puts "\n\n***RESPONSE BODY***\n\n"
         | 
| 128 | 
            +
                      STDERR.puts response.body
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                      FileUtils.mkdir_p("tmp")
         | 
| 131 | 
            +
                      File.open("tmp/fail.html", "w+") {|f| f.write response.body }
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                      `open #{File.expand_path("tmp/fail.html")}` if ENV["DERAILED_DEBUG"]
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                      exit(1)
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
                    response
         | 
| 138 | 
            +
                  end
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
                if WARM_COUNT > 0
         | 
| 141 | 
            +
                  puts "Warming up app: #{WARM_COUNT} times"
         | 
| 142 | 
            +
                  WARM_COUNT.times { call_app }
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
              end
         | 
| 145 | 
            +
            end
         | 
| @@ -0,0 +1,65 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Tree structure used to store and sort require memory costs
         | 
| 4 | 
            +
            # RequireTree.new('get_process_mem')
         | 
| 5 | 
            +
            module DerailedBenchmarks
         | 
| 6 | 
            +
              class RequireTree
         | 
| 7 | 
            +
                REQUIRED_BY = {}
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                attr_reader   :name
         | 
| 10 | 
            +
                attr_writer   :cost
         | 
| 11 | 
            +
                attr_accessor :parent
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def initialize(name)
         | 
| 14 | 
            +
                  @name     = name
         | 
| 15 | 
            +
                  @children = {}
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def <<(tree)
         | 
| 19 | 
            +
                  @children[tree.name.to_s] = tree
         | 
| 20 | 
            +
                  tree.parent = self
         | 
| 21 | 
            +
                  (REQUIRED_BY[tree.name.to_s] ||= []) << self.name
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def [](name)
         | 
| 25 | 
            +
                  @children[name.to_s]
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                # Returns array of child nodes
         | 
| 29 | 
            +
                def children
         | 
| 30 | 
            +
                  @children.values
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def cost
         | 
| 34 | 
            +
                  @cost || 0
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                # Returns sorted array of child nodes from Largest to Smallest
         | 
| 38 | 
            +
                def sorted_children
         | 
| 39 | 
            +
                  children.sort { |c1, c2| c2.cost <=> c1.cost }
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def to_string
         | 
| 43 | 
            +
                  str = +"#{name}: #{cost.round(4)} MiB"
         | 
| 44 | 
            +
                  if parent && REQUIRED_BY[self.name.to_s]
         | 
| 45 | 
            +
                    names = REQUIRED_BY[self.name.to_s].uniq - [parent.name.to_s]
         | 
| 46 | 
            +
                    if names.any?
         | 
| 47 | 
            +
                      str << " (Also required by: #{ names.first(2).join(", ") }"
         | 
| 48 | 
            +
                      str << ", and #{names.count - 2} others" if names.count > 3
         | 
| 49 | 
            +
                      str << ")"
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
                  str
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                # Recursively prints all child nodes
         | 
| 56 | 
            +
                def print_sorted_children(level = 0, out = STDOUT)
         | 
| 57 | 
            +
                  return if cost < ENV['CUT_OFF'].to_f
         | 
| 58 | 
            +
                  out.puts "  " * level + self.to_string
         | 
| 59 | 
            +
                  level += 1
         | 
| 60 | 
            +
                  sorted_children.each do |child|
         | 
| 61 | 
            +
                    child.print_sorted_children(level, out)
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
            end
         | 
| @@ -0,0 +1,128 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'bigdecimal'
         | 
| 4 | 
            +
            require 'statistics'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module DerailedBenchmarks
         | 
| 7 | 
            +
              # A class used to read several benchmark files
         | 
| 8 | 
            +
              # it will parse each file, then sort by average
         | 
| 9 | 
            +
              # time of benchmarks. It can be used to find
         | 
| 10 | 
            +
              # the fastest and slowest examples and give information
         | 
| 11 | 
            +
              # about them such as what the percent difference is
         | 
| 12 | 
            +
              # and if the results are statistically significant
         | 
| 13 | 
            +
              #
         | 
| 14 | 
            +
              # Example:
         | 
| 15 | 
            +
              #
         | 
| 16 | 
            +
              #   branch_info = {}
         | 
| 17 | 
            +
              #   branch_info["loser"]  = { desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), name: "loser" }
         | 
| 18 | 
            +
              #   branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), name: "winner" }
         | 
| 19 | 
            +
              #   stats = DerailedBenchmarks::StatsFromDir.new(branch_info)
         | 
| 20 | 
            +
              #
         | 
| 21 | 
            +
              #   stats.newest.average  # => 10.5
         | 
| 22 | 
            +
              #   stats.oldest.average  # => 11.0
         | 
| 23 | 
            +
              #   stats.significant?    # => true
         | 
| 24 | 
            +
              #   stats.x_faster        # => "1.0476"
         | 
| 25 | 
            +
              class StatsFromDir
         | 
| 26 | 
            +
                FORMAT = "%0.4f"
         | 
| 27 | 
            +
                attr_reader :stats, :oldest, :newest
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def initialize(hash)
         | 
| 30 | 
            +
                  @files = []
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  hash.each do |branch, info_hash|
         | 
| 33 | 
            +
                    file = info_hash.fetch(:file)
         | 
| 34 | 
            +
                    desc = info_hash.fetch(:desc)
         | 
| 35 | 
            +
                    time = info_hash.fetch(:time)
         | 
| 36 | 
            +
                    @files << StatsForFile.new(file: file, desc: desc, time: time, name: branch)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                  @files.sort_by! { |f| f.time }
         | 
| 39 | 
            +
                  @oldest = @files.first
         | 
| 40 | 
            +
                  @newest = @files.last
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def call
         | 
| 44 | 
            +
                  @files.each(&:call)
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  stats_95 = statistical_test(confidence: 95)
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  # If default check is good, see if we also pass a more rigorous test
         | 
| 49 | 
            +
                  # if so, then use the more rigourous test
         | 
| 50 | 
            +
                  if stats_95[:alternative]
         | 
| 51 | 
            +
                    stats_99 = statistical_test(confidence: 99)
         | 
| 52 | 
            +
                    @stats = stats_99 if stats_99[:alternative]
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                  @stats ||= stats_95
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  self
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def statistical_test(series_1=oldest.values, series_2=newest.values, confidence: 95)
         | 
| 60 | 
            +
                  StatisticalTest::KSTest.two_samples(
         | 
| 61 | 
            +
                    group_one: series_1,
         | 
| 62 | 
            +
                    group_two: series_2,
         | 
| 63 | 
            +
                    alpha: (100 - confidence) / 100.0
         | 
| 64 | 
            +
                  )
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def significant?
         | 
| 68 | 
            +
                  @stats[:alternative]
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def d_max
         | 
| 72 | 
            +
                  @stats[:d_max].to_f
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def d_critical
         | 
| 76 | 
            +
                  @stats[:d_critical].to_f
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def x_faster
         | 
| 80 | 
            +
                  (oldest.median/newest.median).to_f
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                def faster?
         | 
| 84 | 
            +
                  newest.median < oldest.median
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                def percent_faster
         | 
| 88 | 
            +
                  (((oldest.median - newest.median) / oldest.median).to_f  * 100)
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                def change_direction
         | 
| 92 | 
            +
                  if faster?
         | 
| 93 | 
            +
                    "FASTER 🚀🚀🚀"
         | 
| 94 | 
            +
                  else
         | 
| 95 | 
            +
                    "SLOWER 🐢🐢🐢"
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                def align
         | 
| 100 | 
            +
                  " " * (percent_faster.to_s.index(".") - x_faster.to_s.index("."))
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                def banner(io = Kernel)
         | 
| 104 | 
            +
                  io.puts
         | 
| 105 | 
            +
                  if significant?
         | 
| 106 | 
            +
                    io.puts "❤️ ❤️ ❤️  (Statistically Significant) ❤️ ❤️ ❤️"
         | 
| 107 | 
            +
                  else
         | 
| 108 | 
            +
                    io.puts "👎👎👎(NOT Statistically Significant) 👎👎👎"
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
                  io.puts
         | 
| 111 | 
            +
                  io.puts "[#{newest.name}] #{newest.desc.inspect} - (#{newest.median} seconds)"
         | 
| 112 | 
            +
                  io.puts "  #{change_direction} by:"
         | 
| 113 | 
            +
                  io.puts "    #{align}#{FORMAT % x_faster}x [older/newer]"
         | 
| 114 | 
            +
                  io.puts "    #{FORMAT % percent_faster}\% [(older - newer) / older * 100]"
         | 
| 115 | 
            +
                  io.puts "[#{oldest.name}] #{oldest.desc.inspect} - (#{oldest.median} seconds)"
         | 
| 116 | 
            +
                  io.puts
         | 
| 117 | 
            +
                  io.puts "Iterations per sample: #{ENV["TEST_COUNT"]}"
         | 
| 118 | 
            +
                  io.puts "Samples: #{newest.values.length}"
         | 
| 119 | 
            +
                  io.puts
         | 
| 120 | 
            +
                  io.puts "Test type: Kolmogorov Smirnov"
         | 
| 121 | 
            +
                  io.puts "Confidence level: #{@stats[:confidence_level] * 100} %"
         | 
| 122 | 
            +
                  io.puts "Is significant? (max > critical): #{significant?}"
         | 
| 123 | 
            +
                  io.puts "D critical: #{d_critical}"
         | 
| 124 | 
            +
                  io.puts "D max: #{d_max}"
         | 
| 125 | 
            +
                  io.puts
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
              end
         | 
| 128 | 
            +
            end
         | 
| @@ -0,0 +1,60 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module DerailedBenchmarks
         | 
| 4 | 
            +
              # A class for reading in benchmark results
         | 
| 5 | 
            +
              # and converting them to numbers for comparison
         | 
| 6 | 
            +
              #
         | 
| 7 | 
            +
              # Example:
         | 
| 8 | 
            +
              #
         | 
| 9 | 
            +
              #  puts `cat muhfile.bench.txt`
         | 
| 10 | 
            +
              #
         | 
| 11 | 
            +
              #    9.590142   0.831269  10.457801 ( 10.0)
         | 
| 12 | 
            +
              #    9.836019   0.837319  10.728024 ( 11.0)
         | 
| 13 | 
            +
              #
         | 
| 14 | 
            +
              #  x = StatsForFile.new(name: "muhcommit", file: "muhfile.bench.txt", desc: "I made it faster", time: Time.now)
         | 
| 15 | 
            +
              #  x.values  #=> [11.437769, 11.792425]
         | 
| 16 | 
            +
              #  x.average # => 10.5
         | 
| 17 | 
            +
              #  x.name    # => "muhfile"
         | 
| 18 | 
            +
              class StatsForFile
         | 
| 19 | 
            +
                attr_reader :name, :values, :desc, :time
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def initialize(file:, name:, desc: "", time: )
         | 
| 22 | 
            +
                  @file = Pathname.new(file)
         | 
| 23 | 
            +
                  FileUtils.touch(@file)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  @name = name
         | 
| 26 | 
            +
                  @desc = desc
         | 
| 27 | 
            +
                  @time = time
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def call
         | 
| 31 | 
            +
                  load_file!
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  @median = (values[(values.length - 1) / 2] + values[values.length/ 2]) / 2.0
         | 
| 34 | 
            +
                  @average = values.inject(:+) / values.length
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def median
         | 
| 38 | 
            +
                  @median.to_f
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def average
         | 
| 42 | 
            +
                  @average.to_f
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                private def load_file!
         | 
| 46 | 
            +
                  @values = []
         | 
| 47 | 
            +
                  @file.each_line do |line|
         | 
| 48 | 
            +
                    line.match(/\( +(\d+\.\d+)\)/)
         | 
| 49 | 
            +
                    begin
         | 
| 50 | 
            +
                      values << BigDecimal($1)
         | 
| 51 | 
            +
                    rescue => e
         | 
| 52 | 
            +
                      raise e, "Problem with file #{@file.inspect}:\n#{@file.read}\n#{e.message}"
         | 
| 53 | 
            +
                    end
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  values.sort!
         | 
| 57 | 
            +
                  values.freeze
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
            end
         | 
| @@ -0,0 +1,292 @@ | |
| 1 | 
            +
            require_relative 'load_tasks'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            namespace :perf do
         | 
| 4 | 
            +
              desc "runs the performance test against two most recent commits of the current app"
         | 
| 5 | 
            +
              task :app do
         | 
| 6 | 
            +
                ENV["DERAILED_PATH_TO_LIBRARY"] = '.'
         | 
| 7 | 
            +
                Rake::Task["perf:library"].invoke
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              desc "runs the same test against two different branches for statistical comparison"
         | 
| 11 | 
            +
              task :library do
         | 
| 12 | 
            +
                begin
         | 
| 13 | 
            +
                  DERAILED_SCRIPT_COUNT = (ENV["DERAILED_SCRIPT_COUNT"] ||= "200").to_i
         | 
| 14 | 
            +
                  ENV["TEST_COUNT"] ||= "200"
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  raise "test count must be at least 2, is set to #{DERAILED_SCRIPT_COUNT}" if DERAILED_SCRIPT_COUNT < 2
         | 
| 17 | 
            +
                  script = ENV["DERAILED_SCRIPT"] || "bundle exec derailed exec perf:test"
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  if ENV["DERAILED_PATH_TO_LIBRARY"]
         | 
| 20 | 
            +
                    library_dir = ENV["DERAILED_PATH_TO_LIBRARY"]
         | 
| 21 | 
            +
                  else
         | 
| 22 | 
            +
                    library_dir = DerailedBenchmarks.rails_path_on_disk
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  raise "Must be a path with a .git directory '#{library_dir}'" unless File.exist?(File.join(library_dir, ".git"))
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  # Use either the explicit SHAs when present or grab last two SHAs from commit history
         | 
| 28 | 
            +
                  # if only one SHA is given, then use it and the last SHA from commit history
         | 
| 29 | 
            +
                  branch_names = []
         | 
| 30 | 
            +
                  branch_names = ENV.fetch("SHAS_TO_TEST").split(",") if ENV["SHAS_TO_TEST"]
         | 
| 31 | 
            +
                  if branch_names.length < 2
         | 
| 32 | 
            +
                    Dir.chdir(library_dir) do
         | 
| 33 | 
            +
                      run!("git checkout '#{branch_names.first}'") unless branch_names.empty?
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                      branches = run!('git log --format="%H" -n 2').chomp.split($/)
         | 
| 36 | 
            +
                      if branch_names.empty?
         | 
| 37 | 
            +
                        branch_names = branches
         | 
| 38 | 
            +
                      else
         | 
| 39 | 
            +
                        branches.shift
         | 
| 40 | 
            +
                        branch_names << branches.shift
         | 
| 41 | 
            +
                      end
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  current_library_branch = ""
         | 
| 46 | 
            +
                  Dir.chdir(library_dir) { current_library_branch = run!('git describe --contains --all HEAD').chomp }
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  out_dir = Pathname.new("tmp/compare_branches/#{Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')}")
         | 
| 49 | 
            +
                  out_dir.mkpath
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  branches_to_test = branch_names.each_with_object({}) {|elem, hash| hash[elem] = out_dir + "#{elem.gsub('/', ':')}.bench.txt" }
         | 
| 52 | 
            +
                  branch_info = {}
         | 
| 53 | 
            +
                  branch_to_sha = {}
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  branches_to_test.each do |branch, file|
         | 
| 56 | 
            +
                    Dir.chdir(library_dir) do
         | 
| 57 | 
            +
                      run!("git checkout '#{branch}'")
         | 
| 58 | 
            +
                      description = run!("git log --oneline --format=%B -n 1 HEAD | head -n 1").strip
         | 
| 59 | 
            +
                      time_stamp  = run!("git log -n 1 --pretty=format:%ci").strip # https://stackoverflow.com/a/25921837/147390
         | 
| 60 | 
            +
                      short_sha   = run!("git rev-parse --short HEAD").strip
         | 
| 61 | 
            +
                      branch_to_sha[branch] = short_sha
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                      branch_info[short_sha] = { desc: description, time: DateTime.parse(time_stamp), file: file }
         | 
| 64 | 
            +
                    end
         | 
| 65 | 
            +
                    run!("#{script}")
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  puts
         | 
| 69 | 
            +
                  puts
         | 
| 70 | 
            +
                  branches_to_test.each.with_index do |(branch, _), i|
         | 
| 71 | 
            +
                    short_sha = branch_to_sha[branch]
         | 
| 72 | 
            +
                    desc      = branch_info[short_sha][:desc]
         | 
| 73 | 
            +
                    puts "Testing #{i + 1}: #{short_sha}: #{desc}"
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
                  puts
         | 
| 76 | 
            +
                  puts
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  raise "SHAs to test must be different" if branch_info.length == 1
         | 
| 79 | 
            +
                  stats = DerailedBenchmarks::StatsFromDir.new(branch_info)
         | 
| 80 | 
            +
                  puts "Env var no longer has any affect DERAILED_STOP_VALID_COUNT" if ENV["DERAILED_STOP_VALID_COUNT"]
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                  DERAILED_SCRIPT_COUNT.times do |i|
         | 
| 83 | 
            +
                    puts "Sample: #{i.next}/#{DERAILED_SCRIPT_COUNT} iterations per sample: #{ENV['TEST_COUNT']}"
         | 
| 84 | 
            +
                    branches_to_test.each do |branch, file|
         | 
| 85 | 
            +
                      Dir.chdir(library_dir) { run!("git checkout '#{branch}'") }
         | 
| 86 | 
            +
                      run!(" #{script} 2>&1 | tail -n 1 >> '#{file}'")
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    if (i % 50).zero?
         | 
| 90 | 
            +
                      puts "Intermediate result"
         | 
| 91 | 
            +
                      stats.call.banner
         | 
| 92 | 
            +
                      puts "Continuing execution"
         | 
| 93 | 
            +
                    end
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                ensure
         | 
| 97 | 
            +
                  if library_dir && current_library_branch
         | 
| 98 | 
            +
                    puts "Resetting git dir of '#{library_dir.to_s}' to #{current_library_branch.inspect}"
         | 
| 99 | 
            +
                    Dir.chdir(library_dir) do
         | 
| 100 | 
            +
                      run!("git checkout '#{current_library_branch}'")
         | 
| 101 | 
            +
                    end
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  if stats
         | 
| 105 | 
            +
                    stats.call.banner
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    result_file = out_dir + "results.txt"
         | 
| 108 | 
            +
                    File.open(result_file, "w") do |f|
         | 
| 109 | 
            +
                      stats.banner(f)
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                    puts "Output: #{result_file.to_s}"
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
              end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
              desc "hits the url TEST_COUNT times"
         | 
| 118 | 
            +
              task :test => [:setup] do
         | 
| 119 | 
            +
                require 'benchmark'
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                Benchmark.bm { |x|
         | 
| 122 | 
            +
                  x.report("#{TEST_COUNT} derailed requests") {
         | 
| 123 | 
            +
                    TEST_COUNT.times {
         | 
| 124 | 
            +
                      call_app
         | 
| 125 | 
            +
                    }
         | 
| 126 | 
            +
                  }
         | 
| 127 | 
            +
                }
         | 
| 128 | 
            +
              end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
              desc "stackprof"
         | 
| 131 | 
            +
              task :stackprof => [:setup] do
         | 
| 132 | 
            +
                # [:wall, :cpu, :object]
         | 
| 133 | 
            +
                begin
         | 
| 134 | 
            +
                  require 'stackprof'
         | 
| 135 | 
            +
                rescue LoadError
         | 
| 136 | 
            +
                  raise "Add stackprof to your gemfile to continue `gem 'stackprof', group: :development`"
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
                TEST_COUNT = (ENV["TEST_COUNT"] ||= "100").to_i
         | 
| 139 | 
            +
                file = "tmp/#{Time.now.iso8601}-stackprof-cpu-myapp.dump"
         | 
| 140 | 
            +
                StackProf.run(mode: :cpu, out: file) do
         | 
| 141 | 
            +
                  Rake::Task["perf:test"].invoke
         | 
| 142 | 
            +
                end
         | 
| 143 | 
            +
                cmd = "stackprof #{file}"
         | 
| 144 | 
            +
                puts "Running `#{cmd}`. Execute `stackprof --help` for more info"
         | 
| 145 | 
            +
                puts `#{cmd}`
         | 
| 146 | 
            +
              end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
              task :kernel_require_patch do
         | 
| 149 | 
            +
                require 'derailed_benchmarks/core_ext/kernel_require.rb'
         | 
| 150 | 
            +
              end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
              desc "show memory usage caused by invoking require per gem"
         | 
| 153 | 
            +
              task :mem => [:kernel_require_patch, :setup] do
         | 
| 154 | 
            +
                puts "## Impact of `require <file>` on RAM"
         | 
| 155 | 
            +
                puts
         | 
| 156 | 
            +
                puts "Showing all `require <file>` calls that consume #{ENV['CUT_OFF']} MiB or more of RSS"
         | 
| 157 | 
            +
                puts "Configure with `CUT_OFF=0` for all entries or `CUT_OFF=5` for few entries"
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                puts "Note: Files only count against RAM on their first load."
         | 
| 160 | 
            +
                puts "      If multiple libraries require the same file, then"
         | 
| 161 | 
            +
                puts "       the 'cost' only shows up under the first library"
         | 
| 162 | 
            +
                puts
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                call_app
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                TOP_REQUIRE.print_sorted_children
         | 
| 167 | 
            +
              end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
              desc "outputs memory usage over time"
         | 
| 170 | 
            +
              task :mem_over_time => [:setup] do
         | 
| 171 | 
            +
                require 'get_process_mem'
         | 
| 172 | 
            +
                puts "PID: #{Process.pid}"
         | 
| 173 | 
            +
                ram = GetProcessMem.new
         | 
| 174 | 
            +
                @keep_going = true
         | 
| 175 | 
            +
                begin
         | 
| 176 | 
            +
                  unless ENV["SKIP_FILE_WRITE"]
         | 
| 177 | 
            +
                    ruby = `ruby -v`.chomp
         | 
| 178 | 
            +
                    FileUtils.mkdir_p("tmp")
         | 
| 179 | 
            +
                    file = File.open("tmp/#{Time.now.iso8601}-#{ruby}-memory-#{TEST_COUNT}-times.txt", 'w')
         | 
| 180 | 
            +
                    file.sync = true
         | 
| 181 | 
            +
                  end
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                  ram_thread = Thread.new do
         | 
| 184 | 
            +
                    while @keep_going
         | 
| 185 | 
            +
                      mb = ram.mb
         | 
| 186 | 
            +
                      STDOUT.puts mb
         | 
| 187 | 
            +
                      file.puts mb unless ENV["SKIP_FILE_WRITE"]
         | 
| 188 | 
            +
                      sleep 5
         | 
| 189 | 
            +
                    end
         | 
| 190 | 
            +
                  end
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                  TEST_COUNT.times {
         | 
| 193 | 
            +
                    call_app
         | 
| 194 | 
            +
                  }
         | 
| 195 | 
            +
                ensure
         | 
| 196 | 
            +
                  @keep_going = false
         | 
| 197 | 
            +
                  ram_thread.join
         | 
| 198 | 
            +
                  file.close unless ENV["SKIP_FILE_WRITE"]
         | 
| 199 | 
            +
                end
         | 
| 200 | 
            +
              end
         | 
| 201 | 
            +
             | 
| 202 | 
            +
              task :ram_over_time do
         | 
| 203 | 
            +
                raise "Use mem_over_time"
         | 
| 204 | 
            +
              end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
              desc "iterations per second"
         | 
| 207 | 
            +
              task :ips => [:setup] do
         | 
| 208 | 
            +
                require 'benchmark/ips'
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                Benchmark.ips do |x|
         | 
| 211 | 
            +
                  x.warmup = Float(ENV["IPS_WARMUP"] || 2)
         | 
| 212 | 
            +
                  x.time = Float(ENV["IPS_TIME"] || 5)
         | 
| 213 | 
            +
                  x.suite = ENV["IPS_SUITE"] if ENV["IPS_SUITE"]
         | 
| 214 | 
            +
                  x.iterations = Integer(ENV["IPS_ITERATIONS"] || 1)
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                  x.report("ips") { call_app }
         | 
| 217 | 
            +
                end
         | 
| 218 | 
            +
              end
         | 
| 219 | 
            +
             | 
| 220 | 
            +
              desc "outputs GC::Profiler.report data while app is called TEST_COUNT times"
         | 
| 221 | 
            +
              task :gc => [:setup] do
         | 
| 222 | 
            +
                GC::Profiler.enable
         | 
| 223 | 
            +
                TEST_COUNT.times { call_app }
         | 
| 224 | 
            +
                GC::Profiler.report
         | 
| 225 | 
            +
                GC::Profiler.disable
         | 
| 226 | 
            +
              end
         | 
| 227 | 
            +
             | 
| 228 | 
            +
              desc "outputs allocated object diff after app is called TEST_COUNT times"
         | 
| 229 | 
            +
              task :allocated_objects => [:setup] do
         | 
| 230 | 
            +
                call_app
         | 
| 231 | 
            +
                GC.start
         | 
| 232 | 
            +
                GC.disable
         | 
| 233 | 
            +
                start = ObjectSpace.count_objects
         | 
| 234 | 
            +
                TEST_COUNT.times { call_app }
         | 
| 235 | 
            +
                finish = ObjectSpace.count_objects
         | 
| 236 | 
            +
                GC.enable
         | 
| 237 | 
            +
                finish.each do |k,v|
         | 
| 238 | 
            +
                  puts k => (v - start[k]) / TEST_COUNT.to_f
         | 
| 239 | 
            +
                end
         | 
| 240 | 
            +
              end
         | 
| 241 | 
            +
             | 
| 242 | 
            +
             | 
| 243 | 
            +
              desc "profiles ruby allocation"
         | 
| 244 | 
            +
              task :objects => [:setup] do
         | 
| 245 | 
            +
                require 'memory_profiler'
         | 
| 246 | 
            +
                call_app
         | 
| 247 | 
            +
                GC.start
         | 
| 248 | 
            +
             | 
| 249 | 
            +
                num = Integer(ENV["TEST_COUNT"] || 1)
         | 
| 250 | 
            +
                opts = {}
         | 
| 251 | 
            +
                opts[:ignore_files] = /#{ENV['IGNORE_FILES_REGEXP']}/ if ENV['IGNORE_FILES_REGEXP']
         | 
| 252 | 
            +
                opts[:allow_files]  = "#{ENV['ALLOW_FILES']}"         if ENV['ALLOW_FILES']
         | 
| 253 | 
            +
             | 
| 254 | 
            +
                puts "Running #{num} times"
         | 
| 255 | 
            +
                report = MemoryProfiler.report(opts) do
         | 
| 256 | 
            +
                  num.times { call_app }
         | 
| 257 | 
            +
                end
         | 
| 258 | 
            +
                report.pretty_print
         | 
| 259 | 
            +
              end
         | 
| 260 | 
            +
             | 
| 261 | 
            +
              desc "heap analyzer"
         | 
| 262 | 
            +
              task :heap => [:setup] do
         | 
| 263 | 
            +
                require 'objspace'
         | 
| 264 | 
            +
             | 
| 265 | 
            +
                file_name = "tmp/#{Time.now.iso8601}-heap.dump"
         | 
| 266 | 
            +
                FileUtils.mkdir_p("tmp")
         | 
| 267 | 
            +
                ObjectSpace.trace_object_allocations_start
         | 
| 268 | 
            +
                puts "Running #{ TEST_COUNT } times"
         | 
| 269 | 
            +
                TEST_COUNT.times {
         | 
| 270 | 
            +
                  call_app
         | 
| 271 | 
            +
                }
         | 
| 272 | 
            +
                GC.start
         | 
| 273 | 
            +
             | 
| 274 | 
            +
                puts "Heap file generated: #{ file_name.inspect }"
         | 
| 275 | 
            +
                ObjectSpace.dump_all(output: File.open(file_name, 'w'))
         | 
| 276 | 
            +
             | 
| 277 | 
            +
                require 'heapy'
         | 
| 278 | 
            +
             | 
| 279 | 
            +
                Heapy::Analyzer.new(file_name).analyze
         | 
| 280 | 
            +
             | 
| 281 | 
            +
                puts ""
         | 
| 282 | 
            +
                puts "Run `$ heapy --help` for more options"
         | 
| 283 | 
            +
                puts ""
         | 
| 284 | 
            +
                puts "Also try uploading #{file_name.inspect} to http://tenderlove.github.io/heap-analyzer/"
         | 
| 285 | 
            +
              end
         | 
| 286 | 
            +
             | 
| 287 | 
            +
              def run!(cmd)
         | 
| 288 | 
            +
                out = `#{cmd}`
         | 
| 289 | 
            +
                raise "Error while running #{cmd.inspect}: #{out}" unless $?.success?
         | 
| 290 | 
            +
                out
         | 
| 291 | 
            +
              end
         | 
| 292 | 
            +
            end
         |