├── .devcontainer └── devcontainer.json ├── .github └── workflows │ ├── check_changelog.yml │ └── ci.yml ├── .gitignore ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── README.md ├── Rakefile ├── bin └── derailed ├── derailed_benchmarks.gemspec ├── gemfiles ├── .bundle │ └── config ├── rails_5_1.gemfile ├── rails_5_2.gemfile ├── rails_6_0.gemfile ├── rails_6_1.gemfile ├── rails_7_0.gemfile ├── rails_7_1.gemfile ├── rails_7_2.gemfile ├── rails_8_0.gemfile └── rails_head.gemfile ├── lib ├── derailed_benchmarks.rb └── derailed_benchmarks │ ├── auth_helper.rb │ ├── auth_helpers │ └── devise.rb │ ├── core_ext │ └── kernel_require.rb │ ├── git │ ├── commit.rb │ ├── in_path.rb │ └── switch_project.rb │ ├── git_switch_project.rb │ ├── load_tasks.rb │ ├── require_tree.rb │ ├── stats_for_file.rb │ ├── stats_from_dir.rb │ ├── tasks.rb │ └── version.rb └── test ├── derailed_benchmarks ├── core_ext │ └── kernel_require_test.rb ├── git_switch_project_test.rb ├── require_tree_test.rb └── stats_from_dir_test.rb ├── derailed_test.rb ├── fixtures ├── require │ ├── autoload_child.rb │ ├── autoload_parent.rb │ ├── child_one.rb │ ├── child_two.rb │ ├── load_child.rb │ ├── load_parent.rb │ ├── parent_one.rb │ ├── raise_child.rb │ ├── relative_child.rb │ └── relative_child_two.rb └── stats │ └── significant │ ├── loser.bench.txt │ └── winner.bench.txt ├── integration └── tasks_test.rb ├── rails_app ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── javascripts │ │ │ └── authenticated.js │ │ └── stylesheets │ │ │ └── authenticated.css │ ├── controllers │ │ ├── application_controller.rb │ │ ├── authenticated_controller.rb │ │ ├── pages_controller.rb │ │ └── users_controller.rb │ ├── helpers │ │ ├── application_helper.rb │ │ └── authenticated_helper.rb │ ├── models │ │ └── user.rb │ └── views │ │ ├── authenticated │ │ └── index.html.erb │ │ ├── layouts │ │ └── application.html.erb │ │ └── pages │ │ └── index.html.erb ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── devise.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ └── session_store.rb │ ├── locales │ │ ├── devise.en.yml │ │ ├── en.yml │ │ └── es.yml │ ├── routes.rb │ └── storage.yml ├── db │ ├── migrate │ │ └── 20141210070547_devise_create_users.rb │ └── schema.rb ├── perf.rake ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── favicon.ico │ ├── javascripts │ │ ├── application.js │ │ ├── controls.js │ │ ├── dragdrop.js │ │ ├── effects.js │ │ ├── prototype.js │ │ └── rails.js │ └── stylesheets │ │ └── .gitkeep └── script │ └── rails ├── support └── integration_case.rb └── test_helper.rb /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/ruby 3 | { 4 | "name": "Ruby", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/ruby:1-3.3-bullseye" 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | // "postCreateCommand": "ruby --version", 16 | 17 | // Configure tool-specific properties. 18 | // "customizations": {}, 19 | 20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 21 | // "remoteUser": "root" 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/check_changelog.yml: -------------------------------------------------------------------------------- 1 | name: Check Changelog 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, edited, synchronize] 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Check that CHANGELOG is touched 12 | run: | 13 | cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | include: 14 | - ruby: "2.5" 15 | gemfile: gemfiles/rails_5_1.gemfile 16 | - ruby: "2.5" 17 | gemfile: gemfiles/rails_5_2.gemfile 18 | - ruby: "2.5" 19 | gemfile: gemfiles/rails_6_0.gemfile 20 | - ruby: "2.5" 21 | gemfile: gemfiles/rails_6_1.gemfile 22 | - ruby: "2.6" 23 | gemfile: gemfiles/rails_5_2.gemfile 24 | - ruby: "2.6" 25 | gemfile: gemfiles/rails_6_0.gemfile 26 | - ruby: "2.6" 27 | gemfile: gemfiles/rails_6_1.gemfile 28 | - ruby: "2.7" 29 | gemfile: gemfiles/rails_5_1.gemfile 30 | - ruby: "2.7" 31 | gemfile: gemfiles/rails_5_2.gemfile 32 | - ruby: "2.7" 33 | gemfile: gemfiles/rails_6_0.gemfile 34 | - ruby: "2.7" 35 | gemfile: gemfiles/rails_6_1.gemfile 36 | - ruby: "2.7" 37 | gemfile: gemfiles/rails_7_0.gemfile 38 | - ruby: "2.7" 39 | gemfile: gemfiles/rails_7_1.gemfile 40 | - ruby: "3.0" 41 | gemfile: gemfiles/rails_6_0.gemfile 42 | - ruby: "3.0" 43 | gemfile: gemfiles/rails_6_1.gemfile 44 | - ruby: "3.0" 45 | gemfile: gemfiles/rails_7_0.gemfile 46 | - ruby: "3.0" 47 | gemfile: gemfiles/rails_7_1.gemfile 48 | - ruby: "3.1" 49 | gemfile: gemfiles/rails_6_0.gemfile 50 | - ruby: "3.1" 51 | gemfile: gemfiles/rails_6_1.gemfile 52 | - ruby: "3.1" 53 | gemfile: gemfiles/rails_7_0.gemfile 54 | - ruby: "3.1" 55 | gemfile: gemfiles/rails_7_1.gemfile 56 | - ruby: "3.1" 57 | gemfile: gemfiles/rails_7_2.gemfile 58 | - ruby: "3.2" 59 | gemfile: gemfiles/rails_6_0.gemfile 60 | - ruby: "3.2" 61 | gemfile: gemfiles/rails_6_1.gemfile 62 | - ruby: "3.2" 63 | gemfile: gemfiles/rails_7_0.gemfile 64 | - ruby: "3.2" 65 | gemfile: gemfiles/rails_7_1.gemfile 66 | - ruby: "3.2" 67 | gemfile: gemfiles/rails_7_2.gemfile 68 | - ruby: "3.2" 69 | gemfile: gemfiles/rails_8_0.gemfile 70 | - ruby: "3.3" 71 | gemfile: gemfiles/rails_6_0.gemfile 72 | - ruby: "3.3" 73 | gemfile: gemfiles/rails_6_1.gemfile 74 | - ruby: "3.3" 75 | gemfile: gemfiles/rails_7_0.gemfile 76 | - ruby: "3.3" 77 | gemfile: gemfiles/rails_7_1.gemfile 78 | - ruby: "3.3" 79 | gemfile: gemfiles/rails_7_2.gemfile 80 | - ruby: "3.3" 81 | gemfile: gemfiles/rails_8_0.gemfile 82 | - ruby: "3.4" 83 | gemfile: gemfiles/rails_7_0.gemfile 84 | - ruby: "3.4" 85 | gemfile: gemfiles/rails_7_1.gemfile 86 | - ruby: "3.4" 87 | gemfile: gemfiles/rails_7_2.gemfile 88 | - ruby: "3.4" 89 | gemfile: gemfiles/rails_8_0.gemfile 90 | - ruby: "head" 91 | gemfile: gemfiles/rails_head.gemfile 92 | env: 93 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 94 | steps: 95 | - name: Checkout code 96 | uses: actions/checkout@v4 97 | - name: Set up Ruby 98 | uses: ruby/setup-ruby@v1 99 | with: 100 | ruby-version: ${{ matrix.ruby }} 101 | bundler-cache: true 102 | - name: Set Git config 103 | run: | 104 | git config --global user.email "user@example.com" 105 | git config --global user.name "Github Action Bot" 106 | - name: Run test 107 | run: bundle exec rake test 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.gem 3 | /test/rails_app/tmp/* 4 | /test/rails_app/log/* 5 | /test/rails_app/db/* 6 | *.sqlite3 7 | 8 | Gemfile.lock 9 | gemfiles/*.lock 10 | .idea 11 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise 'rails_5_1' do 4 | gem 'rails', '~> 5.1.0' 5 | end 6 | 7 | appraise 'rails_5_2' do 8 | gem 'rails', '~> 5.2.0' 9 | end 10 | 11 | appraise 'rails_6_0' do 12 | gem 'rails', '~> 6.0.0' 13 | end 14 | 15 | appraise 'rails_6_1' do 16 | gem 'rails', '~> 6.1.0' 17 | 18 | # https://stackoverflow.com/questions/70500220/rails-7-ruby-3-1-loaderror-cannot-load-such-file-net-smtp 19 | gem 'net-smtp', require: false 20 | gem 'net-imap', require: false 21 | gem 'net-pop', require: false 22 | end 23 | 24 | appraise 'rails_7_0' do 25 | gem 'rails', '~> 7.0' 26 | end 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## HEAD 2 | 3 | - Add tests for rails 8, ruby 3.4 (https://github.com/zombocom/derailed_benchmarks/pull/260) 4 | - Gemspec: correctly compare ruby versions, loosen syntax_suggest version constraint (https://github.com/zombocom/derailed_benchmarks/pull/259) 5 | 6 | ## 2.2.1 7 | 8 | - `get_process_mem` version requirements relaxed (https://github.com/zombocom/derailed_benchmarks/pull/252) 9 | 10 | ## 2.2.0 11 | 12 | - Support REQUEST_METHOD, REQUEST_BODY, CONTENT_TYPE, and CONTENT_LENGTH env vars (https://github.com/zombocom/derailed_benchmarks/pull/234, https://github.com/zombocom/derailed_benchmarks/pull/122) 13 | - Support ruby-statistics 4.x (https://github.com/zombocom/derailed_benchmarks/pull/238, 14 | https://github.com/zombocom/derailed_benchmarks/pull/239, https://github.com/zombocom/derailed_benchmarks/pull/247) 15 | - Repair tests, support ruby-statistics in ruby < 3.0 (https://github.com/zombocom/derailed_benchmarks/pull/241) 16 | - Test Rails 7.1 and 7.2 (https://github.com/zombocom/derailed_benchmarks/pull/242) 17 | - Switch from dead_end to syntax_suggest (https://github.com/zombocom/derailed_benchmarks/pull/243) 18 | - require `ruby2_keywords` so drb doesn't break in ruby < 2.7 (https://github.com/zombocom/derailed_benchmarks/pull/244) 19 | - support relative BUNDLE_GEMFILE in tests (https://github.com/zombocom/derailed_benchmarks/pull/245) 20 | 21 | ## 2.1.2 22 | 23 | - Support DERAILED_SKIP_RAILS_REQUIRES (https://github.com/zombocom/derailed_benchmarks/pull/199) 24 | - Support rails 7 for bundle exec derailed exec mem (https://github.com/zombocom/derailed_benchmarks/pull/212) 25 | - Update the gemspec's homepage to the current repo URL (https://github.com/zombocom/derailed_benchmarks/pull/212) 26 | 27 | ## 2.1.1 28 | 29 | - Fix Thor's deprecation warning by implementing `exit_on_failure?` (https://github.com/schneems/derailed_benchmarks/pull/195) 30 | 31 | ## 2.1.0 32 | 33 | - Add `perf:heap_diff` tool (https://github.com/schneems/derailed_benchmarks/pull/193) 34 | 35 | ## 2.0.1 36 | 37 | - `rack-test` dependency added (https://github.com/schneems/derailed_benchmarks/pull/187) 38 | 39 | ## 2.0.0 40 | 41 | - Syntax errors easier to debug with `dead_end` gem (https://github.com/schneems/derailed_benchmarks/pull/182) 42 | - Minimum ruby version is now 2.5 (https://github.com/schneems/derailed_benchmarks/pull/183) 43 | - Histograms are now printed side-by-side (https://github.com/schneems/derailed_benchmarks/pull/179) 44 | 45 | ## 1.8.1 46 | 47 | - Derailed now tracks memory use from `load` in addition to `require` (https://github.com/schneems/derailed_benchmarks/pull/178) 48 | - Correct logging of unsuccessful curl requests to file (https://github.com/schneems/derailed_benchmarks/pull/172) 49 | 50 | ## 1.8.0 51 | 52 | - Ruby 2.2 is now officialy supported and tested (https://github.com/schneems/derailed_benchmarks/pull/177) 53 | 54 | ## 1.7.0 55 | 56 | - Add histogram support to `perf:library` (https://github.com/schneems/derailed_benchmarks/pull/169) 57 | - Fix bug with `Kernel#require` patch when Zeitwerk is enabled (https://github.com/schneems/derailed_benchmarks/pull/170) 58 | 59 | ## 1.6.0 60 | 61 | - Added the `perf:app` command to compare commits within the same application. (https://github.com/schneems/derailed_benchmarks/pull/157) 62 | - Allow Rails < 7 and 1.0 <= Thor < 2 (https://github.com/schneems/derailed_benchmarks/pull/168) 63 | 64 | ## 1.5.0 65 | 66 | - Test `perf:library` results against 99% confidence interval in addition to 95% (https://github.com/schneems/derailed_benchmarks/pull/165) 67 | - Change default, `perf:library` tests do not stop automatically any more (https://github.com/schneems/derailed_benchmarks/pull/164) 68 | 69 | ## 1.4.4 70 | 71 | - Fix alignment of deicmals in output (https://github.com/schneems/derailed_benchmarks/pull/161) 72 | 73 | ## 1.4.3 74 | 75 | - perf:library now uses median instead of average (https://github.com/schneems/derailed_benchmarks/pull/160) 76 | 77 | ## 1.4.2 78 | 79 | - Fixed syntax error that resulted in ensure end error inside tasks.rb for older rubies (https://github.com/schneems/derailed_benchmarks/pull/155) 80 | - Fix case in perf:library where the same SHA could be tested against itself (https://github.com/schneems/derailed_benchmarks/pull/153) 81 | 82 | ## 1.4.1 83 | 84 | - Rake dependency now allows for Rake 13 (https://github.com/schneems/derailed_benchmarks/pull/151) 85 | 86 | ## 1.4.0 87 | 88 | - Allow configuration of `perf:ips` benchmark. 89 | - Fix bug with `require_relative` [#142](https://github.com/schneems/derailed_benchmarks/pull/142) 90 | - Introduce `perf:library` to profile patches to libraries (like Rails) [#135](https://github.com/schneems/derailed_benchmarks/pull/135), [#139](https://github.com/schneems/derailed_benchmarks/pull/139), [#140](https://github.com/schneems/derailed_benchmarks/pull/140), [#141](https://github.com/schneems/derailed_benchmarks/pull/141) 91 | 92 | ## 1.3.6 93 | 94 | - `require_relative` is now measured [commit](https://github.com/schneems/derailed_benchmarks/commit/af11bcc46a4fa24f79e4897a51034927a56e077e) 95 | - Fix bug preventing a specific Rails 6 file from being loaded (https://github.com/schneems/derailed_benchmarks/pull/134) 96 | - `exit(1)` is called instead of raise (https://github.com/schneems/derailed_benchmarks/pull/127) 97 | 98 | ## [1.3.5] 99 | 100 | - Output of `test` now emits the word "derailed" for easier grepping. 101 | - Fix "already initialized constant" warning 102 | 103 | ## [1.3.4] 104 | 105 | - Allow for "warming up tasks" via WARM_COUNT env var #119 106 | 107 | ## [1.3.3] 108 | 109 | - Make all paths added to $LOAD_PATH absolute instead of relative to allow for use with apps that use bootsnap. 110 | 111 | ## [1.3.2] 112 | 113 | - Allow for use with Rack 11. 114 | 115 | 116 | ## [1.3.1] 117 | 118 | - Allow for use with Rack 11. 119 | 120 | 121 | ## [1.3.0] - 2015-01-07 122 | 123 | - Allow environment variable to skip Active Record setup. 124 | - Allow Rack 2 to work with Derailed. 125 | 126 | ## [1.1.3] - 2015-10-15 127 | 128 | - Update docs 129 | 130 | ## [1.1.2] - 2015-10-05 131 | 132 | - Added ability to use TEST_COUNT environment variable with `perf:heap`. 133 | 134 | ## [1.1.1] - 2015-10-01 135 | 136 | - Added ability to create a heap dump `perf:heap`. 137 | 138 | ## [1.1.0] - 2015-09-09 139 | 140 | - Set custom auth user using a lambda in perf.rake 141 | - Changed `perf:ram_over_time` changed to `perf:mem_over_time` 142 | - Fixed gem warnings 143 | 144 | ## [1.0.1] - 2015-06-20 145 | 146 | - `bundle:mem` and similar tasks now keep track of duplicate requires and display them along side of memory requirements. This makes it easier to identify where components are used by multiple libraries 147 | - Add rake to gemspec which gets rid of `Unresolved specs during Gem::Specification.reset:` warning 148 | - Outputs of memory are now done in [mebibytes](https://en.wikipedia.org/wiki/Mebibyte), a more accurate unit for the value we're measuring (hint: it's what you think MB is). 149 | 150 | ## [1.0.0] - 2015-05-14 151 | 152 | - Added `derailed` command line utility. Can be used with just a Gemfile using command `$ derailed bundle:mem` and `$ derailed bundle:objects`. All existing Rake tasks can now be called with `$ derailed exec` such as `$ derailed exec perf:mem`. 153 | - Changed memory_profiler task to be `perf:objects` instead of `perf:mem`. 154 | - Changed boot time memory measurement to `perf:mem` instead of `perf:require_bench` 155 | - Released seperate [derailed](https://github.com/schneems/derailed) gem that is a wrapper for this gem. I.e. installing that gem installs this one. Easier to remember, less words to type. Also means there's no colision using the `derailed` namespace for executables inside of the `derailed_benchmarks`. 156 | 157 | ## [0.0.0] - 2014-08-15 158 | 159 | - Initial release 160 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development, :test do 6 | gem "sqlite3", '~> 1.4', :platform => [:ruby, :mswin, :mingw] 7 | gem "activerecord-jdbcsqlite3-adapter", '~> 1.3.13', :platform => :jruby 8 | gem "test-unit", "~> 3.0" 9 | end 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require 'rubygems' 5 | require 'bundler' 6 | require "bundler/gem_tasks" 7 | 8 | begin 9 | Bundler.setup(:default, :development, :test) 10 | rescue Bundler::BundlerError => e 11 | $stderr.puts e.message 12 | $stderr.puts "Run `bundle install` to install missing gems" 13 | exit e.status_code 14 | end 15 | 16 | require 'rake' 17 | 18 | require 'rake/testtask' 19 | 20 | Rake::TestTask.new(:test) do |t| 21 | t.libs << 'lib' 22 | t.libs << 'test' 23 | t.pattern = 'test/**/*_test.rb' 24 | t.verbose = false 25 | end 26 | 27 | task default: :test 28 | 29 | 30 | -------------------------------------------------------------------------------- /bin/derailed: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | unless File.respond_to? :realpath 5 | class File #:nodoc: 6 | def self.realpath path 7 | return realpath(File.readlink(path)) if symlink?(path) 8 | path 9 | end 10 | end 11 | end 12 | lib = File.expand_path(File.dirname(File.realpath(__FILE__)) + '/../lib') 13 | $: << lib 14 | 15 | 16 | require File.join(lib, 'derailed_benchmarks.rb') 17 | 18 | Bundler.setup 19 | 20 | require 'thor' 21 | 22 | class DerailedBenchmarkCLI < Thor 23 | def self.exit_on_failure? 24 | true 25 | end 26 | 27 | desc "exec", "executes given derailed benchmark" 28 | def exec(task = nil) 29 | setup_bundler! 30 | require 'derailed_benchmarks' 31 | require 'rake' 32 | Rake::TaskManager.record_task_metadata = true 33 | require 'derailed_benchmarks/tasks' 34 | 35 | perf_rakefile = File.expand_path(".", "perf.rake") 36 | load perf_rakefile if File.exist?(perf_rakefile) 37 | 38 | if task.nil? || task == "--help" 39 | Rake.application.tasks.map do |task, n| 40 | next unless task.comment 41 | puts " $ derailed exec #{task.name} # #{task.comment}" 42 | end 43 | else 44 | task = "perf:#{task}" unless Rake::Task.task_defined?(task) 45 | Rake::Task[task].invoke 46 | end 47 | end 48 | 49 | desc "bundle:objects", "measures objects created by gems" 50 | define_method(:"bundle:objects") do |env = "production"| 51 | setup_bundler! 52 | env = [:default] + env.split(",") 53 | puts "Measuring objects created by gems in groups #{ env.inspect }" 54 | require 'memory_profiler' 55 | report = MemoryProfiler.report do 56 | Bundler.require(*env) 57 | end 58 | report.pretty_print 59 | end 60 | 61 | map :"bundler:objects" => :"bundle:objects" 62 | 63 | desc "bundle:mem", "measures memory used by gems at boot time" 64 | define_method(:"bundle:mem") do |env = "production"| 65 | env = [:default] + env.split(",") 66 | require 'get_process_mem' 67 | mem = GetProcessMem.new 68 | require 'derailed_benchmarks/core_ext/kernel_require' 69 | before = mem.mb 70 | setup_bundler! 71 | Bundler.require(*env) 72 | after = mem.mb 73 | TOP_REQUIRE.print_sorted_children 74 | end 75 | map :"bundler:mem" => :"bundle:mem" 76 | 77 | private 78 | def setup_bundler! 79 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 80 | require 'bundler/setup' 81 | 82 | begin 83 | if ENV["DERAILED_SKIP_RAILS_REQUIRES"] 84 | # do nothing. your app will handle requiring Rails for booting. 85 | elsif ENV["DERAILED_SKIP_ACTIVE_RECORD"] 86 | require "action_controller/railtie" 87 | require "sprockets/railtie" 88 | require "rails/test_unit/railtie" 89 | else 90 | require 'rails/all' 91 | end 92 | rescue LoadError 93 | end 94 | end 95 | end 96 | 97 | DerailedBenchmarkCLI.start(ARGV) 98 | -------------------------------------------------------------------------------- /derailed_benchmarks.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # frozen_string_literal: true 3 | 4 | lib = File.expand_path('../lib', __FILE__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | require 'derailed_benchmarks/version' 7 | 8 | Gem::Specification.new do |gem| 9 | gem.name = "derailed_benchmarks" 10 | gem.version = DerailedBenchmarks::VERSION 11 | gem.authors = ["Richard Schneeman"] 12 | gem.email = ["richard.schneeman+rubygems@gmail.com"] 13 | gem.description = %q{ Go faster, off the Rails } 14 | gem.summary = %q{ Benchmarks designed to performance test your ENTIRE site } 15 | gem.homepage = "https://github.com/zombocom/derailed_benchmarks" 16 | gem.license = "MIT" 17 | 18 | gem.files = `git ls-files`.split($/) 19 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 20 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 21 | gem.require_paths = ["lib"] 22 | 23 | gem.required_ruby_version = ">= 2.5.0" 24 | 25 | gem.add_dependency "heapy", "~> 0" 26 | gem.add_dependency "memory_profiler", ">= 0", "< 2" 27 | gem.add_dependency "get_process_mem" 28 | gem.add_dependency "benchmark-ips", "~> 2" 29 | gem.add_dependency "rack", ">= 1" 30 | gem.add_dependency "rake", "> 10", "< 14" 31 | gem.add_dependency "thor", ">= 0.19", "< 2" 32 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.0") 33 | gem.add_dependency "ruby-statistics", ">= 4.0.1" 34 | else 35 | gem.add_dependency "ruby-statistics", ">= 2.1" 36 | end 37 | if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2") 38 | gem.add_dependency "syntax_suggest", ">= 1.1.0" 39 | end 40 | gem.add_dependency "mini_histogram", ">= 0.3.0" 41 | gem.add_dependency "rack-test", ">= 0" 42 | gem.add_dependency "base64", ">= 0" 43 | gem.add_dependency "mutex_m", ">= 0" 44 | gem.add_dependency "bigdecimal", ">= 0" 45 | gem.add_dependency "drb", ">= 0" 46 | gem.add_dependency "logger", ">= 0" 47 | gem.add_dependency "ostruct", ">= 0" 48 | gem.add_dependency "ruby2_keywords", ">= 0" 49 | 50 | gem.add_development_dependency "appraisal" 51 | gem.add_development_dependency "webrick", ">= 0" 52 | gem.add_development_dependency "capybara", "~> 2" 53 | gem.add_development_dependency "m" 54 | gem.add_development_dependency "rails", "> 3", "< 8.1" 55 | gem.add_development_dependency "devise", "> 3", "< 6" 56 | end 57 | -------------------------------------------------------------------------------- /gemfiles/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_RETRY: "1" 3 | -------------------------------------------------------------------------------- /gemfiles/rails_5_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.1.0" 6 | 7 | group :development, :test do 8 | gem "sqlite3", platform: [:ruby, :mswin, :mingw] 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.13", platform: :jruby 10 | gem "test-unit", "~> 3.0" 11 | end 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /gemfiles/rails_5_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.2.0" 6 | 7 | group :development, :test do 8 | gem "sqlite3", platform: [:ruby, :mswin, :mingw] 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.13", platform: :jruby 10 | gem "test-unit", "~> 3.0" 11 | end 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /gemfiles/rails_6_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 6.0.0" 6 | 7 | group :development, :test do 8 | gem "sqlite3", '~> 1.4', platform: [:ruby, :mswin, :mingw] 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.13", platform: :jruby 10 | gem "test-unit", "~> 3.0" 11 | gem "psych", "~> 3.0" 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/rails_6_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 6.1.0" 6 | gem "net-smtp", require: false 7 | gem "net-imap", require: false 8 | gem "net-pop", require: false 9 | 10 | group :development, :test do 11 | gem "sqlite3", '~> 1.4', platform: [:ruby, :mswin, :mingw] 12 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.13", platform: :jruby 13 | gem "test-unit", "~> 3.0" 14 | end 15 | 16 | gemspec path: "../" 17 | -------------------------------------------------------------------------------- /gemfiles/rails_7_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 7.0.0" 6 | 7 | group :development, :test do 8 | gem "sqlite3", '~> 1.4', platform: [:ruby, :mswin, :mingw] 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.13", platform: :jruby 10 | gem "test-unit", "~> 3.0" 11 | end 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /gemfiles/rails_7_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 7.1.0" 6 | 7 | group :development, :test do 8 | gem "sqlite3", '~> 1.4', platform: [:ruby, :mswin, :mingw] 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.13", platform: :jruby 10 | gem "test-unit", "~> 3.0" 11 | end 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /gemfiles/rails_7_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 7.2.0" 6 | 7 | group :development, :test do 8 | gem "sqlite3", '~> 1.4', platform: [:ruby, :mswin, :mingw] 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.13", platform: :jruby 10 | gem "test-unit", "~> 3.0" 11 | end 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /gemfiles/rails_8_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 8.0.0" 6 | 7 | group :development, :test do 8 | gem "sqlite3", '~> 2.1', platform: [:ruby, :mswin, :mingw] 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.13", platform: :jruby 10 | gem "test-unit", "~> 3.0" 11 | end 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /gemfiles/rails_head.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # $ BUNDLE_GEMFILE="gemfiles/rails_git.gemfile" bundle exec m test/integration/tasks_test.rb:50 4 | 5 | source "https://rubygems.org" 6 | 7 | gem "rails", github: "rails/rails", branch: "main" 8 | 9 | gem 'devise', github: "plataformatec/devise", branch: "main" 10 | 11 | group :development, :test do 12 | gem "sqlite3", platform: [:ruby, :mswin, :mingw] 13 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.13", platform: :jruby 14 | gem "test-unit", "~> 3.0" 15 | gem "rackup" 16 | end 17 | 18 | gemspec path: "../" 19 | 20 | ENV['USING_RAILS_GIT'] = "1" 21 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'time' 4 | require 'bundler' 5 | require 'get_process_mem' 6 | 7 | require 'syntax_suggest' 8 | 9 | module DerailedBenchmarks 10 | def self.gem_is_bundled?(name) 11 | specs = ::Bundler.locked_gems.specs.each_with_object({}) {|spec, hash| hash[spec.name] = spec } 12 | specs[name] 13 | end 14 | 15 | class << self 16 | attr_accessor :auth 17 | end 18 | 19 | def self.rails_path_on_disk 20 | require 'rails/version' 21 | rails_version_file = Rails.method(:version).source_location[0] 22 | path = Pathname.new(rails_version_file).expand_path.parent.parent 23 | 24 | while path != Pathname.new("/") 25 | basename = path.expand_path.basename.to_s 26 | 27 | break if basename.start_with?("rails") && basename != "railties" 28 | path = path.parent 29 | end 30 | raise "Could not find rails folder on a folder in #{rails_version_file}" if path == Pathname.new("/") 31 | path.expand_path 32 | end 33 | 34 | def self.add_auth(app) 35 | if use_auth = ENV['USE_AUTH'] 36 | puts "Auth: #{use_auth}" 37 | auth.add_app(app) 38 | else 39 | app 40 | end 41 | end 42 | end 43 | 44 | require 'derailed_benchmarks/require_tree' 45 | require 'derailed_benchmarks/auth_helper' 46 | 47 | require 'derailed_benchmarks/stats_for_file' 48 | require 'derailed_benchmarks/stats_from_dir' 49 | require 'derailed_benchmarks/git/switch_project' 50 | 51 | if DerailedBenchmarks.gem_is_bundled?("devise") 52 | DerailedBenchmarks.auth = DerailedBenchmarks::AuthHelpers::Devise.new 53 | end 54 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/auth_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'securerandom' 4 | 5 | module DerailedBenchmarks 6 | # Base helper class. Can be used to authenticate different strategies 7 | # The root app will be wrapped by an authentication action 8 | class AuthHelper 9 | attr_reader :app 10 | 11 | # Put any coded needed to set up or initialize your authentication module here 12 | def setup 13 | raise "Must subclass" 14 | end 15 | 16 | # Gets called for every request. Place all auth logic here. 17 | # Return value is expected to be an valid Rack response array. 18 | # If you do not manually `app.call(env)` here, the client app 19 | # will never be called. 20 | def call(env) 21 | raise "Must subclass" 22 | end 23 | 24 | # Returns self and sets the target app 25 | def add_app(app) 26 | raise "App is required argument" unless app 27 | @app = app 28 | setup 29 | self 30 | end 31 | end 32 | end 33 | 34 | require 'derailed_benchmarks/auth_helpers/devise' 35 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/auth_helpers/devise.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DerailedBenchmarks 4 | class AuthHelpers 5 | # Devise helper for authenticating requests 6 | # Setup adds necessarry test methods, user provides a sample user. 7 | # The authenticate method is called on every request when authentication is enabled 8 | class Devise < AuthHelper 9 | attr_writer :user 10 | 11 | # Include devise test helpers and turn on test mode 12 | # We need to do this on the class level 13 | def setup 14 | # self.class.instance_eval do 15 | require 'devise' 16 | require 'warden' 17 | extend ::Warden::Test::Helpers 18 | extend ::Devise::TestHelpers 19 | Warden.test_mode! 20 | # end 21 | end 22 | 23 | def user 24 | if @user 25 | @user = @user.call if @user.is_a?(Proc) 26 | @user 27 | else 28 | password = SecureRandom.hex 29 | @user = User.first_or_create!(email: "#{SecureRandom.hex}@example.com", password: password, password_confirmation: password) 30 | end 31 | end 32 | 33 | # Logs the user in, then call the parent app 34 | def call(env) 35 | login_as(user) 36 | app.call(env) 37 | end 38 | end 39 | end 40 | end 41 | 42 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/core_ext/kernel_require.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'get_process_mem' 4 | require 'derailed_benchmarks/require_tree' 5 | 6 | ENV['CUT_OFF'] ||= "0.3" 7 | 8 | # This file contains classes and monkey patches to measure the amount of memory 9 | # useage requiring an individual file adds. 10 | 11 | # Monkey patch kernel to ensure that all `require` calls call the same 12 | # method 13 | module Kernel 14 | REQUIRE_STACK = [] 15 | 16 | module_function 17 | 18 | alias_method :original_require, :require 19 | alias_method :original_require_relative, :require_relative 20 | alias_method(:original_load, :load) 21 | 22 | def load(file, wrap = false) 23 | measure_memory_impact(file) do |file| 24 | original_load(file) 25 | end 26 | end 27 | 28 | def require(file) 29 | measure_memory_impact(file) do |file| 30 | original_require(file) 31 | end 32 | end 33 | 34 | def require_relative(file) 35 | if Pathname.new(file).absolute? 36 | require file 37 | else 38 | require File.expand_path("../#{file}", caller_locations(1, 1)[0].absolute_path) 39 | end 40 | end 41 | 42 | private 43 | 44 | # The core extension we use to measure require time of all requires 45 | # When a file is required we create a tree node with its file name. 46 | # We then push it onto a stack, this is because requiring a file can 47 | # require other files before it is finished. 48 | # 49 | # When a child file is required, a tree node is created and the child file 50 | # is pushed onto the parents tree. We then repeat the process as child 51 | # files may require additional files. 52 | # 53 | # When a require returns we remove it from the require stack so we don't 54 | # accidentally push additional children nodes to it. We then store the 55 | # memory cost of the require in the tree node. 56 | def measure_memory_impact(file, &block) 57 | mem = GetProcessMem.new 58 | node = DerailedBenchmarks::RequireTree.new(file) 59 | 60 | parent = REQUIRE_STACK.last 61 | parent << node 62 | REQUIRE_STACK.push(node) 63 | begin 64 | before = mem.mb 65 | block.call file 66 | ensure 67 | REQUIRE_STACK.pop # node 68 | after = mem.mb 69 | end 70 | node.cost = after - before 71 | end 72 | end 73 | 74 | 75 | # I honestly have no idea why this Object delegation is needed 76 | # I keep staring at bootsnap and it doesn't have to do this 77 | # is there a bug in their implementation they haven't caught or 78 | # am I doing something different? 79 | class Object 80 | private 81 | def load(path, wrap = false) 82 | Kernel.load(path, wrap) 83 | end 84 | 85 | def require(path) 86 | Kernel.require(path) 87 | end 88 | end 89 | 90 | # Top level node that will store all require information for the entire app 91 | TOP_REQUIRE = DerailedBenchmarks::RequireTree.new("TOP") 92 | REQUIRE_STACK.push(TOP_REQUIRE) 93 | TOP_REQUIRE.cost = GetProcessMem.new.mb 94 | 95 | def TOP_REQUIRE.print_sorted_children(*args) 96 | self.cost = GetProcessMem.new.mb - self.cost 97 | super 98 | end 99 | 100 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/git/commit.rb: -------------------------------------------------------------------------------- 1 | module DerailedBenchmarks 2 | # Represents a specific commit in a git repo 3 | # 4 | # Can be used to get information from the commit or to check it out 5 | # 6 | # commit = GitCommit.new(path: "path/to/repo", ref: "6e642963acec0ff64af51bd6fba8db3c4176ed6e") 7 | # commit.short_sha # => "6e64296" 8 | # commit.checkout! # Will check out the current commit at the repo in the path 9 | class Git::Commit 10 | attr_reader :ref, :description, :time, :short_sha, :log 11 | 12 | def initialize(path: , ref: , log_dir: Pathname.new("/dev/null")) 13 | @in_git_path = Git::InPath.new(path) 14 | @ref = ref 15 | @log = log_dir.join("#{file_safe_ref}.bench.txt") 16 | 17 | Dir.chdir(path) do 18 | checkout! 19 | @description = @in_git_path.description 20 | @short_sha = @in_git_path.short_sha 21 | @time = @in_git_path.time 22 | end 23 | end 24 | 25 | alias :desc :description 26 | alias :file :log 27 | 28 | def checkout! 29 | @in_git_path.checkout!(ref) 30 | end 31 | 32 | private def file_safe_ref 33 | ref.gsub('/', ':') 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/git/in_path.rb: -------------------------------------------------------------------------------- 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 60 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/git/switch_project.rb: -------------------------------------------------------------------------------- 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 129 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/git_switch_project.rb: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/load_tasks.rb: -------------------------------------------------------------------------------- 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 | # Disables CSRF protection because of non-GET requests 28 | DERAILED_APP.config.action_controller.allow_forgery_protection = false 29 | 30 | if DERAILED_APP.respond_to?(:initialized?) 31 | DERAILED_APP.initialize! unless DERAILED_APP.initialized? 32 | else 33 | DERAILED_APP.initialize! unless DERAILED_APP.instance_variable_get(:@initialized) 34 | end 35 | 36 | if !ENV["DERAILED_SKIP_ACTIVE_RECORD"] && defined? ActiveRecord 37 | if defined? ActiveRecord::Tasks::DatabaseTasks 38 | ActiveRecord::Tasks::DatabaseTasks.create_current 39 | else # Rails 3.2 40 | raise "No valid database for #{ENV['RAILS_ENV']}, please create one" unless ActiveRecord::Base.connection.active?.inspect 41 | end 42 | 43 | ActiveRecord::Migrator.migrations_paths = DERAILED_APP.paths['db/migrate'].to_a 44 | ActiveRecord::Migration.verbose = true 45 | 46 | # https://github.com/plataformatec/devise/blob/master/test/orm/active_record.rb 47 | if Rails.version >= "7.1" 48 | ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths).migrate 49 | elsif Rails.version >= "6.0" 50 | ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths, ActiveRecord::SchemaMigration).migrate 51 | elsif Rails.version.start_with?("5.2") 52 | ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths).migrate 53 | else 54 | ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, nil) 55 | end 56 | end 57 | 58 | DERAILED_APP.config.consider_all_requests_local = true 59 | end 60 | 61 | task :rack_load do 62 | puts "You're not using Rails" 63 | puts "You need to tell derailed how to boot your app" 64 | puts "In your perf.rake add:" 65 | puts 66 | puts "namespace :perf do" 67 | puts " task :rack_load do" 68 | puts " # DERAILED_APP = your code here" 69 | puts " end" 70 | puts "end" 71 | end 72 | 73 | task :setup do 74 | if DerailedBenchmarks.gem_is_bundled?("railties") 75 | Rake::Task["perf:rails_load"].invoke 76 | else 77 | Rake::Task["perf:rack_load"].invoke 78 | end 79 | 80 | WARM_COUNT = (ENV['WARM_COUNT'] || 0).to_i 81 | TEST_COUNT = (ENV['TEST_COUNT'] || ENV['CNT'] || 1_000).to_i 82 | PATH_TO_HIT = ENV["PATH_TO_HIT"] || ENV['ENDPOINT'] || "/" 83 | REQUEST_METHOD = ENV["REQUEST_METHOD"] || "GET" 84 | REQUEST_BODY = ENV["REQUEST_BODY"] 85 | puts "Method: #{REQUEST_METHOD}" 86 | puts "Endpoint: #{ PATH_TO_HIT.inspect }" 87 | 88 | # See https://www.rubydoc.info/github/rack/rack/file/SPEC#The_Environment 89 | # All HTTP_ variables are accepted in the Rack environment hash, except HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH. 90 | # For those, the HTTP_ prefix has to be removed. 91 | HTTP_HEADER_PREFIX = "HTTP_".freeze 92 | HTTP_HEADER_REGEXP = /^#{HTTP_HEADER_PREFIX}.+|CONTENT_(TYPE|LENGTH)$/ 93 | RACK_ENV_HASH = ENV.select { |key| key =~ HTTP_HEADER_REGEXP } 94 | 95 | HTTP_HEADERS = RACK_ENV_HASH.keys.inject({}) do |hash, rack_header_name| 96 | # e.g. "HTTP_ACCEPT_CHARSET" -> "Accept-Charset" 97 | upper_case_header_name = 98 | if rack_header_name.start_with?(HTTP_HEADER_PREFIX) 99 | rack_header_name[HTTP_HEADER_PREFIX.size..-1] 100 | else 101 | rack_header_name 102 | end 103 | 104 | header_name = upper_case_header_name.split("_").map(&:downcase).map(&:capitalize).join("-") 105 | 106 | hash[header_name] = RACK_ENV_HASH[rack_header_name] 107 | hash 108 | end 109 | puts "HTTP headers: #{HTTP_HEADERS}" unless HTTP_HEADERS.empty? 110 | 111 | CURL_HTTP_HEADER_ARGS = HTTP_HEADERS.map { |http_header_name, value| "-H \"#{http_header_name}: #{value}\"" }.join(" ") 112 | CURL_BODY_ARG = REQUEST_BODY ? "-d '#{REQUEST_BODY}'" : nil 113 | 114 | if REQUEST_METHOD != "GET" && REQUEST_BODY 115 | RACK_ENV_HASH["GATEWAY_INTERFACE"] = "CGI/1.1" 116 | RACK_ENV_HASH[:input] = REQUEST_BODY.dup 117 | puts "Body: #{REQUEST_BODY}" 118 | end 119 | 120 | require 'rack/test' 121 | 122 | DERAILED_APP = DerailedBenchmarks.add_auth(Object.class_eval { remove_const(:DERAILED_APP) }) 123 | if server = ENV["USE_SERVER"] 124 | @port = (3000..3900).to_a.sample 125 | puts "Port: #{ @port.inspect }" 126 | puts "Server: #{ server.inspect }" 127 | thread = Thread.new do 128 | # rack 3 doesn't have Rack::Server 129 | require 'rackup' unless defined?(Rack::Server) 130 | server_class = defined?(Rack::Server) ? Rack::Server : Rackup::Server 131 | server_class.start(app: DERAILED_APP, :Port => @port, environment: "none", server: server) 132 | end 133 | sleep 1 134 | 135 | def call_app(path = File.join("/", PATH_TO_HIT)) 136 | cmd = "curl -X #{REQUEST_METHOD} #{CURL_HTTP_HEADER_ARGS} #{CURL_BODY_ARG} -s --fail 'http://localhost:#{@port}#{path}' 2>&1" 137 | response = `#{cmd}` 138 | unless $?.success? 139 | STDERR.puts "Couldn't call app." 140 | STDERR.puts "Bad request to #{cmd.inspect} \n\n***RESPONSE***:\n\n#{ response.inspect }" 141 | 142 | FileUtils.mkdir_p("tmp") 143 | File.open("tmp/fail.html", "w+") {|f| f.write response } 144 | 145 | `open #{File.expand_path("tmp/fail.html")}` if ENV["DERAILED_DEBUG"] 146 | 147 | exit(1) 148 | end 149 | end 150 | else 151 | @app = Rack::MockRequest.new(DERAILED_APP) 152 | 153 | def call_app 154 | response = @app.request(REQUEST_METHOD, PATH_TO_HIT, RACK_ENV_HASH) 155 | if response.status != 200 156 | STDERR.puts "Couldn't call app. Bad request to #{PATH_TO_HIT}! Resulted in #{response.status} status." 157 | STDERR.puts "\n\n***RESPONSE BODY***\n\n" 158 | STDERR.puts response.body 159 | 160 | FileUtils.mkdir_p("tmp") 161 | File.open("tmp/fail.html", "w+") {|f| f.write response.body } 162 | 163 | `open #{File.expand_path("tmp/fail.html")}` if ENV["DERAILED_DEBUG"] 164 | 165 | exit(1) 166 | end 167 | response 168 | end 169 | end 170 | if WARM_COUNT > 0 171 | puts "Warming up app: #{WARM_COUNT} times" 172 | WARM_COUNT.times { call_app } 173 | end 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/require_tree.rb: -------------------------------------------------------------------------------- 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 | @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 26 | end 27 | 28 | def <<(tree) 29 | @children[tree.name.to_s] = tree 30 | tree.parent = self 31 | (REQUIRED_BY[tree.name.to_s] ||= []) << self.name 32 | end 33 | 34 | def [](name) 35 | @children[name.to_s] 36 | end 37 | 38 | # Returns array of child nodes 39 | def children 40 | @children.values 41 | end 42 | 43 | def cost 44 | @cost || 0 45 | end 46 | 47 | # Returns sorted array of child nodes from Largest to Smallest 48 | def sorted_children 49 | children.sort { |c1, c2| c2.cost <=> c1.cost } 50 | end 51 | 52 | def to_string 53 | str = String.new("#{name}: #{cost.round(4)} MiB") 54 | if parent && REQUIRED_BY[self.name.to_s] 55 | names = REQUIRED_BY[self.name.to_s].uniq - [parent.name.to_s] 56 | if names.any? 57 | str << " (Also required by: #{ names.first(2).join(", ") }" 58 | str << ", and #{names.count - 2} others" if names.count > 3 59 | str << ")" 60 | end 61 | end 62 | str 63 | end 64 | 65 | # Recursively prints all child nodes 66 | def print_sorted_children(level = 0, out = STDOUT) 67 | return if cost < ENV['CUT_OFF'].to_f 68 | out.puts " " * level + self.to_string 69 | level += 1 70 | sorted_children.each do |child| 71 | child.print_sorted_children(level, out) 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/stats_for_file.rb: -------------------------------------------------------------------------------- 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, :short_sha 20 | 21 | def initialize(file:, name:, desc: "", time: , short_sha: nil) 22 | @file = Pathname.new(file) 23 | FileUtils.touch(@file) 24 | 25 | @name = name 26 | @desc = desc 27 | @time = time 28 | @short_sha = short_sha 29 | end 30 | 31 | def call 32 | load_file! 33 | return if values.empty? 34 | 35 | @median = (values[(values.length - 1) / 2] + values[values.length/ 2]) / 2.0 36 | @average = values.inject(:+) / values.length 37 | end 38 | 39 | def empty? 40 | values.empty? 41 | end 42 | 43 | def median 44 | @median.to_f 45 | end 46 | 47 | def average 48 | @average.to_f 49 | end 50 | 51 | private def load_file! 52 | @values = [] 53 | @file.each_line do |line| 54 | line.match(/\( +(\d+\.\d+)\)/) 55 | begin 56 | values << BigDecimal($1) 57 | rescue => e 58 | raise e, "Problem with file #{@file.inspect}:\n#{@file.read}\n#{e.message}" 59 | end 60 | end 61 | 62 | values.sort! 63 | values.freeze 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/stats_from_dir.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bigdecimal' 4 | # keep this in sync with gemspec 5 | if RUBY_VERSION >= '3.0' 6 | require 'ruby-statistics' 7 | else 8 | require 'statistics' 9 | end 10 | require 'stringio' 11 | require 'mini_histogram' 12 | require 'mini_histogram/plot' 13 | 14 | module DerailedBenchmarks 15 | # A class used to read several benchmark files 16 | # it will parse each file, then sort by average 17 | # time of benchmarks. It can be used to find 18 | # the fastest and slowest examples and give information 19 | # about them such as what the percent difference is 20 | # and if the results are statistically significant 21 | # 22 | # Example: 23 | # 24 | # branch_info = {} 25 | # branch_info["loser"] = { desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), name: "loser" } 26 | # branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), name: "winner" } 27 | # stats = DerailedBenchmarks::StatsFromDir.new(branch_info) 28 | # 29 | # stats.newest.average # => 10.5 30 | # stats.oldest.average # => 11.0 31 | # stats.significant? # => true 32 | # stats.x_faster # => "1.0476" 33 | class StatsFromDir 34 | FORMAT = "%0.4f" 35 | attr_reader :stats, :oldest, :newest 36 | 37 | def initialize(input) 38 | @files = [] 39 | 40 | if input.is_a?(Hash) 41 | hash = input 42 | hash.each do |branch, info_hash| 43 | file = info_hash.fetch(:file) 44 | desc = info_hash.fetch(:desc) 45 | time = info_hash.fetch(:time) 46 | short_sha = info_hash[:short_sha] 47 | @files << StatsForFile.new(file: file, desc: desc, time: time, name: branch, short_sha: short_sha) 48 | end 49 | else 50 | input.each do |commit| 51 | @files << StatsForFile.new( 52 | file: commit.file, 53 | desc: commit.desc, 54 | time: commit.time, 55 | name: commit.ref, 56 | short_sha: commit.short_sha 57 | ) 58 | end 59 | end 60 | @files.sort_by! { |f| f.time } 61 | @oldest = @files.first 62 | @newest = @files.last 63 | end 64 | 65 | def call 66 | @files.each(&:call) 67 | 68 | return self if @files.detect(&:empty?) 69 | 70 | stats_95 = statistical_test(confidence: 95) 71 | 72 | # If default check is good, see if we also pass a more rigorous test 73 | # if so, then use the more rigourous test 74 | if stats_95[:alternative] 75 | stats_99 = statistical_test(confidence: 99) 76 | @stats = stats_99 if stats_99[:alternative] 77 | end 78 | @stats ||= stats_95 79 | 80 | self 81 | end 82 | 83 | def statistical_test(series_1=oldest.values, series_2=newest.values, confidence: 95) 84 | StatisticalTest::KSTest.two_samples( 85 | group_one: series_1, 86 | group_two: series_2, 87 | alpha: (100 - confidence) / 100.0 88 | ) 89 | end 90 | 91 | def significant? 92 | @stats[:alternative] 93 | end 94 | 95 | def d_max 96 | @stats[:d_max].to_f 97 | end 98 | 99 | def d_critical 100 | @stats[:d_critical].to_f 101 | end 102 | 103 | def x_faster 104 | (oldest.median/newest.median).to_f 105 | end 106 | 107 | def faster? 108 | newest.median < oldest.median 109 | end 110 | 111 | def percent_faster 112 | (((oldest.median - newest.median) / oldest.median).to_f * 100) 113 | end 114 | 115 | def change_direction 116 | if faster? 117 | "FASTER 🚀🚀🚀" 118 | else 119 | "SLOWER 🐢🐢🐢" 120 | end 121 | end 122 | 123 | def align 124 | " " * (percent_faster.to_s.index(".") - x_faster.to_s.index(".")) 125 | end 126 | 127 | def histogram(io = $stdout) 128 | dual_histogram = MiniHistogram.dual_plot do |a, b| 129 | a.values = newest.values 130 | a.options = { 131 | title: "\n [#{newest.short_sha || newest.name}] description:\n #{newest.desc.inspect}", 132 | xlabel: "# of runs in range" 133 | } 134 | b.values = oldest.values 135 | b.options = { 136 | title: "\n [#{oldest.short_sha || oldest.name}] description:\n #{oldest.desc.inspect}", 137 | xlabel: "# of runs in range" 138 | } 139 | end 140 | 141 | io.puts 142 | io.puts "Histograms (time ranges are in seconds):" 143 | io.puts(dual_histogram) 144 | io.puts 145 | end 146 | 147 | def banner(io = $stdout) 148 | return if @files.detect(&:empty?) 149 | 150 | io.puts 151 | if significant? 152 | io.puts "❤️ ❤️ ❤️ (Statistically Significant) ❤️ ❤️ ❤️" 153 | else 154 | io.puts "👎👎👎(NOT Statistically Significant) 👎👎👎" 155 | end 156 | io.puts 157 | io.puts "[#{newest.short_sha || newest.name}] (#{FORMAT % newest.median} seconds) #{newest.desc.inspect} ref: #{newest.name.inspect}" 158 | io.puts " #{change_direction} by:" 159 | io.puts " #{align}#{FORMAT % x_faster}x [older/newer]" 160 | io.puts " #{FORMAT % percent_faster}\% [(older - newer) / older * 100]" 161 | io.puts "[#{oldest.short_sha || oldest.name}] (#{FORMAT % oldest.median} seconds) #{oldest.desc.inspect} ref: #{oldest.name.inspect}" 162 | io.puts 163 | io.puts "Iterations per sample: #{ENV["TEST_COUNT"]}" 164 | io.puts "Samples: #{newest.values.length}" 165 | io.puts 166 | io.puts "Test type: Kolmogorov Smirnov" 167 | io.puts "Confidence level: #{@stats[:confidence_level] * 100} %" 168 | io.puts "Is significant? (max > critical): #{significant?}" 169 | io.puts "D critical: #{d_critical}" 170 | io.puts "D max: #{d_max}" 171 | 172 | histogram(io) 173 | 174 | io.puts 175 | end 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/tasks.rb: -------------------------------------------------------------------------------- 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"].chomp 21 | else 22 | library_dir = DerailedBenchmarks.rails_path_on_disk 23 | end 24 | library_dir = Pathname.new(library_dir) 25 | 26 | out_dir = Pathname.new("tmp/compare_branches/#{Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')}") 27 | out_dir.mkpath 28 | 29 | ref_string = ENV["SHAS_TO_TEST"] || ENV["REFS_TO_TEST"] || "" 30 | 31 | project = DerailedBenchmarks::Git::SwitchProject.new( 32 | path: library_dir, 33 | ref_array: ref_string.split(","), 34 | log_dir: out_dir 35 | ) 36 | 37 | stats = DerailedBenchmarks::StatsFromDir.new(project.commits) 38 | 39 | # Advertise branch names early to make sure people know what they're testing 40 | puts 41 | puts 42 | project.commits.each_with_index do |commit, i| 43 | puts "Testing #{i + 1}: #{commit.short_sha}: #{commit.description}" 44 | end 45 | puts 46 | puts 47 | 48 | project.restore_branch_on_return do 49 | DERAILED_SCRIPT_COUNT.times do |i| 50 | puts "Sample: #{i.next}/#{DERAILED_SCRIPT_COUNT} iterations per sample: #{ENV['TEST_COUNT']}" 51 | project.commits.each do |commit| 52 | commit.checkout! 53 | 54 | output = run!("#{script} 2>&1") 55 | commit.log.open("a") {|f| f.puts output.lines.last } 56 | end 57 | 58 | if (i % 50).zero? 59 | puts "Intermediate result" 60 | stats.call.banner 61 | puts "Continuing execution" 62 | end 63 | end 64 | end 65 | 66 | ensure 67 | if stats 68 | stats.call.banner 69 | 70 | result_file = out_dir.join("results.txt") 71 | File.open(result_file, "w") do |f| 72 | stats.banner(f) 73 | end 74 | 75 | puts "Output: #{result_file.to_s}" 76 | end 77 | end 78 | end 79 | 80 | desc "hits the url TEST_COUNT times" 81 | task :test => [:setup] do 82 | require 'benchmark' 83 | 84 | Benchmark.bm { |x| 85 | x.report("#{TEST_COUNT} derailed requests") { 86 | TEST_COUNT.times { 87 | call_app 88 | } 89 | } 90 | } 91 | end 92 | 93 | desc "stackprof" 94 | task :stackprof => [:setup] do 95 | # [:wall, :cpu, :object] 96 | begin 97 | require 'stackprof' 98 | rescue LoadError 99 | raise "Add stackprof to your gemfile to continue `gem 'stackprof', group: :development`" 100 | end 101 | TEST_COUNT = (ENV["TEST_COUNT"] ||= "100").to_i 102 | file = "tmp/#{Time.now.iso8601}-stackprof-cpu-myapp.dump" 103 | StackProf.run(mode: :cpu, out: file) do 104 | Rake::Task["perf:test"].invoke 105 | end 106 | cmd = "stackprof #{file}" 107 | puts "Running `#{cmd}`. Execute `stackprof --help` for more info" 108 | puts `#{cmd}` 109 | end 110 | 111 | task :kernel_require_patch do 112 | require 'derailed_benchmarks/core_ext/kernel_require.rb' 113 | end 114 | 115 | desc "show memory usage caused by invoking require per gem" 116 | task :mem => [:kernel_require_patch, :setup] do 117 | puts "## Impact of `require ` on RAM" 118 | puts 119 | puts "Showing all `require ` calls that consume #{ENV['CUT_OFF']} MiB or more of RSS" 120 | puts "Configure with `CUT_OFF=0` for all entries or `CUT_OFF=5` for few entries" 121 | 122 | puts "Note: Files only count against RAM on their first load." 123 | puts " If multiple libraries require the same file, then" 124 | puts " the 'cost' only shows up under the first library" 125 | puts 126 | 127 | call_app 128 | 129 | TOP_REQUIRE.print_sorted_children 130 | end 131 | 132 | desc "outputs memory usage over time" 133 | task :mem_over_time => [:setup] do 134 | require 'get_process_mem' 135 | puts "PID: #{Process.pid}" 136 | ram = GetProcessMem.new 137 | @keep_going = true 138 | begin 139 | unless ENV["SKIP_FILE_WRITE"] 140 | ruby = `ruby -v`.chomp 141 | FileUtils.mkdir_p("tmp") 142 | file = File.open("tmp/#{Time.now.iso8601}-#{ruby}-memory-#{TEST_COUNT}-times.txt", 'w') 143 | file.sync = true 144 | end 145 | 146 | ram_thread = Thread.new do 147 | while @keep_going 148 | mb = ram.mb 149 | STDOUT.puts mb 150 | file.puts mb unless ENV["SKIP_FILE_WRITE"] 151 | sleep 5 152 | end 153 | end 154 | 155 | TEST_COUNT.times { 156 | call_app 157 | } 158 | ensure 159 | @keep_going = false 160 | ram_thread.join 161 | file.close unless ENV["SKIP_FILE_WRITE"] 162 | end 163 | end 164 | 165 | task :ram_over_time do 166 | raise "Use mem_over_time" 167 | end 168 | 169 | desc "iterations per second" 170 | task :ips => [:setup] do 171 | require 'benchmark/ips' 172 | 173 | Benchmark.ips do |x| 174 | x.warmup = Float(ENV["IPS_WARMUP"] || 2) 175 | x.time = Float(ENV["IPS_TIME"] || 5) 176 | x.suite = ENV["IPS_SUITE"] if ENV["IPS_SUITE"] 177 | x.iterations = Integer(ENV["IPS_ITERATIONS"] || 1) 178 | 179 | x.report("ips") { call_app } 180 | end 181 | end 182 | 183 | desc "outputs GC::Profiler.report data while app is called TEST_COUNT times" 184 | task :gc => [:setup] do 185 | GC::Profiler.enable 186 | TEST_COUNT.times { call_app } 187 | GC::Profiler.report 188 | GC::Profiler.disable 189 | end 190 | 191 | desc "outputs allocated object diff after app is called TEST_COUNT times" 192 | task :allocated_objects => [:setup] do 193 | call_app 194 | GC.start 195 | GC.disable 196 | start = ObjectSpace.count_objects 197 | TEST_COUNT.times { call_app } 198 | finish = ObjectSpace.count_objects 199 | GC.enable 200 | finish.each do |k,v| 201 | puts k => (v - start[k]) / TEST_COUNT.to_f 202 | end 203 | end 204 | 205 | 206 | desc "profiles ruby allocation" 207 | task :objects => [:setup] do 208 | require 'memory_profiler' 209 | call_app 210 | GC.start 211 | 212 | num = Integer(ENV["TEST_COUNT"] || 1) 213 | opts = {} 214 | opts[:ignore_files] = /#{ENV['IGNORE_FILES_REGEXP']}/ if ENV['IGNORE_FILES_REGEXP'] 215 | opts[:allow_files] = "#{ENV['ALLOW_FILES']}" if ENV['ALLOW_FILES'] 216 | 217 | puts "Running #{num} times" 218 | report = MemoryProfiler.report(opts) do 219 | num.times { call_app } 220 | end 221 | report.pretty_print 222 | end 223 | 224 | desc "heap analyzer" 225 | task :heap => [:setup] do 226 | require 'objspace' 227 | 228 | file_name = "tmp/#{Time.now.iso8601}-heap.dump" 229 | FileUtils.mkdir_p("tmp") 230 | ObjectSpace.trace_object_allocations_start 231 | puts "Running #{ TEST_COUNT } times" 232 | TEST_COUNT.times { 233 | call_app 234 | } 235 | GC.start 236 | 237 | puts "Heap file generated: #{ file_name.inspect }" 238 | ObjectSpace.dump_all(output: File.open(file_name, 'w')) 239 | 240 | require 'heapy' 241 | 242 | Heapy::Analyzer.new(file_name).analyze 243 | 244 | puts "" 245 | puts "Run `$ heapy --help` for more options" 246 | puts "" 247 | puts "Also try uploading #{file_name.inspect} to http://tenderlove.github.io/heap-analyzer/" 248 | end 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://medium.com/klaxit-techblog/tracking-a-ruby-memory-leak-in-2021-9eb56575f731#875b to understand better what you are reading." 284 | end 285 | 286 | def run!(cmd) 287 | out = `#{cmd}` 288 | raise "Error while running #{cmd.inspect}: #{out}" unless $?.success? 289 | out 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /lib/derailed_benchmarks/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DerailedBenchmarks 4 | VERSION = "2.2.1" 5 | end 6 | -------------------------------------------------------------------------------- /test/derailed_benchmarks/core_ext/kernel_require_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class KernelRequireTest < ActiveSupport::TestCase 6 | setup do 7 | require 'derailed_benchmarks/core_ext/kernel_require' 8 | GC.disable 9 | end 10 | 11 | teardown do 12 | GC.enable 13 | DerailedBenchmarks::RequireTree.reset! 14 | end 15 | 16 | test "profiles load" do 17 | in_fork do 18 | require fixtures_dir("require/load_parent.rb") 19 | 20 | parent = assert_node_in_parent("load_parent.rb", TOP_REQUIRE) 21 | 22 | assert_node_in_parent("load_child.rb", parent) 23 | end 24 | end 25 | 26 | test "profiles autoload" do 27 | skip if RUBY_VERSION.start_with?("2.2") # Fails on CI, I can't install Ruby 2.2 locally to debug https://stackoverflow.com/questions/63926460/install-ruby-2-2-on-mac-osx-catalina-with-ruby-install, https://github.com/postmodern/ruby-install/issues/375 28 | 29 | in_fork do 30 | require fixtures_dir("require/autoload_parent.rb") 31 | parent = assert_node_in_parent("autoload_parent.rb", TOP_REQUIRE) 32 | 33 | assert_node_in_parent("autoload_child.rb", parent) 34 | end 35 | end 36 | 37 | test "core extension profiles useage" do 38 | in_fork do 39 | require fixtures_dir("require/parent_one.rb") 40 | parent = assert_node_in_parent("parent_one.rb", TOP_REQUIRE) 41 | assert_node_in_parent("child_one.rb", parent) 42 | child_two = assert_node_in_parent("child_two.rb", parent) 43 | assert_node_in_parent("relative_child", parent) 44 | assert_node_in_parent("relative_child_two", parent) 45 | assert_node_in_parent("raise_child.rb", child_two) 46 | end 47 | end 48 | 49 | # Checks to see that the given file name is present in the 50 | # parent tree node and that the memory of that file 51 | # is less than the parent (since the parent should include itself 52 | # plus its children) 53 | # 54 | # Returns the child node 55 | def assert_node_in_parent(file_name, parent) 56 | file = fixtures_dir(File.join("require", file_name)) 57 | node = parent[file] 58 | assert node, "Expected: #{parent.name} to include: #{file.to_s} but it did not.\nChildren: #{parent.children.map(&:name).map(&:to_s)}" 59 | unless parent == TOP_REQUIRE 60 | assert node.cost < parent.cost, "Expected: #{node.name.inspect} (#{node.cost}) to cost less than: #{parent.name.inspect} (#{parent.cost})" 61 | end 62 | node 63 | end 64 | 65 | # Used to get semi-clean process memory 66 | # It would be better to run the requires in a totally different process 67 | # but...that would take engineering 68 | # 69 | # If I was going to do that, I would find a way to serialize RequireTree 70 | # into a json structure with file names and costs, run the script 71 | # dump the json to a file, then in this process read the file and 72 | # run assertions 73 | def in_fork 74 | Tempfile.create("stdout") do |tmp_file| 75 | pid = fork do 76 | $stdout.reopen(tmp_file, "w") 77 | $stderr.reopen(tmp_file, "w") 78 | $stdout.sync = true 79 | $stderr.sync = true 80 | yield 81 | Kernel.exit!(0) # needed for https://github.com/seattlerb/minitest/pull/683 82 | end 83 | Process.waitpid(pid) 84 | 85 | if $?.success? 86 | print File.read(tmp_file) 87 | else 88 | raise File.read(tmp_file) 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/derailed_benchmarks/git_switch_project_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class GitSwitchProjectTest < ActiveSupport::TestCase 6 | test "tells me when it's not pointing at a git project" do 7 | exception = assert_raises { 8 | DerailedBenchmarks::Git::SwitchProject.new(path: "/dev/null") 9 | } 10 | assert_includes(exception.message, '.git directory') 11 | end 12 | 13 | test "dirty gemspec cleaning" do 14 | Dir.mktmpdir do |dir| 15 | run!("git clone https://github.com/sharpstone/default_ruby #{dir} 2>&1 && cd #{dir} && git checkout 6e642963acec0ff64af51bd6fba8db3c4176ed6e 2>&1 && git checkout -b mybranch 2>&1") 16 | run!("cd #{dir} && echo lol > foo.gemspec && git add .") 17 | 18 | io = StringIO.new 19 | project = DerailedBenchmarks::Git::SwitchProject.new(path: dir, io: io) 20 | 21 | assert project.dirty? 22 | refute project.clean? 23 | 24 | project.restore_branch_on_return do 25 | project.commits.map(&:checkout!) 26 | end 27 | 28 | assert_includes io.string, "Bundler modifies gemspec files" 29 | assert_includes io.string, "Applying stash" 30 | end 31 | end 32 | 33 | test "works on a git repo" do 34 | Dir.mktmpdir do |dir| 35 | run!("git clone https://github.com/sharpstone/default_ruby #{dir} 2>&1 && cd #{dir} && git checkout 6e642963acec0ff64af51bd6fba8db3c4176ed6e 2>&1 && git checkout -b mybranch 2>&1") 36 | 37 | # finds shas when none given 38 | project = DerailedBenchmarks::Git::SwitchProject.new(path: dir) 39 | 40 | assert_equal ["6e642963acec0ff64af51bd6fba8db3c4176ed6e", "da748a59340be8b950e7bbbfb32077eb67d70c3c"], project.commits.map(&:ref) 41 | first_commit = project.commits.first 42 | 43 | assert_equal "CI test support", first_commit.description 44 | assert_equal "6e64296", first_commit.short_sha 45 | assert_equal "/dev/null/6e642963acec0ff64af51bd6fba8db3c4176ed6e.bench.txt", first_commit.log.to_s 46 | assert_equal DateTime.parse("Tue, 14 Apr 2020 13:26:03 -0500"), first_commit.time 47 | 48 | assert_equal "mybranch", project.current_branch_or_sha 49 | 50 | # Finds shas when 1 is given 51 | project = DerailedBenchmarks::Git::SwitchProject.new(path: dir, ref_array: ["da748a59340be8b950e7bbbfb32077eb67d70c3c"]) 52 | 53 | assert_equal ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "5c09f748957d2098182762004adee27d1ff83160"], project.commits.map(&:ref) 54 | 55 | 56 | # Returns correct refs if given 57 | project = DerailedBenchmarks::Git::SwitchProject.new(path: dir, ref_array: ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "9b19275a592f148e2a53b87ead4ccd8c747539c9"]) 58 | 59 | assert_equal ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "9b19275a592f148e2a53b87ead4ccd8c747539c9"], project.commits.map(&:ref) 60 | 61 | first_commit = project.commits.first 62 | 63 | first_commit.checkout! 64 | 65 | assert_equal first_commit.short_sha, project.current_branch_or_sha 66 | 67 | # Test restore_branch_on_return 68 | project.restore_branch_on_return(quiet: true) do 69 | project.commits.last.checkout! 70 | 71 | assert_equal project.commits.last.short_sha, project.current_branch_or_sha 72 | end 73 | 74 | assert_equal project.commits.first.short_sha, project.current_branch_or_sha 75 | 76 | exception = assert_raise { 77 | DerailedBenchmarks::Git::SwitchProject.new(path: dir, ref_array: ["6e642963acec0ff64af51bd6fba8db3c4176ed6e", "mybranch"]) 78 | } 79 | 80 | assert_includes(exception.message, 'Duplicate SHA resolved "6e64296"') 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/derailed_benchmarks/require_tree_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class RequireTree < ActiveSupport::TestCase 6 | 7 | def tree(name) 8 | DerailedBenchmarks::RequireTree.new(name) 9 | end 10 | 11 | def teardown 12 | DerailedBenchmarks::RequireTree.reset! 13 | end 14 | 15 | test "default_cost" do 16 | parent = tree("parent") 17 | assert_equal 0, parent.cost 18 | value = rand(0..100) 19 | parent.cost = value 20 | 21 | assert_equal value, parent.cost 22 | end 23 | 24 | test "stores child" do 25 | parent = tree("parent") 26 | child = tree("child") 27 | parent << child 28 | 29 | # [](name) 30 | assert_equal child, parent["child"] 31 | # children 32 | assert_equal [child], parent.children 33 | assert_equal [child], parent.sorted_children 34 | end 35 | 36 | test "sorts children" do 37 | parent = tree("parent") 38 | parent.cost = rand(5..10) 39 | small = tree("small") 40 | small.cost = rand(10..100) 41 | 42 | large = tree("large") 43 | large.cost = small.cost + 1 44 | 45 | parent << small 46 | parent << large 47 | 48 | expected = [large, small] 49 | assert_equal expected, parent.sorted_children 50 | 51 | expected = <<-OUT 52 | parent: #{ parent.cost.round(4) } MiB 53 | large: #{ large.cost.round(4) } MiB 54 | small: #{ small.cost.round(4) } MiB 55 | OUT 56 | capture = StringIO.new 57 | 58 | parent.print_sorted_children(0, capture) 59 | 60 | assert_equal expected, capture.string 61 | end 62 | 63 | test "attributes duplicate children" do 64 | parent = tree("parent") 65 | parent.cost = rand(5..10) 66 | small = tree("small") 67 | small.cost = rand(10..100) 68 | 69 | large = tree("large") 70 | large.cost = small.cost + 1 71 | 72 | dup = tree("large") 73 | dup.cost = 0.4 74 | small << dup 75 | 76 | parent << small 77 | parent << large 78 | 79 | expected = [large, small] 80 | assert_equal expected, parent.sorted_children 81 | 82 | expected = [dup] 83 | assert_equal expected, small.sorted_children 84 | 85 | expected = <<-OUT 86 | parent: #{ parent.cost.round(4) } MiB 87 | large: #{ large.cost.round(4) } MiB (Also required by: small) 88 | small: #{ small.cost.round(4) } MiB 89 | large: #{ dup.cost.round(4) } MiB (Also required by: parent) 90 | OUT 91 | capture = StringIO.new 92 | parent.print_sorted_children(0, capture) 93 | assert_equal expected, capture.string 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/derailed_benchmarks/stats_from_dir_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class StatsFromDirTest < ActiveSupport::TestCase 6 | test "empty files" do 7 | Dir.mktmpdir do |dir| 8 | dir = Pathname.new(dir) 9 | branch_info = {} 10 | branch_info["loser"] = { desc: "Old commit", time: Time.now, file: dir.join("loser"), name: "loser" } 11 | branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner"), name: "winner" } 12 | stats = DerailedBenchmarks::StatsFromDir.new(branch_info).call 13 | io = StringIO.new 14 | stats.call.banner(io: io) # Doesn't error 15 | assert_equal "", io.read 16 | end 17 | end 18 | 19 | test "that it works" do 20 | dir = fixtures_dir("stats/significant") 21 | branch_info = {} 22 | branch_info["loser"] = { desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), name: "loser" } 23 | branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), name: "winner" } 24 | stats = DerailedBenchmarks::StatsFromDir.new(branch_info).call 25 | 26 | newest = stats.newest 27 | oldest = stats.oldest 28 | 29 | assert newest.average < oldest.average 30 | 31 | assert_equal "winner", newest.name 32 | assert_equal "loser", oldest.name 33 | 34 | assert_in_delta 0.26, stats.d_max, 0.01 35 | assert_in_delta 0.2145966026289347, stats.d_critical, 0.00001 36 | assert_equal true, stats.significant? 37 | 38 | format = DerailedBenchmarks::StatsFromDir::FORMAT 39 | assert_equal "1.0062", format % stats.x_faster 40 | assert_equal "0.6147", format % stats.percent_faster 41 | 42 | assert_equal "11.3844", format % newest.median 43 | end 44 | 45 | test "histogram output" do 46 | dir = fixtures_dir("stats/significant") 47 | branch_info = {} 48 | branch_info["loser"] = { desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), short_sha: "5594a2d" } 49 | branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), short_sha: "f1ab117" } 50 | stats = DerailedBenchmarks::StatsFromDir.new(branch_info).call 51 | 52 | io = StringIO.new 53 | stats.call.banner(io) 54 | 55 | puts io.string 56 | 57 | assert_match(/11\.2 , 11\.28/, io.string) 58 | assert_match(/11\.8 , 11\.88/, io.string) 59 | end 60 | 61 | 62 | test "alignment" do 63 | dir = fixtures_dir("stats/significant") 64 | branch_info = {} 65 | branch_info["loser"] = { desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), name: "loser" } 66 | branch_info["winner"] = { desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), name: "winner" } 67 | stats = DerailedBenchmarks::StatsFromDir.new(branch_info).call 68 | def stats.percent_faster 69 | -0.1 70 | end 71 | 72 | def stats.x_faster 73 | 0.9922 74 | end 75 | 76 | assert_equal 1, stats.align.length 77 | end 78 | 79 | test "banner faster" do 80 | dir = fixtures_dir("stats/significant") 81 | Branch_info = {} 82 | 83 | require 'ostruct' 84 | commits = [] 85 | commits << OpenStruct.new({ desc: "Old commit", time: Time.now, file: dir.join("loser.bench.txt"), ref: "loser", short_sha: "aaaaa" }) 86 | commits << OpenStruct.new({ desc: "I am the new commit", time: Time.now + 1, file: dir.join("winner.bench.txt"), ref: "winner", short_sha: "bbbbb" }) 87 | stats = DerailedBenchmarks::StatsFromDir.new(commits).call 88 | newest = stats.newest 89 | oldest = stats.oldest 90 | 91 | # Test fixture for banner 92 | def stats.d_max 93 | "0.037" 94 | end 95 | 96 | def stats.d_critical 97 | "0.001" 98 | end 99 | 100 | def newest.median 101 | 10.5 102 | end 103 | 104 | def oldest.median 105 | 11.0 106 | end 107 | 108 | expected = <<-EOM 109 | [bbbbb] (10.5000 seconds) "I am the new commit" ref: "winner" 110 | FASTER 🚀🚀🚀 by: 111 | 1.0476x [older/newer] 112 | 4.5455% [(older - newer) / older * 100] 113 | [aaaaa] (11.0000 seconds) "Old commit" ref: "loser" 114 | EOM 115 | 116 | actual = StringIO.new 117 | stats.banner(actual) 118 | 119 | assert_match expected, actual.string 120 | end 121 | 122 | test "banner slower" do 123 | dir = fixtures_dir("stats/significant") 124 | branch_info = {} 125 | branch_info["loser"] = { desc: "I am the new commit", time: Time.now, file: dir.join("loser.bench.txt"), name: "loser" } 126 | branch_info["winner"] = { desc: "Old commit", time: Time.now - 10, file: dir.join("winner.bench.txt"), name: "winner" } 127 | stats = DerailedBenchmarks::StatsFromDir.new(branch_info).call 128 | newest = stats.newest 129 | oldest = stats.oldest 130 | 131 | def oldest.median 132 | 10.5 133 | end 134 | 135 | def newest.median 136 | 11.0 137 | end 138 | 139 | expected = <<-EOM 140 | [loser] (11.0000 seconds) "I am the new commit" ref: "loser" 141 | SLOWER 🐢🐢🐢 by: 142 | 0.9545x [older/newer] 143 | -4.7619% [(older - newer) / older * 100] 144 | [winner] (10.5000 seconds) "Old commit" ref: "winner" 145 | EOM 146 | 147 | actual = StringIO.new 148 | stats.banner(actual) 149 | 150 | assert_match expected, actual.string 151 | end 152 | 153 | test "stats from samples with slightly different sizes" do 154 | stats = DerailedBenchmarks::StatsFromDir.new({}) 155 | out = stats.statistical_test([100,101,102, 100, 101, 99], [1,3, 3, 2]) 156 | assert out[:alternative] 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /test/derailed_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class DerailedBenchmarksTest < ActiveSupport::TestCase 6 | test "truth" do 7 | assert_kind_of Module, DerailedBenchmarks 8 | end 9 | 10 | test "gem_is_bundled?" do 11 | assert DerailedBenchmarks.gem_is_bundled?("rack") 12 | refute DerailedBenchmarks.gem_is_bundled?("wicked") 13 | end 14 | 15 | test "readme contains correct output" do 16 | readme_path = File.join(__dir__, "..", "README.md") 17 | lines = File.foreach(readme_path) 18 | lineno = 1 19 | expected = lines.lazy.drop_while { |line| 20 | lineno += 1 21 | line != "$ bundle exec derailed exec --help\n" 22 | }.drop(1).take_while { |line| line != "```\n" }.force.join.split("\n").sort 23 | actual = `bundle exec derailed exec --help`.split("\n").sort 24 | assert_equal( 25 | expected, 26 | actual, 27 | "Please update README.md:#{lineno}" 28 | ) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/fixtures/require/autoload_child.rb: -------------------------------------------------------------------------------- 1 | @retained = String.new("") 2 | 1_000_000.times.map { @retained << "A" } 3 | 4 | module AutoLoadChild 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/require/autoload_parent.rb: -------------------------------------------------------------------------------- 1 | @retained = String.new("") 2 | 1_000_000.times.map { @retained << "A" } 3 | 4 | autoload :AutoLoadChild, File.join(__dir__, 'autoload_child.rb') 5 | 6 | if AutoLoadChild 7 | # yay 8 | end 9 | -------------------------------------------------------------------------------- /test/fixtures/require/child_one.rb: -------------------------------------------------------------------------------- 1 | class ChildOne 2 | @retained = String.new("") 3 | 50_000.times.map { @retained << "A" } 4 | end 5 | -------------------------------------------------------------------------------- /test/fixtures/require/child_two.rb: -------------------------------------------------------------------------------- 1 | class ChildTwo 2 | @retained = String.new("") 3 | 200_000.times.map { @retained << "A" } 4 | end 5 | 6 | begin 7 | require File.expand_path('../raise_child.rb', __FILE__) 8 | rescue 9 | end 10 | -------------------------------------------------------------------------------- /test/fixtures/require/load_child.rb: -------------------------------------------------------------------------------- 1 | @retained = String.new("") 2 | 1_000_000.times.map { @retained << "A" } 3 | 4 | -------------------------------------------------------------------------------- /test/fixtures/require/load_parent.rb: -------------------------------------------------------------------------------- 1 | @retained = String.new("") 2 | 1_000_000.times.map { @retained << "A" } 3 | 4 | load File.join(__dir__, "load_child.rb") 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/require/parent_one.rb: -------------------------------------------------------------------------------- 1 | class ParentOne 2 | @retained = String.new("") 3 | 1_000_000.times.map { @retained << "A" } 4 | end 5 | require File.expand_path('../child_one.rb', __FILE__) 6 | require File.expand_path('../child_two.rb', __FILE__) 7 | require_relative 'relative_child' 8 | require_relative File.expand_path('relative_child_two', __dir__) 9 | -------------------------------------------------------------------------------- /test/fixtures/require/raise_child.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RaiseChild 4 | end 5 | 6 | raise "Ohno" 7 | -------------------------------------------------------------------------------- /test/fixtures/require/relative_child.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RelativeChild 4 | end 5 | -------------------------------------------------------------------------------- /test/fixtures/require/relative_child_two.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RelativeChildTwo 4 | end 5 | -------------------------------------------------------------------------------- /test/fixtures/stats/significant/loser.bench.txt: -------------------------------------------------------------------------------- 1 | 9.590142 0.831269 10.457801 ( 11.437769) 2 | 9.836019 0.837319 10.728024 ( 11.792425) 3 | 9.889497 0.837097 10.762795 ( 11.747066) 4 | 9.532349 0.835770 10.407767 ( 11.343758) 5 | 9.498824 0.821246 10.366225 ( 11.282013) 6 | 9.531201 0.834812 10.412715 ( 11.330127) 7 | 9.583804 0.830178 10.449280 ( 11.384867) 8 | 9.681340 0.834776 10.553697 ( 11.496842) 9 | 9.629079 0.820234 10.487276 ( 11.406677) 10 | 9.616845 0.818370 10.481193 ( 11.395825) 11 | 9.738706 0.825397 10.600923 ( 11.541978) 12 | 9.613132 0.827242 10.477559 ( 11.406065) 13 | 9.486763 0.818159 10.342470 ( 11.305299) 14 | 9.590618 0.833308 10.468615 ( 11.428548) 15 | 9.725126 0.842955 10.604727 ( 11.580922) 16 | 9.598757 0.846951 10.491222 ( 11.462324) 17 | 9.484803 0.836242 10.366269 ( 11.328111) 18 | 9.591107 0.818305 10.455931 ( 11.381660) 19 | 9.745620 0.841107 10.623715 ( 11.572584) 20 | 9.670502 0.826673 10.535566 ( 11.464770) 21 | 9.573957 0.825235 10.439166 ( 11.358507) 22 | 9.468308 0.817744 10.330541 ( 11.240717) 23 | 9.799312 0.833922 10.670213 ( 11.645225) 24 | 9.575413 0.828039 10.444712 ( 11.366111) 25 | 9.632808 0.828399 10.506581 ( 11.439761) 26 | 9.599766 0.829294 10.467003 ( 11.427869) 27 | 9.521930 0.828257 10.388242 ( 11.347915) 28 | 9.842608 0.815513 10.694427 ( 11.667158) 29 | 9.590377 0.837459 10.467418 ( 11.433524) 30 | 9.729984 0.819101 10.587020 ( 11.539660) 31 | 9.540025 0.819396 10.396442 ( 11.314444) 32 | 9.615953 0.827258 10.479946 ( 11.414527) 33 | 9.572009 0.824862 10.438432 ( 11.362800) 34 | 9.612657 0.818645 10.476568 ( 11.385235) 35 | 9.755889 0.823267 10.615302 ( 11.545301) 36 | 9.493372 0.813202 10.345646 ( 11.254617) 37 | 9.529610 0.816457 10.391484 ( 11.305237) 38 | 9.575646 0.828898 10.449636 ( 11.374993) 39 | 9.533278 0.828915 10.405592 ( 11.347798) 40 | 9.692731 0.845925 10.577313 ( 11.545701) 41 | 9.662406 0.835481 10.543032 ( 11.511367) 42 | 9.588803 0.834782 10.468166 ( 11.427231) 43 | 9.696038 0.832612 10.573877 ( 11.545533) 44 | 9.612567 0.833363 10.491381 ( 11.410244) 45 | 9.471584 0.836005 10.352883 ( 11.303213) 46 | 9.682906 0.829932 10.558423 ( 11.466843) 47 | 9.676123 0.825750 10.548111 ( 11.468789) 48 | 9.509686 0.826678 10.380435 ( 11.290658) 49 | 9.552683 0.826631 10.421387 ( 11.337799) 50 | 9.579964 0.829423 10.447095 ( 11.358198) 51 | 9.506519 0.812635 10.357147 ( 11.313867) 52 | 9.654363 0.839408 10.531093 ( 11.515562) 53 | 9.576167 0.833579 10.447897 ( 11.421267) 54 | 9.498507 0.826285 10.370417 ( 11.336780) 55 | 9.758637 0.842156 10.645638 ( 11.595915) 56 | 9.635031 0.836329 10.516094 ( 11.475492) 57 | 9.934052 0.825471 10.794286 ( 11.812346) 58 | 9.652537 0.821982 10.520060 ( 11.434903) 59 | 9.526788 0.820300 10.384780 ( 11.306397) 60 | 9.473180 0.812507 10.329689 ( 11.233813) 61 | 9.862016 0.841529 10.757393 ( 11.734586) 62 | 9.534627 0.821267 10.392666 ( 11.313970) 63 | 9.640884 0.837997 10.515254 ( 11.489616) 64 | 9.535812 0.826216 10.407273 ( 11.318032) 65 | 9.588703 0.851935 10.476997 ( 11.462256) 66 | 9.574569 0.834756 10.454909 ( 11.404434) 67 | 9.650073 0.839516 10.535755 ( 11.488113) 68 | 9.551275 0.822510 10.415396 ( 11.378195) 69 | 9.627255 0.829954 10.500136 ( 11.458503) 70 | 9.560385 0.814457 10.419578 ( 11.333235) 71 | 9.572809 0.819290 10.438854 ( 11.349594) 72 | 9.660163 0.824722 10.530198 ( 11.443437) 73 | 9.661319 0.837408 10.550881 ( 11.512634) 74 | 9.637423 0.837322 10.520727 ( 11.432594) 75 | 9.664915 0.825478 10.526599 ( 11.464716) 76 | 9.644935 0.814938 10.505644 ( 11.424535) 77 | 9.799771 0.835598 10.671993 ( 11.613622) 78 | 9.791496 0.840368 10.676233 ( 11.643770) 79 | 9.760101 0.850254 10.648067 ( 11.619884) 80 | 9.784358 0.829651 10.658058 ( 11.632889) 81 | 9.727932 0.844568 10.616464 ( 11.578881) 82 | 9.776381 0.847439 10.663001 ( 11.648257) 83 | 9.839221 0.835333 10.714699 ( 11.670378) 84 | 9.697873 0.836432 10.570815 ( 11.541265) 85 | 9.867105 0.836122 10.741859 ( 11.681261) 86 | 9.675377 0.826509 10.539536 ( 11.465271) 87 | 9.703541 0.830895 10.578611 ( 11.502074) 88 | 9.717583 0.832110 10.586737 ( 11.531415) 89 | 9.784151 0.842351 10.662311 ( 11.647167) 90 | 9.741646 0.832834 10.612608 ( 11.580701) 91 | 9.687384 0.798745 10.525026 ( 11.493736) 92 | 9.698579 0.851183 10.586010 ( 11.588731) 93 | 9.712651 0.823867 10.573837 ( 11.540969) 94 | 9.657543 0.829349 10.524768 ( 11.443846) 95 | 9.675987 0.807980 10.521943 ( 11.451106) 96 | 9.744757 0.817850 10.600094 ( 11.535379) 97 | 9.683474 0.836913 10.557015 ( 11.525771) 98 | 9.922540 0.843157 10.805096 ( 11.808377) 99 | 9.696813 0.821768 10.554695 ( 11.464342) 100 | 9.760965 0.836511 10.636968 ( 11.594082) 101 | -------------------------------------------------------------------------------- /test/fixtures/stats/significant/winner.bench.txt: -------------------------------------------------------------------------------- 1 | 9.558387 0.795543 10.392696 ( 11.309311) 2 | 9.524045 0.803011 10.364301 ( 11.318477) 3 | 9.534609 0.804759 10.383564 ( 11.340585) 4 | 9.535700 0.800444 10.373048 ( 11.289682) 5 | 9.532372 0.794646 10.371722 ( 11.287656) 6 | 9.556350 0.822103 10.425949 ( 11.413659) 7 | 9.586525 0.824110 10.456246 ( 11.429651) 8 | 9.551907 0.830509 10.428443 ( 11.411978) 9 | 9.518711 0.834491 10.398652 ( 11.376422) 10 | 9.569772 0.827570 10.442956 ( 11.413585) 11 | 9.618950 0.829319 10.485139 ( 11.440848) 12 | 9.556727 0.807981 10.401758 ( 11.328267) 13 | 9.480701 0.804683 10.322360 ( 11.245781) 14 | 9.563369 0.801410 10.409686 ( 11.334188) 15 | 9.493082 0.805298 10.335983 ( 11.248441) 16 | 9.681861 0.803602 10.524930 ( 11.456107) 17 | 9.614529 0.781155 10.444055 ( 11.364476) 18 | 9.597825 0.806409 10.442217 ( 11.365743) 19 | 9.538346 0.813941 10.388972 ( 11.346084) 20 | 9.538091 0.808328 10.391165 ( 11.346197) 21 | 9.502600 0.812638 10.360783 ( 11.306602) 22 | 9.571149 0.826238 10.449697 ( 11.411387) 23 | 9.531260 0.821429 10.390722 ( 11.532200) 24 | 9.611447 0.783734 10.431579 ( 11.351863) 25 | 9.533522 0.806067 10.384192 ( 11.296454) 26 | 9.586843 0.820340 10.444013 ( 11.383357) 27 | 9.615441 0.804255 10.456321 ( 11.385184) 28 | 9.462530 0.803438 10.302507 ( 11.223665) 29 | 9.676985 0.789649 10.511461 ( 11.427901) 30 | 9.574692 0.816601 10.427670 ( 11.374204) 31 | 9.596892 0.803796 10.437442 ( 11.362358) 32 | 9.562942 0.815001 10.415687 ( 11.383593) 33 | 9.622502 0.804110 10.470848 ( 11.488275) 34 | 9.766782 0.828892 10.632272 ( 11.635267) 35 | 9.612909 0.804247 10.455650 ( 11.421374) 36 | 9.537415 0.805782 10.390754 ( 11.294518) 37 | 9.763286 0.805568 10.614687 ( 11.533764) 38 | 9.507627 0.806313 10.350967 ( 11.299277) 39 | 9.469710 0.803944 10.312100 ( 11.232190) 40 | 9.535007 0.795200 10.371960 ( 11.292289) 41 | 9.530755 0.797043 10.372644 ( 11.289316) 42 | 9.588961 0.806621 10.431681 ( 11.368492) 43 | 9.592512 0.808849 10.446866 ( 11.359820) 44 | 9.653610 0.803463 10.501491 ( 11.419194) 45 | 9.547770 0.812003 10.405405 ( 11.368690) 46 | 9.682181 0.812963 10.530854 ( 11.485025) 47 | 9.491677 0.807396 10.344595 ( 11.281067) 48 | 9.587365 0.813596 10.442915 ( 11.394766) 49 | 9.569528 0.814968 10.421925 ( 11.395829) 50 | 9.499610 0.806958 10.342308 ( 11.266410) 51 | 9.470981 0.802210 10.311858 ( 11.228286) 52 | 9.562924 0.794929 10.395599 ( 11.322258) 53 | 9.601453 0.810256 10.456259 ( 11.374217) 54 | 9.505371 0.799272 10.354669 ( 11.279456) 55 | 9.457992 0.795362 10.289520 ( 11.205184) 56 | 9.628120 0.787671 10.453407 ( 11.377989) 57 | 9.627611 0.805838 10.470388 ( 11.399739) 58 | 9.675034 0.812966 10.532779 ( 11.515440) 59 | 9.612906 0.807182 10.457964 ( 11.434272) 60 | 9.480996 0.803877 10.325013 ( 11.265876) 61 | 9.717399 0.823376 10.577638 ( 11.569749) 62 | 9.665028 0.809491 10.511645 ( 11.488256) 63 | 9.512832 0.805858 10.363675 ( 11.339722) 64 | 9.654066 0.807307 10.506755 ( 11.426100) 65 | 9.865550 0.794908 10.703626 ( 11.618194) 66 | 9.652618 0.793610 10.493186 ( 11.419415) 67 | 9.499487 0.796346 10.341364 ( 11.250758) 68 | 9.544258 0.797515 10.385862 ( 11.284281) 69 | 9.739863 0.794279 10.570723 ( 11.509588) 70 | 9.487554 0.785309 10.316760 ( 11.233325) 71 | 9.481721 0.803731 10.329705 ( 11.255686) 72 | 9.466643 0.802025 10.313663 ( 11.234516) 73 | 9.565479 0.798706 10.406513 ( 11.374955) 74 | 9.546849 0.818211 10.409684 ( 11.368566) 75 | 9.559145 0.813582 10.418666 ( 11.401304) 76 | 9.547626 0.787676 10.380384 ( 11.305801) 77 | 9.731920 0.806463 10.576084 ( 11.499545) 78 | 9.634309 0.804944 10.477565 ( 11.398455) 79 | 9.663389 0.797418 10.499369 ( 11.418504) 80 | 9.741374 0.818880 10.597056 ( 11.575796) 81 | 9.683985 0.804469 10.527844 ( 11.457434) 82 | 9.739006 0.808335 10.587852 ( 11.513780) 83 | 9.761998 0.818945 10.618427 ( 11.614032) 84 | 9.737508 0.819736 10.593885 ( 11.588014) 85 | 9.735949 0.821038 10.595284 ( 11.552597) 86 | 9.750022 0.814069 10.601283 ( 11.567239) 87 | 9.700983 0.801116 10.542112 ( 11.471005) 88 | 9.720313 0.798207 10.555314 ( 11.473235) 89 | 9.685407 0.811225 10.534452 ( 11.467112) 90 | 9.677940 0.809071 10.526291 ( 11.447495) 91 | 9.609120 0.813429 10.467227 ( 11.372680) 92 | 9.712403 0.810281 10.560867 ( 11.485852) 93 | 9.748022 0.817132 10.603028 ( 11.522460) 94 | 9.737389 0.801790 10.576720 ( 11.522855) 95 | 9.709541 0.795349 10.542238 ( 11.544047) 96 | 9.658660 0.819237 10.515718 ( 11.520783) 97 | 9.765426 0.829642 10.632481 ( 11.615062) 98 | 9.731822 0.809695 10.578871 ( 11.558062) 99 | 9.575340 0.800450 10.421430 ( 11.318465) 100 | 9.682845 0.796365 10.515529 ( 11.435012) 101 | -------------------------------------------------------------------------------- /test/integration/tasks_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'shellwords' 5 | 6 | class TasksTest < ActiveSupport::TestCase 7 | 8 | def setup 9 | FileUtils.mkdir_p(rails_app_path('tmp')) 10 | end 11 | 12 | def teardown 13 | FileUtils.remove_entry_secure(rails_app_path('tmp')) 14 | end 15 | 16 | def run!(cmd) 17 | out = `#{cmd}` 18 | raise "Could not run #{cmd}, output: #{out}" unless $?.success? 19 | out 20 | end 21 | 22 | def rake(cmd, options = {}) 23 | assert_success = options.key?(:assert_success) ? options[:assert_success] : true 24 | env = options[:env] || {} 25 | env_string = env.map {|key, value| "#{key.shellescape}=#{value.to_s.shellescape}" }.join(" ") 26 | cmd = "env #{env_string} bundle exec rake -f perf.rake #{cmd} --trace" 27 | result = Bundler.with_original_env do 28 | # Ensure relative BUNDLE_GEMFILE is expanded so path is still correct after cd 29 | ENV['BUNDLE_GEMFILE'] = File.expand_path(ENV['BUNDLE_GEMFILE']) if ENV['BUNDLE_GEMFILE'] 30 | `cd '#{rails_app_path}' && #{cmd} 2>&1` 31 | end 32 | if assert_success && !$?.success? 33 | puts result 34 | raise "Expected '#{cmd}' to return a success status.\nOutput: #{result}" 35 | end 36 | 37 | result 38 | end 39 | 40 | test 'non-rails library with branch specified' do 41 | skip unless ENV['USING_RAILS_WICKED_BRANCH'] 42 | 43 | gem_path = run!("bundle info wicked --path") 44 | env = { "TEST_COUNT" => 10, "DERAILED_SCRIPT_COUNT" => 2, "DERAILED_PATH_TO_LIBRARY" => gem_path} 45 | puts rake "perf:library", { env: env } 46 | end 47 | 48 | test 'rails perf:library from git' do 49 | # BUNDLE_GEMFILE="gemfiles/rails_git.gemfile" bundle exec m test/integration/tasks_test.rb: 50 | 51 | skip # unless ENV['USING_RAILS_GIT'] 52 | 53 | env = { "TEST_COUNT" => 2, "DERAILED_SCRIPT_COUNT" => 2, 54 | "SHAS_TO_TEST" => "fd9308a2925e862435859e1803e720e6eebe4bb6,aa85e897312396b5c6993d8092b9aff7faa93011"} 55 | puts rake "perf:library", { env: env } 56 | end 57 | 58 | test "rails perf:library with bad script" do 59 | # BUNDLE_GEMFILE="gemfiles/rails_git.gemfile" bundle exec m test/integration/tasks_test.rb: 60 | 61 | skip # unless ENV['USING_RAILS_GIT'] 62 | 63 | error = assert_raises { 64 | env = { "DERAILED_SCRIPT" => "nopenopenop", "TEST_COUNT" => 2, "DERAILED_SCRIPT_COUNT" => 2, 65 | "SHAS_TO_TEST" => "fd9308a2925e862435859e1803e720e6eebe4bb6,aa85e897312396b5c6993d8092b9aff7faa93011"} 66 | puts rake "perf:library", { env: env } 67 | } 68 | 69 | assert error.message =~ /nopenopenop:( command)? not found/, "Expected #{error.message} to include /nopenopenop: (command)? not found/ but it did not" 70 | end 71 | 72 | test 'hitting authenticated devise apps' do 73 | env = { "PATH_TO_HIT" => "authenticated", "USE_AUTH" => "true", "TEST_COUNT" => "2" } 74 | result = rake 'perf:test', env: env 75 | assert_match 'Auth: true', result 76 | 77 | env["USE_SERVER"] = "webrick" 78 | result = rake 'perf:test', env: env 79 | assert_match 'Auth: true', result 80 | assert_match 'Server: "webrick"', result 81 | end 82 | 83 | test 'authenticate with a custom user' do 84 | env = { "AUTH_CUSTOM_USER" => "true", "PATH_TO_HIT" => "authenticated", "USE_AUTH" => "true", "TEST_COUNT" => "2" } 85 | result = rake 'perf:test', env: env 86 | assert_match 'Auth: true', result 87 | end 88 | 89 | test 'test' do 90 | rake "perf:test" 91 | end 92 | 93 | test 'app' do 94 | skip unless ENV['USING_RAILS_GIT'] 95 | run!("cd #{rails_app_path} && git init . && git add . && git commit -m first && git commit --allow-empty -m second") 96 | env = { "TEST_COUNT" => 10, "DERAILED_SCRIPT_COUNT" => 2 } 97 | puts rake "perf:app", { env: env } 98 | end 99 | 100 | test 'TEST_COUNT' do 101 | result = rake "perf:test", env: { "TEST_COUNT" => 1 } 102 | assert_match "1 derailed requests", result 103 | end 104 | 105 | test 'WARM_COUNT' do 106 | result = rake "perf:test", env: { "WARM_COUNT" => 1 } 107 | assert_match "Warming up app:", result 108 | end 109 | 110 | test 'PATH_TO_HIT' do 111 | env = { "PATH_TO_HIT" => 'foo', "TEST_COUNT" => "2" } 112 | result = rake "perf:test", env: env 113 | assert_match 'Endpoint: "foo"', result 114 | 115 | env["USE_SERVER"] = "webrick" 116 | result = rake "perf:test", env: env 117 | assert_match 'Endpoint: "foo"', result 118 | assert_match 'Server: "webrick"', result 119 | end 120 | 121 | test 'HTTP headers' do 122 | env = { 123 | "PATH_TO_HIT" => 'foo_secret', 124 | "TEST_COUNT" => "2", 125 | "HTTP_AUTHORIZATION" => "Basic #{Base64.strict_encode64("admin:secret")}", 126 | "HTTP_CACHE_CONTROL" => "no-cache" 127 | } 128 | result = rake "perf:test", env: env 129 | assert_match 'Endpoint: "foo_secret"', result 130 | assert_match (/"Authorization"\s?=>\s?"Basic YWRtaW46c2VjcmV0"/), result 131 | assert_match (/"Cache-Control"\s?=>\s?"no-cache"/), result 132 | 133 | env["USE_SERVER"] = "webrick" 134 | result = rake "perf:test", env: env 135 | assert_match (/"Authorization"\s?=>\s?"Basic YWRtaW46c2VjcmV0"/), result 136 | assert_match (/"Cache-Control"\s?=>\s?"no-cache"/), result 137 | end 138 | 139 | test 'CONTENT_TYPE' do 140 | env = { 141 | "REQUEST_METHOD" => "POST", 142 | "PATH_TO_HIT" => "users", 143 | "CONTENT_TYPE" => "application/json", 144 | "REQUEST_BODY" => '{"user":{"email":"foo@bar.com","password":"123456","password_confirmation":"123456"}}', 145 | "TEST_COUNT" => "2" 146 | } 147 | 148 | result = rake "perf:test", env: env 149 | assert_match 'Body: {"user":{"email":"foo@bar.com","password":"123456","password_confirmation":"123456"}}', result 150 | assert_match(/HTTP headers: {"Content-Type"\s?=>\s?"application\/json"}/, result) 151 | 152 | env["USE_SERVER"] = "webrick" 153 | result = rake "perf:test", env: env 154 | assert_match 'Body: {"user":{"email":"foo@bar.com","password":"123456","password_confirmation":"123456"}}', result 155 | assert_match(/HTTP headers: {"Content-Type"\s?=>\s?"application\/json"}/, result) 156 | end 157 | 158 | test 'REQUEST_METHOD and REQUEST_BODY' do 159 | env = { 160 | "REQUEST_METHOD" => "POST", 161 | "PATH_TO_HIT" => "users", 162 | "REQUEST_BODY" => "user%5Bemail%5D=foo%40bar.com&user%5Bpassword%5D=123456&user%5Bpassword_confirmation%5D=123456", 163 | "TEST_COUNT" => "2" 164 | } 165 | 166 | result = rake "perf:test", env: env 167 | assert_match 'Endpoint: "users"', result 168 | assert_match 'Method: POST', result 169 | assert_match 'Body: user%5Bemail%5D=foo%40bar.com&user%5Bpassword%5D=123456&user%5Bpassword_confirmation%5D=123456', result 170 | 171 | env["USE_SERVER"] = "webrick" 172 | result = rake "perf:test", env: env 173 | assert_match 'Method: POST', result 174 | assert_match 'Body: user%5Bemail%5D=foo%40bar.com&user%5Bpassword%5D=123456&user%5Bpassword_confirmation%5D=123456', result 175 | end 176 | 177 | test 'USE_SERVER' do 178 | result = rake "perf:test", env: { "USE_SERVER" => 'webrick', "TEST_COUNT" => "2" } 179 | assert_match 'Server: "webrick"', result 180 | end 181 | 182 | test '' do 183 | end 184 | 185 | test 'objects' do 186 | rake "perf:objects" 187 | end 188 | 189 | test 'mem' do 190 | rake "perf:mem" 191 | end 192 | 193 | test 'mem_over_time' do 194 | rake "perf:mem_over_time" 195 | end 196 | 197 | test 'ips' do 198 | rake "perf:ips" 199 | end 200 | 201 | test 'heap_diff' do 202 | rake "perf:heap_diff", env: { "TEST_COUNT" => 5 } 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /test/rails_app/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require File.expand_path('../config/application', __FILE__) 7 | require 'rake' 8 | 9 | Dummy::Application.load_tasks 10 | -------------------------------------------------------------------------------- /test/rails_app/app/assets/config/manifest.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zombocom/derailed_benchmarks/e8b29db93acf7d3318b9afb42d8516b905020afb/test/rails_app/app/assets/config/manifest.js -------------------------------------------------------------------------------- /test/rails_app/app/assets/javascripts/authenticated.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /test/rails_app/app/assets/stylesheets/authenticated.css: -------------------------------------------------------------------------------- 1 | /* 2 | Place all the styles related to the matching controller here. 3 | They will automatically be included in application.css. 4 | */ 5 | -------------------------------------------------------------------------------- /test/rails_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | if respond_to?(:before_filter) && !respond_to?(:before_action) 5 | class << self 6 | alias :before_action :before_filter 7 | end 8 | end 9 | 10 | protect_from_forgery 11 | before_action :pull_out_locale 12 | 13 | 14 | def pull_out_locale 15 | I18n.locale = params[:locale] if params[:locale].present? 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/rails_app/app/controllers/authenticated_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AuthenticatedController < ApplicationController 4 | before_action :authenticate_user! 5 | 6 | def index 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/rails_app/app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PagesController < ApplicationController 4 | http_basic_authenticate_with name: "admin", password: "secret", only: :secret 5 | 6 | def index 7 | end 8 | 9 | def secret 10 | render action: 'index' 11 | end 12 | 13 | private 14 | end 15 | -------------------------------------------------------------------------------- /test/rails_app/app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | def create 3 | User.create!(user_params) 4 | 5 | head :created 6 | end 7 | 8 | private 9 | 10 | def user_params 11 | params.require(:user).permit(:email, :password, :password_confirmation) 12 | end 13 | end -------------------------------------------------------------------------------- /test/rails_app/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | end 5 | -------------------------------------------------------------------------------- /test/rails_app/app/helpers/authenticated_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AuthenticatedHelper 4 | end 5 | -------------------------------------------------------------------------------- /test/rails_app/app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ActiveRecord::Base 4 | # Include default devise modules. Others available are: 5 | # :confirmable, :lockable, :timeoutable and :omniauthable 6 | devise :database_authenticatable, :recoverable, 7 | :registerable, :rememberable, :timeoutable, 8 | :trackable, :validatable 9 | 10 | # Setup accessible (or protected) attributes for your model 11 | # attr_accessible :email, :password, :password_confirmation, :remember_me 12 | # attr_accessible :title, :body 13 | end 14 | -------------------------------------------------------------------------------- /test/rails_app/app/views/authenticated/index.html.erb: -------------------------------------------------------------------------------- 1 | sup 2 | -------------------------------------------------------------------------------- /test/rails_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%# stylesheet_link_tag :all %> 6 | <%# javascript_include_tag :defaults %> 7 | <%= csrf_meta_tag %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/rails_app/app/views/pages/index.html.erb: -------------------------------------------------------------------------------- 1 | ohai 2 | -------------------------------------------------------------------------------- /test/rails_app/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require ::File.expand_path('../config/environment', __FILE__) 6 | run Dummy::Application 7 | -------------------------------------------------------------------------------- /test/rails_app/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path('../boot', __FILE__) 4 | 5 | require "active_model/railtie" 6 | require "active_record/railtie" 7 | require "action_controller/railtie" 8 | require "action_view/railtie" 9 | require "action_mailer/railtie" 10 | 11 | Bundler.require :default 12 | require 'devise' 13 | 14 | module Dummy 15 | class Application < Rails::Application 16 | config.load_defaults Rails.version.to_f 17 | 18 | config.action_mailer.default_url_options = { host: 'localhost:3000' } 19 | 20 | # Settings in config/environments/* take precedence over those specified here. 21 | # Application configuration should go into files in config/initializers 22 | # -- all .rb files in that directory are automatically loaded. 23 | 24 | # Custom directories with classes and modules you want to be autoloadable. 25 | # config.autoload_paths += %W(#{config.root}/extras) 26 | 27 | # Only load the plugins named here, in the order given (default is alphabetical). 28 | # :all can be used as a placeholder for all plugins not explicitly named. 29 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 30 | 31 | # Activate observers that should always be running. 32 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 33 | 34 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 35 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 36 | # config.time_zone = 'Central Time (US & Canada)' 37 | 38 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 39 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 40 | # config.i18n.default_locale = :de 41 | 42 | # JavaScript files you want as :defaults (application.js is always included). 43 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 44 | 45 | # Configure the default encoding used in templates for Ruby 1.9. 46 | config.encoding = "utf-8" 47 | 48 | # Configure sensitive parameters which will be filtered from the log file. 49 | config.filter_parameters += [:password] 50 | config.serve_static_assets = true 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/rails_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 5 | 6 | if File.exist?(gemfile) 7 | ENV['BUNDLE_GEMFILE'] ||= gemfile 8 | require 'bundler' 9 | Bundler.setup 10 | end 11 | 12 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /test/rails_app/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | 4 | default: &default 5 | adapter: sqlite3 6 | pool: 5 7 | timeout: 5000 8 | 9 | development: 10 | <<: *default 11 | database: db/development.sqlite3 12 | 13 | # Warning: The database defined as "test" will be erased and 14 | # re-generated from your development database when you run "rake". 15 | # Do not set this db to the same as development or production. 16 | test: 17 | <<: *default 18 | database: db/test.sqlite3 19 | 20 | production: 21 | <<: *default 22 | database: db/production.sqlite3 23 | -------------------------------------------------------------------------------- /test/rails_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the rails application 4 | require File.expand_path('../application', __FILE__) 5 | 6 | # Initialize the rails application 7 | if Rails.application.config.active_record.sqlite3.respond_to?(:represent_boolean_as_integer) 8 | Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true 9 | end 10 | 11 | Dummy::Application.initialize! 12 | -------------------------------------------------------------------------------- /test/rails_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb 5 | 6 | # In the development environment your application's code is reloaded on 7 | # every request. This slows down response time but is perfect for development 8 | # since you don't have to restart the webserver when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Log error messages when you accidentally call methods on nil. 12 | config.whiny_nils = true 13 | 14 | config.eager_load = false 15 | 16 | # Show full error reports and disable caching 17 | config.consider_all_requests_local = true 18 | config.action_controller.perform_caching = false 19 | 20 | # Don't care if the mailer can't send 21 | config.action_mailer.raise_delivery_errors = false 22 | 23 | # Print deprecation notices to the Rails logger 24 | config.active_support.deprecation = :log 25 | 26 | # Only use best-standards-support built into browsers 27 | config.action_dispatch.best_standards_support = :builtin 28 | end 29 | 30 | -------------------------------------------------------------------------------- /test/rails_app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb 5 | 6 | # The production environment is meant for finished, "live" apps. 7 | # Code is not reloaded between requests 8 | config.cache_classes = true 9 | 10 | # Full error reports are disabled and caching is turned on 11 | config.consider_all_requests_local = false 12 | config.action_controller.perform_caching = true 13 | 14 | config.eager_load = true 15 | 16 | # Specifies the header that your server uses for sending files 17 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 18 | 19 | # For nginx: 20 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 21 | 22 | # If you have no front-end server that supports something like X-Sendfile, 23 | # just comment this out and Rails will serve the files 24 | 25 | # See everything in the log (default is :info) 26 | # config.log_level = :debug 27 | 28 | # Use a different logger for distributed setups 29 | # config.logger = SyslogLogger.new 30 | 31 | # Use a different cache store in production 32 | # config.cache_store = :mem_cache_store 33 | 34 | # Disable Rails's static asset server 35 | # In production, Apache or nginx will already do this 36 | config.serve_static_assets = false 37 | 38 | # Enable serving of images, stylesheets, and javascripts from an asset server 39 | # config.action_controller.asset_host = "http://assets.example.com" 40 | 41 | # Disable delivery errors, bad email addresses will be ignored 42 | # config.action_mailer.raise_delivery_errors = false 43 | 44 | # Enable threaded mode 45 | # config.threadsafe! 46 | 47 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 48 | # the I18n.default_locale when a translation can not be found) 49 | config.i18n.fallbacks = true 50 | 51 | # Send deprecation notices to registered listeners 52 | config.active_support.deprecation = :notify 53 | end 54 | -------------------------------------------------------------------------------- /test/rails_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb 5 | 6 | # The test environment is used exclusively to run your application's 7 | # test suite. You never need to work with it otherwise. Remember that 8 | # your test database is "scratch space" for the test suite and is wiped 9 | # and recreated between test runs. Don't rely on the data there! 10 | config.cache_classes = true 11 | 12 | # Log error messages when you accidentally call methods on nil. 13 | config.whiny_nils = true 14 | 15 | config.eager_load = false 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Use SQL instead of Active Record's schema dumper when creating the test database. 33 | # This is necessary if your schema can't be completely dumped by the schema dumper, 34 | # like if you have constraints or database-specific column types 35 | # config.active_record.schema_format = :sql 36 | 37 | # Print deprecation notices to the stderr 38 | config.active_support.deprecation = :stderr 39 | end 40 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 6 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 7 | 8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 9 | # Rails.backtrace_cleaner.remove_silencers! 10 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Use this hook to configure devise mailer, warden hooks and so forth. 4 | # Many of these configuration options can be set straight in your model. 5 | Devise.setup do |config| 6 | # The secret key used by Devise. Devise uses this key to generate 7 | # random tokens. Changing this key will render invalid all existing 8 | # confirmation, reset password and unlock tokens in the database. 9 | config.secret_key = '527617f417a15170a26737856777918ab0e2665b59d41a183160eba6b038aaf81a3ebde5b5a8463cd2de1455462e40a37bcae057d580fbc4b251ceba3e85be84' 10 | 11 | # ==> Mailer Configuration 12 | # Configure the e-mail address which will be shown in Devise::Mailer, 13 | # note that it will be overwritten if you use your own mailer class 14 | # with default "from" parameter. 15 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' 16 | 17 | # Configure the class responsible to send e-mails. 18 | # config.mailer = 'Devise::Mailer' 19 | 20 | # ==> ORM configuration 21 | # Load and configure the ORM. Supports :active_record (default) and 22 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 23 | # available as additional gems. 24 | require 'devise/orm/active_record' 25 | 26 | # ==> Configuration for any authentication mechanism 27 | # Configure which keys are used when authenticating a user. The default is 28 | # just :email. You can configure it to use [:username, :subdomain], so for 29 | # authenticating a user, both parameters are required. Remember that those 30 | # parameters are used only when authenticating and not when retrieving from 31 | # session. If you need permissions, you should implement that in a before filter. 32 | # You can also supply a hash where the value is a boolean determining whether 33 | # or not authentication should be aborted when the value is not present. 34 | # config.authentication_keys = [ :email ] 35 | 36 | # Configure parameters from the request object used for authentication. Each entry 37 | # given should be a request method and it will automatically be passed to the 38 | # find_for_authentication method and considered in your model lookup. For instance, 39 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 40 | # The same considerations mentioned for authentication_keys also apply to request_keys. 41 | # config.request_keys = [] 42 | 43 | # Configure which authentication keys should be case-insensitive. 44 | # These keys will be downcased upon creating or modifying a user and when used 45 | # to authenticate or find a user. Default is :email. 46 | config.case_insensitive_keys = [ :email ] 47 | 48 | # Configure which authentication keys should have whitespace stripped. 49 | # These keys will have whitespace before and after removed upon creating or 50 | # modifying a user and when used to authenticate or find a user. Default is :email. 51 | config.strip_whitespace_keys = [ :email ] 52 | 53 | # Tell if authentication through request.params is enabled. True by default. 54 | # It can be set to an array that will enable params authentication only for the 55 | # given strategies, for example, `config.params_authenticatable = [:database]` will 56 | # enable it only for database (email + password) authentication. 57 | # config.params_authenticatable = true 58 | 59 | # Tell if authentication through HTTP Auth is enabled. False by default. 60 | # It can be set to an array that will enable http authentication only for the 61 | # given strategies, for example, `config.http_authenticatable = [:database]` will 62 | # enable it only for database authentication. The supported strategies are: 63 | # :database = Support basic authentication with authentication key + password 64 | # config.http_authenticatable = false 65 | 66 | # If http headers should be returned for AJAX requests. True by default. 67 | # config.http_authenticatable_on_xhr = true 68 | 69 | # The realm used in Http Basic Authentication. 'Application' by default. 70 | # config.http_authentication_realm = 'Application' 71 | 72 | # It will change confirmation, password recovery and other workflows 73 | # to behave the same regardless if the e-mail provided was right or wrong. 74 | # Does not affect registerable. 75 | # config.paranoid = true 76 | 77 | # By default Devise will store the user in session. You can skip storage for 78 | # particular strategies by setting this option. 79 | # Notice that if you are skipping storage for all authentication paths, you 80 | # may want to disable generating routes to Devise's sessions controller by 81 | # passing skip: :sessions to `devise_for` in your config/routes.rb 82 | config.skip_session_storage = [:http_auth] 83 | 84 | # By default, Devise cleans up the CSRF token on authentication to 85 | # avoid CSRF token fixation attacks. This means that, when using AJAX 86 | # requests for sign in and sign up, you need to get a new CSRF token 87 | # from the server. You can disable this option at your own risk. 88 | # config.clean_up_csrf_token_on_authentication = true 89 | 90 | # ==> Configuration for :database_authenticatable 91 | # For bcrypt, this is the cost for hashing the password and defaults to 10. If 92 | # using other encryptors, it sets how many times you want the password re-encrypted. 93 | # 94 | # Limiting the stretches to just one in testing will increase the performance of 95 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 96 | # a value less than 10 in other environments. Note that, for bcrypt (the default 97 | # encryptor), the cost increases exponentially with the number of stretches (e.g. 98 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 99 | config.stretches = Rails.env.test? ? 1 : 10 100 | 101 | # Setup a pepper to generate the encrypted password. 102 | # config.pepper = '5cecc5d3d942e712c236b7f2b0ebda84c47642864d58b5ff4c76b7f6f5523d5d6ffdeed82ea742792889393527f67f684c00aa72ef1530d81b84713ee8fecca8' 103 | 104 | # ==> Configuration for :confirmable 105 | # A period that the user is allowed to access the website even without 106 | # confirming their account. For instance, if set to 2.days, the user will be 107 | # able to access the website for two days without confirming their account, 108 | # access will be blocked just in the third day. Default is 0.days, meaning 109 | # the user cannot access the website without confirming their account. 110 | # config.allow_unconfirmed_access_for = 2.days 111 | 112 | # A period that the user is allowed to confirm their account before their 113 | # token becomes invalid. For example, if set to 3.days, the user can confirm 114 | # their account within 3 days after the mail was sent, but on the fourth day 115 | # their account can't be confirmed with the token any more. 116 | # Default is nil, meaning there is no restriction on how long a user can take 117 | # before confirming their account. 118 | # config.confirm_within = 3.days 119 | 120 | # If true, requires any email changes to be confirmed (exactly the same way as 121 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 122 | # db field (see migrations). Until confirmed, new email is stored in 123 | # unconfirmed_email column, and copied to email column on successful confirmation. 124 | config.reconfirmable = true 125 | 126 | # Defines which key will be used when confirming an account 127 | # config.confirmation_keys = [ :email ] 128 | 129 | # ==> Configuration for :rememberable 130 | # The time the user will be remembered without asking for credentials again. 131 | # config.remember_for = 2.weeks 132 | 133 | # If true, extends the user's remember period when remembered via cookie. 134 | # config.extend_remember_period = false 135 | 136 | # Options to be passed to the created cookie. For instance, you can set 137 | # secure: true in order to force SSL only cookies. 138 | # config.rememberable_options = {} 139 | 140 | # ==> Configuration for :validatable 141 | # Range for password length. 142 | config.password_length = 8..128 143 | 144 | # Email regex used to validate email formats. It simply asserts that 145 | # one (and only one) @ exists in the given string. This is mainly 146 | # to give user feedback and not to assert the e-mail validity. 147 | # config.email_regexp = /\A[^@]+@[^@]+\z/ 148 | 149 | # ==> Configuration for :timeoutable 150 | # The time you want to timeout the user session without activity. After this 151 | # time the user will be asked for credentials again. Default is 30 minutes. 152 | # config.timeout_in = 30.minutes 153 | 154 | # If true, expires auth token on session timeout. 155 | # config.expire_auth_token_on_timeout = false 156 | 157 | # ==> Configuration for :lockable 158 | # Defines which strategy will be used to lock an account. 159 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 160 | # :none = No lock strategy. You should handle locking by yourself. 161 | # config.lock_strategy = :failed_attempts 162 | 163 | # Defines which key will be used when locking and unlocking an account 164 | # config.unlock_keys = [ :email ] 165 | 166 | # Defines which strategy will be used to unlock an account. 167 | # :email = Sends an unlock link to the user email 168 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 169 | # :both = Enables both strategies 170 | # :none = No unlock strategy. You should handle unlocking by yourself. 171 | # config.unlock_strategy = :both 172 | 173 | # Number of authentication tries before locking an account if lock_strategy 174 | # is failed attempts. 175 | # config.maximum_attempts = 20 176 | 177 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 178 | # config.unlock_in = 1.hour 179 | 180 | # Warn on the last attempt before the account is locked. 181 | # config.last_attempt_warning = false 182 | 183 | # ==> Configuration for :recoverable 184 | # 185 | # Defines which key will be used when recovering the password for an account 186 | # config.reset_password_keys = [ :email ] 187 | 188 | # Time interval you can reset your password with a reset password key. 189 | # Don't put a too small interval or your users won't have the time to 190 | # change their passwords. 191 | config.reset_password_within = 6.hours 192 | 193 | # ==> Configuration for :encryptable 194 | # Allow you to use another encryption algorithm besides bcrypt (default). You can use 195 | # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, 196 | # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) 197 | # and :restful_authentication_sha1 (then you should set stretches to 10, and copy 198 | # REST_AUTH_SITE_KEY to pepper). 199 | # 200 | # Require the `devise-encryptable` gem when using anything other than bcrypt 201 | # config.encryptor = :sha512 202 | 203 | # ==> Scopes configuration 204 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 205 | # "users/sessions/new". It's turned off by default because it's slower if you 206 | # are using only default views. 207 | # config.scoped_views = false 208 | 209 | # Configure the default scope given to Warden. By default it's the first 210 | # devise role declared in your routes (usually :user). 211 | # config.default_scope = :user 212 | 213 | # Set this configuration to false if you want /users/sign_out to sign out 214 | # only the current scope. By default, Devise signs out all scopes. 215 | # config.sign_out_all_scopes = true 216 | 217 | # ==> Navigation configuration 218 | # Lists the formats that should be treated as navigational. Formats like 219 | # :html, should redirect to the sign in page when the user does not have 220 | # access, but formats like :xml or :json, should return 401. 221 | # 222 | # If you have any extra navigational formats, like :iphone or :mobile, you 223 | # should add them to the navigational formats lists. 224 | # 225 | # The "*/*" below is required to match Internet Explorer requests. 226 | # config.navigational_formats = ['*/*', :html] 227 | 228 | # The default HTTP method used to sign out a resource. Default is :delete. 229 | config.sign_out_via = :delete 230 | 231 | # ==> OmniAuth 232 | # Add a new OmniAuth provider. Check the wiki for more information on setting 233 | # up on your models and hooks. 234 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' 235 | 236 | # ==> Warden configuration 237 | # If you want to use other strategies, that are not supported by Devise, or 238 | # change the failure app, you can configure them inside the config.warden block. 239 | # 240 | # config.warden do |manager| 241 | # manager.intercept_401 = false 242 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 243 | # end 244 | 245 | # ==> Mountable engine configurations 246 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 247 | # is mountable, there are some extra configurations to be taken into account. 248 | # The following options are available, assuming the engine is mounted as: 249 | # 250 | # mount MyEngine, at: '/my_engine' 251 | # 252 | # The router that invoked `devise_for`, in the example above, would be: 253 | # config.router_name = :my_engine 254 | # 255 | # When using omniauth, Devise cannot automatically set Omniauth path, 256 | # so you need to do it manually. For the users scope, it would be: 257 | # config.omniauth_path_prefix = '/my_engine/users/auth' 258 | end 259 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new inflection rules using the following format 6 | # (all these examples are active by default): 7 | # ActiveSupport::Inflector.inflections do |inflect| 8 | # inflect.plural /^(ox)$/i, '\1en' 9 | # inflect.singular /^(ox)en/i, '\1' 10 | # inflect.irregular 'person', 'people' 11 | # inflect.uncountable %w( fish sheep ) 12 | # end 13 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new mime types for use in respond_to blocks: 6 | # Mime::Type.register "text/richtext", :rtf 7 | # Mime::Type.register_alias "text/html", :iphone 8 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Your secret key for verifying the integrity of signed cookies. 6 | # If you change this key, all old signed cookies will become invalid! 7 | # Make sure the secret is at least 30 characters and all random, 8 | # no regular words or you'll be exposed to dictionary attacks. 9 | if Dummy::Application.config.respond_to?(:secret_key_base) 10 | Dummy::Application.config.secret_key_base = 'bedc31c5fff702ea808045bbbc5123455f1c00ecd005a1f667a5f04332100a6abf22cfcee2b3d39b8f677c03bb6503cf1c3b65c1287b9e13bd0d20c6431ec6ab' 11 | else 12 | Dummy::Application.config.secret_token = 'bedc31c5fff702ea808045bbbc5123455f1c00ecd005a1f667a5f04332100a6abf22cfcee2b3d39b8f677c03bb6503cf1c3b65c1287b9e13bd0d20c6431ec6ab' 13 | end 14 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | Dummy::Application.config.session_store :cookie_store, :key => '_dummy_session' 6 | 7 | # Use the database for sessions instead of the cookie-based default, 8 | # which shouldn't be used to store highly confidential information 9 | # (create the session table with "rails generate session_migration") 10 | # Dummy::Application.config.session_store :active_record_store 11 | -------------------------------------------------------------------------------- /test/rails_app/config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your account was successfully confirmed." 7 | send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid email or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account will be locked." 15 | not_found_in_database: "Invalid email or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your account before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock Instructions" 26 | omniauth_callbacks: 27 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 28 | success: "Successfully authenticated from %{kind} account." 29 | passwords: 30 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 31 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 32 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 33 | updated: "Your password was changed successfully. You are now signed in." 34 | updated_not_active: "Your password was changed successfully." 35 | registrations: 36 | destroyed: "Bye! Your account was successfully cancelled. We hope to see you again soon." 37 | signed_up: "Welcome! You have signed up successfully." 38 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 39 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 40 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account." 41 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address." 42 | updated: "You updated your account successfully." 43 | sessions: 44 | signed_in: "Signed in successfully." 45 | signed_out: "Signed out successfully." 46 | unlocks: 47 | send_instructions: "You will receive an email with instructions about how to unlock your account in a few minutes." 48 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions about how to unlock it in a few minutes." 49 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 50 | errors: 51 | messages: 52 | already_confirmed: "was already confirmed, please try signing in" 53 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 54 | expired: "has expired, please request a new one" 55 | not_found: "not found" 56 | not_locked: "was not locked" 57 | not_saved: 58 | one: "1 error prohibited this %{resource} from being saved:" 59 | other: "%{count} errors prohibited this %{resource} from being saved:" 60 | -------------------------------------------------------------------------------- /test/rails_app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | wicked: 7 | first: "first" 8 | second: "second" 9 | last_step: "last_step" 10 | -------------------------------------------------------------------------------- /test/rails_app/config/locales/es.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for Spanish. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | es: 5 | hello: "hola mundo" 6 | wicked: 7 | first: "uno" 8 | second: "dos" 9 | last_step: "último_paso" 10 | 11 | -------------------------------------------------------------------------------- /test/rails_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.routes.draw do 4 | devise_for :users 5 | 6 | # The priority is based upon order of creation: 7 | # first created -> highest priority. 8 | 9 | # Sample of regular route: 10 | # match 'products/:id' => 'catalog#view' 11 | # Keep in mind you can assign values other than :controller and :action 12 | 13 | # Sample of named route: 14 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 15 | # This route can be invoked with purchase_url(:id => product.id) 16 | 17 | # Sample resource route (maps HTTP verbs to controller actions automatically): 18 | # resources :products 19 | 20 | # Sample resource route with options: 21 | # resources :products do 22 | # member do 23 | # get 'short' 24 | # post 'toggle' 25 | # end 26 | # 27 | # collection do 28 | # get 'sold' 29 | # end 30 | # end 31 | 32 | # Sample resource route with sub-resources: 33 | # resources :products do 34 | # resources :comments, :sales 35 | # resource :seller 36 | # end 37 | 38 | # Sample resource route with more complex sub-resources 39 | # resources :products do 40 | # resources :comments 41 | # resources :sales do 42 | # get 'recent', :on => :collection 43 | # end 44 | # end 45 | 46 | # Sample resource route within a namespace: 47 | # namespace :admin do 48 | # # Directs /admin/products/* to Admin::ProductsController 49 | # # (app/controllers/admin/products_controller.rb) 50 | # resources :products 51 | # end 52 | 53 | # You can have the root of your site routed with "root" 54 | # just remember to delete public/index.html. 55 | root to: "pages#index" 56 | 57 | get "foo", to: "pages#index" 58 | get "foo_secret", to: "pages#secret" 59 | post "users", to: "users#create" 60 | 61 | get "authenticated", to: "authenticated#index" 62 | 63 | # See how all your routes lay out with "rake routes" 64 | 65 | # This is a legacy wild controller route that's not recommended for RESTful applications. 66 | # Note: This route will make all actions in every controller accessible via GET requests. 67 | # match ':controller(/:action(/:id(.:format)))' 68 | end 69 | -------------------------------------------------------------------------------- /test/rails_app/config/storage.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zombocom/derailed_benchmarks/e8b29db93acf7d3318b9afb42d8516b905020afb/test/rails_app/config/storage.yml -------------------------------------------------------------------------------- /test/rails_app/db/migrate/20141210070547_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | inherited_class = Rails.version < "5" ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] 4 | class DeviseCreateUsers < inherited_class 5 | def change 6 | create_table(:users) do |t| 7 | ## Database authenticatable 8 | t.string :email, null: false, default: "" 9 | t.string :encrypted_password, null: false, default: "" 10 | 11 | ## Recoverable 12 | t.string :reset_password_token 13 | t.datetime :reset_password_sent_at 14 | 15 | ## Rememberable 16 | t.datetime :remember_created_at 17 | 18 | ## Trackable 19 | t.integer :sign_in_count, default: 0, null: false 20 | t.datetime :current_sign_in_at 21 | t.datetime :last_sign_in_at 22 | t.string :current_sign_in_ip 23 | t.string :last_sign_in_ip 24 | 25 | ## Confirmable 26 | # t.string :confirmation_token 27 | # t.datetime :confirmed_at 28 | # t.datetime :confirmation_sent_at 29 | # t.string :unconfirmed_email # Only if using reconfirmable 30 | 31 | ## Lockable 32 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 33 | # t.string :unlock_token # Only if unlock strategy is :email or :both 34 | # t.datetime :locked_at 35 | 36 | 37 | t.timestamps 38 | end 39 | 40 | add_index :users, :email, unique: true 41 | add_index :users, :reset_password_token, unique: true 42 | # add_index :users, :confirmation_token, unique: true 43 | # add_index :users, :unlock_token, unique: true 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/rails_app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | # This file is auto-generated from the current state of the database. Instead 5 | # of editing this file, please use the migrations feature of Active Record to 6 | # incrementally modify your database, and then regenerate this schema definition. 7 | # 8 | # Note that this schema.rb definition is the authoritative source for your 9 | # database schema. If you need to create the application database on another 10 | # system, you should be using db:schema:load, not running all the migrations 11 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 12 | # you'll amass, the slower it'll run and the greater likelihood for issues). 13 | # 14 | # It's strongly recommended that you check this file into your version control system. 15 | 16 | ActiveRecord::Schema.define(version: 20141210070547) do 17 | 18 | create_table "users", force: true do |t| 19 | t.string "email", default: "", null: false 20 | t.string "encrypted_password", default: "", null: false 21 | t.string "reset_password_token" 22 | t.datetime "reset_password_sent_at" 23 | t.datetime "remember_created_at" 24 | t.integer "sign_in_count", default: 0, null: false 25 | t.datetime "current_sign_in_at" 26 | t.datetime "last_sign_in_at" 27 | t.string "current_sign_in_ip" 28 | t.string "last_sign_in_ip" 29 | t.datetime "created_at" 30 | t.datetime "updated_at" 31 | end 32 | 33 | add_index "users", ["email"], name: "index_users_on_email", unique: true 34 | add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true 35 | end 36 | -------------------------------------------------------------------------------- /test/rails_app/perf.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH << File.expand_path("../../../lib", __FILE__) 4 | 5 | require 'derailed_benchmarks' 6 | require 'derailed_benchmarks/tasks' 7 | 8 | if ENV['AUTH_CUSTOM_USER'] 9 | DerailedBenchmarks.auth.user = -> { User.first_or_create!(email: "user@example.com", password: 'password') } 10 | end 11 | -------------------------------------------------------------------------------- /test/rails_app/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/rails_app/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/rails_app/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |

We've been notified about this issue and we'll take a look at it shortly.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/rails_app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zombocom/derailed_benchmarks/e8b29db93acf7d3318b9afb42d8516b905020afb/test/rails_app/public/favicon.ico -------------------------------------------------------------------------------- /test/rails_app/public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // Place your application-specific JavaScript functions and classes here 2 | // This file is automatically included by javascript_include_tag :defaults 3 | -------------------------------------------------------------------------------- /test/rails_app/public/javascripts/dragdrop.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 2 | 3 | // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // 5 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 6 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 7 | 8 | if(Object.isUndefined(Effect)) 9 | throw("dragdrop.js requires including script.aculo.us' effects.js library"); 10 | 11 | var Droppables = { 12 | drops: [], 13 | 14 | remove: function(element) { 15 | this.drops = this.drops.reject(function(d) { return d.element==$(element) }); 16 | }, 17 | 18 | add: function(element) { 19 | element = $(element); 20 | var options = Object.extend({ 21 | greedy: true, 22 | hoverclass: null, 23 | tree: false 24 | }, arguments[1] || { }); 25 | 26 | // cache containers 27 | if(options.containment) { 28 | options._containers = []; 29 | var containment = options.containment; 30 | if(Object.isArray(containment)) { 31 | containment.each( function(c) { options._containers.push($(c)) }); 32 | } else { 33 | options._containers.push($(containment)); 34 | } 35 | } 36 | 37 | if(options.accept) options.accept = [options.accept].flatten(); 38 | 39 | Element.makePositioned(element); // fix IE 40 | options.element = element; 41 | 42 | this.drops.push(options); 43 | }, 44 | 45 | findDeepestChild: function(drops) { 46 | deepest = drops[0]; 47 | 48 | for (i = 1; i < drops.length; ++i) 49 | if (Element.isParent(drops[i].element, deepest.element)) 50 | deepest = drops[i]; 51 | 52 | return deepest; 53 | }, 54 | 55 | isContained: function(element, drop) { 56 | var containmentNode; 57 | if(drop.tree) { 58 | containmentNode = element.treeNode; 59 | } else { 60 | containmentNode = element.parentNode; 61 | } 62 | return drop._containers.detect(function(c) { return containmentNode == c }); 63 | }, 64 | 65 | isAffected: function(point, element, drop) { 66 | return ( 67 | (drop.element!=element) && 68 | ((!drop._containers) || 69 | this.isContained(element, drop)) && 70 | ((!drop.accept) || 71 | (Element.classNames(element).detect( 72 | function(v) { return drop.accept.include(v) } ) )) && 73 | Position.within(drop.element, point[0], point[1]) ); 74 | }, 75 | 76 | deactivate: function(drop) { 77 | if(drop.hoverclass) 78 | Element.removeClassName(drop.element, drop.hoverclass); 79 | this.last_active = null; 80 | }, 81 | 82 | activate: function(drop) { 83 | if(drop.hoverclass) 84 | Element.addClassName(drop.element, drop.hoverclass); 85 | this.last_active = drop; 86 | }, 87 | 88 | show: function(point, element) { 89 | if(!this.drops.length) return; 90 | var drop, affected = []; 91 | 92 | this.drops.each( function(drop) { 93 | if(Droppables.isAffected(point, element, drop)) 94 | affected.push(drop); 95 | }); 96 | 97 | if(affected.length>0) 98 | drop = Droppables.findDeepestChild(affected); 99 | 100 | if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); 101 | if (drop) { 102 | Position.within(drop.element, point[0], point[1]); 103 | if(drop.onHover) 104 | drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); 105 | 106 | if (drop != this.last_active) Droppables.activate(drop); 107 | } 108 | }, 109 | 110 | fire: function(event, element) { 111 | if(!this.last_active) return; 112 | Position.prepare(); 113 | 114 | if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) 115 | if (this.last_active.onDrop) { 116 | this.last_active.onDrop(element, this.last_active.element, event); 117 | return true; 118 | } 119 | }, 120 | 121 | reset: function() { 122 | if(this.last_active) 123 | this.deactivate(this.last_active); 124 | } 125 | }; 126 | 127 | var Draggables = { 128 | drags: [], 129 | observers: [], 130 | 131 | register: function(draggable) { 132 | if(this.drags.length == 0) { 133 | this.eventMouseUp = this.endDrag.bindAsEventListener(this); 134 | this.eventMouseMove = this.updateDrag.bindAsEventListener(this); 135 | this.eventKeypress = this.keyPress.bindAsEventListener(this); 136 | 137 | Event.observe(document, "mouseup", this.eventMouseUp); 138 | Event.observe(document, "mousemove", this.eventMouseMove); 139 | Event.observe(document, "keypress", this.eventKeypress); 140 | } 141 | this.drags.push(draggable); 142 | }, 143 | 144 | unregister: function(draggable) { 145 | this.drags = this.drags.reject(function(d) { return d==draggable }); 146 | if(this.drags.length == 0) { 147 | Event.stopObserving(document, "mouseup", this.eventMouseUp); 148 | Event.stopObserving(document, "mousemove", this.eventMouseMove); 149 | Event.stopObserving(document, "keypress", this.eventKeypress); 150 | } 151 | }, 152 | 153 | activate: function(draggable) { 154 | if(draggable.options.delay) { 155 | this._timeout = setTimeout(function() { 156 | Draggables._timeout = null; 157 | window.focus(); 158 | Draggables.activeDraggable = draggable; 159 | }.bind(this), draggable.options.delay); 160 | } else { 161 | window.focus(); // allows keypress events if window isn't currently focused, fails for Safari 162 | this.activeDraggable = draggable; 163 | } 164 | }, 165 | 166 | deactivate: function() { 167 | this.activeDraggable = null; 168 | }, 169 | 170 | updateDrag: function(event) { 171 | if(!this.activeDraggable) return; 172 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 173 | // Mozilla-based browsers fire successive mousemove events with 174 | // the same coordinates, prevent needless redrawing (moz bug?) 175 | if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; 176 | this._lastPointer = pointer; 177 | 178 | this.activeDraggable.updateDrag(event, pointer); 179 | }, 180 | 181 | endDrag: function(event) { 182 | if(this._timeout) { 183 | clearTimeout(this._timeout); 184 | this._timeout = null; 185 | } 186 | if(!this.activeDraggable) return; 187 | this._lastPointer = null; 188 | this.activeDraggable.endDrag(event); 189 | this.activeDraggable = null; 190 | }, 191 | 192 | keyPress: function(event) { 193 | if(this.activeDraggable) 194 | this.activeDraggable.keyPress(event); 195 | }, 196 | 197 | addObserver: function(observer) { 198 | this.observers.push(observer); 199 | this._cacheObserverCallbacks(); 200 | }, 201 | 202 | removeObserver: function(element) { // element instead of observer fixes mem leaks 203 | this.observers = this.observers.reject( function(o) { return o.element==element }); 204 | this._cacheObserverCallbacks(); 205 | }, 206 | 207 | notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' 208 | if(this[eventName+'Count'] > 0) 209 | this.observers.each( function(o) { 210 | if(o[eventName]) o[eventName](eventName, draggable, event); 211 | }); 212 | if(draggable.options[eventName]) draggable.options[eventName](draggable, event); 213 | }, 214 | 215 | _cacheObserverCallbacks: function() { 216 | ['onStart','onEnd','onDrag'].each( function(eventName) { 217 | Draggables[eventName+'Count'] = Draggables.observers.select( 218 | function(o) { return o[eventName]; } 219 | ).length; 220 | }); 221 | } 222 | }; 223 | 224 | /*--------------------------------------------------------------------------*/ 225 | 226 | var Draggable = Class.create({ 227 | initialize: function(element) { 228 | var defaults = { 229 | handle: false, 230 | reverteffect: function(element, top_offset, left_offset) { 231 | var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; 232 | new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, 233 | queue: {scope:'_draggable', position:'end'} 234 | }); 235 | }, 236 | endeffect: function(element) { 237 | var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; 238 | new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 239 | queue: {scope:'_draggable', position:'end'}, 240 | afterFinish: function(){ 241 | Draggable._dragging[element] = false 242 | } 243 | }); 244 | }, 245 | zindex: 1000, 246 | revert: false, 247 | quiet: false, 248 | scroll: false, 249 | scrollSensitivity: 20, 250 | scrollSpeed: 15, 251 | snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } 252 | delay: 0 253 | }; 254 | 255 | if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) 256 | Object.extend(defaults, { 257 | starteffect: function(element) { 258 | element._opacity = Element.getOpacity(element); 259 | Draggable._dragging[element] = true; 260 | new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 261 | } 262 | }); 263 | 264 | var options = Object.extend(defaults, arguments[1] || { }); 265 | 266 | this.element = $(element); 267 | 268 | if(options.handle && Object.isString(options.handle)) 269 | this.handle = this.element.down('.'+options.handle, 0); 270 | 271 | if(!this.handle) this.handle = $(options.handle); 272 | if(!this.handle) this.handle = this.element; 273 | 274 | if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { 275 | options.scroll = $(options.scroll); 276 | this._isScrollChild = Element.childOf(this.element, options.scroll); 277 | } 278 | 279 | Element.makePositioned(this.element); // fix IE 280 | 281 | this.options = options; 282 | this.dragging = false; 283 | 284 | this.eventMouseDown = this.initDrag.bindAsEventListener(this); 285 | Event.observe(this.handle, "mousedown", this.eventMouseDown); 286 | 287 | Draggables.register(this); 288 | }, 289 | 290 | destroy: function() { 291 | Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); 292 | Draggables.unregister(this); 293 | }, 294 | 295 | currentDelta: function() { 296 | return([ 297 | parseInt(Element.getStyle(this.element,'left') || '0'), 298 | parseInt(Element.getStyle(this.element,'top') || '0')]); 299 | }, 300 | 301 | initDrag: function(event) { 302 | if(!Object.isUndefined(Draggable._dragging[this.element]) && 303 | Draggable._dragging[this.element]) return; 304 | if(Event.isLeftClick(event)) { 305 | // abort on form elements, fixes a Firefox issue 306 | var src = Event.element(event); 307 | if((tag_name = src.tagName.toUpperCase()) && ( 308 | tag_name=='INPUT' || 309 | tag_name=='SELECT' || 310 | tag_name=='OPTION' || 311 | tag_name=='BUTTON' || 312 | tag_name=='TEXTAREA')) return; 313 | 314 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 315 | var pos = this.element.cumulativeOffset(); 316 | this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); 317 | 318 | Draggables.activate(this); 319 | Event.stop(event); 320 | } 321 | }, 322 | 323 | startDrag: function(event) { 324 | this.dragging = true; 325 | if(!this.delta) 326 | this.delta = this.currentDelta(); 327 | 328 | if(this.options.zindex) { 329 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); 330 | this.element.style.zIndex = this.options.zindex; 331 | } 332 | 333 | if(this.options.ghosting) { 334 | this._clone = this.element.cloneNode(true); 335 | this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); 336 | if (!this._originallyAbsolute) 337 | Position.absolutize(this.element); 338 | this.element.parentNode.insertBefore(this._clone, this.element); 339 | } 340 | 341 | if(this.options.scroll) { 342 | if (this.options.scroll == window) { 343 | var where = this._getWindowScroll(this.options.scroll); 344 | this.originalScrollLeft = where.left; 345 | this.originalScrollTop = where.top; 346 | } else { 347 | this.originalScrollLeft = this.options.scroll.scrollLeft; 348 | this.originalScrollTop = this.options.scroll.scrollTop; 349 | } 350 | } 351 | 352 | Draggables.notify('onStart', this, event); 353 | 354 | if(this.options.starteffect) this.options.starteffect(this.element); 355 | }, 356 | 357 | updateDrag: function(event, pointer) { 358 | if(!this.dragging) this.startDrag(event); 359 | 360 | if(!this.options.quiet){ 361 | Position.prepare(); 362 | Droppables.show(pointer, this.element); 363 | } 364 | 365 | Draggables.notify('onDrag', this, event); 366 | 367 | this.draw(pointer); 368 | if(this.options.change) this.options.change(this); 369 | 370 | if(this.options.scroll) { 371 | this.stopScrolling(); 372 | 373 | var p; 374 | if (this.options.scroll == window) { 375 | with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } 376 | } else { 377 | p = Position.page(this.options.scroll); 378 | p[0] += this.options.scroll.scrollLeft + Position.deltaX; 379 | p[1] += this.options.scroll.scrollTop + Position.deltaY; 380 | p.push(p[0]+this.options.scroll.offsetWidth); 381 | p.push(p[1]+this.options.scroll.offsetHeight); 382 | } 383 | var speed = [0,0]; 384 | if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); 385 | if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); 386 | if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); 387 | if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); 388 | this.startScrolling(speed); 389 | } 390 | 391 | // fix AppleWebKit rendering 392 | if(Prototype.Browser.WebKit) window.scrollBy(0,0); 393 | 394 | Event.stop(event); 395 | }, 396 | 397 | finishDrag: function(event, success) { 398 | this.dragging = false; 399 | 400 | if(this.options.quiet){ 401 | Position.prepare(); 402 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 403 | Droppables.show(pointer, this.element); 404 | } 405 | 406 | if(this.options.ghosting) { 407 | if (!this._originallyAbsolute) 408 | Position.relativize(this.element); 409 | delete this._originallyAbsolute; 410 | Element.remove(this._clone); 411 | this._clone = null; 412 | } 413 | 414 | var dropped = false; 415 | if(success) { 416 | dropped = Droppables.fire(event, this.element); 417 | if (!dropped) dropped = false; 418 | } 419 | if(dropped && this.options.onDropped) this.options.onDropped(this.element); 420 | Draggables.notify('onEnd', this, event); 421 | 422 | var revert = this.options.revert; 423 | if(revert && Object.isFunction(revert)) revert = revert(this.element); 424 | 425 | var d = this.currentDelta(); 426 | if(revert && this.options.reverteffect) { 427 | if (dropped == 0 || revert != 'failure') 428 | this.options.reverteffect(this.element, 429 | d[1]-this.delta[1], d[0]-this.delta[0]); 430 | } else { 431 | this.delta = d; 432 | } 433 | 434 | if(this.options.zindex) 435 | this.element.style.zIndex = this.originalZ; 436 | 437 | if(this.options.endeffect) 438 | this.options.endeffect(this.element); 439 | 440 | Draggables.deactivate(this); 441 | Droppables.reset(); 442 | }, 443 | 444 | keyPress: function(event) { 445 | if(event.keyCode!=Event.KEY_ESC) return; 446 | this.finishDrag(event, false); 447 | Event.stop(event); 448 | }, 449 | 450 | endDrag: function(event) { 451 | if(!this.dragging) return; 452 | this.stopScrolling(); 453 | this.finishDrag(event, true); 454 | Event.stop(event); 455 | }, 456 | 457 | draw: function(point) { 458 | var pos = this.element.cumulativeOffset(); 459 | if(this.options.ghosting) { 460 | var r = Position.realOffset(this.element); 461 | pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; 462 | } 463 | 464 | var d = this.currentDelta(); 465 | pos[0] -= d[0]; pos[1] -= d[1]; 466 | 467 | if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { 468 | pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; 469 | pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; 470 | } 471 | 472 | var p = [0,1].map(function(i){ 473 | return (point[i]-pos[i]-this.offset[i]) 474 | }.bind(this)); 475 | 476 | if(this.options.snap) { 477 | if(Object.isFunction(this.options.snap)) { 478 | p = this.options.snap(p[0],p[1],this); 479 | } else { 480 | if(Object.isArray(this.options.snap)) { 481 | p = p.map( function(v, i) { 482 | return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); 483 | } else { 484 | p = p.map( function(v) { 485 | return (v/this.options.snap).round()*this.options.snap }.bind(this)); 486 | } 487 | }} 488 | 489 | var style = this.element.style; 490 | if((!this.options.constraint) || (this.options.constraint=='horizontal')) 491 | style.left = p[0] + "px"; 492 | if((!this.options.constraint) || (this.options.constraint=='vertical')) 493 | style.top = p[1] + "px"; 494 | 495 | if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering 496 | }, 497 | 498 | stopScrolling: function() { 499 | if(this.scrollInterval) { 500 | clearInterval(this.scrollInterval); 501 | this.scrollInterval = null; 502 | Draggables._lastScrollPointer = null; 503 | } 504 | }, 505 | 506 | startScrolling: function(speed) { 507 | if(!(speed[0] || speed[1])) return; 508 | this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; 509 | this.lastScrolled = new Date(); 510 | this.scrollInterval = setInterval(this.scroll.bind(this), 10); 511 | }, 512 | 513 | scroll: function() { 514 | var current = new Date(); 515 | var delta = current - this.lastScrolled; 516 | this.lastScrolled = current; 517 | if(this.options.scroll == window) { 518 | with (this._getWindowScroll(this.options.scroll)) { 519 | if (this.scrollSpeed[0] || this.scrollSpeed[1]) { 520 | var d = delta / 1000; 521 | this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); 522 | } 523 | } 524 | } else { 525 | this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; 526 | this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; 527 | } 528 | 529 | Position.prepare(); 530 | Droppables.show(Draggables._lastPointer, this.element); 531 | Draggables.notify('onDrag', this); 532 | if (this._isScrollChild) { 533 | Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); 534 | Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; 535 | Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; 536 | if (Draggables._lastScrollPointer[0] < 0) 537 | Draggables._lastScrollPointer[0] = 0; 538 | if (Draggables._lastScrollPointer[1] < 0) 539 | Draggables._lastScrollPointer[1] = 0; 540 | this.draw(Draggables._lastScrollPointer); 541 | } 542 | 543 | if(this.options.change) this.options.change(this); 544 | }, 545 | 546 | _getWindowScroll: function(w) { 547 | var T, L, W, H; 548 | with (w.document) { 549 | if (w.document.documentElement && documentElement.scrollTop) { 550 | T = documentElement.scrollTop; 551 | L = documentElement.scrollLeft; 552 | } else if (w.document.body) { 553 | T = body.scrollTop; 554 | L = body.scrollLeft; 555 | } 556 | if (w.innerWidth) { 557 | W = w.innerWidth; 558 | H = w.innerHeight; 559 | } else if (w.document.documentElement && documentElement.clientWidth) { 560 | W = documentElement.clientWidth; 561 | H = documentElement.clientHeight; 562 | } else { 563 | W = body.offsetWidth; 564 | H = body.offsetHeight; 565 | } 566 | } 567 | return { top: T, left: L, width: W, height: H }; 568 | } 569 | }); 570 | 571 | Draggable._dragging = { }; 572 | 573 | /*--------------------------------------------------------------------------*/ 574 | 575 | var SortableObserver = Class.create({ 576 | initialize: function(element, observer) { 577 | this.element = $(element); 578 | this.observer = observer; 579 | this.lastValue = Sortable.serialize(this.element); 580 | }, 581 | 582 | onStart: function() { 583 | this.lastValue = Sortable.serialize(this.element); 584 | }, 585 | 586 | onEnd: function() { 587 | Sortable.unmark(); 588 | if(this.lastValue != Sortable.serialize(this.element)) 589 | this.observer(this.element) 590 | } 591 | }); 592 | 593 | var Sortable = { 594 | SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, 595 | 596 | sortables: { }, 597 | 598 | _findRootElement: function(element) { 599 | while (element.tagName.toUpperCase() != "BODY") { 600 | if(element.id && Sortable.sortables[element.id]) return element; 601 | element = element.parentNode; 602 | } 603 | }, 604 | 605 | options: function(element) { 606 | element = Sortable._findRootElement($(element)); 607 | if(!element) return; 608 | return Sortable.sortables[element.id]; 609 | }, 610 | 611 | destroy: function(element){ 612 | element = $(element); 613 | var s = Sortable.sortables[element.id]; 614 | 615 | if(s) { 616 | Draggables.removeObserver(s.element); 617 | s.droppables.each(function(d){ Droppables.remove(d) }); 618 | s.draggables.invoke('destroy'); 619 | 620 | delete Sortable.sortables[s.element.id]; 621 | } 622 | }, 623 | 624 | create: function(element) { 625 | element = $(element); 626 | var options = Object.extend({ 627 | element: element, 628 | tag: 'li', // assumes li children, override with tag: 'tagname' 629 | dropOnEmpty: false, 630 | tree: false, 631 | treeTag: 'ul', 632 | overlap: 'vertical', // one of 'vertical', 'horizontal' 633 | constraint: 'vertical', // one of 'vertical', 'horizontal', false 634 | containment: element, // also takes array of elements (or id's); or false 635 | handle: false, // or a CSS class 636 | only: false, 637 | delay: 0, 638 | hoverclass: null, 639 | ghosting: false, 640 | quiet: false, 641 | scroll: false, 642 | scrollSensitivity: 20, 643 | scrollSpeed: 15, 644 | format: this.SERIALIZE_RULE, 645 | 646 | // these take arrays of elements or ids and can be 647 | // used for better initialization performance 648 | elements: false, 649 | handles: false, 650 | 651 | onChange: Prototype.emptyFunction, 652 | onUpdate: Prototype.emptyFunction 653 | }, arguments[1] || { }); 654 | 655 | // clear any old sortable with same element 656 | this.destroy(element); 657 | 658 | // build options for the draggables 659 | var options_for_draggable = { 660 | revert: true, 661 | quiet: options.quiet, 662 | scroll: options.scroll, 663 | scrollSpeed: options.scrollSpeed, 664 | scrollSensitivity: options.scrollSensitivity, 665 | delay: options.delay, 666 | ghosting: options.ghosting, 667 | constraint: options.constraint, 668 | handle: options.handle }; 669 | 670 | if(options.starteffect) 671 | options_for_draggable.starteffect = options.starteffect; 672 | 673 | if(options.reverteffect) 674 | options_for_draggable.reverteffect = options.reverteffect; 675 | else 676 | if(options.ghosting) options_for_draggable.reverteffect = function(element) { 677 | element.style.top = 0; 678 | element.style.left = 0; 679 | }; 680 | 681 | if(options.endeffect) 682 | options_for_draggable.endeffect = options.endeffect; 683 | 684 | if(options.zindex) 685 | options_for_draggable.zindex = options.zindex; 686 | 687 | // build options for the droppables 688 | var options_for_droppable = { 689 | overlap: options.overlap, 690 | containment: options.containment, 691 | tree: options.tree, 692 | hoverclass: options.hoverclass, 693 | onHover: Sortable.onHover 694 | }; 695 | 696 | var options_for_tree = { 697 | onHover: Sortable.onEmptyHover, 698 | overlap: options.overlap, 699 | containment: options.containment, 700 | hoverclass: options.hoverclass 701 | }; 702 | 703 | // fix for gecko engine 704 | Element.cleanWhitespace(element); 705 | 706 | options.draggables = []; 707 | options.droppables = []; 708 | 709 | // drop on empty handling 710 | if(options.dropOnEmpty || options.tree) { 711 | Droppables.add(element, options_for_tree); 712 | options.droppables.push(element); 713 | } 714 | 715 | (options.elements || this.findElements(element, options) || []).each( function(e,i) { 716 | var handle = options.handles ? $(options.handles[i]) : 717 | (options.handle ? $(e).select('.' + options.handle)[0] : e); 718 | options.draggables.push( 719 | new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); 720 | Droppables.add(e, options_for_droppable); 721 | if(options.tree) e.treeNode = element; 722 | options.droppables.push(e); 723 | }); 724 | 725 | if(options.tree) { 726 | (Sortable.findTreeElements(element, options) || []).each( function(e) { 727 | Droppables.add(e, options_for_tree); 728 | e.treeNode = element; 729 | options.droppables.push(e); 730 | }); 731 | } 732 | 733 | // keep reference 734 | this.sortables[element.identify()] = options; 735 | 736 | // for onupdate 737 | Draggables.addObserver(new SortableObserver(element, options.onUpdate)); 738 | 739 | }, 740 | 741 | // return all suitable-for-sortable elements in a guaranteed order 742 | findElements: function(element, options) { 743 | return Element.findChildren( 744 | element, options.only, options.tree ? true : false, options.tag); 745 | }, 746 | 747 | findTreeElements: function(element, options) { 748 | return Element.findChildren( 749 | element, options.only, options.tree ? true : false, options.treeTag); 750 | }, 751 | 752 | onHover: function(element, dropon, overlap) { 753 | if(Element.isParent(dropon, element)) return; 754 | 755 | if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { 756 | return; 757 | } else if(overlap>0.5) { 758 | Sortable.mark(dropon, 'before'); 759 | if(dropon.previousSibling != element) { 760 | var oldParentNode = element.parentNode; 761 | element.style.visibility = "hidden"; // fix gecko rendering 762 | dropon.parentNode.insertBefore(element, dropon); 763 | if(dropon.parentNode!=oldParentNode) 764 | Sortable.options(oldParentNode).onChange(element); 765 | Sortable.options(dropon.parentNode).onChange(element); 766 | } 767 | } else { 768 | Sortable.mark(dropon, 'after'); 769 | var nextElement = dropon.nextSibling || null; 770 | if(nextElement != element) { 771 | var oldParentNode = element.parentNode; 772 | element.style.visibility = "hidden"; // fix gecko rendering 773 | dropon.parentNode.insertBefore(element, nextElement); 774 | if(dropon.parentNode!=oldParentNode) 775 | Sortable.options(oldParentNode).onChange(element); 776 | Sortable.options(dropon.parentNode).onChange(element); 777 | } 778 | } 779 | }, 780 | 781 | onEmptyHover: function(element, dropon, overlap) { 782 | var oldParentNode = element.parentNode; 783 | var droponOptions = Sortable.options(dropon); 784 | 785 | if(!Element.isParent(dropon, element)) { 786 | var index; 787 | 788 | var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); 789 | var child = null; 790 | 791 | if(children) { 792 | var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); 793 | 794 | for (index = 0; index < children.length; index += 1) { 795 | if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { 796 | offset -= Element.offsetSize (children[index], droponOptions.overlap); 797 | } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { 798 | child = index + 1 < children.length ? children[index + 1] : null; 799 | break; 800 | } else { 801 | child = children[index]; 802 | break; 803 | } 804 | } 805 | } 806 | 807 | dropon.insertBefore(element, child); 808 | 809 | Sortable.options(oldParentNode).onChange(element); 810 | droponOptions.onChange(element); 811 | } 812 | }, 813 | 814 | unmark: function() { 815 | if(Sortable._marker) Sortable._marker.hide(); 816 | }, 817 | 818 | mark: function(dropon, position) { 819 | // mark on ghosting only 820 | var sortable = Sortable.options(dropon.parentNode); 821 | if(sortable && !sortable.ghosting) return; 822 | 823 | if(!Sortable._marker) { 824 | Sortable._marker = 825 | ($('dropmarker') || Element.extend(document.createElement('DIV'))). 826 | hide().addClassName('dropmarker').setStyle({position:'absolute'}); 827 | document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); 828 | } 829 | var offsets = dropon.cumulativeOffset(); 830 | Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); 831 | 832 | if(position=='after') 833 | if(sortable.overlap == 'horizontal') 834 | Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); 835 | else 836 | Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); 837 | 838 | Sortable._marker.show(); 839 | }, 840 | 841 | _tree: function(element, options, parent) { 842 | var children = Sortable.findElements(element, options) || []; 843 | 844 | for (var i = 0; i < children.length; ++i) { 845 | var match = children[i].id.match(options.format); 846 | 847 | if (!match) continue; 848 | 849 | var child = { 850 | id: encodeURIComponent(match ? match[1] : null), 851 | element: element, 852 | parent: parent, 853 | children: [], 854 | position: parent.children.length, 855 | container: $(children[i]).down(options.treeTag) 856 | }; 857 | 858 | /* Get the element containing the children and recurse over it */ 859 | if (child.container) 860 | this._tree(child.container, options, child); 861 | 862 | parent.children.push (child); 863 | } 864 | 865 | return parent; 866 | }, 867 | 868 | tree: function(element) { 869 | element = $(element); 870 | var sortableOptions = this.options(element); 871 | var options = Object.extend({ 872 | tag: sortableOptions.tag, 873 | treeTag: sortableOptions.treeTag, 874 | only: sortableOptions.only, 875 | name: element.id, 876 | format: sortableOptions.format 877 | }, arguments[1] || { }); 878 | 879 | var root = { 880 | id: null, 881 | parent: null, 882 | children: [], 883 | container: element, 884 | position: 0 885 | }; 886 | 887 | return Sortable._tree(element, options, root); 888 | }, 889 | 890 | /* Construct a [i] index for a particular node */ 891 | _constructIndex: function(node) { 892 | var index = ''; 893 | do { 894 | if (node.id) index = '[' + node.position + ']' + index; 895 | } while ((node = node.parent) != null); 896 | return index; 897 | }, 898 | 899 | sequence: function(element) { 900 | element = $(element); 901 | var options = Object.extend(this.options(element), arguments[1] || { }); 902 | 903 | return $(this.findElements(element, options) || []).map( function(item) { 904 | return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; 905 | }); 906 | }, 907 | 908 | setSequence: function(element, new_sequence) { 909 | element = $(element); 910 | var options = Object.extend(this.options(element), arguments[2] || { }); 911 | 912 | var nodeMap = { }; 913 | this.findElements(element, options).each( function(n) { 914 | if (n.id.match(options.format)) 915 | nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; 916 | n.parentNode.removeChild(n); 917 | }); 918 | 919 | new_sequence.each(function(ident) { 920 | var n = nodeMap[ident]; 921 | if (n) { 922 | n[1].appendChild(n[0]); 923 | delete nodeMap[ident]; 924 | } 925 | }); 926 | }, 927 | 928 | serialize: function(element) { 929 | element = $(element); 930 | var options = Object.extend(Sortable.options(element), arguments[1] || { }); 931 | var name = encodeURIComponent( 932 | (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); 933 | 934 | if (options.tree) { 935 | return Sortable.tree(element, arguments[1]).children.map( function (item) { 936 | return [name + Sortable._constructIndex(item) + "[id]=" + 937 | encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); 938 | }).flatten().join('&'); 939 | } else { 940 | return Sortable.sequence(element, arguments[1]).map( function(item) { 941 | return name + "[]=" + encodeURIComponent(item); 942 | }).join('&'); 943 | } 944 | } 945 | }; 946 | 947 | // Returns true if child is contained within element 948 | Element.isParent = function(child, element) { 949 | if (!child.parentNode || child == element) return false; 950 | if (child.parentNode == element) return true; 951 | return Element.isParent(child.parentNode, element); 952 | }; 953 | 954 | Element.findChildren = function(element, only, recursive, tagName) { 955 | if(!element.hasChildNodes()) return null; 956 | tagName = tagName.toUpperCase(); 957 | if(only) only = [only].flatten(); 958 | var elements = []; 959 | $A(element.childNodes).each( function(e) { 960 | if(e.tagName && e.tagName.toUpperCase()==tagName && 961 | (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) 962 | elements.push(e); 963 | if(recursive) { 964 | var grandchildren = Element.findChildren(e, only, recursive, tagName); 965 | if(grandchildren) elements.push(grandchildren); 966 | } 967 | }); 968 | 969 | return (elements.length>0 ? elements.flatten() : []); 970 | }; 971 | 972 | Element.offsetSize = function (element, type) { 973 | return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; 974 | }; -------------------------------------------------------------------------------- /test/rails_app/public/javascripts/rails.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | Ajax.Responders.register({ 3 | onCreate: function(request) { 4 | var token = $$('meta[name=csrf-token]')[0]; 5 | if (token) { 6 | if (!request.options.requestHeaders) request.options.requestHeaders = {}; 7 | request.options.requestHeaders['X-CSRF-Token'] = token.readAttribute('content'); 8 | } 9 | } 10 | }); 11 | 12 | // Technique from Juriy Zaytsev 13 | // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ 14 | function isEventSupported(eventName) { 15 | var el = document.createElement('div'); 16 | eventName = 'on' + eventName; 17 | var isSupported = (eventName in el); 18 | if (!isSupported) { 19 | el.setAttribute(eventName, 'return;'); 20 | isSupported = typeof el[eventName] == 'function'; 21 | } 22 | el = null; 23 | return isSupported; 24 | } 25 | 26 | function isForm(element) { 27 | return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM'; 28 | } 29 | 30 | function isInput(element) { 31 | if (Object.isElement(element)) { 32 | var name = element.nodeName.toUpperCase(); 33 | return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA'; 34 | } 35 | else return false; 36 | } 37 | 38 | var submitBubbles = isEventSupported('submit'), 39 | changeBubbles = isEventSupported('change'); 40 | 41 | if (!submitBubbles || !changeBubbles) { 42 | // augment the Event.Handler class to observe custom events when needed 43 | Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap( 44 | function(init, element, eventName, selector, callback) { 45 | init(element, eventName, selector, callback); 46 | // is the handler being attached to an element that doesn't support this event? 47 | if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) || 48 | (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) { 49 | // "submit" => "emulated:submit" 50 | this.eventName = 'emulated:' + this.eventName; 51 | } 52 | } 53 | ); 54 | } 55 | 56 | if (!submitBubbles) { 57 | // discover forms on the page by observing focus events which always bubble 58 | document.on('focusin', 'form', function(focusEvent, form) { 59 | // special handler for the real "submit" event (one-time operation) 60 | if (!form.retrieve('emulated:submit')) { 61 | form.on('submit', function(submitEvent) { 62 | var emulated = form.fire('emulated:submit', submitEvent, true); 63 | // if custom event received preventDefault, cancel the real one too 64 | if (emulated.returnValue === false) submitEvent.preventDefault(); 65 | }); 66 | form.store('emulated:submit', true); 67 | } 68 | }); 69 | } 70 | 71 | if (!changeBubbles) { 72 | // discover form inputs on the page 73 | document.on('focusin', 'input, select, textarea', function(focusEvent, input) { 74 | // special handler for real "change" events 75 | if (!input.retrieve('emulated:change')) { 76 | input.on('change', function(changeEvent) { 77 | input.fire('emulated:change', changeEvent, true); 78 | }); 79 | input.store('emulated:change', true); 80 | } 81 | }); 82 | } 83 | 84 | function handleRemote(element) { 85 | var method, url, params; 86 | 87 | var event = element.fire("ajax:before"); 88 | if (event.stopped) return false; 89 | 90 | if (element.tagName.toLowerCase() === 'form') { 91 | method = element.readAttribute('method') || 'post'; 92 | url = element.readAttribute('action'); 93 | // serialize the form with respect to the submit button that was pressed 94 | params = element.serialize({ submit: element.retrieve('rails:submit-button') }); 95 | // clear the pressed submit button information 96 | element.store('rails:submit-button', null); 97 | } else { 98 | method = element.readAttribute('data-method') || 'get'; 99 | url = element.readAttribute('href'); 100 | params = {}; 101 | } 102 | 103 | new Ajax.Request(url, { 104 | method: method, 105 | parameters: params, 106 | evalScripts: true, 107 | 108 | onCreate: function(response) { element.fire("ajax:create", response); }, 109 | onComplete: function(response) { element.fire("ajax:complete", response); }, 110 | onSuccess: function(response) { element.fire("ajax:success", response); }, 111 | onFailure: function(response) { element.fire("ajax:failure", response); } 112 | }); 113 | 114 | element.fire("ajax:after"); 115 | } 116 | 117 | function insertHiddenField(form, name, value) { 118 | form.insert(new Element('input', { type: 'hidden', name: name, value: value })); 119 | } 120 | 121 | function handleMethod(element) { 122 | var method = element.readAttribute('data-method'), 123 | url = element.readAttribute('href'), 124 | csrf_param = $$('meta[name=csrf-param]')[0], 125 | csrf_token = $$('meta[name=csrf-token]')[0]; 126 | 127 | var form = new Element('form', { method: "POST", action: url, style: "display: none;" }); 128 | $(element.parentNode).insert(form); 129 | 130 | if (method !== 'post') { 131 | insertHiddenField(form, '_method', method); 132 | } 133 | 134 | if (csrf_param) { 135 | insertHiddenField(form, csrf_param.readAttribute('content'), csrf_token.readAttribute('content')); 136 | } 137 | 138 | form.submit(); 139 | } 140 | 141 | function disableFormElements(form) { 142 | form.select('input[type=submit][data-disable-with]').each(function(input) { 143 | input.store('rails:original-value', input.getValue()); 144 | input.setValue(input.readAttribute('data-disable-with')).disable(); 145 | }); 146 | } 147 | 148 | function enableFormElements(form) { 149 | form.select('input[type=submit][data-disable-with]').each(function(input) { 150 | input.setValue(input.retrieve('rails:original-value')).enable(); 151 | }); 152 | } 153 | 154 | function allowAction(element) { 155 | var message = element.readAttribute('data-confirm'); 156 | return !message || confirm(message); 157 | } 158 | 159 | document.on('click', 'a[data-confirm], a[data-remote], a[data-method]', function(event, link) { 160 | if (!allowAction(link)) { 161 | event.stop(); 162 | return false; 163 | } 164 | 165 | if (link.readAttribute('data-remote')) { 166 | handleRemote(link); 167 | event.stop(); 168 | } else if (link.readAttribute('data-method')) { 169 | handleMethod(link); 170 | event.stop(); 171 | } 172 | }); 173 | 174 | document.on("click", "form input[type=submit], form button[type=submit], form button:not([type])", function(event, button) { 175 | // register the pressed submit button 176 | event.findElement('form').store('rails:submit-button', button.name || false); 177 | }); 178 | 179 | document.on("submit", function(event) { 180 | var form = event.findElement(); 181 | 182 | if (!allowAction(form)) { 183 | event.stop(); 184 | return false; 185 | } 186 | 187 | if (form.readAttribute('data-remote')) { 188 | handleRemote(form); 189 | event.stop(); 190 | } else { 191 | disableFormElements(form); 192 | } 193 | }); 194 | 195 | document.on('ajax:create', 'form', function(event, form) { 196 | if (form == event.findElement()) disableFormElements(form); 197 | }); 198 | 199 | document.on('ajax:complete', 'form', function(event, form) { 200 | if (form == event.findElement()) enableFormElements(form); 201 | }); 202 | })(); 203 | -------------------------------------------------------------------------------- /test/rails_app/public/stylesheets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zombocom/derailed_benchmarks/e8b29db93acf7d3318b9afb42d8516b905020afb/test/rails_app/public/stylesheets/.gitkeep -------------------------------------------------------------------------------- /test/rails_app/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 5 | 6 | APP_PATH = File.expand_path('../../config/application', __FILE__) 7 | require File.expand_path('../../config/boot', __FILE__) 8 | require 'rails/commands' 9 | -------------------------------------------------------------------------------- /test/support/integration_case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Define a bare test case to use with Capybara 4 | class ActiveSupport::IntegrationCase < ActiveSupport::TestCase 5 | # include Capybara::DSL 6 | include Rails.application.routes.url_helpers 7 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'bundler/setup' 5 | 6 | # Configure Rails Envinronment 7 | ENV["RAILS_ENV"] = "test" 8 | 9 | require "ruby2_keywords" if RUBY_VERSION < "2.7" 10 | 11 | require 'rails' 12 | require 'rails/test_help' 13 | 14 | require 'stringio' 15 | require 'pathname' 16 | 17 | require 'derailed_benchmarks' 18 | 19 | require File.expand_path("../rails_app/config/environment.rb", __FILE__) 20 | 21 | ActionMailer::Base.delivery_method = :test 22 | ActionMailer::Base.perform_deliveries = true 23 | ActionMailer::Base.default_url_options[:host] = "test.com" 24 | 25 | Rails.backtrace_cleaner.remove_silencers! 26 | 27 | # Configure capybara for integration testing 28 | require "capybara/rails" 29 | Capybara.default_driver = :rack_test 30 | Capybara.default_selector = :css 31 | 32 | require_relative "rails_app/config/environment" 33 | 34 | # https://github.com/plataformatec/devise/blob/master/test/orm/active_record.rb 35 | migrate_path = File.expand_path("../rails_app/db/migrate", __FILE__) 36 | if Rails.version >= "7.1" 37 | ActiveRecord::MigrationContext.new(migrate_path).migrate 38 | elsif Rails.version >= "6.0" 39 | ActiveRecord::MigrationContext.new(migrate_path, ActiveRecord::SchemaMigration).migrate 40 | elsif Rails.version.start_with? "5.2" 41 | ActiveRecord::MigrationContext.new(migrate_path).migrate 42 | else 43 | ActiveRecord::Migrator.migrate(migrate_path) 44 | end 45 | 46 | ActiveRecord::Migration.maintain_test_schema! 47 | 48 | # Load support files 49 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 50 | 51 | class ActiveSupport::IntegrationCase 52 | def assert_has_content?(content) 53 | assert has_content?(content), "Expected #{page.body} to include #{content.inspect}" 54 | end 55 | end 56 | 57 | def fixtures_dir(name = "") 58 | root_path("test/fixtures").join(name) 59 | end 60 | 61 | def root_path(name = "") 62 | Pathname.new(File.expand_path("../..", __FILE__)).join(name) 63 | end 64 | 65 | def rails_app_path(name = "") 66 | root_path("test/rails_app").join(name) 67 | end 68 | 69 | def run!(cmd) 70 | output = `#{cmd}` 71 | raise "Cmd #{cmd} failed:\n#{output}" unless $?.success? 72 | output 73 | end 74 | --------------------------------------------------------------------------------