diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 860fcad6..7c904088 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -1,10 +1,19 @@ name: Benchmarks +# Only changes that can affect a benchmark run it: README-only changes do not. on: push: branches: [ main ] + paths: &benchmark_paths + - 'code/**/*.rb' + - 'Gemfile' + - 'Rakefile' + - 'compose.yaml' + - 'docker/**' + - '.github/workflows/benchmarks.yml' pull_request: branches: [ main ] + paths: *benchmark_paths jobs: rake: @@ -27,7 +36,38 @@ jobs: - name: Set Share Env if: github.ref_name == 'main' run: | - echo "SHARE=1" >> $GITHUB_ENV + echo "SHARE=1" >> "$GITHUB_ENV" - uses: actions/checkout@v4 + with: + # A pull_request checks out a merge commit: HEAD^1 is the base. + fetch-depth: 2 + # On a pull request, run only the benchmark files it changes, unless it + # changes something every benchmark depends on. Pushes to main run all. + - name: Pick benchmarks + id: pick + if: github.event_name == 'pull_request' + run: | + changed=$(git diff --name-only HEAD^1 HEAD) + if echo "$changed" | grep -qE '^(Gemfile|Rakefile|compose\.yaml|docker/|\.github/workflows/benchmarks\.yml)'; then + echo "Shared files changed, running every benchmark" + else + files=$(git diff --name-only --diff-filter=d HEAD^1 HEAD -- 'code/**/*.rb' | tr '\n' ' ') + if [ -z "$files" ]; then + # For example a PR that only deletes a benchmark. + echo "No benchmark to run" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "Running: $files" + echo "files=$files" >> "$GITHUB_OUTPUT" + fi + fi - name: Run benchmarks on ${{ matrix.ruby }} - run: docker compose run --rm -T ${{ matrix.ruby }} + if: steps.pick.outputs.skip != 'true' + env: + FILES: ${{ steps.pick.outputs.files }} + run: | + # Unquoted on purpose: one argument per file. set -f keeps names like + # dig-vs-[]-vs-fetch.rb from being read as glob patterns. + set -f + # shellcheck disable=SC2086 + docker compose run --rm -T ${{ matrix.ruby }} $FILES diff --git a/Gemfile b/Gemfile index 00d364d9..853ad202 100644 --- a/Gemfile +++ b/Gemfile @@ -5,4 +5,13 @@ gem 'benchmark-ips', '>= 2.0' gem 'activesupport', '>= 2.2.1' gem 'e2mmap' +# Needed on Ruby 4.0+, where ostruct is no longer a default gem (3.4 warns). +# Keep the `if`: older Rubies benchmark the ostruct they ship, and the gem +# does not even parse on Ruby 2.1. +gem 'ostruct' if RUBY_VERSION >= '3.4' + +# JRuby 9.1's bundled jruby-openssl cannot complete a TLS handshake with +# ips.fastruby.io, so sharing results (SHARE=1) crashed every benchmark. +gem 'jruby-openssl', '>= 0.10' if RUBY_ENGINE == 'jruby' && Gem::Version.new(JRUBY_VERSION) < Gem::Version.new('9.2') + gem 'rake' diff --git a/Rakefile b/Rakefile index d6abdec5..a040a1e3 100644 --- a/Rakefile +++ b/Rakefile @@ -1,14 +1,18 @@ desc "run benchmark in current ruby" task :run_benchmark do + failed = [] + Dir["code/general/*.rb"].each do |benchmark| puts "$ ruby -v #{benchmark}" - system("ruby", "-v", "-W0", benchmark) + failed << benchmark unless system("ruby", "-v", "-W0", benchmark) end Dir["code/*/*.rb"].reject { |path| path =~ /^code\/general/ }.each do |benchmark| puts "$ ruby -v #{benchmark}" - system("ruby", "-v", "-W0", benchmark) + failed << benchmark unless system("ruby", "-v", "-W0", benchmark) end + + abort "Failed benchmarks:\n#{failed.join("\n")}" unless failed.empty? end task default: :run_benchmark diff --git a/code/hash/dig-vs-[]-vs-fetch.rb b/code/hash/dig-vs-[]-vs-fetch.rb index d2964416..35bfe5fb 100644 --- a/code/hash/dig-vs-[]-vs-fetch.rb +++ b/code/hash/dig-vs-[]-vs-fetch.rb @@ -3,8 +3,10 @@ h = { a: { b: { c: { d: { e: "foo" } } } } } Benchmark.ips do |x| - x.report "Hash#dig" do - h.dig(:a, :b, :c, :d, :e) + if RUBY_VERSION >= "2.3.0" + x.report "Hash#dig" do + h.dig(:a, :b, :c, :d, :e) + end end x.report "Hash#[]" do diff --git a/code/hash/slice-native-vs-before-native.rb b/code/hash/slice-native-vs-before-native.rb index 85cc70f3..cee3505e 100644 --- a/code/hash/slice-native-vs-before-native.rb +++ b/code/hash/slice-native-vs-before-native.rb @@ -41,7 +41,7 @@ def slow end Benchmark.ips do |x| - x.report('Hash#native-slice ') { fastest } + x.report('Hash#native-slice ') { fastest } if RUBY_VERSION >= '2.5.0' x.report('Array#each ') { faster } x.report('Array#each_w/_object') { fast } x.report('Hash#select-include ') { slow } diff --git a/code/string/remove-extra-spaces-or-other-chars.rb b/code/string/remove-extra-spaces-or-other-chars.rb index fb1349f7..32c9c5c4 100644 --- a/code/string/remove-extra-spaces-or-other-chars.rb +++ b/code/string/remove-extra-spaces-or-other-chars.rb @@ -1,10 +1,11 @@ require 'benchmark/ips' -PASSAGE = <<~LIPSUM - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. - Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +# A plain heredoc, not <<~, which is Ruby 2.3+ (older Rubies cannot parse the file at all). +PASSAGE = <&2 + exit 1 +fi