From 80db701ee7cf17352e96c1a7ddd755ac15250950 Mon Sep 17 00:00:00 2001 From: "Daniel P. Clark" <6ftdan@gmail.com> Date: Wed, 7 Oct 2026 04:44:24 +0000 Subject: [PATCH 01/10] Upgrade to Rutie 0.13.1 and require Ruby 3.2 to 3.4 Rutie 0.13 is the Ruby 3.2, 3.3 and 3.4 line and drops Ruby 2, so the gem's Ruby requirement moves with it, as does the CI matrix (Linux and Windows on each of the three, macOS on 3.4) and the release tarballs. The extension's own code compiles unchanged. What Ruby 3 changed around the extension: * thermite reads GitHub release feeds with REXML, which Ruby 3 no longer loads without a gem, so the gem now depends on rexml. * rubocop 0.53 can't load its configuration on Ruby 3 (Psych 4 changed `safe_load`), so the development dependency moves to rubocop 1.x with rubocop-performance for the Performance cops the configuration enables. The configuration keeps the same cops under their current names; the Rails cops, which never applied to this gem, are dropped. * The ruby/spec and mspec submodules move from their 2021 commits to the ruby/spec suite for Ruby 3.2.9 and the mspec it was run with, the last suite tested against 3.2, 3.3 and 3.4 together. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CXvfgWzuq28ewqT3AyhX4Z --- .github/workflows/ci.yml | 10 ++-- .rubocop.yml | 105 ++++++++++----------------------------- Cargo.lock | 4 +- Cargo.toml | 2 +- README.md | 2 +- faster_path.gemspec | 9 ++-- spec/mspec | 2 +- spec/ruby_spec | 2 +- 8 files changed, 42 insertions(+), 94 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72e84fc..73a77d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,9 +23,9 @@ jobs: strategy: fail-fast: false matrix: - # Rutie 0.10, which the extension is built with, supports Ruby 2.5 to 2.7 + # Rutie 0.13, which the extension is built with, supports Ruby 3.2 to 3.4 os: [ubuntu-22.04, windows-latest] - ruby: ['2.5', '2.6', '2.7'] + ruby: ['3.2', '3.3', '3.4'] suite: - name: regression env: WITH_REGRESSION=true @@ -35,7 +35,7 @@ jobs: env: ENCODING=true TEST_MONKEYPATCHES=true WITH_REGRESSION=true include: - os: macos-latest - ruby: '2.7' + ruby: '3.4' suite: name: regression env: WITH_REGRESSION=true @@ -65,10 +65,10 @@ jobs: strategy: matrix: os: [ubuntu-22.04] - ruby: ['2.5', '2.6', '2.7'] + ruby: ['3.2', '3.3', '3.4'] include: - os: macos-latest - ruby: '2.7' + ruby: '3.4' steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable diff --git a/.rubocop.yml b/.rubocop.yml index 1bfaf38..129d8e0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,10 @@ +plugins: + - rubocop-performance + AllCops: + TargetRubyVersion: 3.2 + NewCops: disable + SuggestExtensions: false Include: - 'Gemfile' - 'Rakefile' @@ -88,7 +94,7 @@ Layout/EndAlignment: Description: 'Align ends correctly.' Enabled: true -Lint/EndInMethod: +Style/EndBlock: Description: 'END blocks should not be placed inside method definitions.' Enabled: true @@ -105,7 +111,7 @@ Lint/FormatParameterMismatch: Description: 'The number of parameters to format/sprint must match the fields.' Enabled: true -Lint/HandleExceptions: +Lint/SuppressedException: Description: "Don't suppress exception." StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions' Enabled: true @@ -154,7 +160,7 @@ Lint/ShadowingOuterLocalVariable: for block arguments or block local variables. Enabled: true -Lint/StringConversionInInterpolation: +Lint/RedundantStringCoercion: Description: 'Checks for Object#to_s usage in string interpolation.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s' Enabled: true @@ -186,7 +192,7 @@ Lint/UselessAssignment: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' Enabled: true -Lint/UselessComparison: +Lint/BinaryOperatorWithIdenticalOperands: Description: 'Checks for comparison of something with itself.' Enabled: true @@ -229,7 +235,7 @@ Metrics/CyclomaticComplexity: of test cases needed to validate a method. Enabled: true -Metrics/LineLength: +Layout/LineLength: Description: 'Limit lines to 80 characters.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits' Enabled: false @@ -290,7 +296,7 @@ Performance/ReverseEach: Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code' Enabled: true -Performance/Sample: +Style/Sample: Description: >- Use `sample` instead of `shuffle.first`, `shuffle.last`, and `shuffle[Fixnum]`. @@ -333,7 +339,7 @@ Performance/EndWith: Performance/FixedSize: Enabled: true -Performance/LstripRstrip: +Style/Strip: Enabled: true Performance/RangeInclude: @@ -348,7 +354,7 @@ Performance/RedundantMatch: Performance/RedundantMerge: Enabled: true -Performance/RedundantSortBy: +Style/RedundantSortBy: Enabled: true Performance/RegexpMatch: @@ -360,58 +366,6 @@ Performance/StartWith: Performance/TimesMap: Enabled: true -##################### Rails ################################## - -Rails/ActionFilter: - Description: 'Enforces consistent use of action filter methods.' - Enabled: false - -Rails/Date: - Description: >- - Checks the correct usage of date aware methods, - such as Date.today, Date.current etc. - Enabled: false - -Rails/Delegate: - Description: 'Prefer delegate method for delegations.' - Enabled: false - -Rails/FindBy: - Description: 'Prefer find_by over where.first.' - Enabled: false - -Rails/FindEach: - Description: 'Prefer all.find_each over all.find.' - Enabled: false - -Rails/HasAndBelongsToMany: - Description: 'Prefer has_many :through to has_and_belongs_to_many.' - Enabled: false - -Rails/Output: - Description: 'Checks for calls to puts, print, etc.' - Enabled: false - -Rails/ReadWriteAttribute: - Description: >- - Checks for read_attribute(:attr) and - write_attribute(:attr, val). - Enabled: false - -Rails/ScopeArgs: - Description: 'Checks the arguments of ActiveRecord scopes.' - Enabled: false - -Rails/TimeZone: - Description: 'Checks the correct usage of time zone aware methods.' - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time' - Reference: 'http://danilenko.org/2012/7/6/rails_timezones' - Enabled: false - -Rails/Validation: - Description: 'Use validates :attribute, hash of validations.' - Enabled: false - ################## Style ################################# Layout/AccessModifierIndentation: @@ -428,20 +382,20 @@ Style/Alias: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method' Enabled: false -Layout/AlignArray: +Layout/ArrayAlignment: Description: >- Align the elements of an array literal if they span more than one line. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays' Enabled: true -Layout/AlignHash: +Layout/HashAlignment: Description: >- Align the elements of a hash literal if they span more than one line. Enabled: true -Layout/AlignParameters: +Layout/ParameterAlignment: Description: >- Align the parameters of a method call if they span more than one line. WRONG! Only indent 2 (don't use this). @@ -500,10 +454,6 @@ Style/BlockDelimiters: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' Enabled: true -Style/BracesAroundHashParameters: - Description: 'Enforce braces style around hash parameters.' - Enabled: false - Style/CaseEquality: Description: 'Avoid explicit use of the case equality operator(===).' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality' @@ -640,11 +590,6 @@ Style/EmptyLiteral: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash' Enabled: false -Style/EndBlock: - Description: 'Avoid the use of END blocks.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks' - Enabled: true - Layout/EndOfLine: Description: 'Use Unix-style line endings.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf' @@ -673,7 +618,7 @@ Layout/FirstParameterIndentation: Description: 'Checks the indentation of the first parameter in a method call.' Enabled: false -Style/FlipFlop: +Lint/FlipFlop: Description: 'Checks for flip flops' StyleGuide: 'https://blog.newrelic.com/2015/02/24/weird-ruby-part-3-fun-flip-flop-phenom/' Enabled: true @@ -727,13 +672,13 @@ Layout/IndentationWidth: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' Enabled: true -Layout/IndentArray: +Layout/FirstArrayElementIndentation: Description: >- Checks the indentation of the first element in an array literal. Enabled: false -Layout/IndentHash: +Layout/FirstHashElementIndentation: Description: 'Checks the indentation of the first key in a hash literal.' Enabled: false @@ -905,7 +850,7 @@ Style/PerlBackrefs: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers' Enabled: true -Naming/PredicateName: +Naming/PredicatePrefix: Description: 'Check the names of predicate methods.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark' Enabled: false @@ -1111,12 +1056,12 @@ Style/SymbolProc: Description: 'Use symbols as procs instead of blocks when possible.' Enabled: false -Layout/Tab: +Layout/IndentationStyle: Description: 'No hard tabs.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' Enabled: true -Layout/TrailingBlankLines: +Layout/TrailingEmptyLines: Description: 'Checks trailing blank lines and final newline.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof' Enabled: true @@ -1153,11 +1098,11 @@ Style/UnlessElse: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless' Enabled: true -Style/UnneededCapitalW: +Style/RedundantCapitalW: Description: 'Checks for %W when interpolation is not needed.' Enabled: true -Style/UnneededPercentQ: +Style/RedundantPercentQ: Description: 'Checks for %q/%Q when single quotes or double quotes would do.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q' Enabled: true diff --git a/Cargo.lock b/Cargo.lock index 0c87d7c..e53fcdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "rutie" -version = "0.10.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97641d6bd3731d263d914141e960a8b515bf5ff4ca6c961b25ca4b2694c6490" +checksum = "30d1529470ad0b69b81a6d0224632d00c01b46ef671452e24407512165b7ed84" dependencies = [ "lazy_static", "libc", diff --git a/Cargo.toml b/Cargo.toml index 0982b33..ef15e93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,5 +17,5 @@ name = "faster_path" crate-type = ["cdylib"] [dependencies] -rutie = "0.10.2" +rutie = "0.13.1" memchr = "2" diff --git a/README.md b/README.md index e160186..a5857a7 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ As mentioned earlier Sprockets, which handles assets, changed away from using `P ## Requirements -* Ruby 2.5, 2.6 or 2.7 (the Rust extension is built with [Rutie](https://github.com/danielpclark/rutie) 0.10, which supports these) +* Ruby 3.2, 3.3 or 3.4 (the Rust extension is built with [Rutie](https://github.com/danielpclark/rutie) 0.13, which supports these) * Rust 1.71 or later ## Installation diff --git a/faster_path.gemspec b/faster_path.gemspec index 9616f74..6c681b0 100644 --- a/faster_path.gemspec +++ b/faster_path.gemspec @@ -24,17 +24,20 @@ Gem::Specification.new do |spec| spec.extensions = ['ext/Rakefile'] spec.require_paths = ['lib'] - # Rutie 0.10, which the Rust extension is built with, supports Ruby 2.5 to 2.7 - spec.required_ruby_version = ['>= 2.5', '< 3.0'] + # Rutie 0.13, which the Rust extension is built with, supports Ruby 3.2 to 3.4 + spec.required_ruby_version = ['>= 3.2', '< 3.5'] spec.add_dependency 'bundler', '>= 1.12' spec.add_dependency 'rake', '>= 12.3' spec.add_dependency 'thermite', '0.13.0' + # thermite reads GitHub release feeds with REXML, a bundled gem since Ruby 3.0 + spec.add_dependency 'rexml', '~> 3.2' spec.add_development_dependency 'read_source', '~> 0.2.6' spec.add_development_dependency 'minitest', '~> 5.11' spec.add_development_dependency 'minitest-reporters', '~> 1.1' spec.add_development_dependency 'color_pound_spec_reporter', '~> 0.0.9' - spec.add_development_dependency 'rubocop', '0.53' + spec.add_development_dependency 'rubocop', '~> 1.80' + spec.add_development_dependency 'rubocop-performance', '~> 1.20' spec.add_development_dependency 'stop_watch', '~> 1.0' if !ENV['CI'] && ENV['GRAPH'] spec.add_development_dependency 'gruff', '~> 0.7.0' diff --git a/spec/mspec b/spec/mspec index 0091e8a..5470479 160000 --- a/spec/mspec +++ b/spec/mspec @@ -1 +1 @@ -Subproject commit 0091e8a62e954717cd54641f935eaf1403692041 +Subproject commit 54704795e21128a930af2021c72c49cb87065134 diff --git a/spec/ruby_spec b/spec/ruby_spec index c503335..97f0762 160000 --- a/spec/ruby_spec +++ b/spec/ruby_spec @@ -1 +1 @@ -Subproject commit c503335d3d9f6ec6ef24de60a0716c34af69b64f +Subproject commit 97f076242b7fc6e60703e6a6053365065cd6fc30 From 95b099a678574eb5a8df2069258864d314c450bc Mon Sep 17 00:00:00 2001 From: "Daniel P. Clark" <6ftdan@gmail.com> Date: Wed, 7 Oct 2026 04:44:24 +0000 Subject: [PATCH 02/10] Support the level argument of File.dirname Ruby 3.1 gave `File.dirname` a second argument, the number of trailing components to remove, and the monkeypatch and refinement only took one, so the ruby/spec specs for it errored with "wrong number of arguments" once the suite moved to Ruby 3.2. `FasterPath.dirname` now takes the level too: it is converted like Ruby's `NUM2INT` (`to_int`, a negative level is an `ArgumentError`), and the dirname is taken that many times, stopping at the root like Ruby does. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CXvfgWzuq28ewqT3AyhX4Z --- lib/faster_path/optional/monkeypatches.rb | 4 +- lib/faster_path/optional/refinements.rb | 4 +- src/pathname.rs | 46 ++++++++++++++++++++--- test/dirname_test.rb | 35 +++++++++++++++++ test/refinements/dirname_test.rb | 11 +++++- 5 files changed, 89 insertions(+), 11 deletions(-) diff --git a/lib/faster_path/optional/monkeypatches.rb b/lib/faster_path/optional/monkeypatches.rb index 70027af..9909bb6 100644 --- a/lib/faster_path/optional/monkeypatches.rb +++ b/lib/faster_path/optional/monkeypatches.rb @@ -24,10 +24,10 @@ def self.extname(pth) end # @private :nodoc: - def self.dirname(pth) + def self.dirname(pth, level = 1) pth = pth.to_path if pth.respond_to? :to_path raise TypeError unless pth.is_a? String - FasterPath.dirname(pth) + FasterPath.dirname(pth, level) end end end diff --git a/lib/faster_path/optional/refinements.rb b/lib/faster_path/optional/refinements.rb index f5149cd..c47911c 100644 --- a/lib/faster_path/optional/refinements.rb +++ b/lib/faster_path/optional/refinements.rb @@ -20,10 +20,10 @@ def self.extname(pth) end # @private :nodoc: - def self.dirname(pth) + def self.dirname(pth, level = 1) pth = pth.to_path if pth.respond_to? :to_path raise TypeError unless pth.is_a? String - FasterPath.dirname(pth) + FasterPath.dirname(pth, level) end end end diff --git a/src/pathname.rs b/src/pathname.rs index 5a88f51..2a7e741 100644 --- a/src/pathname.rs +++ b/src/pathname.rs @@ -3,7 +3,7 @@ use std::ffi::OsStr; use std::fs; use std::io; -use rutie::{AnyException, AnyObject, Array, Boolean, Class, NilClass, Object, RString}; +use rutie::{AnyException, AnyObject, Array, Boolean, Class, Integer, NilClass, Object, RString, Symbol}; use crate::basename; use crate::chop_basename::{self, is_relative}; @@ -15,7 +15,7 @@ use crate::path_parsing::Rules; use crate::plus; use crate::relative_path_from::{self, RelativePathError}; use crate::ruby::{ - argument_error, check_max_arguments, error, new_pathname, path_like_to_string, pathname_class, + argument_error, check_max_arguments, error, inspect, new_pathname, path_like_to_string, pathname_class, truthy_argument, EncodingId, EncodingOf, PathArgument, PathString, RubyResult, }; @@ -152,11 +152,47 @@ pub fn pn_is_directory(arguments: &[AnyObject]) -> RubyResult { Ok(Boolean::new(is_directory).into()) } +// `File.dirname(path, level = 1)`: `level` is the number of trailing +// components to remove (Ruby 3.1+). pub fn pn_dirname(arguments: &[AnyObject]) -> RubyResult { - check_max_arguments(arguments, 1)?; + check_max_arguments(arguments, 2)?; let argument = PathArgument::new(arguments, 0)?; let path = argument.path()?; - Ok(path.to_ruby(&dirname::dirname(RULES, path.bytes())).into()) + let level = match arguments.get(1) { + Some(level) => level_argument(level)?, + None => 1, + }; + let mut result: Cow<[u8]> = Cow::Borrowed(path.bytes()); + for _ in 0..level { + let parent = dirname::dirname(RULES, &result); + if *parent == *result { + // The root ("/", "." or "C:/"), which is its own dirname + break; + } + result = Cow::Owned(parent.into_owned()); + } + Ok(path.to_ruby(&result).into()) +} + +// The `level` argument of `File.dirname`, converted like Ruby's `NUM2INT`. +fn level_argument(object: &AnyObject) -> Result { + let integer = match object.try_convert_to::() { + Ok(integer) => integer, + Err(_) => { + let responds = object.protect_send("respond_to?", &[Symbol::new("to_int").into()])?; + if !responds.value().is_true() { + let class_name = object.protect_send("class", &[]).ok().and_then(|class| inspect(&class).ok()).unwrap_or_default(); + return Err(error(&Class::type_error(), &format!("no implicit conversion of {} into Integer", class_name))); + } + let converted = object.protect_send("to_int", &[])?; + converted.try_convert_to::()? + } + }; + let level = integer.to_i64(); + if level < 0 { + return Err(argument_error(&format!("negative level: {}", level))); + } + Ok(level as usize) } // Returns an array of `String`s @@ -315,7 +351,7 @@ fn system_call_error(err: &io::Error, path: &AnyObject, func: &str) -> AnyExcept #[cfg(unix)] { if let Some(errno) = err.raw_os_error() { - let arguments = [path.clone(), rutie::Integer::new(errno.into()).into(), RString::new_utf8(func).into()]; + let arguments = [path.clone(), Integer::new(errno.into()).into(), RString::new_utf8(func).into()]; if let Ok(exception) = Class::system_call_error().protect_send("new", &arguments) { if let Ok(exception) = exception.try_convert_to::() { return exception; diff --git a/test/dirname_test.rb b/test/dirname_test.rb index 4edcdf2..5177728 100644 --- a/test/dirname_test.rb +++ b/test/dirname_test.rb @@ -11,6 +11,41 @@ def test_it_returns_all_the_components_of_filename_except_the_last_one assert_equal('/foo', FasterPath.dirname('/foo/foo')) end + def test_it_removes_level_trailing_components + assert_equal('/home', FasterPath.dirname('/home/jason/poot.txt', 2)) + assert_equal('/home/jason', FasterPath.dirname('/home/jason', 0)) + assert_equal('/', FasterPath.dirname('/home/jason', 2)) + assert_equal('/', FasterPath.dirname('/home/jason', 10)) + assert_equal('.', FasterPath.dirname('a/b', 2)) + assert_equal('.', FasterPath.dirname('a/b', 10)) + assert_equal('a', FasterPath.dirname('a//b//c//', 2)) + assert_equal(String, FasterPath.dirname('/home/jason', 0).class) + end + + def test_level_agrees_with_file_dirname + ['/home/jason/poot.txt', 'a/b/c', '/a', 'a', '', '/', './b/./', '/foo/../.', 'a//b//c//'].each do |path| + 0.upto(4) do |level| + assert_equal(File.dirname(path, level), FasterPath.dirname(path, level), "#{path.inspect}, #{level}") + end + end + end + + def test_level_is_converted_with_to_int + level = Object.new + def level.to_int + 2 + end + assert_equal('/home', FasterPath.dirname('/home/jason/poot.txt', level)) + end + + def test_level_must_be_a_non_negative_integer + error = assert_raises(ArgumentError) { FasterPath.dirname('/home/jason', -1) } + assert_equal('negative level: -1', error.message) + error = assert_raises(TypeError) { FasterPath.dirname('/home/jason', '1') } + assert_equal('no implicit conversion of String into Integer', error.message) + assert_raises(ArgumentError) { FasterPath.dirname('/home/jason', 1, 2) } + end + def test_it_returns_a_string assert_kind_of(String, FasterPath.dirname("foo")) end diff --git a/test/refinements/dirname_test.rb b/test/refinements/dirname_test.rb index 712592a..a590714 100644 --- a/test/refinements/dirname_test.rb +++ b/test/refinements/dirname_test.rb @@ -4,8 +4,8 @@ class RefinedFile using FasterPath::RefineFile - def dirname(name) - File.dirname(name) + def dirname(name, *level) + File.dirname(name, *level) end end @@ -45,4 +45,11 @@ def test_it_gets_dirnames_correctly assert_equal("/foo/..", @refined_file.dirname("/foo/../.")) assert_equal("foo", @refined_file.dirname("foo/../")) end + + def test_it_takes_a_level + assert_equal('/home', @refined_file.dirname('/home/jason/poot.txt', 2)) + assert_equal('/home/jason', @refined_file.dirname('/home/jason', 0)) + assert_equal('/', @refined_file.dirname('/home/jason', 10)) + assert_raises(ArgumentError) { @refined_file.dirname('/home/jason', -1) } + end end From 85b7433a94cee04e23a29b275fabf9bc656a2ef1 Mon Sep 17 00:00:00 2001 From: "Daniel P. Clark" <6ftdan@gmail.com> Date: Wed, 7 Oct 2026 04:44:55 +0000 Subject: [PATCH 03/10] Bump the gem to 0.5.0 The move from Ruby 2.5 to 2.7 to Ruby 3.2 to 3.4 drops every Ruby the 0.4 line supported, so this is a new minor version. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CXvfgWzuq28ewqT3AyhX4Z --- README.md | 2 +- lib/faster_path/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a5857a7..c2cf848 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ curl -sSf https://sh.rustup.rs | sh Add this line to your application's Gemfile: ```ruby -gem 'faster_path', '~> 0.4.0' +gem 'faster_path', '~> 0.5.0' ``` And then execute: diff --git a/lib/faster_path/version.rb b/lib/faster_path/version.rb index 54bd02d..7847e1a 100644 --- a/lib/faster_path/version.rb +++ b/lib/faster_path/version.rb @@ -1,3 +1,3 @@ module FasterPath - VERSION = "0.4.0" + VERSION = "0.5.0" end From 94edd6ef9e565ceae432fefc9acb5661697df3a4 Mon Sep 17 00:00:00 2001 From: "Daniel P. Clark" <6ftdan@gmail.com> Date: Wed, 7 Oct 2026 04:45:09 +0000 Subject: [PATCH 04/10] Lower the floors of absolute? and relative? for Ruby 3 Ruby 3's `Pathname#absolute?` is a single regexp match, not the `chop_basename` loop of Ruby 2, so the Rust version is a smaller improvement over it (about 25% on Ruby 3.4, 45% on 3.3) and the old 80% floors failed. These are provisional floors so `rake test` passes; the next commit re-measures every method on Ruby 3. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CXvfgWzuq28ewqT3AyhX4Z --- test/pbench/pbench_suite.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pbench/pbench_suite.rb b/test/pbench/pbench_suite.rb index 2d0c813..663f744 100644 --- a/test/pbench/pbench_suite.rb +++ b/test/pbench/pbench_suite.rb @@ -45,7 +45,7 @@ end } PBENCHES[:"absolute?"] = { - min: 80, + min: 20, new: lambda do |x| x.times do FasterPath.absolute?("/hello") @@ -314,7 +314,7 @@ end } PBENCHES[:"relative?"] = { - min: 80, + min: 30, new: lambda do |x| x.times do FasterPath.relative?("/hello") From 0894d5588ca4b955417ff0948a9094721b3b248d Mon Sep 17 00:00:00 2001 From: "Daniel P. Clark" <6ftdan@gmail.com> Date: Wed, 7 Oct 2026 05:00:36 +0000 Subject: [PATCH 05/10] Measure pbench by the best of interleaved rounds The benchmark timed all five rounds of Ruby's implementation, then all five of FasterPath's, and compared the averages, so anything else the machine did during one of the two phases landed on one side only. For the cheapest methods that was the whole result: `absolute?` on Ruby 3.4 measured anywhere between -12% and 66% from one run to the next. The rounds now alternate between the two implementations, and each is measured by its best round, which shows the code rather than the interruptions; three runs in a row now agree within a few points. The suite's string literals are frozen too. The `absolute?`, `relative?` and `directory?` benchmarks compared frozen `Pathname` constants with literals allocated on every call to FasterPath, two allocations the monkeypatch (which passes `@path`) never makes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CXvfgWzuq28ewqT3AyhX4Z --- test/pbench/pbench.rb | 32 +++++++++++++++----------------- test/pbench/pbench_suite.rb | 2 ++ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/test/pbench/pbench.rb b/test/pbench/pbench.rb index e6f4784..29ff011 100644 --- a/test/pbench/pbench.rb +++ b/test/pbench/pbench.rb @@ -22,31 +22,29 @@ def self.bench_range [10_000 * amplitude] * 5 end + # The improvement of `new_impl` over `baseline`, in percent. + # + # The two run alternately, round by round, so that whatever else the + # machine is doing affects both alike, and each is measured by its best + # round: the slower rounds only show the interruptions, not the code. def performance(baseline, new_impl) range = self.class.bench_range times_a = [] - range.each do |x| - GC.start - t0 = Minitest.clock_time - baseline.call(x) - t = Minitest.clock_time - t0 - times_a << t - end - - # This seems to stabalize the results a bit - sleep 0.02; GC.start - times_b = [] range.each do |x| - GC.start - t0 = Minitest.clock_time - new_impl.call(x) - t = Minitest.clock_time - t0 - times_b << t + times_a << time(baseline, x) + times_b << time(new_impl, x) end - increase(average(times_a), average(times_b)).round(1) + increase(times_a.min, times_b.min).round(1) + end + + def time(impl, x) + GC.start + t0 = Minitest.clock_time + impl.call(x) + Minitest.clock_time - t0 end # run(hash) diff --git a/test/pbench/pbench_suite.rb b/test/pbench/pbench_suite.rb index 663f744..62415cc 100644 --- a/test/pbench/pbench_suite.rb +++ b/test/pbench/pbench_suite.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'pbench' # PBENCHES[:""] = { From 6e207514830a09c35a12adec20425148d9a109f0 Mon Sep 17 00:00:00 2001 From: "Daniel P. Clark" <6ftdan@gmail.com> Date: Wed, 7 Oct 2026 05:01:15 +0000 Subject: [PATCH 06/10] Describe the benchmark method for Ruby 3 in the README Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CXvfgWzuq28ewqT3AyhX4Z --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c2cf848..6fdcc99 100644 --- a/README.md +++ b/README.md @@ -216,10 +216,15 @@ performance seems to vary based on CPU cache on possibly 64bit/32bit system envi ## Benchmarks The "Time Shaved Off" figures come from the project's Pinch-bench (`rake pbench`, in -`test/pbench`): each method runs against Ruby's own implementation, and the result is how much -less time FasterPath took. They are the median of 3 runs of `LONG_RUN=10 rake pbench` (500,000 -calls of each method, 5 times) on Linux x86_64 with Ruby 2.7.8 and 2.5.7, built with Rust 1.97. -Expect a few points of difference from run to run. +`test/pbench`): each method runs against Ruby's own implementation, alternating with it round by +round, and the result is how much less time FasterPath's best round took than Ruby's best round. +They are the median of 3 runs of `LONG_RUN=10 rake pbench` (500,000 calls of each method, 5 +rounds) on Linux x86_64 with Ruby 3.4.6 and 3.2.9, built with Rust 1.97. Expect a few points of +difference from run to run. + +The figures are smaller than they were on Ruby 2 for the cheapest methods, `absolute?` and +`relative?` above all: Ruby 3's own `Pathname#absolute?` is a single regexp match, where Ruby 2's +walked the path with `chop_basename`, so there is less left to shave off. `rake pbench` is also the performance regression test: each method has a floor in `test/pbench/pbench_suite.rb` (about half of the improvement in the table above) and the task @@ -230,8 +235,10 @@ enforced. ### Before and after the Rutie 0.10 upgrade -Measured the same way, alternating runs of the code before the upgrade (built with Rutie 0.6) -and after it. Higher is better; the change is in percentage points. +The last figures on Ruby 2, from the Rutie 0.10 upgrade (faster_path 0.4.0): Ruby 2.7.8 and +2.5.7, alternating runs of the code before the upgrade (built with Rutie 0.6) and after it, with +the benchmark then measuring the average of 5 rounds run one implementation after the other. +Higher is better; the change is in percentage points. |Method|Ruby 2.7 before|Ruby 2.7 after|Change|Ruby 2.5 before|Ruby 2.5 after|Change| |---|:---:|:---:|:---:|:---:|:---:|:---:| From 5b0277bd95569c2f368158e0eae3675d68b22c2d Mon Sep 17 00:00:00 2001 From: "Daniel P. Clark" <6ftdan@gmail.com> Date: Wed, 7 Oct 2026 05:03:53 +0000 Subject: [PATCH 07/10] Return "." from File.dirname("", 0) like Ruby `File.dirname("", 0)` is "." in Ruby, as with any level, where the new level support returned the empty path unchanged. The test that compared the two passed locally only because it ran with the monkeypatches on, comparing FasterPath with itself; it now checks fixed expectations taken from Ruby 3.3, so it means the same with and without them. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CXvfgWzuq28ewqT3AyhX4Z --- src/pathname.rs | 4 ++++ test/dirname_test.rb | 19 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/pathname.rs b/src/pathname.rs index 2a7e741..39950ed 100644 --- a/src/pathname.rs +++ b/src/pathname.rs @@ -171,6 +171,10 @@ pub fn pn_dirname(arguments: &[AnyObject]) -> RubyResult { } result = Cow::Owned(parent.into_owned()); } + if result.is_empty() { + // `File.dirname("", 0)` is "." like `File.dirname("")` + result = Cow::Borrowed(b"."); + } Ok(path.to_ruby(&result).into()) } diff --git a/test/dirname_test.rb b/test/dirname_test.rb index 5177728..6cf69ab 100644 --- a/test/dirname_test.rb +++ b/test/dirname_test.rb @@ -22,10 +22,23 @@ def test_it_removes_level_trailing_components assert_equal(String, FasterPath.dirname('/home/jason', 0).class) end + # What File.dirname(path, level) returns on Ruby 3.3 for levels 0 to 4 + LEVELS = { + '/home/jason/poot.txt' => ['/home/jason/poot.txt', '/home/jason', '/home', '/', '/'], + 'a/b/c' => ['a/b/c', 'a/b', 'a', '.', '.'], + '/a' => ['/a', '/', '/', '/', '/'], + 'a' => ['a', '.', '.', '.', '.'], + '' => ['.', '.', '.', '.', '.'], + '/' => ['/', '/', '/', '/', '/'], + './b/./' => ['./b/./', './b', '.', '.', '.'], + '/foo/../.' => ['/foo/../.', '/foo/..', '/foo', '/', '/'], + 'a//b//c//' => ['a//b//c//', 'a//b', 'a', '.', '.'] + }.freeze + def test_level_agrees_with_file_dirname - ['/home/jason/poot.txt', 'a/b/c', '/a', 'a', '', '/', './b/./', '/foo/../.', 'a//b//c//'].each do |path| - 0.upto(4) do |level| - assert_equal(File.dirname(path, level), FasterPath.dirname(path, level), "#{path.inspect}, #{level}") + LEVELS.each do |path, expected| + expected.each_with_index do |dirname, level| + assert_equal(dirname, FasterPath.dirname(path, level), "#{path.inspect}, #{level}") end end end From b6b147701bdd29f8c6f5375c8601e013c55d79c5 Mon Sep 17 00:00:00 2001 From: "Daniel P. Clark" <6ftdan@gmail.com> Date: Wed, 7 Oct 2026 05:13:13 +0000 Subject: [PATCH 08/10] Re-measure the benchmarks on Ruby 3.4 and 3.2 and set the floors The README tables now show Ruby 3.4.6 and 3.2.9 (the median of 3 runs of `LONG_RUN=10 rake pbench`, measured by the best of interleaved rounds), in place of the Ruby 2.7 and 2.5 figures, which stay in the Rutie 0.10 section as history. The floors follow the documented rule again: about half of the lower of the two figures, rounded down to a multiple of 5, with the filesystem-bound methods kept at 0 and the _compat methods without one. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CXvfgWzuq28ewqT3AyhX4Z --- README.md | 42 ++++++++++++++++++------------------- test/pbench/pbench_suite.rb | 24 ++++++++++----------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 6fdcc99..5212a3d 100644 --- a/README.md +++ b/README.md @@ -159,25 +159,25 @@ require "faster_path" Current methods implemented: -|FasterPath Rust Implementation|Ruby Implementation|Time Shaved Off (Ruby 2.7)|Time Shaved Off (Ruby 2.5)| +|FasterPath Rust Implementation|Ruby Implementation|Time Shaved Off (Ruby 3.4)|Time Shaved Off (Ruby 3.2)| |---|---|:---:|:---:| -| `FasterPath.absolute?` | `Pathname#absolute?` | 95.7% | 96.7% | -| `FasterPath.add_trailing_separator` | `Pathname#add_trailing_separator` | 85.4% | 83.0% | -| `FasterPath.basename` | `File.basename` | 42.4% | 39.1% | -| `FasterPath.children` | `Pathname#children` | 54.7% | 47.8% | -| `FasterPath.chop_basename` | `Pathname#chop_basename` | 70.4% | 75.1% | -| `FasterPath.cleanpath_aggressive` | `Pathname#cleanpath_aggressive` | 90.3% | 90.6% | -| `FasterPath.cleanpath_conservative` | `Pathname#cleanpath_conservative` | 89.6% | 91.6% | -| `FasterPath.del_trailing_separator` | `Pathname#del_trailing_separator` | 85.3% | 87.6% | -| `FasterPath.directory?` | `Pathname#directory?` | 34.3% | 37.5% | -| `FasterPath.dirname` | `File.dirname` | 56.8% | 54.7% | -| `FasterPath.entries` | `Pathname#entries` | 38.0% | 33.9% | -| `FasterPath.extname` | `File.extname` | 74.0% | 73.1% | -| `FasterPath.has_trailing_separator?` | `Pathname#has_trailing_separator` | 88.5% | 89.1% | -| `FasterPath.join` | `Pathname#join` | 88.1% | 90.3% | -| `FasterPath.plus` | `Pathname#plus` | 91.7% | 93.4% | -| `FasterPath.relative?` | `Pathname#relative?` | 92.8% | 95.2% | -| `FasterPath.relative_path_from` | `Pathname#relative_path_from` | 92.5% | 93.6% | +| `FasterPath.absolute?` | `Pathname#absolute?` | 55.1% | 41.7% | +| `FasterPath.add_trailing_separator` | `Pathname#add_trailing_separator` | 87.7% | 86.2% | +| `FasterPath.basename` | `File.basename` | 28.0% | 28.1% | +| `FasterPath.children` | `Pathname#children` | 64.8% | 58.0% | +| `FasterPath.chop_basename` | `Pathname#chop_basename` | 73.2% | 72.9% | +| `FasterPath.cleanpath_aggressive` | `Pathname#cleanpath_aggressive` | 92.7% | 92.5% | +| `FasterPath.cleanpath_conservative` | `Pathname#cleanpath_conservative` | 91.9% | 92.1% | +| `FasterPath.del_trailing_separator` | `Pathname#del_trailing_separator` | 89.4% | 87.9% | +| `FasterPath.directory?` | `Pathname#directory?` | 40.3% | 41.0% | +| `FasterPath.dirname` | `File.dirname` | 43.5% | 43.6% | +| `FasterPath.entries` | `Pathname#entries` | 57.8% | 46.7% | +| `FasterPath.extname` | `File.extname` | 69.5% | 68.3% | +| `FasterPath.has_trailing_separator?` | `Pathname#has_trailing_separator` | 93.6% | 91.7% | +| `FasterPath.join` | `Pathname#join` | 89.0% | 88.3% | +| `FasterPath.plus` | `Pathname#plus` | 95.4% | 94.2% | +| `FasterPath.relative?` | `Pathname#relative?` | 66.2% | 61.4% | +| `FasterPath.relative_path_from` | `Pathname#relative_path_from` | 95.0% | 94.7% | See [Benchmarks](#benchmarks) for how these are measured. @@ -205,10 +205,10 @@ FasterPath.sledgehammer_everything! These will **not** be included by default in monkey-patches. To try them with monkeypatching use the environment flag of `WITH_REGRESSION`. These methods are here to be improved upon. -|FasterPath Implementation|Ruby Implementation|Time Shaved Off (Ruby 2.7)|Time Shaved Off (Ruby 2.5)| +|FasterPath Implementation|Ruby Implementation|Time Shaved Off (Ruby 3.4)|Time Shaved Off (Ruby 3.2)| |---|---|:---:|:---:| -| `FasterPath.entries_compat` | `Pathname.entries` | 16.8% | 9.2% | -| `FasterPath.children_compat` | `Pathname.children` | 32.1% | 26.6% | +| `FasterPath.entries_compat` | `Pathname.entries` | 32.2% | 18.1% | +| `FasterPath.children_compat` | `Pathname.children` | 44.9% | 35.6% | It's been my observation (and some others) that the Rust implementation of the C code for `File` has similar results but performance seems to vary based on CPU cache on possibly 64bit/32bit system environments. These are not included by default when the monkey patch method `FasterPath.sledgehammer_everything!` is executed. diff --git a/test/pbench/pbench_suite.rb b/test/pbench/pbench_suite.rb index 62415cc..432827d 100644 --- a/test/pbench/pbench_suite.rb +++ b/test/pbench/pbench_suite.rb @@ -62,7 +62,7 @@ end, } PBENCHES[:add_trailing_separator] = { - min: 50, + min: 40, new: lambda do |x| x.times do FasterPath.add_trailing_separator('/hello/world') @@ -77,7 +77,7 @@ end } PBENCHES[:basename] = { - min: 15, + min: 10, new: lambda do |x| x.times do FasterPath.basename("/hello/world") @@ -125,7 +125,7 @@ end } PBENCHES[:chop_basename] = { - min: 50, + min: 35, new: lambda do |x| x.times do FasterPath.chop_basename "/hello/world.txt" @@ -144,7 +144,7 @@ PATHNAME_CA1 = Pathname.new('/../.././../a').freeze PATHNAME_CA2 = Pathname.new('a/b/../../../../c/../d').freeze PBENCHES[:cleanpath_aggressive] = { - min: 80, + min: 45, new: lambda do |x| x.times do Pathname.new(FasterPath.cleanpath_aggressive '/../.././../a') @@ -159,7 +159,7 @@ end } PBENCHES[:cleanpath_conservative] = { - min: 80, + min: 45, new: lambda do |x| x.times do Pathname.new(FasterPath.cleanpath_conservative '/../.././../a') @@ -174,7 +174,7 @@ end } PBENCHES[:del_trailing_separator] = { - min: 70, + min: 40, new: lambda do |x| x.times do FasterPath.del_trailing_separator('/hello/world') @@ -204,7 +204,7 @@ end } PBENCHES[:dirname] = { - min: 30, + min: 20, new: lambda do |x| x.times do FasterPath.dirname "/really/long/path/name/which/ruby/doesnt/like/bar.txt" @@ -250,7 +250,7 @@ end } PBENCHES[:extname] = { - min: 50, + min: 30, new: lambda do |x| x.times do FasterPath.extname('verylongfilename_verylongfilename.rb') @@ -267,7 +267,7 @@ end } PBENCHES[:"has_trailing_separator?"] = { - min: 75, + min: 45, new: lambda do |x| x.times do FasterPath.has_trailing_separator? '////a//aaa/a//a/aaa////' @@ -282,7 +282,7 @@ end } PBENCHES[:join] = { - min: 70, + min: 40, new: lambda do |x| x.times do FasterPath.join('a', 'b') @@ -299,7 +299,7 @@ end } PBENCHES[:plus] = { - min: 80, + min: 45, new: lambda do |x| x.times do FasterPath.plus('a', 'b') @@ -333,7 +333,7 @@ PATHNAME_AB = Pathname("/a/b") PATHNAME_ABCD = Pathname("/a/b/c/d") PBENCHES[:relative_path_from] = { - min: 80, + min: 45, new: lambda do |x| x.times do FasterPath.relative_path_from "/a/b/c/d", "/a/b" From 2464ce2c4bcdb47a9ddf38726047aeba8abb8a50 Mon Sep 17 00:00:00 2001 From: "Daniel P. Clark" <6ftdan@gmail.com> Date: Wed, 7 Oct 2026 05:29:32 +0000 Subject: [PATCH 09/10] Drop the --long-run flag that rake never passed through `rake pbench --long-run` fails with "invalid option: --long-run" before the Rakefile sees ARGV, so the branch that set LONG_RUN from it could never run. LONG_RUN itself, which the README documents, is the way to run longer benchmarks. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CXvfgWzuq28ewqT3AyhX4Z --- Rakefile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Rakefile b/Rakefile index 1220540..67a07a4 100644 --- a/Rakefile +++ b/Rakefile @@ -92,10 +92,8 @@ Rake::TestTask.new(bench: :build_lib) do |t| t.pattern = 'test/**/*_benchmark.rb' end +# LONG_RUN=<1..30> multiplies the calls per round, see test/pbench/pbench.rb Rake::TestTask.new(pbench: :build_lib) do |t| - if ARGV.last == '--long-run' - ENV['LONG_RUN'] = '10' - end t.libs = %w[lib test test/pbench] t.pattern = 'test/pbench/pbench_suite.rb' end From 8c28ec7295181e10af5039f9c3cb906ce44742e8 Mon Sep 17 00:00:00 2001 From: "Daniel P. Clark" <6ftdan@gmail.com> Date: Wed, 7 Oct 2026 06:14:13 +0000 Subject: [PATCH 10/10] Benchmark Ruby 3.4, 3.3 and 3.2 more deeply for the 0.5.0 tables The README tables now have a column for each supported Ruby, measured with `LONG_RUN=30` (5 rounds of 300,000 calls per method, three times the previous depth) on a dedicated CPU core, three runs each; the runs agree within a few points except for the filesystem-bound methods. The floors are about half of each method's lowest figure across the three. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CXvfgWzuq28ewqT3AyhX4Z --- README.md | 55 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 5212a3d..7ea200e 100644 --- a/README.md +++ b/README.md @@ -159,25 +159,25 @@ require "faster_path" Current methods implemented: -|FasterPath Rust Implementation|Ruby Implementation|Time Shaved Off (Ruby 3.4)|Time Shaved Off (Ruby 3.2)| -|---|---|:---:|:---:| -| `FasterPath.absolute?` | `Pathname#absolute?` | 55.1% | 41.7% | -| `FasterPath.add_trailing_separator` | `Pathname#add_trailing_separator` | 87.7% | 86.2% | -| `FasterPath.basename` | `File.basename` | 28.0% | 28.1% | -| `FasterPath.children` | `Pathname#children` | 64.8% | 58.0% | -| `FasterPath.chop_basename` | `Pathname#chop_basename` | 73.2% | 72.9% | -| `FasterPath.cleanpath_aggressive` | `Pathname#cleanpath_aggressive` | 92.7% | 92.5% | -| `FasterPath.cleanpath_conservative` | `Pathname#cleanpath_conservative` | 91.9% | 92.1% | -| `FasterPath.del_trailing_separator` | `Pathname#del_trailing_separator` | 89.4% | 87.9% | -| `FasterPath.directory?` | `Pathname#directory?` | 40.3% | 41.0% | -| `FasterPath.dirname` | `File.dirname` | 43.5% | 43.6% | -| `FasterPath.entries` | `Pathname#entries` | 57.8% | 46.7% | -| `FasterPath.extname` | `File.extname` | 69.5% | 68.3% | -| `FasterPath.has_trailing_separator?` | `Pathname#has_trailing_separator` | 93.6% | 91.7% | -| `FasterPath.join` | `Pathname#join` | 89.0% | 88.3% | -| `FasterPath.plus` | `Pathname#plus` | 95.4% | 94.2% | -| `FasterPath.relative?` | `Pathname#relative?` | 66.2% | 61.4% | -| `FasterPath.relative_path_from` | `Pathname#relative_path_from` | 95.0% | 94.7% | +|FasterPath Rust Implementation|Ruby Implementation|Time Shaved Off (Ruby 3.4)|Time Shaved Off (Ruby 3.3)|Time Shaved Off (Ruby 3.2)| +|---|---|:---:|:---:|:---:| +| `FasterPath.absolute?` | `Pathname#absolute?` | 55.5% | 53.2% | 43.1% | +| `FasterPath.add_trailing_separator` | `Pathname#add_trailing_separator` | 87.4% | 85.3% | 85.8% | +| `FasterPath.basename` | `File.basename` | 30.7% | 22.2% | 27.5% | +| `FasterPath.children` | `Pathname#children` | 65.0% | 55.1% | 55.4% | +| `FasterPath.chop_basename` | `Pathname#chop_basename` | 75.3% | 73.5% | 72.8% | +| `FasterPath.cleanpath_aggressive` | `Pathname#cleanpath_aggressive` | 92.4% | 92.2% | 92.4% | +| `FasterPath.cleanpath_conservative` | `Pathname#cleanpath_conservative` | 91.4% | 91.3% | 91.8% | +| `FasterPath.del_trailing_separator` | `Pathname#del_trailing_separator` | 89.1% | 87.9% | 87.8% | +| `FasterPath.directory?` | `Pathname#directory?` | 41.5% | 39.9% | 44.2% | +| `FasterPath.dirname` | `File.dirname` | 52.0% | 40.3% | 40.8% | +| `FasterPath.entries` | `Pathname#entries` | 57.8% | 42.4% | 45.9% | +| `FasterPath.extname` | `File.extname` | 69.4% | 66.7% | 69.2% | +| `FasterPath.has_trailing_separator?` | `Pathname#has_trailing_separator` | 93.4% | 92.8% | 91.7% | +| `FasterPath.join` | `Pathname#join` | 89.1% | 89.0% | 88.4% | +| `FasterPath.plus` | `Pathname#plus` | 95.5% | 94.7% | 94.5% | +| `FasterPath.relative?` | `Pathname#relative?` | 66.9% | 66.3% | 60.4% | +| `FasterPath.relative_path_from` | `Pathname#relative_path_from` | 94.4% | 94.6% | 94.2% | See [Benchmarks](#benchmarks) for how these are measured. @@ -205,10 +205,10 @@ FasterPath.sledgehammer_everything! These will **not** be included by default in monkey-patches. To try them with monkeypatching use the environment flag of `WITH_REGRESSION`. These methods are here to be improved upon. -|FasterPath Implementation|Ruby Implementation|Time Shaved Off (Ruby 3.4)|Time Shaved Off (Ruby 3.2)| -|---|---|:---:|:---:| -| `FasterPath.entries_compat` | `Pathname.entries` | 32.2% | 18.1% | -| `FasterPath.children_compat` | `Pathname.children` | 44.9% | 35.6% | +|FasterPath Implementation|Ruby Implementation|Time Shaved Off (Ruby 3.4)|Time Shaved Off (Ruby 3.3)|Time Shaved Off (Ruby 3.2)| +|---|---|:---:|:---:|:---:| +| `FasterPath.entries_compat` | `Pathname.entries` | 35.7% | 22.0% | 15.9% | +| `FasterPath.children_compat` | `Pathname.children` | 46.7% | 33.6% | 32.3% | It's been my observation (and some others) that the Rust implementation of the C code for `File` has similar results but performance seems to vary based on CPU cache on possibly 64bit/32bit system environments. These are not included by default when the monkey patch method `FasterPath.sledgehammer_everything!` is executed. @@ -218,16 +218,17 @@ performance seems to vary based on CPU cache on possibly 64bit/32bit system envi The "Time Shaved Off" figures come from the project's Pinch-bench (`rake pbench`, in `test/pbench`): each method runs against Ruby's own implementation, alternating with it round by round, and the result is how much less time FasterPath's best round took than Ruby's best round. -They are the median of 3 runs of `LONG_RUN=10 rake pbench` (500,000 calls of each method, 5 -rounds) on Linux x86_64 with Ruby 3.4.6 and 3.2.9, built with Rust 1.97. Expect a few points of -difference from run to run. +They are the median of 3 runs of `LONG_RUN=30 rake pbench` (5 rounds of 300,000 calls of each +method) on Linux x86_64, each run pinned to one CPU core, with Ruby 3.4.6, 3.3.6 and 3.2.9, built +with Rust 1.97. The three runs agree within a few points for most methods; the ones that touch +the filesystem (`children`, `directory?`, `entries`) vary the most. The figures are smaller than they were on Ruby 2 for the cheapest methods, `absolute?` and `relative?` above all: Ruby 3's own `Pathname#absolute?` is a single regexp match, where Ruby 2's walked the path with `chop_basename`, so there is less left to shave off. `rake pbench` is also the performance regression test: each method has a floor in -`test/pbench/pbench_suite.rb` (about half of the improvement in the table above) and the task +`test/pbench/pbench_suite.rb` (about half of its lowest figure in the table above) and the task fails when a method measures below it, so a change that makes a method slower shows up in `rake test`. On Windows, where `join` is currently slower than Ruby's ([#185](https://github.com/danielpclark/faster_path/issues/185)), the floors are reported but not