Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ jobs:
bundler-cache: true

- name: Run tests
run: bundle exec rake test
run: bundle exec rake test LAZY_LOAD=1
10 changes: 10 additions & 0 deletions benchmark/load.rb
Original file line number Diff line number Diff line change
@@ -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('lazyload') { system('LAZY_LOAD=1 ruby load_faker.rb') }

x.compare!(order: :baseline)
end
8 changes: 8 additions & 0 deletions benchmark/load_faker.rb
Original file line number Diff line number Diff line change
@@ -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')
17 changes: 9 additions & 8 deletions doc/internet/http.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Faker::Internet::HTTP
# Faker::HTTP

Available since version next.

# Keyword arguments: group

```ruby
Faker::Internet::HTTP.status_code #=> 418
Faker::Internet::HTTP.status_code(group: :information) #=> 102
Faker::Internet::HTTP.status_code(group: :successful) #=> 200
Faker::Internet::HTTP.status_code(group: :redirect) #=> 306
Faker::Internet::HTTP.status_code(group: :client_error) #=> 451
Faker::Internet::HTTP.status_code(group: :server_error) #=> 502
```
Faker::HTTP.status_code #=> 418
Faker::HTTP.status_code(group: :information) #=> 102
Faker::HTTP.status_code(group: :successful) #=> 200
Faker::HTTP.status_code(group: :redirect) #=> 306
Faker::HTTP.status_code(group: :client_error) #=> 451
Faker::HTTP.status_code(group: :server_error) #=> 502
```
14 changes: 7 additions & 7 deletions doc/music/music.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
Available since version 1.6.4.

```ruby
Faker::Music.key #=> "C"
Faker::Song.key #=> "C"

Faker::Music.chord #=> "Amaj7"
Faker::Song.chord #=> "Amaj7"

Faker::Music.instrument #=> "Ukelele"
Faker::Song.instrument #=> "Ukelele"

Faker::Music.band #=> "The Beatles"
Faker::Song.band #=> "The Beatles"

Faker::Music.album #=> "Sgt. Pepper's Lonely Hearts Club"
Faker::Song.album #=> "Sgt. Pepper's Lonely Hearts Club"

Faker::Music.genre #=> "Rock"
Faker::Song.genre #=> "Rock"

Faker::Music.mambo_no_5 #=> "Monica"
Faker::Song.mambo_no_5 #=> "Monica"
```
103 changes: 103 additions & 0 deletions experiments/lazy_loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Lazy load experiment results

Branch: sb-ta/lazy-load-experiment
Date: February 10th, 2026
Owner(s): Stefanni Brasil and Thiago Araujo

## Impact

Using `const_missing` to lazy load generators was an idea from talking with Jeremy Evans, who kindly responded our questions about improving faker's performance.

### Changes needed

#### Namespace changes

A few generators had namespaces that didn't match their file name, and most of them were manageable, but two needed to be renamed:

- `Faker::Music` -> `Faker::Song`. `Faker::Music` is a nested namespace.
- `Faker::Internet::HTTP` -> `Faker::HTTP`. `Faker::Internet` is another generator.

Considering that only two generators would be renamed, it wouldn't be a huge burden for users to use this approach.

#### 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

- no additional dependencies needed
- code is extremely faster
- after changing these two generators, which would be breaking changes, we can enable this as an opt-in configuration

## Results

profiler:

[bundle exec vernier run -- ruby -e "require 'faker'" LAZY_LOAD=1](https://share.firefox.dev/3ZuCP55)
[bundle exec vernier run --interval 100 --allocation-interval 10 -- ruby -e "require 'faker'; Faker::Internet.email" LAZY_LOAD=1](https://share.firefox.dev/4601PoA)

benchmarks (Machine specs: Apple M1 Pro 16GB memory on MacOS Sequoia 15.7.3.)

```sh
benchmark % ruby require.rb
took 250.0249999575317ms to load
```

```sh
benchmark % ruby load.rb
ruby 3.3.10 (2025-10-23 revision 343ea05002) [arm64-darwin24]
Warming up --------------------------------------
require 1.000 i/100ms
lazyload 1.000 i/100ms
Calculating -------------------------------------
require 5.874 (± 0.0%) i/s (170.25 ms/i) - 30.000 in 5.115652s
lazyload 12.207 (± 8.2%) i/s (81.92 ms/i) - 61.000 in 5.007059s

Comparison:
require: 5.9 i/s
lazyload: 12.2 i/s - 2.08x faster
```

## Artifacts

### Scripts

Constants were registered using this script:

```ruby
# lazy_load.rb

CATEGORIES = {
Blockchain: 'blockchain',
Books: 'books',
Creature: 'creature',
Default: 'default',
Fantasy: 'fantasy',
Games: 'games',
JapaneseMedia: 'japanese_media',
Locations: 'locations',
Movies: 'movies',
Music: 'music',
Quotes: 'quotes',
Religion: 'religion',
Sports: 'sports',
Travel: 'travel',
TvShows: 'tv_shows'
}.freeze

def template(key)
"# frozen_string_literal: true

module Faker
class #{key}
if ENV['LAZY_LOAD'] == '1'
Faker.lazy_load(self)
end
end
end"
end

CATEGORIES.each do |key, value|
File.write(File.join('lib', 'faker', "#{value}.rb"), template(key))
end
```
40 changes: 38 additions & 2 deletions lib/faker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,43 @@ def disable_enforce_available_locales
end
end
end

if ENV['LAZY_LOAD'] == '1'
def self.load_path(*constants)
constants.map do |class_name|
class_name
.to_s
.gsub('::', '/')
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
.tr('-', '_')
.downcase
end.join('/')
end

def self.lazy_load(klass)
def klass.const_missing(class_name)
load_path = case class_name
when :DnD
Faker.load_path('faker/games/dnd')
else
Faker.load_path(name, class_name)
end

begin
require(load_path)
rescue LoadError
require(load_path.gsub('faker/', 'faker/default/'))
end

const_get(class_name)
end
end

lazy_load(self)
end
end

# require faker objects
Dir.glob(File.join(mydir, 'faker', '/**/*.rb')).each { |file| require file }
if ENV['LAZY_LOAD'] != '1'
Dir.glob(File.join(mydir, 'faker', '/**/*.rb')).each { |file| require file }
end
9 changes: 9 additions & 0 deletions lib/faker/blockchain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

module Faker
class Blockchain
if ENV['LAZY_LOAD'] == '1'
Faker.lazy_load(self)
end
end
end
9 changes: 9 additions & 0 deletions lib/faker/books.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

module Faker
class Books
if ENV['LAZY_LOAD'] == '1'
Faker.lazy_load(self)
end
end
end
9 changes: 9 additions & 0 deletions lib/faker/creature.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

module Faker
class Creature
if ENV['LAZY_LOAD'] == '1'
Faker.lazy_load(self)
end
end
end
9 changes: 9 additions & 0 deletions lib/faker/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

module Faker
class Default
if ENV['LAZY_LOAD'] == '1'
Faker.lazy_load(self)
end
end
end
File renamed without changes.
File renamed without changes.
6 changes: 3 additions & 3 deletions lib/faker/default/html.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class << self
# @faker.version 3.2.1
def heading
level = rand(1..6)
"<h#{level}>#{Lorem.word.capitalize}</h#{level}>"
"<h#{level}>#{Faker::Lorem.word.capitalize}</h#{level}>"
end

##
Expand Down Expand Up @@ -97,7 +97,7 @@ def unordered_list
#
# @faker.version 3.2.1
def code
"<code>#{Lorem.sentence(word_count: 1)}</code>"
"<code>#{Faker::Lorem.sentence(word_count: 1)}</code>"
end

##
Expand Down Expand Up @@ -165,7 +165,7 @@ def link(rel: 'stylesheet')
# Faker::HTML.element(tag: 'div', content: "This is a div with XSS attributes.", attributes: {class: 'xss', onclick: "alert('XSS')"}) #=> "<div class=\"xss\" onclick=\"alert('XSS')\">This is a div with XSS attributes.</div>"
#
# @faker.version 3.2.1
def element(tag: 'div', content: Lorem.sentence(word_count: 3), attributes: { class: Lorem.word, onclick: "#{Lorem.word}()" })
def element(tag: 'div', content: Faker::Lorem.sentence(word_count: 3), attributes: { class: Faker::Lorem.word, onclick: "#{Faker::Lorem.word}()" })
attribute_string = attributes.map { |key, value| "#{key}=\"#{value}\"" }.join(' ')
"<#{tag} #{attribute_string}>#{content}</#{tag}>"
end
Expand Down
46 changes: 46 additions & 0 deletions lib/faker/default/http.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module Faker
class HTTP < Base
STATUS_CODES = {
information: [100, 101, 102, 103],
successful: [200, 201, 202, 203, 204, 205, 206, 207, 208, 226],
redirect: [300, 301, 302, 303, 304, 305, 306, 307, 308],
client_error: [400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412,
413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428,
429, 431, 451],
server_error: [500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511]
}.freeze

STATUS_CODES_GROUPS = STATUS_CODES.keys.freeze

class << self
##
# Produces an HTTP status code
#
# @return [Integer]
#
# @example
# Faker::HTTP.status_code #=> 418
# @example
# Faker::HTTP.status_code(group: :information) #=> 102
# @example
# Faker::HTTP.status_code(group: :successful) #=> 200
# @example
# Faker::HTTP.status_code(group: :redirect) #=> 306
# @example
# Faker::HTTP.status_code(group: :client_error) #=> 451
# @example
# Faker::HTTP.status_code(group: :server_error) #=> 502
#
# @faker.version 2.13.0
def status_code(group: nil)
return STATUS_CODES[STATUS_CODES_GROUPS.sample].sample unless group

raise ArgumentError, 'Invalid HTTP status code group' unless STATUS_CODES_GROUPS.include?(group)

STATUS_CODES[group].sample
end
end
end
end
6 changes: 3 additions & 3 deletions lib/faker/default/internet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ def username(specifier: nil, separators: %w[. _])
end

sample([
Char.prepare(Name.first_name),
[Name.first_name, Name.last_name].map do |name|
Char.prepare(Faker::Name.first_name),
[Faker::Name.first_name, Faker::Name.last_name].map do |name|
Char.prepare(name)
end.join(sample(separators))
])
Expand Down Expand Up @@ -235,7 +235,7 @@ def fix_umlauts(string: '')
# @example
# Faker::Internet.domain_word #=> "senger"
def domain_word
with_locale(:en) { Char.prepare(Company.name.split.first) }
with_locale(:en) { Char.prepare(Faker::Company.name.split.first) }
end

## Returns the domain suffix e.g. com, org, co, biz, info etc.
Expand Down
Loading