diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 283959e203..dc22715ae7 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -58,4 +58,4 @@ jobs: bundler-cache: true - name: Run tests - run: bundle exec rake test + run: AUTOLOAD=1 bundle exec rake test diff --git a/.rubocop.yml b/.rubocop.yml index 7fffa274c6..ac2f959625 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -37,6 +37,9 @@ Metrics/CyclomaticComplexity: Enabled: false Max: 6 +Lint/EmptyClass: + AllowComments: true + Metrics/MethodLength: Description: This cop checks if the length of a method exceeds some maximum value. Enabled: false diff --git a/Gemfile.lock b/Gemfile.lock index 275b69d382..51e150a9d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,7 @@ PATH specs: faker (3.6.0) i18n (>= 1.8.11, < 2) + zeitwerk (~> 2.7.3) GEM remote: https://rubygems.org/ @@ -93,6 +94,7 @@ GEM unicode-emoji (~> 4.1) unicode-emoji (4.2.0) yard (0.9.38) + zeitwerk (2.7.4) PLATFORMS arm64-darwin diff --git a/benchmark/load.rb b/benchmark/load.rb new file mode 100644 index 0000000000..7e828246ca --- /dev/null +++ b/benchmark/load.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'benchmark/ips' + +Benchmark.ips do |x| + x.report('require') { system('ruby load_faker.rb') } + x.report('autoload') { system('AUTOLOAD=1 ruby load_faker.rb') } + + x.compare!(order: :baseline) +end diff --git a/benchmark/load_faker.rb b/benchmark/load_faker.rb new file mode 100644 index 0000000000..3d1c146253 --- /dev/null +++ b/benchmark/load_faker.rb @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +if defined?(Faker) + raise 'fake is already defined...' +end + +load('/Users/stefannibrasil/projects/faker/lib/faker.rb') diff --git a/experiments/autoload.md b/experiments/autoload.md new file mode 100644 index 0000000000..4613fa825d --- /dev/null +++ b/experiments/autoload.md @@ -0,0 +1,59 @@ +# Autoload with zeitwerk experiment results + +Branch: [sb-autoload-zeitwerk-experiment-3207](https://github.com/faker-ruby/faker/compare/sb-autoload-zeitwerk-experiment-3207?expand=1) +Date: February 17th, 2026 +Owner(s): Stefanni Brasil and Thiago Araujo + +## Impact + +We want to compare improving faker's performance by lazy loading the generators or autoloading with Zeitwerk. This document shows the benchmarks, and other changes needed to configure Zeitwerk. + +With the experiments documented, we can assess the pros and cons of maintainability, fewer breaking changes, easier adoption factors as guiding points for choosing the strategy we will move forward. + +### Changes needed + +- load and require generators in the correct order + +Similarly to the [lazy load experiment](./lazy_load.md), it would be required to load `faker/music` and `faker/internet` first, before the nested namespaces such as `Faker::Music::BossaNova`, as they inherit from class `Music`, for example. + +- added a runtime dependency for the library + +- Zeitwerk 2.7 requires Ruby >= 3.2. This isn't a deal breaker because we will remove EOL Ruby 3.1 soon, but it would require releasing that version separately. + +- our other goal besides improving performance, is allowing users to create their own Faker generators. Zeitwerk scans the file systems to setup the autoloads, so it would not setup autoloads for these external generators. + +#### File location changes + +To prevent other generators from erroring out due to namespace clashing, some generators have to be moved around (ex. `Faker::Quote` was moved from `/faker/quotes/quote` to `faker/default/quote`). Users can still use the generators as before, their namespaces didn't change. + +### Benefits + +- less code changes than lazy loading, and lots of customization options available (i.e., eager loading) +- code is extremely faster, but a bit slower than lazy loading +- we can enable this as an opt-in configuration + +## Results + +Machine specs: Apple M1 Pro 16GB memory on MacOS Sequoia 15.7.3. + +profiler: + +[AUTOLOAD=1 bundle exec vernier run -- ruby -e "require 'faker'"](https://share.firefox.dev/4aAJJee) +[bundle exec vernier run -- ruby -e "require 'faker'"](https://share.firefox.dev/4bWViih) + +benchmark: + +```sh +benchmark % ruby load.rb +ruby 3.3.10 (2025-10-23 revision 343ea05002) [arm64-darwin24] +Warming up -------------------------------------- + require 1.000 i/100ms + autoload 1.000 i/100ms +Calculating ------------------------------------- + require 6.026 (± 0.0%) i/s (165.96 ms/i) - 31.000 in 5.145463s + autoload 11.730 (± 0.0%) i/s (85.25 ms/i) - 59.000 in 5.032426s + +Comparison: + require: 6.0 i/s + autoload: 11.7 i/s - 1.95x faster +``` diff --git a/experiments/lazy_load.md b/experiments/lazy_load.md index 43009c7f30..4aba6c9217 100644 --- a/experiments/lazy_load.md +++ b/experiments/lazy_load.md @@ -1,6 +1,6 @@ # Lazy load experiment results -Branch: sb-ta/lazy-load-experiment +Branch: [sb-ta/lazy-load-experiment](https://github.com/faker-ruby/faker/compare/main...sb-ta/lazy-load-experiment) Date: February 10th, 2026 Owner(s): Stefanni Brasil and Thiago Araujo @@ -21,11 +21,12 @@ To prevent other generators from erroring out due to namespace clashing, some ge - no additional dependencies needed - code is extremely faster -- we can enable this as an opt-in configuration +- no breaking changes +- we can enable this as an opt-in configuration, but any other customization would need to be implemented ## Results -Machine specs: Apple M1 Pro 16GB memory on MacOS Sequoia 15.7.3.. +Machine specs: Apple M1 Pro 16GB memory on MacOS Sequoia 15.7.3. profiler: diff --git a/faker.gemspec b/faker.gemspec index 9455a5e572..034e0e3388 100644 --- a/faker.gemspec +++ b/faker.gemspec @@ -29,4 +29,5 @@ Gem::Specification.new do |spec| spec.metadata['rubygems_mfa_required'] = 'true' spec.add_dependency('i18n', '>= 1.8.11', '< 2') + spec.add_dependency 'zeitwerk', '~> 2.7.3' end diff --git a/lib/faker.rb b/lib/faker.rb index f44bc26e5b..564483e410 100644 --- a/lib/faker.rb +++ b/lib/faker.rb @@ -4,6 +4,7 @@ require 'psych' require 'i18n' +require 'zeitwerk' autoload(:OpenSSL, 'openssl') @@ -277,5 +278,28 @@ def disable_enforce_available_locales end end -# require faker objects -Dir.glob(File.join(mydir, 'faker', '/**/*.rb')).each { |file| require file } +if ENV['AUTOLOAD'] == '1' + loader = Zeitwerk::Loader.new + loader.tag = 'faker' + loader.push_dir(File.join(mydir, 'faker'), namespace: Faker) + loader.ignore("#{mydir}/faker/version.rb") + loader.inflector.inflect( + 'dnd' => 'DnD', + 'final_fantasy_xiv' => 'FinalFantasyXIV', + 'html' => 'HTML', + 'the_it_crowd' => 'TheITCrowd', + 'http' => 'HTTP' + ) + loader.collapse( + "#{mydir}/faker/default" + ) + loader.setup +end + +if ENV['AUTOLOAD'] != '1' + rb_files = [] + rb_files << File.join(mydir, 'faker', '*.rb') + rb_files << File.join(mydir, 'faker', '/**/*.rb') + + Dir.glob(rb_files).each { |file| require file } +end diff --git a/lib/faker/blockchain.rb b/lib/faker/blockchain.rb new file mode 100644 index 0000000000..3a06e540da --- /dev/null +++ b/lib/faker/blockchain.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class Blockchain + # reopening class + end +end diff --git a/lib/faker/books.rb b/lib/faker/books.rb new file mode 100644 index 0000000000..95465e956f --- /dev/null +++ b/lib/faker/books.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class Books + # reopening class + end +end diff --git a/lib/faker/creature.rb b/lib/faker/creature.rb new file mode 100644 index 0000000000..e05d2ed54e --- /dev/null +++ b/lib/faker/creature.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class Creature + # reopening class + end +end diff --git a/lib/faker/books/book.rb b/lib/faker/default/book.rb similarity index 100% rename from lib/faker/books/book.rb rename to lib/faker/default/book.rb diff --git a/lib/faker/games/game.rb b/lib/faker/default/game.rb similarity index 100% rename from lib/faker/games/game.rb rename to lib/faker/default/game.rb diff --git a/lib/faker/movies/movie.rb b/lib/faker/default/movie.rb similarity index 100% rename from lib/faker/movies/movie.rb rename to lib/faker/default/movie.rb diff --git a/lib/faker/quotes/quote.rb b/lib/faker/default/quote.rb similarity index 100% rename from lib/faker/quotes/quote.rb rename to lib/faker/default/quote.rb diff --git a/lib/faker/sports/sport.rb b/lib/faker/default/sport.rb similarity index 100% rename from lib/faker/sports/sport.rb rename to lib/faker/default/sport.rb diff --git a/lib/faker/fantasy.rb b/lib/faker/fantasy.rb new file mode 100644 index 0000000000..ac82223023 --- /dev/null +++ b/lib/faker/fantasy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class Fantasy + # reopening class + end +end diff --git a/lib/faker/games.rb b/lib/faker/games.rb new file mode 100644 index 0000000000..c6f882ae6a --- /dev/null +++ b/lib/faker/games.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class Games + # reopening class + end +end diff --git a/lib/faker/default/internet.rb b/lib/faker/internet.rb similarity index 100% rename from lib/faker/default/internet.rb rename to lib/faker/internet.rb diff --git a/lib/faker/default/internet_http.rb b/lib/faker/internet/http.rb similarity index 100% rename from lib/faker/default/internet_http.rb rename to lib/faker/internet/http.rb diff --git a/lib/faker/japanese_media.rb b/lib/faker/japanese_media.rb new file mode 100644 index 0000000000..00f2c54dcb --- /dev/null +++ b/lib/faker/japanese_media.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class JapaneseMedia + # reopening class + end +end diff --git a/lib/faker/locations.rb b/lib/faker/locations.rb new file mode 100644 index 0000000000..32c698f7c0 --- /dev/null +++ b/lib/faker/locations.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class Locations + # reopening class + end +end diff --git a/lib/faker/movies.rb b/lib/faker/movies.rb new file mode 100644 index 0000000000..f73fa1e665 --- /dev/null +++ b/lib/faker/movies.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class Movies + # reopening class + end +end diff --git a/lib/faker/music/music.rb b/lib/faker/music.rb similarity index 100% rename from lib/faker/music/music.rb rename to lib/faker/music.rb diff --git a/lib/faker/music/bossa_nova.rb b/lib/faker/music/bossa_nova.rb index 29dc8ea4f7..7d4f9b7f05 100644 --- a/lib/faker/music/bossa_nova.rb +++ b/lib/faker/music/bossa_nova.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'music' - module Faker class Music class BossaNova < Base diff --git a/lib/faker/music/grateful_dead.rb b/lib/faker/music/grateful_dead.rb index b7d0853e0d..8f1aa33df1 100644 --- a/lib/faker/music/grateful_dead.rb +++ b/lib/faker/music/grateful_dead.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'music' - module Faker class Music class GratefulDead < Base diff --git a/lib/faker/music/pearl_jam.rb b/lib/faker/music/pearl_jam.rb index c1c2ef59ae..78ebcbe6f8 100644 --- a/lib/faker/music/pearl_jam.rb +++ b/lib/faker/music/pearl_jam.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'music' - module Faker class Music class PearlJam < Base diff --git a/lib/faker/music/rush.rb b/lib/faker/music/rush.rb index 2af9596011..b002381c3d 100644 --- a/lib/faker/music/rush.rb +++ b/lib/faker/music/rush.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'music' - module Faker class Music class Rush < Base diff --git a/lib/faker/music/smashing_pumpkins.rb b/lib/faker/music/smashing_pumpkins.rb index 337f78f6aa..92df395fc4 100644 --- a/lib/faker/music/smashing_pumpkins.rb +++ b/lib/faker/music/smashing_pumpkins.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'music' - module Faker class Music class SmashingPumpkins < Base diff --git a/lib/faker/quotes.rb b/lib/faker/quotes.rb new file mode 100644 index 0000000000..a1cb98a9e0 --- /dev/null +++ b/lib/faker/quotes.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class Quotes + # reopening class + end +end diff --git a/lib/faker/religion.rb b/lib/faker/religion.rb new file mode 100644 index 0000000000..a082e8f6ad --- /dev/null +++ b/lib/faker/religion.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + module Religion + # reopening class + end +end diff --git a/lib/faker/sports.rb b/lib/faker/sports.rb new file mode 100644 index 0000000000..2299705be4 --- /dev/null +++ b/lib/faker/sports.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class Sports + # reopening class + end +end diff --git a/lib/faker/travel.rb b/lib/faker/travel.rb new file mode 100644 index 0000000000..468d156dd4 --- /dev/null +++ b/lib/faker/travel.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class Travel + # reopening class + end +end diff --git a/lib/faker/tv_shows.rb b/lib/faker/tv_shows.rb new file mode 100644 index 0000000000..fa207dcbf7 --- /dev/null +++ b/lib/faker/tv_shows.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Faker + class TvShows + # reopening class + end +end diff --git a/test/test_i18n_reload.rb b/test/test_i18n_reload.rb index b86fa47048..c37d12fffd 100644 --- a/test/test_i18n_reload.rb +++ b/test/test_i18n_reload.rb @@ -10,6 +10,7 @@ def test_faker_i18n code = <<-RUBY require 'bundler/inline' require 'test/unit' + require 'zeitwerk' gemfile do source 'https://rubygems.org'