opaque_id 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +28 -0
- data/CODE_OF_CONDUCT.md +11 -11
- data/README.md +82 -0
- data/docs/.gitignore +5 -0
- data/docs/404.html +25 -0
- data/docs/Gemfile +31 -0
- data/docs/Gemfile.lock +335 -0
- data/docs/_config.yml +162 -0
- data/docs/_data/navigation.yml +132 -0
- data/docs/_includes/footer_custom.html +8 -0
- data/docs/_includes/head_custom.html +67 -0
- data/docs/algorithms.md +409 -0
- data/docs/alphabets.md +521 -0
- data/docs/api-reference.md +594 -0
- data/docs/assets/css/custom.scss +798 -0
- data/docs/assets/images/favicon.svg +17 -0
- data/docs/assets/images/og-image.svg +65 -0
- data/docs/configuration.md +548 -0
- data/docs/development.md +567 -0
- data/docs/getting-started.md +256 -0
- data/docs/index.md +132 -0
- data/docs/installation.md +377 -0
- data/docs/performance.md +488 -0
- data/docs/robots.txt +11 -0
- data/docs/security.md +598 -0
- data/docs/usage.md +414 -0
- data/docs/use-cases.md +569 -0
- data/lib/opaque_id/version.rb +1 -1
- data/tasks/0003-prd-documentation-site.md +191 -0
- data/tasks/tasks-0003-prd-documentation-site.md +84 -0
- metadata +27 -2
- data/sig/opaque_id.rbs +0 -4
data/docs/performance.md
ADDED
@@ -0,0 +1,488 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Performance
|
4
|
+
nav_order: 8
|
5
|
+
description: "Performance benchmarks, optimization tips, and scalability considerations"
|
6
|
+
permalink: /performance/
|
7
|
+
---
|
8
|
+
|
9
|
+
# Performance
|
10
|
+
|
11
|
+
OpaqueId is designed for high performance with optimized algorithms and efficient memory usage. This guide covers benchmarks, optimization strategies, and scalability considerations.
|
12
|
+
|
13
|
+
## Performance Characteristics
|
14
|
+
|
15
|
+
### Generation Speed
|
16
|
+
|
17
|
+
OpaqueId is designed for high performance with optimized algorithms for different alphabet sizes and ID lengths.
|
18
|
+
|
19
|
+
#### Algorithm Performance
|
20
|
+
|
21
|
+
- **Fast Path (64-character alphabets)**: Uses bitwise operations for maximum speed with no rejection sampling overhead
|
22
|
+
- **Unbiased Path (Other alphabets)**: Uses rejection sampling for unbiased distribution with slight performance overhead
|
23
|
+
- **Performance scales linearly** with ID length and batch size
|
24
|
+
|
25
|
+
#### ID Length Impact
|
26
|
+
|
27
|
+
- **Shorter IDs**: Faster generation due to less computation
|
28
|
+
- **Longer IDs**: More secure but require more computation
|
29
|
+
- **Memory usage**: Scales linearly with ID length and batch size
|
30
|
+
|
31
|
+
### Memory Usage
|
32
|
+
|
33
|
+
OpaqueId is memory-efficient with predictable memory consumption patterns.
|
34
|
+
|
35
|
+
#### Memory Characteristics
|
36
|
+
|
37
|
+
- **Efficient memory usage**: Minimal memory overhead per ID generation
|
38
|
+
- **Linear scaling**: Memory usage scales predictably with ID length and batch size
|
39
|
+
- **Garbage collection**: Minimal impact on garbage collection due to efficient string handling
|
40
|
+
|
41
|
+
#### Memory Optimization
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
# Efficient batch generation
|
45
|
+
def generate_batch_efficient(count, size, alphabet)
|
46
|
+
# Pre-allocate result array
|
47
|
+
results = Array.new(count)
|
48
|
+
|
49
|
+
# Generate IDs in batches to control memory
|
50
|
+
batch_size = 10000
|
51
|
+
count.times_slice(batch_size) do |batch|
|
52
|
+
batch.each_with_index do |_, i|
|
53
|
+
results[i] = OpaqueId.generate(size: size, alphabet: alphabet)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Force garbage collection for large batches
|
57
|
+
GC.start if count > 100000
|
58
|
+
end
|
59
|
+
|
60
|
+
results
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
### Collision Probability
|
65
|
+
|
66
|
+
OpaqueId provides extremely low collision probabilities for practical use cases.
|
67
|
+
|
68
|
+
#### Collision Analysis
|
69
|
+
|
70
|
+
The collision probability depends on:
|
71
|
+
|
72
|
+
- **ID length**: Longer IDs have exponentially lower collision probability
|
73
|
+
- **Alphabet size**: Larger alphabets provide more entropy per character
|
74
|
+
- **Total IDs generated**: Collision probability increases with the square of the number of IDs
|
75
|
+
|
76
|
+
#### Mathematical Foundation
|
77
|
+
|
78
|
+
- **21-character alphanumeric (62 chars)**: ~125 bits of entropy
|
79
|
+
- **21-character standard (64 chars)**: ~126 bits of entropy
|
80
|
+
- **Collision probability**: Follows the birthday paradox formula
|
81
|
+
- **Practical safety**: Collisions are extremely unlikely for typical use cases
|
82
|
+
|
83
|
+
## Performance Characteristics
|
84
|
+
|
85
|
+
### Algorithm Performance
|
86
|
+
|
87
|
+
#### Fast Path (64-character alphabets)
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
# Optimized for 64-character alphabets
|
91
|
+
# Uses bitwise operations for maximum speed
|
92
|
+
# No rejection sampling overhead
|
93
|
+
# Linear time complexity: O(n)
|
94
|
+
```
|
95
|
+
|
96
|
+
#### Unbiased Path (Other alphabets)
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
# Uses rejection sampling for unbiased distribution
|
100
|
+
# Slight performance overhead for uniformity
|
101
|
+
# Linear time complexity: O(n × rejection_rate)
|
102
|
+
```
|
103
|
+
|
104
|
+
### Scalability Analysis
|
105
|
+
|
106
|
+
#### Linear Scaling
|
107
|
+
|
108
|
+
- **ID length**: Performance scales linearly with ID length
|
109
|
+
- **Batch size**: Performance remains consistent across different batch sizes
|
110
|
+
- **Memory usage**: Scales predictably with both ID length and batch size
|
111
|
+
|
112
|
+
## Real-World Performance
|
113
|
+
|
114
|
+
### ActiveRecord Integration
|
115
|
+
|
116
|
+
OpaqueId adds minimal overhead to ActiveRecord operations:
|
117
|
+
|
118
|
+
- **Automatic generation**: IDs are generated during model creation
|
119
|
+
- **Minimal performance impact**: Generation overhead is typically negligible
|
120
|
+
- **Memory efficient**: No significant memory overhead per record
|
121
|
+
|
122
|
+
### Batch Operations
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
# Bulk insert with pre-generated IDs
|
126
|
+
ids = 100000.times.map { OpaqueId.generate }
|
127
|
+
|
128
|
+
users_data = ids.map.with_index do |id, index|
|
129
|
+
{ opaque_id: id, name: "User #{index + 1}" }
|
130
|
+
end
|
131
|
+
|
132
|
+
User.insert_all(users_data)
|
133
|
+
# Much faster than individual creates
|
134
|
+
```
|
135
|
+
|
136
|
+
### API Performance
|
137
|
+
|
138
|
+
- **Database indexing**: Ensure proper indexes on opaque_id columns for optimal lookup performance
|
139
|
+
- **Query performance**: Lookups by opaque_id should be fast with proper indexing
|
140
|
+
- **Response times**: Performance depends on database configuration and indexing
|
141
|
+
|
142
|
+
## Optimization Tips
|
143
|
+
|
144
|
+
### 1. Choose Optimal Alphabet Size
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
# For maximum performance, use 64-character alphabets
|
148
|
+
class User < ApplicationRecord
|
149
|
+
include OpaqueId::Model
|
150
|
+
|
151
|
+
# Fastest generation
|
152
|
+
self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET
|
153
|
+
end
|
154
|
+
|
155
|
+
# For specific requirements, consider performance impact
|
156
|
+
class Order < ApplicationRecord
|
157
|
+
include OpaqueId::Model
|
158
|
+
|
159
|
+
# Numeric-only (slower but meets requirements)
|
160
|
+
self.opaque_id_alphabet = "0123456789"
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
### 2. Optimize ID Length
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
# Shorter IDs for better performance
|
168
|
+
class ShortUrl < ApplicationRecord
|
169
|
+
include OpaqueId::Model
|
170
|
+
|
171
|
+
# 8 characters: faster generation
|
172
|
+
self.opaque_id_length = 8
|
173
|
+
end
|
174
|
+
|
175
|
+
# Longer IDs for better security
|
176
|
+
class ApiKey < ApplicationRecord
|
177
|
+
include OpaqueId::Model
|
178
|
+
|
179
|
+
# 32 characters: higher security
|
180
|
+
self.opaque_id_length = 32
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
184
|
+
### 3. Batch Generation
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
# Generate IDs in batches for better performance
|
188
|
+
def generate_batch_ids(count, size: 21, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
|
189
|
+
# Pre-allocate array
|
190
|
+
results = Array.new(count)
|
191
|
+
|
192
|
+
# Generate all IDs
|
193
|
+
count.times do |i|
|
194
|
+
results[i] = OpaqueId.generate(size: size, alphabet: alphabet)
|
195
|
+
end
|
196
|
+
|
197
|
+
results
|
198
|
+
end
|
199
|
+
|
200
|
+
# Usage
|
201
|
+
ids = generate_batch_ids(10000)
|
202
|
+
# Much faster than individual calls
|
203
|
+
```
|
204
|
+
|
205
|
+
### 4. Memory Management
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
# For large-scale generation, manage memory carefully
|
209
|
+
def generate_large_batch(count, size: 21, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
|
210
|
+
results = []
|
211
|
+
batch_size = 10000
|
212
|
+
|
213
|
+
(count / batch_size).times do
|
214
|
+
batch = batch_size.times.map { OpaqueId.generate(size: size, alphabet: alphabet) }
|
215
|
+
results.concat(batch)
|
216
|
+
|
217
|
+
# Force garbage collection for large batches
|
218
|
+
GC.start if count > 100000
|
219
|
+
end
|
220
|
+
|
221
|
+
results
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
225
|
+
### 5. Database Optimization
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
# Ensure proper database indexing
|
229
|
+
class AddOpaqueIdToUsers < ActiveRecord::Migration[8.0]
|
230
|
+
def change
|
231
|
+
add_column :users, :opaque_id, :string
|
232
|
+
add_index :users, :opaque_id, unique: true
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Use database-specific optimizations
|
237
|
+
# PostgreSQL: Partial indexes for specific use cases
|
238
|
+
# MySQL: Use appropriate storage engine (InnoDB)
|
239
|
+
# SQLite: Consider WAL mode for better concurrency
|
240
|
+
```
|
241
|
+
|
242
|
+
## Performance Testing
|
243
|
+
|
244
|
+
### Benchmark Scripts
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
# Basic performance benchmark
|
248
|
+
require 'benchmark'
|
249
|
+
|
250
|
+
def benchmark_generation(count = 1000000)
|
251
|
+
puts "Generating #{count} opaque IDs..."
|
252
|
+
|
253
|
+
time = Benchmark.measure do
|
254
|
+
count.times { OpaqueId.generate }
|
255
|
+
end
|
256
|
+
|
257
|
+
puts "Time: #{time.real.round(2)} seconds"
|
258
|
+
puts "Rate: #{(count / time.real).round(0)} IDs/second"
|
259
|
+
puts "Memory: #{`ps -o rss= -p #{Process.pid}`.to_i / 1024}MB"
|
260
|
+
end
|
261
|
+
|
262
|
+
# Run benchmark
|
263
|
+
benchmark_generation(1000000)
|
264
|
+
```
|
265
|
+
|
266
|
+
### Memory Profiling
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
# Memory usage analysis
|
270
|
+
require 'memory_profiler'
|
271
|
+
|
272
|
+
def profile_memory(count = 100000)
|
273
|
+
report = MemoryProfiler.report do
|
274
|
+
count.times { OpaqueId.generate }
|
275
|
+
end
|
276
|
+
|
277
|
+
puts "Total allocated: #{report.total_allocated_memsize / 1024 / 1024}MB"
|
278
|
+
puts "Total retained: #{report.total_retained_memsize / 1024 / 1024}MB"
|
279
|
+
puts "Objects allocated: #{report.total_allocated}"
|
280
|
+
puts "Objects retained: #{report.total_retained}"
|
281
|
+
end
|
282
|
+
|
283
|
+
# Run memory profile
|
284
|
+
profile_memory(100000)
|
285
|
+
```
|
286
|
+
|
287
|
+
### Load Testing
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
# Concurrent generation testing
|
291
|
+
require 'concurrent'
|
292
|
+
|
293
|
+
def load_test(threads = 10, ids_per_thread = 100000)
|
294
|
+
puts "Testing #{threads} threads, #{ids_per_thread} IDs each..."
|
295
|
+
|
296
|
+
start_time = Time.now
|
297
|
+
|
298
|
+
# Create thread pool
|
299
|
+
pool = Concurrent::FixedThreadPool.new(threads)
|
300
|
+
|
301
|
+
# Submit tasks
|
302
|
+
futures = threads.times.map do
|
303
|
+
pool.post do
|
304
|
+
ids_per_thread.times.map { OpaqueId.generate }
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Wait for completion
|
309
|
+
results = futures.map(&:value!)
|
310
|
+
|
311
|
+
end_time = Time.now
|
312
|
+
total_ids = threads * ids_per_thread
|
313
|
+
duration = end_time - start_time
|
314
|
+
|
315
|
+
puts "Total IDs: #{total_ids}"
|
316
|
+
puts "Duration: #{duration.round(2)} seconds"
|
317
|
+
puts "Rate: #{(total_ids / duration).round(0)} IDs/second"
|
318
|
+
puts "Per thread: #{(ids_per_thread / duration).round(0)} IDs/second"
|
319
|
+
|
320
|
+
pool.shutdown
|
321
|
+
pool.wait_for_termination
|
322
|
+
end
|
323
|
+
|
324
|
+
# Run load test
|
325
|
+
load_test(10, 100000)
|
326
|
+
```
|
327
|
+
|
328
|
+
## Performance Comparison
|
329
|
+
|
330
|
+
### vs. Other ID Generation Methods
|
331
|
+
|
332
|
+
OpaqueId compares favorably to other ID generation methods:
|
333
|
+
|
334
|
+
- **SecureRandom.hex**: OpaqueId provides more customization and collision handling
|
335
|
+
- **SecureRandom.uuid**: OpaqueId offers configurable length and alphabet
|
336
|
+
- **NanoID (gem)**: OpaqueId uses native Ruby methods without external dependencies
|
337
|
+
- **Database auto-increment**: OpaqueId provides security and URL-safety at minimal performance cost
|
338
|
+
|
339
|
+
### vs. Database-Generated IDs
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
# Database auto-increment vs OpaqueId
|
343
|
+
|
344
|
+
# Database auto-increment
|
345
|
+
# Pros: Fast, sequential, small storage
|
346
|
+
# Cons: Predictable, not URL-safe, reveals count
|
347
|
+
|
348
|
+
# OpaqueId
|
349
|
+
# Pros: Unpredictable, URL-safe, configurable
|
350
|
+
# Cons: Slightly slower, larger storage
|
351
|
+
```
|
352
|
+
|
353
|
+
## Production Considerations
|
354
|
+
|
355
|
+
### 1. Monitoring
|
356
|
+
|
357
|
+
```ruby
|
358
|
+
# Add performance monitoring
|
359
|
+
class User < ApplicationRecord
|
360
|
+
include OpaqueId::Model
|
361
|
+
|
362
|
+
private
|
363
|
+
|
364
|
+
def set_opaque_id
|
365
|
+
start_time = Time.now
|
366
|
+
self.opaque_id = generate_opaque_id
|
367
|
+
duration = Time.now - start_time
|
368
|
+
|
369
|
+
# Log slow generation
|
370
|
+
Rails.logger.warn "Slow opaque_id generation: #{duration}ms" if duration > 0.001
|
371
|
+
end
|
372
|
+
end
|
373
|
+
```
|
374
|
+
|
375
|
+
### 2. Caching
|
376
|
+
|
377
|
+
```ruby
|
378
|
+
# Cache generated IDs for frequently accessed records
|
379
|
+
class User < ApplicationRecord
|
380
|
+
include OpaqueId::Model
|
381
|
+
|
382
|
+
def opaque_id
|
383
|
+
@opaque_id ||= super
|
384
|
+
end
|
385
|
+
end
|
386
|
+
```
|
387
|
+
|
388
|
+
### 3. Background Generation
|
389
|
+
|
390
|
+
```ruby
|
391
|
+
# Generate IDs in background for bulk operations
|
392
|
+
class BulkUserCreator
|
393
|
+
def self.create_users(count)
|
394
|
+
# Generate IDs in background
|
395
|
+
ids = Concurrent::Future.execute do
|
396
|
+
count.times.map { OpaqueId.generate }
|
397
|
+
end
|
398
|
+
|
399
|
+
# Create users with pre-generated IDs
|
400
|
+
users_data = ids.value.map.with_index do |id, index|
|
401
|
+
{ opaque_id: id, name: "User #{index + 1}" }
|
402
|
+
end
|
403
|
+
|
404
|
+
User.insert_all(users_data)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
```
|
408
|
+
|
409
|
+
### 4. Error Handling
|
410
|
+
|
411
|
+
```ruby
|
412
|
+
# Handle generation failures gracefully
|
413
|
+
class User < ApplicationRecord
|
414
|
+
include OpaqueId::Model
|
415
|
+
|
416
|
+
private
|
417
|
+
|
418
|
+
def set_opaque_id
|
419
|
+
retries = 0
|
420
|
+
max_retries = opaque_id_max_retry
|
421
|
+
|
422
|
+
begin
|
423
|
+
self.opaque_id = generate_opaque_id
|
424
|
+
rescue OpaqueId::GenerationError => e
|
425
|
+
retries += 1
|
426
|
+
if retries < max_retries
|
427
|
+
retry
|
428
|
+
else
|
429
|
+
Rails.logger.error "Failed to generate opaque_id after #{max_retries} retries: #{e.message}"
|
430
|
+
raise
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
```
|
436
|
+
|
437
|
+
## Best Practices
|
438
|
+
|
439
|
+
### 1. Choose Appropriate Configuration
|
440
|
+
|
441
|
+
```ruby
|
442
|
+
# High-performance applications
|
443
|
+
self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET
|
444
|
+
self.opaque_id_length = 21
|
445
|
+
|
446
|
+
# High-security applications
|
447
|
+
self.opaque_id_alphabet = OpaqueId::ALPHANUMERIC_ALPHABET
|
448
|
+
self.opaque_id_length = 32
|
449
|
+
|
450
|
+
# Human-readable applications
|
451
|
+
self.opaque_id_alphabet = OpaqueId::ALPHANUMERIC_ALPHABET
|
452
|
+
self.opaque_id_length = 15
|
453
|
+
```
|
454
|
+
|
455
|
+
### 2. Optimize for Your Use Case
|
456
|
+
|
457
|
+
```ruby
|
458
|
+
# Short URLs - prioritize performance
|
459
|
+
self.opaque_id_length = 8
|
460
|
+
self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET
|
461
|
+
|
462
|
+
# API keys - prioritize security
|
463
|
+
self.opaque_id_length = 32
|
464
|
+
self.opaque_id_alphabet = OpaqueId::ALPHANUMERIC_ALPHABET
|
465
|
+
|
466
|
+
# User IDs - balance performance and security
|
467
|
+
self.opaque_id_length = 21
|
468
|
+
self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET
|
469
|
+
```
|
470
|
+
|
471
|
+
### 3. Monitor Performance
|
472
|
+
|
473
|
+
```ruby
|
474
|
+
# Add performance monitoring
|
475
|
+
# Track generation time
|
476
|
+
# Monitor memory usage
|
477
|
+
# Alert on slow generation
|
478
|
+
# Log performance metrics
|
479
|
+
```
|
480
|
+
|
481
|
+
## Next Steps
|
482
|
+
|
483
|
+
Now that you understand performance characteristics:
|
484
|
+
|
485
|
+
1. **Explore [Algorithms](algorithms.md)** for technical algorithm details
|
486
|
+
2. **Check out [Security](security.md)** for security considerations
|
487
|
+
3. **Review [Configuration](configuration.md)** for performance optimization
|
488
|
+
4. **Read [API Reference](api-reference.md)** for complete performance documentation
|