OpaqueId Benchmarks
This document provides benchmark scripts that you can run to test OpaqueId’s performance and uniqueness characteristics on your own system.
Performance Benchmarks
SecureRandom Comparison Test
#!/usr/bin/env ruby
require 'opaque_id'
require 'securerandom'
puts "OpaqueId vs SecureRandom Comparison"
puts "=" * 50
# Test different Ruby standard library methods
methods = {
'OpaqueId.generate' => -> { OpaqueId.generate },
'SecureRandom.urlsafe_base64' => -> { SecureRandom.urlsafe_base64 },
'SecureRandom.urlsafe_base64(16)' => -> { SecureRandom.urlsafe_base64(16) },
'SecureRandom.hex(9)' => -> { SecureRandom.hex(9) },
'SecureRandom.alphanumeric(18)' => -> { SecureRandom.alphanumeric(18) }
}
count = 10000
puts "Performance comparison (#{count} IDs each):"
puts "-" * 50
methods.each do |name, method|
start_time = Time.now
ids = count.times.map { method.call }
end_time = Time.now
duration = end_time - start_time
rate = (count / duration).round(0)
# Check uniqueness
unique_count = ids.uniq.length
collisions = count - unique_count
# Check characteristics
sample_id = ids.first
length = sample_id.length
has_uppercase = sample_id.match?(/[A-Z]/)
has_lowercase = sample_id.match?(/[a-z]/)
has_numbers = sample_id.match?(/[0-9]/)
has_special = sample_id.match?(/[^A-Za-z0-9]/)
puts "#{name.ljust(30)}: #{duration.round(4)}s (#{rate} IDs/sec)"
puts " Length: #{length}, Collisions: #{collisions}"
puts " Sample: '#{sample_id}'"
puts " Chars: #{has_uppercase ? 'A-Z' : ''}#{has_lowercase ? 'a-z' : ''}#{has_numbers ? '0-9' : ''}#{has_special ? 'special' : ''}"
puts
end
puts "Comparison completed."
Basic Performance Test
#!/usr/bin/env ruby
require 'opaque_id'
puts "OpaqueId Performance Benchmark"
puts "=" * 40
# Test different batch sizes
[100, 1000, 10000, 100000].each do |count|
start_time = Time.now
count.times { OpaqueId.generate }
end_time = Time.now
duration = end_time - start_time
rate = (count / duration).round(0)
puts "#{count.to_s.rjust(6)} IDs: #{duration.round(4)}s (#{rate} IDs/sec)"
end
puts "\nPerformance test completed."
Alphabet Performance Comparison
#!/usr/bin/env ruby
require 'opaque_id'
puts "Alphabet Performance Comparison"
puts "=" * 40
alphabets = {
'SLUG_LIKE_ALPHABET' => OpaqueId::SLUG_LIKE_ALPHABET,
'ALPHANUMERIC_ALPHABET' => OpaqueId::ALPHANUMERIC_ALPHABET,
'STANDARD_ALPHABET' => OpaqueId::STANDARD_ALPHABET
}
count = 10000
alphabets.each do |name, alphabet|
start_time = Time.now
count.times { OpaqueId.generate(alphabet: alphabet) }
end_time = Time.now
duration = end_time - start_time
rate = (count / duration).round(0)
puts "#{name.ljust(20)}: #{duration.round(4)}s (#{rate} IDs/sec)"
end
puts "\nAlphabet comparison completed."
Size Performance Test
#!/usr/bin/env ruby
require 'opaque_id'
puts "Size Performance Test"
puts "=" * 40
sizes = [8, 12, 18, 24, 32, 48, 64]
count = 10000
sizes.each do |size|
start_time = Time.now
count.times { OpaqueId.generate(size: size) }
end_time = Time.now
duration = end_time - start_time
rate = (count / duration).round(0)
puts "Size #{size.to_s.rjust(2)}: #{duration.round(4)}s (#{rate} IDs/sec)"
end
puts "\nSize performance test completed."
Uniqueness Tests
Collision Probability Test
#!/usr/bin/env ruby
require 'opaque_id'
puts "Collision Probability Test"
puts "=" * 40
# Test different sample sizes
[1000, 10000, 100000].each do |count|
puts "\nTesting #{count} IDs..."
start_time = Time.now
ids = count.times.map { OpaqueId.generate }
end_time = Time.now
unique_ids = ids.uniq
collisions = count - unique_ids.length
collision_rate = (collisions.to_f / count * 100).round(6)
puts " Generated: #{count} IDs in #{(end_time - start_time).round(4)}s"
puts " Unique: #{unique_ids.length} IDs"
puts " Collisions: #{collisions} (#{collision_rate}%)"
puts " Uniqueness: #{collision_rate == 0 ? '✅ Perfect' : '⚠️ Collisions detected'}"
end
puts "\nCollision test completed."
Birthday Paradox Test
#!/usr/bin/env ruby
require 'opaque_id'
puts "Birthday Paradox Test"
puts "=" * 40
# Test the birthday paradox with different sample sizes
# For 18-character slug-like alphabet (36 chars), we have 36^18 possible combinations
# This is approximately 10^28, so collisions should be extremely rare
sample_sizes = [1000, 10000, 50000, 100000]
sample_sizes.each do |count|
puts "\nTesting #{count} IDs for birthday paradox..."
start_time = Time.now
ids = count.times.map { OpaqueId.generate }
end_time = Time.now
unique_ids = ids.uniq
collisions = count - unique_ids.length
# Calculate theoretical collision probability
# For 18-char slug-like alphabet: 36^18 ≈ 10^28 possible combinations
alphabet_size = 36
id_length = 18
total_possibilities = alphabet_size ** id_length
# Approximate birthday paradox probability
# P(collision) ≈ 1 - e^(-n(n-1)/(2*N)) where n=sample_size, N=total_possibilities
n = count
N = total_possibilities
theoretical_prob = 1 - Math.exp(-(n * (n - 1)) / (2.0 * N))
puts " Sample size: #{count}"
puts " Total possibilities: #{total_possibilities.to_s.reverse.gsub(/(\d{3})(?=.)/, '\1,').reverse}"
puts " Theoretical collision probability: #{theoretical_prob.round(20)}"
puts " Actual collisions: #{collisions}"
puts " Result: #{collisions == 0 ? '✅ No collisions (as expected)' : '⚠️ Collisions detected'}"
end
puts "\nBirthday paradox test completed."
Alphabet Distribution Test
#!/usr/bin/env ruby
require 'opaque_id'
puts "Alphabet Distribution Test"
puts "=" * 40
# Test that all characters in the alphabet are used roughly equally
alphabet = OpaqueId::SLUG_LIKE_ALPHABET
count = 100000
puts "Testing distribution for #{alphabet.length}-character alphabet..."
puts "Sample size: #{count} IDs"
start_time = Time.now
ids = count.times.map { OpaqueId.generate }
end_time = Time.now
# Count character frequency
char_counts = Hash.new(0)
ids.each do |id|
id.each_char { |char| char_counts[char] += 1 }
end
total_chars = ids.join.length
expected_per_char = total_chars.to_f / alphabet.length
puts "\nCharacter distribution:"
puts "Character | Count | Expected | Deviation"
puts "-" * 45
alphabet.each_char do |char|
count = char_counts[char]
deviation = ((count - expected_per_char) / expected_per_char * 100).round(2)
puts "#{char.ljust(8)} | #{count.to_s.rjust(5)} | #{expected_per_char.round(1).to_s.rjust(8)} | #{deviation.to_s.rjust(6)}%"
end
# Calculate chi-square test for uniform distribution
chi_square = alphabet.each_char.sum do |char|
observed = char_counts[char]
expected = expected_per_char
((observed - expected) ** 2) / expected
end
puts "\nChi-square statistic: #{chi_square.round(4)}"
puts "Distribution: #{chi_square < 30 ? '✅ Appears uniform' : '⚠️ May not be uniform'}"
puts "\nDistribution test completed."
Running the Benchmarks
Quick Performance Test
# Run basic performance test
ruby -e "
require 'opaque_id'
puts 'OpaqueId Performance Test'
puts '=' * 30
[100, 1000, 10000].each do |count|
start = Time.now
count.times { OpaqueId.generate }
duration = Time.now - start
rate = (count / duration).round(0)
puts \"#{count.to_s.rjust(5)} IDs: #{duration.round(4)}s (#{rate} IDs/sec)\"
end
"
Quick Uniqueness Test
# Run basic uniqueness test
ruby -e "
require 'opaque_id'
puts 'OpaqueId Uniqueness Test'
puts '=' * 30
count = 10000
ids = count.times.map { OpaqueId.generate }
unique = ids.uniq
collisions = count - unique.length
puts \"Generated: #{count} IDs\"
puts \"Unique: #{unique.length} IDs\"
puts \"Collisions: #{collisions}\"
puts \"Result: #{collisions == 0 ? 'Perfect uniqueness' : 'Collisions detected'}\"
"
Expected Results
Performance Expectations
On a modern system, you should expect:
- 100 IDs: < 0.001s (100,000+ IDs/sec)
- 1,000 IDs: < 0.01s (100,000+ IDs/sec)
- 10,000 IDs: < 0.1s (100,000+ IDs/sec)
- 100,000 IDs: < 1s (100,000+ IDs/sec)
Uniqueness Expectations
- 1,000 IDs: 0 collisions (100% unique)
- 10,000 IDs: 0 collisions (100% unique)
- 100,000 IDs: 0 collisions (100% unique)
- 1,000,000 IDs: 0 collisions (100% unique)
The theoretical collision probability for 1 million IDs is approximately 10^-16, making collisions virtually impossible in practice.
System Requirements
These benchmarks require:
- Ruby 2.7+ (for optimal performance)
- OpaqueId gem installed
- Sufficient memory for large sample sizes
For the largest tests (100,000+ IDs), ensure you have at least 100MB of available memory.
Why Not Just Use SecureRandom?
Ruby’s SecureRandom already provides secure random generation. Here’s how OpaqueId compares:
SecureRandom.urlsafe_base64 vs OpaqueId
| Feature | SecureRandom.urlsafe_base64 | OpaqueId.generate |
|---|---|---|
| Length | 22 characters (fixed) | 18 characters (configurable) |
| Alphabet | A-Z, a-z, 0-9, -, _ (64 chars) | 0-9, a-z (36 chars) |
| URL Safety | ✅ Yes | ✅ Yes |
| Double-click selectable | ❌ No (contains special chars) | ✅ Yes (no special chars) |
| Configurable length | ❌ No | ✅ Yes |
| Configurable alphabet | ❌ No | ✅ Yes |
| ActiveRecord integration | ❌ Manual | ✅ Built-in |
| Rails generator | ❌ No | ✅ Yes |
When to Use Each
Use SecureRandom.urlsafe_base64 when:
- You need maximum entropy (22 chars vs 18)
- You don’t mind special characters (-, _)
- You don’t need double-click selection
- You’re building a simple solution
Use OpaqueId when:
- You want slug-like IDs (no special characters)
- You need double-click selectable IDs
- You want configurable length and alphabet
- You’re using ActiveRecord models
- You want consistent ID length (default: 18 characters)
Performance Comparison
Run the SecureRandom Comparison Test to see how OpaqueId compares to various SecureRandom methods on your system.
Migration from nanoid.rb
The nanoid.rb gem is considered obsolete for Ruby 2.7+ because SecureRandom provides similar functionality. OpaqueId provides an alternative with different defaults and Rails integration.