Skip to content

Commit 23a08ab

Browse files
committed
Optimize Internet username, email, password, and base64
- username: pick the username shape (first-name-only vs first+last) before generating names, so the discarded variant is never built. - password: reuse frozen digit/special-char tables and grow the character bag in place with concat instead of += (which copies the whole array each time). - base64: reuse frozen urlsafe/standard character tables and append into a pre-sized String instead of building an intermediate array. - sanitize_email_local_part: replace invalid characters with a single gsub against a character-class regex instead of rebuilding the allowed-character array and scanning it per character. Benchmark (Ruby 3.4.9, arm64-darwin25, benchmark-ips): require 'benchmark/ips' require 'faker' Benchmark.ips do |x| x.config(warmup: 1, time: 2) x.report('username') { Faker::Internet.username } x.report('email') { Faker::Internet.email } x.report('password') { Faker::Internet.password } x.report('base64') { Faker::Internet.base64 } end Results: main: username 41.048k (+/-14.7%) i/s email 21.683k (+/- 0.7%) i/s password 394.792k (+/- 4.8%) i/s base64 183.509k (+/- 2.4%) i/s this commit: username 79.313k (+/-18.2%) i/s (~1.9x) email 37.246k (+/- 1.3%) i/s (~1.7x) password 451.683k (+/- 1.0%) i/s (~1.14x) base64 462.621k (+/- 0.5%) i/s (~2.5x)
1 parent 9b07803 commit 23a08ab

1 file changed

Lines changed: 37 additions & 30 deletions

File tree

lib/faker/internet.rb

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,23 @@ class Internet < Base
1818
[198..198, 18..19, 0..255, 1..255] # 198.18.0.0/15 - Used for benchmark testing of inter-network communications between subnets
1919
].each(&:freeze).freeze
2020

21+
# Characters that are not allowed in the local part of an email address,
22+
# i.e. anything other than alphanumerics and !#$%&'*+-/=?^_`{|}~.
23+
# @private
24+
INVALID_EMAIL_LOCAL_PART_CHARS = %r{[^0-9A-Za-z!#$%&'*+\-/=?^_`{|}~.]}
25+
26+
# Character sets used by #base64
27+
# @private
28+
BASE64_URLSAFE_CHARS = [*'0'..'9', *'A'..'Z', *'a'..'z', '-', '_'].freeze
29+
# @private
30+
BASE64_CHARS = [*'0'..'9', *'A'..'Z', *'a'..'z', '+', '/'].freeze
31+
32+
# Character sets used by #password
33+
# @private
34+
PASSWORD_DIGITS = Array('0'..'9').freeze
35+
# @private
36+
PASSWORD_SPECIAL_CHARS = %w[! @ # $ % ^ & *].freeze
37+
2138
class << self
2239
##
2340
# Returns the email address
@@ -96,12 +113,15 @@ def username(specifier: nil, separators: %w[. _])
96113
return result[0...specifier.max]
97114
end
98115

99-
sample([
100-
Char.prepare(Faker::Name.first_name),
101-
[Faker::Name.first_name, Faker::Name.last_name].map do |name|
102-
Char.prepare(name)
103-
end.join(sample(separators))
104-
])
116+
# Pick the shape of the username first so we only generate the
117+
# name parts we actually need.
118+
if rand(2).zero?
119+
Char.prepare(Faker::Name.first_name)
120+
else
121+
[Faker::Name.first_name, Faker::Name.last_name].map do |name|
122+
Char.prepare(name)
123+
end.join(sample(separators))
124+
end
105125
end
106126
end
107127

@@ -154,22 +174,22 @@ def password(min_length: 8, max_length: 16, mix_case: true, special_characters:
154174
# use lower_chars by default and add upper_chars if mix_case
155175
lower_chars = self::LLetters
156176
password << sample(lower_chars)
157-
character_bag += lower_chars
177+
character_bag.concat(lower_chars)
158178

159-
digits = ('0'..'9').to_a
179+
digits = PASSWORD_DIGITS
160180
password << sample(digits)
161-
character_bag += digits
181+
character_bag.concat(digits)
162182

163183
if mix_case
164184
upper_chars = self::ULetters
165185
password << sample(upper_chars)
166-
character_bag += upper_chars
186+
character_bag.concat(upper_chars)
167187
end
168188

169189
if special_characters
170-
special_chars = %w[! @ # $ % ^ & *]
190+
special_chars = PASSWORD_SPECIAL_CHARS
171191
password << sample(special_chars)
172-
character_bag += special_chars
192+
character_bag.concat(special_chars)
173193
end
174194

175195
password << sample(character_bag) while password.length < target_length
@@ -528,14 +548,10 @@ def uuid
528548
#
529549
# @faker.version 2.11.0
530550
def base64(length: 16, padding: false, urlsafe: true)
531-
char_range = [
532-
Array('0'..'9'),
533-
Array('A'..'Z'),
534-
Array('a'..'z'),
535-
urlsafe ? %w[- _] : %w[+ /]
536-
].flatten
537-
s = Array.new(length) { sample(char_range) }.join
538-
s += '=' if padding
551+
char_range = urlsafe ? BASE64_URLSAFE_CHARS : BASE64_CHARS
552+
s = ::String.new('', capacity: length + 1)
553+
length.times { s << sample(char_range) }
554+
s << '=' if padding
539555
s
540556
end
541557

@@ -562,16 +578,7 @@ def user(*args)
562578
private
563579

564580
def sanitize_email_local_part(local_part)
565-
char_range = [
566-
Array('0'..'9'),
567-
Array('A'..'Z'),
568-
Array('a'..'z'),
569-
"!#$%&'*+-/=?^_`{|}~.".chars
570-
].flatten
571-
572-
local_part.chars.map do |char|
573-
char_range.include?(char) ? char : '#'
574-
end.join
581+
local_part.gsub(INVALID_EMAIL_LOCAL_PART_CHARS, '#')
575582
end
576583

577584
def construct_email(local_part, domain_name)

0 commit comments

Comments
 (0)