lazy_init 0.1.1 → 0.2.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/CHANGELOG.md +19 -2
- data/README.md +26 -8
- data/benchmarks/benchmark.rb +616 -145
- data/benchmarks/benchmark_performance.rb +3 -66
- data/benchmarks/benchmark_threads.rb +83 -89
- data/lazy_init.gemspec +5 -5
- data/lib/lazy_init/class_methods.rb +565 -242
- data/lib/lazy_init/instance_methods.rb +90 -66
- data/lib/lazy_init/ruby_capabilities.rb +39 -0
- data/lib/lazy_init/version.rb +1 -1
- data/lib/lazy_init.rb +1 -0
- metadata +19 -18
data/benchmarks/benchmark.rb
CHANGED
@@ -5,11 +5,10 @@ require 'benchmark/ips'
|
|
5
5
|
require_relative '../lib/lazy_init'
|
6
6
|
|
7
7
|
class LazyInitBenchmark
|
8
|
-
VERSION = '
|
8
|
+
VERSION = '2.0.0'
|
9
9
|
|
10
10
|
def initialize
|
11
11
|
@results = {}
|
12
|
-
configure_lazy_init
|
13
12
|
puts header
|
14
13
|
end
|
15
14
|
|
@@ -17,11 +16,11 @@ class LazyInitBenchmark
|
|
17
16
|
run_basic_patterns
|
18
17
|
run_computational_complexity
|
19
18
|
run_dependency_injection
|
20
|
-
# run_conditional_loading
|
21
19
|
run_class_level_shared
|
22
20
|
run_method_memoization
|
23
21
|
run_timeout_overhead
|
24
22
|
run_exception_handling
|
23
|
+
run_thread_safety
|
25
24
|
run_real_world_scenarios
|
26
25
|
|
27
26
|
print_summary
|
@@ -29,17 +28,10 @@ class LazyInitBenchmark
|
|
29
28
|
|
30
29
|
private
|
31
30
|
|
32
|
-
def configure_lazy_init
|
33
|
-
LazyInit.configure do |config|
|
34
|
-
config.track_performance = false
|
35
|
-
config.debug = false
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
31
|
def header
|
40
32
|
<<~HEADER
|
41
33
|
===================================================================
|
42
|
-
LazyInit Performance Benchmark v#{VERSION}
|
34
|
+
LazyInit Performance Benchmark v#{VERSION} (Fixed Methodology)
|
43
35
|
===================================================================
|
44
36
|
Ruby: #{RUBY_VERSION} (#{RUBY_ENGINE})
|
45
37
|
Platform: #{RUBY_PLATFORM}
|
@@ -51,14 +43,22 @@ class LazyInitBenchmark
|
|
51
43
|
def benchmark_comparison(category, test_name, manual_impl, lazy_impl, warmup: true)
|
52
44
|
puts "\n--- #{test_name} ---"
|
53
45
|
|
54
|
-
#
|
46
|
+
# Enhanced warmup for more reliable results
|
55
47
|
if warmup
|
56
|
-
|
57
|
-
|
48
|
+
puts ' Warming up...'
|
49
|
+
5.times do
|
50
|
+
manual_impl.call
|
51
|
+
lazy_impl.call
|
52
|
+
end
|
53
|
+
|
54
|
+
# Additional warmup for GC stabilization
|
55
|
+
GC.start
|
56
|
+
sleep 0.01
|
58
57
|
end
|
59
58
|
|
60
|
-
# Run benchmark
|
59
|
+
# Run benchmark with more iterations for stability
|
61
60
|
suite = Benchmark.ips do |x|
|
61
|
+
x.config(time: 5, warmup: 2)
|
62
62
|
x.report('Manual', &manual_impl)
|
63
63
|
x.report('LazyInit', &lazy_impl)
|
64
64
|
x.compare!
|
@@ -105,14 +105,18 @@ class LazyInitBenchmark
|
|
105
105
|
# ========================================
|
106
106
|
|
107
107
|
def run_basic_patterns
|
108
|
-
puts "\n" + '=' *
|
108
|
+
puts "\n" + '=' * 30
|
109
109
|
puts '1. BASIC LAZY INITIALIZATION PATTERNS'
|
110
|
-
puts '=' *
|
110
|
+
puts '=' * 30
|
111
111
|
|
112
|
-
# Hot path performance
|
112
|
+
# Hot path performance - fair comparison after both are initialized
|
113
113
|
manual_basic = create_manual_basic
|
114
114
|
lazy_basic = create_lazy_basic
|
115
115
|
|
116
|
+
# Pre-initialize both for hot path test
|
117
|
+
manual_basic.expensive_value
|
118
|
+
lazy_basic.expensive_value
|
119
|
+
|
116
120
|
benchmark_comparison(
|
117
121
|
'Basic Patterns',
|
118
122
|
'Hot Path (after initialization)',
|
@@ -120,7 +124,7 @@ class LazyInitBenchmark
|
|
120
124
|
-> { lazy_basic.expensive_value }
|
121
125
|
)
|
122
126
|
|
123
|
-
# Cold start performance
|
127
|
+
# Cold start performance - fresh instances each time
|
124
128
|
benchmark_comparison(
|
125
129
|
'Basic Patterns',
|
126
130
|
'Cold Start (new instances)',
|
@@ -128,12 +132,21 @@ class LazyInitBenchmark
|
|
128
132
|
-> { create_lazy_basic.expensive_value },
|
129
133
|
warmup: false
|
130
134
|
)
|
135
|
+
|
136
|
+
# Memory overhead test - check after many instances
|
137
|
+
benchmark_comparison(
|
138
|
+
'Basic Patterns',
|
139
|
+
'Memory Overhead (100 instances)',
|
140
|
+
-> { 100.times { create_manual_basic } },
|
141
|
+
-> { 100.times { create_lazy_basic } },
|
142
|
+
warmup: false
|
143
|
+
)
|
131
144
|
end
|
132
145
|
|
133
146
|
def run_computational_complexity
|
134
|
-
puts "\n" + '=' *
|
147
|
+
puts "\n" + '=' * 30
|
135
148
|
puts '2. COMPUTATIONAL COMPLEXITY SCENARIOS'
|
136
|
-
puts '=' *
|
149
|
+
puts '=' * 30
|
137
150
|
|
138
151
|
# Light computation
|
139
152
|
manual_light = create_manual_light
|
@@ -170,9 +183,9 @@ class LazyInitBenchmark
|
|
170
183
|
end
|
171
184
|
|
172
185
|
def run_dependency_injection
|
173
|
-
puts "\n" + '=' *
|
186
|
+
puts "\n" + '=' * 30
|
174
187
|
puts '3. DEPENDENCY INJECTION PERFORMANCE'
|
175
|
-
puts '=' *
|
188
|
+
puts '=' * 30
|
176
189
|
|
177
190
|
# Simple dependencies
|
178
191
|
manual_deps = create_manual_deps
|
@@ -195,89 +208,149 @@ class LazyInitBenchmark
|
|
195
208
|
-> { manual_complex.service },
|
196
209
|
-> { lazy_complex.service }
|
197
210
|
)
|
198
|
-
end
|
199
211
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
# puts "="*60
|
212
|
+
# Mixed access patterns
|
213
|
+
manual_mixed = create_manual_complex_deps
|
214
|
+
lazy_mixed = create_lazy_complex_deps
|
204
215
|
|
205
|
-
|
206
|
-
|
207
|
-
|
216
|
+
benchmark_comparison(
|
217
|
+
'Dependency Injection',
|
218
|
+
'Mixed Access Pattern',
|
219
|
+
lambda {
|
220
|
+
manual_mixed.config
|
221
|
+
manual_mixed.database
|
222
|
+
manual_mixed.service
|
223
|
+
},
|
224
|
+
lambda {
|
225
|
+
lazy_mixed.config
|
226
|
+
lazy_mixed.database
|
227
|
+
lazy_mixed.service
|
228
|
+
}
|
229
|
+
)
|
230
|
+
end
|
208
231
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
# -> { lazy_true.feature }
|
214
|
-
# )
|
232
|
+
def run_class_level_shared
|
233
|
+
puts "\n" + '=' * 30
|
234
|
+
puts '4. CLASS-LEVEL SHARED RESOURCES'
|
235
|
+
puts '=' * 30
|
215
236
|
|
216
|
-
|
217
|
-
|
218
|
-
|
237
|
+
# FIXED: Don't pre-initialize class variables
|
238
|
+
benchmark_comparison(
|
239
|
+
'Class-Level Resources',
|
240
|
+
'Shared Resources (Cold)',
|
241
|
+
-> { create_manual_class_var.shared_resource },
|
242
|
+
-> { create_lazy_class_var.shared_resource }
|
243
|
+
)
|
219
244
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
# -> { manual_false.feature },
|
224
|
-
# -> { lazy_false.feature }
|
225
|
-
# )
|
226
|
-
# end
|
245
|
+
# Hot path for class variables
|
246
|
+
manual_class_hot = create_manual_class_var
|
247
|
+
lazy_class_hot = create_lazy_class_var
|
227
248
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
puts '=' * 60
|
249
|
+
# Initialize both
|
250
|
+
manual_class_hot.shared_resource
|
251
|
+
lazy_class_hot.shared_resource
|
232
252
|
|
233
|
-
|
234
|
-
|
253
|
+
benchmark_comparison(
|
254
|
+
'Class-Level Resources',
|
255
|
+
'Shared Resources (Hot)',
|
256
|
+
-> { manual_class_hot.shared_resource },
|
257
|
+
-> { lazy_class_hot.shared_resource }
|
258
|
+
)
|
235
259
|
|
260
|
+
# Multiple instances accessing same class variable
|
236
261
|
benchmark_comparison(
|
237
262
|
'Class-Level Resources',
|
238
|
-
'
|
239
|
-
|
240
|
-
|
263
|
+
'Multiple Instances Access',
|
264
|
+
lambda {
|
265
|
+
instances = 5.times.map { create_manual_class_var }
|
266
|
+
instances.each(&:shared_resource)
|
267
|
+
},
|
268
|
+
lambda {
|
269
|
+
instances = 5.times.map { create_lazy_class_var }
|
270
|
+
instances.each(&:shared_resource)
|
271
|
+
}
|
241
272
|
)
|
242
273
|
end
|
243
274
|
|
244
275
|
def run_method_memoization
|
245
|
-
puts "\n" + '=' *
|
246
|
-
puts '
|
247
|
-
puts '=' *
|
276
|
+
puts "\n" + '=' * 30
|
277
|
+
puts '5. METHOD-LOCAL MEMOIZATION'
|
278
|
+
puts '=' * 30
|
248
279
|
|
280
|
+
# FIXED: Fair comparison with same caching behavior
|
249
281
|
manual_memo = create_manual_memo
|
250
282
|
lazy_memo = create_lazy_memo
|
251
283
|
|
284
|
+
# Test with same key (cache hit scenario)
|
252
285
|
benchmark_comparison(
|
253
286
|
'Method Memoization',
|
254
|
-
'
|
287
|
+
'Same Key Cache Hit',
|
255
288
|
-> { manual_memo.expensive_calc(100) },
|
256
289
|
-> { lazy_memo.expensive_calc(100) }
|
257
290
|
)
|
291
|
+
|
292
|
+
# Test with different keys (cache miss scenario)
|
293
|
+
key_counter = 0
|
294
|
+
benchmark_comparison(
|
295
|
+
'Method Memoization',
|
296
|
+
'Different Keys',
|
297
|
+
lambda {
|
298
|
+
key_counter += 1
|
299
|
+
create_manual_memo.expensive_calc(key_counter)
|
300
|
+
},
|
301
|
+
lambda {
|
302
|
+
key_counter += 1
|
303
|
+
create_lazy_memo.expensive_calc(key_counter)
|
304
|
+
},
|
305
|
+
warmup: false
|
306
|
+
)
|
307
|
+
|
308
|
+
# Test cache performance with many keys
|
309
|
+
benchmark_comparison(
|
310
|
+
'Method Memoization',
|
311
|
+
'Many Keys Performance',
|
312
|
+
lambda {
|
313
|
+
memo = create_manual_memo
|
314
|
+
100.times { |i| memo.expensive_calc(i) }
|
315
|
+
},
|
316
|
+
lambda {
|
317
|
+
memo = create_lazy_memo
|
318
|
+
100.times { |i| memo.expensive_calc(i) }
|
319
|
+
}
|
320
|
+
)
|
258
321
|
end
|
259
322
|
|
260
323
|
def run_timeout_overhead
|
261
|
-
puts "\n" + '=' *
|
262
|
-
puts '
|
263
|
-
puts '=' *
|
324
|
+
puts "\n" + '=' * 30
|
325
|
+
puts '6. TIMEOUT OVERHEAD'
|
326
|
+
puts '=' * 30
|
264
327
|
|
265
328
|
no_timeout = create_no_timeout
|
266
329
|
with_timeout = create_with_timeout
|
267
330
|
|
268
331
|
benchmark_comparison(
|
269
332
|
'Timeout Support',
|
270
|
-
'Timeout vs
|
333
|
+
'No Timeout vs With Timeout',
|
271
334
|
-> { no_timeout.quick_operation },
|
272
335
|
-> { with_timeout.quick_operation }
|
273
336
|
)
|
337
|
+
|
338
|
+
# Test timeout configuration overhead
|
339
|
+
benchmark_comparison(
|
340
|
+
'Timeout Support',
|
341
|
+
'Timeout Configuration Cost',
|
342
|
+
-> { create_no_timeout.quick_operation },
|
343
|
+
-> { create_with_timeout.quick_operation },
|
344
|
+
warmup: false
|
345
|
+
)
|
274
346
|
end
|
275
347
|
|
276
348
|
def run_exception_handling
|
277
|
-
puts "\n" + '=' *
|
278
|
-
puts '
|
279
|
-
puts '=' *
|
349
|
+
puts "\n" + '=' * 30
|
350
|
+
puts '7. EXCEPTION HANDLING OVERHEAD'
|
351
|
+
puts '=' * 30
|
280
352
|
|
353
|
+
# FIXED: Same exception handling behavior
|
281
354
|
manual_exception = create_manual_exception
|
282
355
|
lazy_exception = create_lazy_exception
|
283
356
|
|
@@ -287,12 +360,134 @@ class LazyInitBenchmark
|
|
287
360
|
-> { manual_exception.failing_method },
|
288
361
|
-> { lazy_exception.failing_method }
|
289
362
|
)
|
363
|
+
|
364
|
+
# Test exception caching behavior
|
365
|
+
manual_exception_cached = create_manual_exception_cached
|
366
|
+
lazy_exception_cached = create_lazy_exception_cached
|
367
|
+
|
368
|
+
benchmark_comparison(
|
369
|
+
'Exception Handling',
|
370
|
+
'Exception Caching',
|
371
|
+
lambda {
|
372
|
+
begin
|
373
|
+
manual_exception_cached.always_fails
|
374
|
+
rescue StandardError
|
375
|
+
# Exception not cached in manual approach
|
376
|
+
end
|
377
|
+
},
|
378
|
+
lambda {
|
379
|
+
begin
|
380
|
+
lazy_exception_cached.always_fails
|
381
|
+
rescue StandardError
|
382
|
+
# Exception cached in LazyInit
|
383
|
+
end
|
384
|
+
}
|
385
|
+
)
|
386
|
+
end
|
387
|
+
|
388
|
+
def run_thread_safety
|
389
|
+
puts "\n" + '=' * 30
|
390
|
+
puts '8. THREAD SAFETY PERFORMANCE'
|
391
|
+
puts '=' * 30
|
392
|
+
|
393
|
+
# This is where LazyInit should shine - testing concurrent access
|
394
|
+
manual_concurrent = create_manual_concurrent
|
395
|
+
lazy_concurrent = create_lazy_concurrent
|
396
|
+
|
397
|
+
benchmark_comparison(
|
398
|
+
'Thread Safety',
|
399
|
+
'Concurrent Access (10 threads)',
|
400
|
+
lambda {
|
401
|
+
threads = 10.times.map do
|
402
|
+
Thread.new { manual_concurrent.thread_safe_value }
|
403
|
+
end
|
404
|
+
threads.each(&:join)
|
405
|
+
},
|
406
|
+
lambda {
|
407
|
+
threads = 10.times.map do
|
408
|
+
Thread.new { lazy_concurrent.thread_safe_value }
|
409
|
+
end
|
410
|
+
threads.each(&:join)
|
411
|
+
}
|
412
|
+
)
|
413
|
+
|
414
|
+
# Test under high contention
|
415
|
+
benchmark_comparison(
|
416
|
+
'Thread Safety',
|
417
|
+
'High Contention (50 threads)',
|
418
|
+
lambda {
|
419
|
+
instance = create_manual_concurrent
|
420
|
+
threads = 50.times.map do
|
421
|
+
Thread.new { instance.thread_safe_value }
|
422
|
+
end
|
423
|
+
threads.each(&:join)
|
424
|
+
},
|
425
|
+
lambda {
|
426
|
+
instance = create_lazy_concurrent
|
427
|
+
threads = 50.times.map do
|
428
|
+
Thread.new { instance.thread_safe_value }
|
429
|
+
end
|
430
|
+
threads.each(&:join)
|
431
|
+
}
|
432
|
+
)
|
433
|
+
|
434
|
+
# Test reset safety under concurrent access
|
435
|
+
benchmark_comparison(
|
436
|
+
'Thread Safety',
|
437
|
+
'Concurrent Reset Safety',
|
438
|
+
lambda {
|
439
|
+
instance = create_manual_concurrent
|
440
|
+
threads = []
|
441
|
+
|
442
|
+
# Readers
|
443
|
+
10.times do
|
444
|
+
threads << Thread.new do
|
445
|
+
20.times { instance.resettable_value }
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# Resetters
|
450
|
+
2.times do
|
451
|
+
threads << Thread.new do
|
452
|
+
5.times do
|
453
|
+
sleep 0.001
|
454
|
+
instance.reset_resettable_value!
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
threads.each(&:join)
|
460
|
+
},
|
461
|
+
lambda {
|
462
|
+
instance = create_lazy_concurrent
|
463
|
+
threads = []
|
464
|
+
|
465
|
+
# Readers
|
466
|
+
10.times do
|
467
|
+
threads << Thread.new do
|
468
|
+
20.times { instance.resettable_value }
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
# Resetters
|
473
|
+
2.times do
|
474
|
+
threads << Thread.new do
|
475
|
+
5.times do
|
476
|
+
sleep 0.001
|
477
|
+
instance.reset_resettable_value!
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
threads.each(&:join)
|
483
|
+
}
|
484
|
+
)
|
290
485
|
end
|
291
486
|
|
292
487
|
def run_real_world_scenarios
|
293
|
-
puts "\n" + '=' *
|
488
|
+
puts "\n" + '=' * 30
|
294
489
|
puts '9. REAL-WORLD SCENARIOS'
|
295
|
-
puts '=' *
|
490
|
+
puts '=' * 30
|
296
491
|
|
297
492
|
manual_webapp = create_manual_webapp
|
298
493
|
lazy_webapp = create_lazy_webapp
|
@@ -303,10 +498,44 @@ class LazyInitBenchmark
|
|
303
498
|
-> { manual_webapp.application },
|
304
499
|
-> { lazy_webapp.application }
|
305
500
|
)
|
501
|
+
|
502
|
+
# Rails-like service pattern
|
503
|
+
benchmark_comparison(
|
504
|
+
'Real-World',
|
505
|
+
'Service Container Pattern',
|
506
|
+
lambda {
|
507
|
+
container = create_manual_service_container
|
508
|
+
container.user_service
|
509
|
+
container.notification_service
|
510
|
+
container.payment_service
|
511
|
+
},
|
512
|
+
lambda {
|
513
|
+
container = create_lazy_service_container
|
514
|
+
container.user_service
|
515
|
+
container.notification_service
|
516
|
+
container.payment_service
|
517
|
+
}
|
518
|
+
)
|
519
|
+
|
520
|
+
# Background job pattern
|
521
|
+
benchmark_comparison(
|
522
|
+
'Real-World',
|
523
|
+
'Background Job Setup',
|
524
|
+
lambda {
|
525
|
+
job = create_manual_background_job
|
526
|
+
job.setup_dependencies
|
527
|
+
job.process_data('test_data')
|
528
|
+
},
|
529
|
+
lambda {
|
530
|
+
job = create_lazy_background_job
|
531
|
+
job.setup_dependencies
|
532
|
+
job.process_data('test_data')
|
533
|
+
}
|
534
|
+
)
|
306
535
|
end
|
307
536
|
|
308
537
|
# ========================================
|
309
|
-
# FACTORY METHODS
|
538
|
+
# FACTORY METHODS (FIXED)
|
310
539
|
# ========================================
|
311
540
|
|
312
541
|
def create_manual_basic
|
@@ -470,38 +699,9 @@ class LazyInitBenchmark
|
|
470
699
|
end.new
|
471
700
|
end
|
472
701
|
|
473
|
-
|
474
|
-
Class.new do
|
475
|
-
attr_accessor :feature_enabled
|
476
|
-
|
477
|
-
def initialize(enabled)
|
478
|
-
@feature_enabled = enabled
|
479
|
-
end
|
480
|
-
|
481
|
-
def feature
|
482
|
-
return nil unless feature_enabled
|
483
|
-
|
484
|
-
@feature ||= 'Feature loaded'
|
485
|
-
end
|
486
|
-
end.new(enabled)
|
487
|
-
end
|
488
|
-
|
489
|
-
# def create_lazy_conditional(enabled)
|
490
|
-
# Class.new do
|
491
|
-
# extend LazyInit
|
492
|
-
# attr_accessor :feature_enabled
|
493
|
-
|
494
|
-
# def initialize(enabled)
|
495
|
-
# @feature_enabled = enabled
|
496
|
-
# end
|
497
|
-
|
498
|
-
# lazy_attr_reader :feature, if_condition: -> { feature_enabled } do
|
499
|
-
# "Feature loaded"
|
500
|
-
# end
|
501
|
-
# end.new(enabled)
|
502
|
-
# end
|
503
|
-
|
702
|
+
# FIXED: Class variable tests without pre-initialization
|
504
703
|
def create_manual_class_var
|
704
|
+
# Create fresh class each time to avoid shared state
|
505
705
|
Class.new do
|
506
706
|
def self.shared_resource
|
507
707
|
@@shared_resource ||= "Shared resource #{rand(1000)}"
|
@@ -514,19 +714,17 @@ class LazyInitBenchmark
|
|
514
714
|
end
|
515
715
|
|
516
716
|
def create_lazy_class_var
|
517
|
-
|
717
|
+
# Create fresh class each time without pre-initialization
|
718
|
+
Class.new do
|
518
719
|
extend LazyInit
|
519
720
|
|
520
721
|
lazy_class_variable :shared_resource do
|
521
722
|
"Shared resource #{rand(1000)}"
|
522
723
|
end
|
523
|
-
end
|
524
|
-
|
525
|
-
# Initialize the class variable
|
526
|
-
klass.shared_resource
|
527
|
-
klass.new
|
724
|
+
end.new
|
528
725
|
end
|
529
726
|
|
727
|
+
# FIXED: Method memoization with fair comparison
|
530
728
|
def create_manual_memo
|
531
729
|
Class.new do
|
532
730
|
def expensive_calc(key)
|
@@ -540,8 +738,15 @@ class LazyInitBenchmark
|
|
540
738
|
Class.new do
|
541
739
|
include LazyInit
|
542
740
|
|
741
|
+
def initialize
|
742
|
+
@memo_cache = {}
|
743
|
+
end
|
744
|
+
|
543
745
|
def expensive_calc(key)
|
544
|
-
|
746
|
+
# Fair comparison: cache per key like manual version
|
747
|
+
return @memo_cache[key] if @memo_cache.key?(key)
|
748
|
+
|
749
|
+
@memo_cache[key] = lazy_once { "computed_#{key}_#{rand(1000)}" }
|
545
750
|
end
|
546
751
|
end.new
|
547
752
|
end
|
@@ -566,6 +771,7 @@ class LazyInitBenchmark
|
|
566
771
|
end.new
|
567
772
|
end
|
568
773
|
|
774
|
+
# FIXED: Exception handling with same behavior
|
569
775
|
def create_manual_exception
|
570
776
|
Class.new do
|
571
777
|
def failing_method
|
@@ -590,6 +796,78 @@ class LazyInitBenchmark
|
|
590
796
|
end.new
|
591
797
|
end
|
592
798
|
|
799
|
+
# NEW: Exception caching behavior tests
|
800
|
+
def create_manual_exception_cached
|
801
|
+
Class.new do
|
802
|
+
def always_fails
|
803
|
+
# Manual approach doesn't cache exceptions
|
804
|
+
raise StandardError, 'Always fails'
|
805
|
+
end
|
806
|
+
end.new
|
807
|
+
end
|
808
|
+
|
809
|
+
def create_lazy_exception_cached
|
810
|
+
Class.new do
|
811
|
+
extend LazyInit
|
812
|
+
|
813
|
+
lazy_attr_reader :always_fails do
|
814
|
+
raise StandardError, 'Always fails'
|
815
|
+
end
|
816
|
+
end.new
|
817
|
+
end
|
818
|
+
|
819
|
+
# NEW: Thread safety test implementations
|
820
|
+
def create_manual_concurrent
|
821
|
+
Class.new do
|
822
|
+
def initialize
|
823
|
+
@mutex = Mutex.new
|
824
|
+
@reset_mutex = Mutex.new
|
825
|
+
end
|
826
|
+
|
827
|
+
def thread_safe_value
|
828
|
+
return @thread_safe_value if defined?(@thread_safe_value)
|
829
|
+
|
830
|
+
@mutex.synchronize do
|
831
|
+
return @thread_safe_value if defined?(@thread_safe_value)
|
832
|
+
|
833
|
+
@thread_safe_value = "thread_safe_#{rand(1000)}"
|
834
|
+
end
|
835
|
+
end
|
836
|
+
|
837
|
+
def resettable_value
|
838
|
+
return @resettable_value if defined?(@resettable_value)
|
839
|
+
|
840
|
+
@mutex.synchronize do
|
841
|
+
return @resettable_value if defined?(@resettable_value)
|
842
|
+
|
843
|
+
@resettable_value = "resettable_#{rand(1000)}"
|
844
|
+
end
|
845
|
+
end
|
846
|
+
|
847
|
+
def reset_resettable_value!
|
848
|
+
@reset_mutex.synchronize do
|
849
|
+
@mutex.synchronize do
|
850
|
+
remove_instance_variable(:@resettable_value) if defined?(@resettable_value)
|
851
|
+
end
|
852
|
+
end
|
853
|
+
end
|
854
|
+
end.new
|
855
|
+
end
|
856
|
+
|
857
|
+
def create_lazy_concurrent
|
858
|
+
Class.new do
|
859
|
+
extend LazyInit
|
860
|
+
|
861
|
+
lazy_attr_reader :thread_safe_value do
|
862
|
+
"thread_safe_#{rand(1000)}"
|
863
|
+
end
|
864
|
+
|
865
|
+
lazy_attr_reader :resettable_value do
|
866
|
+
"resettable_#{rand(1000)}"
|
867
|
+
end
|
868
|
+
end.new
|
869
|
+
end
|
870
|
+
|
593
871
|
def create_manual_webapp
|
594
872
|
Class.new do
|
595
873
|
def config
|
@@ -676,34 +954,155 @@ class LazyInitBenchmark
|
|
676
954
|
end.new
|
677
955
|
end
|
678
956
|
|
957
|
+
# NEW: Additional realistic test cases
|
958
|
+
def create_manual_service_container
|
959
|
+
Class.new do
|
960
|
+
def database_config
|
961
|
+
@database_config ||= { url: 'postgresql://localhost/app' }
|
962
|
+
end
|
963
|
+
|
964
|
+
def redis_config
|
965
|
+
@redis_config ||= { url: 'redis://localhost:6379' }
|
966
|
+
end
|
967
|
+
|
968
|
+
def user_service
|
969
|
+
@user_service ||= begin
|
970
|
+
database_config
|
971
|
+
"UserService using #{database_config[:url]}"
|
972
|
+
end
|
973
|
+
end
|
974
|
+
|
975
|
+
def notification_service
|
976
|
+
@notification_service ||= begin
|
977
|
+
redis_config
|
978
|
+
user_service
|
979
|
+
"NotificationService using #{redis_config[:url]} and #{user_service}"
|
980
|
+
end
|
981
|
+
end
|
982
|
+
|
983
|
+
def payment_service
|
984
|
+
@payment_service ||= begin
|
985
|
+
user_service
|
986
|
+
"PaymentService using #{user_service}"
|
987
|
+
end
|
988
|
+
end
|
989
|
+
end.new
|
990
|
+
end
|
991
|
+
|
992
|
+
def create_lazy_service_container
|
993
|
+
Class.new do
|
994
|
+
extend LazyInit
|
995
|
+
|
996
|
+
lazy_attr_reader :database_config do
|
997
|
+
{ url: 'postgresql://localhost/app' }
|
998
|
+
end
|
999
|
+
|
1000
|
+
lazy_attr_reader :redis_config do
|
1001
|
+
{ url: 'redis://localhost:6379' }
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
lazy_attr_reader :user_service, depends_on: [:database_config] do
|
1005
|
+
"UserService using #{database_config[:url]}"
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
lazy_attr_reader :notification_service, depends_on: %i[redis_config user_service] do
|
1009
|
+
"NotificationService using #{redis_config[:url]} and #{user_service}"
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
lazy_attr_reader :payment_service, depends_on: [:user_service] do
|
1013
|
+
"PaymentService using #{user_service}"
|
1014
|
+
end
|
1015
|
+
end.new
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
def create_manual_background_job
|
1019
|
+
Class.new do
|
1020
|
+
def setup_dependencies
|
1021
|
+
processor
|
1022
|
+
storage
|
1023
|
+
logger
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
def processor
|
1027
|
+
@processor ||= 'DataProcessor initialized'
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
def storage
|
1031
|
+
@storage ||= begin
|
1032
|
+
processor
|
1033
|
+
"StorageService using #{processor}"
|
1034
|
+
end
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
def logger
|
1038
|
+
@logger ||= 'Logger initialized'
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
def process_data(data)
|
1042
|
+
setup_dependencies
|
1043
|
+
"Processing #{data} with #{processor}, #{storage}, #{logger}"
|
1044
|
+
end
|
1045
|
+
end.new
|
1046
|
+
end
|
1047
|
+
|
1048
|
+
def create_lazy_background_job
|
1049
|
+
Class.new do
|
1050
|
+
extend LazyInit
|
1051
|
+
|
1052
|
+
lazy_attr_reader :processor do
|
1053
|
+
'DataProcessor initialized'
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
lazy_attr_reader :storage, depends_on: [:processor] do
|
1057
|
+
"StorageService using #{processor}"
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
lazy_attr_reader :logger do
|
1061
|
+
'Logger initialized'
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
def setup_dependencies
|
1065
|
+
processor
|
1066
|
+
storage
|
1067
|
+
logger
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
def process_data(data)
|
1071
|
+
setup_dependencies
|
1072
|
+
"Processing #{data} with #{processor}, #{storage}, #{logger}"
|
1073
|
+
end
|
1074
|
+
end.new
|
1075
|
+
end
|
1076
|
+
|
679
1077
|
# ========================================
|
680
|
-
# RESULTS & SUMMARY
|
1078
|
+
# RESULTS & SUMMARY (Enhanced)
|
681
1079
|
# ========================================
|
682
1080
|
|
683
1081
|
def print_summary
|
684
|
-
puts "\n" + '=' *
|
685
|
-
puts 'BENCHMARK SUMMARY'
|
686
|
-
puts '=' *
|
1082
|
+
puts "\n" + '=' * 30
|
1083
|
+
puts 'BENCHMARK SUMMARY (Fixed Methodology)'
|
1084
|
+
puts '=' * 30
|
687
1085
|
|
688
1086
|
print_detailed_results
|
689
1087
|
print_performance_analysis
|
1088
|
+
print_thread_safety_analysis
|
690
1089
|
print_recommendations
|
691
1090
|
end
|
692
1091
|
|
693
1092
|
def print_detailed_results
|
694
1093
|
puts "\nDetailed Results:"
|
695
|
-
puts '-' *
|
1094
|
+
puts '-' * 30
|
696
1095
|
|
697
1096
|
@results.each do |category, tests|
|
698
1097
|
puts "\n#{category}:"
|
699
1098
|
|
700
1099
|
tests.each do |test_name, data|
|
701
|
-
overhead_str = if data[:overhead_percent] < -
|
1100
|
+
overhead_str = if data[:overhead_percent] < -20
|
702
1101
|
"#{(-data[:overhead_percent]).round(1)}% faster"
|
703
|
-
elsif data[:
|
704
|
-
"#{data[:ratio]}x slower"
|
705
|
-
else
|
1102
|
+
elsif data[:ratio] < 1.2
|
706
1103
|
'similar performance'
|
1104
|
+
else
|
1105
|
+
"#{data[:ratio]}x slower"
|
707
1106
|
end
|
708
1107
|
|
709
1108
|
puts " #{test_name}:"
|
@@ -715,9 +1114,9 @@ class LazyInitBenchmark
|
|
715
1114
|
end
|
716
1115
|
|
717
1116
|
def print_performance_analysis
|
718
|
-
puts "\n" + '=' *
|
1117
|
+
puts "\n" + '=' * 30
|
719
1118
|
puts 'PERFORMANCE ANALYSIS'
|
720
|
-
puts '=' *
|
1119
|
+
puts '=' * 30
|
721
1120
|
|
722
1121
|
all_ratios = @results.values.flat_map(&:values).map { |data| data[:ratio] }
|
723
1122
|
avg_ratio = (all_ratios.sum / all_ratios.size).round(2)
|
@@ -725,48 +1124,108 @@ class LazyInitBenchmark
|
|
725
1124
|
max_ratio = all_ratios.max.round(2)
|
726
1125
|
|
727
1126
|
puts "\nOverall Performance Impact:"
|
728
|
-
puts " Average
|
1127
|
+
puts " Average overhead: #{avg_ratio}x"
|
729
1128
|
puts " Best case: #{min_ratio}x slower"
|
730
1129
|
puts " Worst case: #{max_ratio}x slower"
|
731
1130
|
|
732
|
-
# Analyze
|
1131
|
+
# Analyze hot path vs cold start
|
1132
|
+
basic = @results['Basic Patterns']
|
1133
|
+
if basic
|
1134
|
+
hot_path = basic['Hot Path (after initialization)']
|
1135
|
+
cold_start = basic['Cold Start (new instances)']
|
1136
|
+
|
1137
|
+
if hot_path && cold_start
|
1138
|
+
puts "\nHot Path vs Cold Start:"
|
1139
|
+
puts " Hot path overhead: #{hot_path[:ratio]}x"
|
1140
|
+
puts " Cold start overhead: #{cold_start[:ratio]}x"
|
1141
|
+
|
1142
|
+
if hot_path[:ratio] < 1.5
|
1143
|
+
puts ' ✓ Excellent hot path performance!'
|
1144
|
+
elsif cold_start[:ratio] > hot_path[:ratio] * 3
|
1145
|
+
puts ' ⚠ Cold start overhead is significant'
|
1146
|
+
end
|
1147
|
+
end
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
# Analyze computational complexity impact
|
733
1151
|
computational = @results['Computational Complexity']
|
734
|
-
|
735
|
-
|
736
|
-
|
1152
|
+
return unless computational
|
1153
|
+
|
1154
|
+
light_ratio = computational['Lightweight (sum 1..10)']&.[](:ratio)
|
1155
|
+
heavy_ratio = computational['Heavy (filter+sqrt 1..10000)']&.[](:ratio)
|
1156
|
+
|
1157
|
+
return unless light_ratio && heavy_ratio
|
1158
|
+
|
1159
|
+
puts "\nComputational Complexity Impact:"
|
1160
|
+
puts " Light computation overhead: #{light_ratio}x"
|
1161
|
+
puts " Heavy computation overhead: #{heavy_ratio}x"
|
1162
|
+
|
1163
|
+
if (light_ratio - heavy_ratio).abs < 0.5
|
1164
|
+
puts ' • Overhead is consistent across complexity levels'
|
1165
|
+
elsif light_ratio > heavy_ratio
|
1166
|
+
puts ' • Better suited for expensive operations'
|
1167
|
+
end
|
1168
|
+
end
|
1169
|
+
|
1170
|
+
def print_thread_safety_analysis
|
1171
|
+
thread_safety = @results['Thread Safety']
|
1172
|
+
return unless thread_safety
|
1173
|
+
|
1174
|
+
puts "\n" + '=' * 50
|
1175
|
+
puts 'THREAD SAFETY ANALYSIS'
|
1176
|
+
puts '=' * 50
|
1177
|
+
|
1178
|
+
concurrent = thread_safety['Concurrent Access (10 threads)']
|
1179
|
+
high_contention = thread_safety['High Contention (50 threads)']
|
1180
|
+
reset_safety = thread_safety['Concurrent Reset Safety']
|
737
1181
|
|
738
|
-
|
739
|
-
|
740
|
-
|
1182
|
+
if concurrent
|
1183
|
+
puts "\nConcurrent Access Performance:"
|
1184
|
+
puts " 10 threads: #{concurrent[:ratio]}x slower"
|
1185
|
+
puts " 50 threads: #{high_contention[:ratio]}x slower" if high_contention
|
1186
|
+
|
1187
|
+
if concurrent[:ratio] < 2.0
|
1188
|
+
puts ' ✓ Good concurrent performance!'
|
1189
|
+
elsif concurrent[:ratio] < 5.0
|
1190
|
+
puts ' ⚠ Moderate concurrent overhead'
|
741
1191
|
else
|
742
|
-
puts
|
1192
|
+
puts ' ❌ High concurrent overhead'
|
743
1193
|
end
|
744
1194
|
end
|
745
1195
|
|
746
|
-
|
747
|
-
return unless conditional
|
748
|
-
|
749
|
-
true_ratio = conditional['Condition True'][:ratio]
|
750
|
-
false_ratio = conditional['Condition False'][:ratio]
|
1196
|
+
return unless reset_safety
|
751
1197
|
|
752
|
-
|
1198
|
+
puts "\nReset Safety:"
|
1199
|
+
puts " Concurrent reset overhead: #{reset_safety[:ratio]}x"
|
753
1200
|
|
754
|
-
|
1201
|
+
if reset_safety[:ratio] < 10.0
|
1202
|
+
puts ' ✓ Thread-safe resets are reasonably efficient'
|
1203
|
+
else
|
1204
|
+
puts ' ⚠ Thread-safe resets have significant overhead'
|
1205
|
+
end
|
755
1206
|
end
|
756
1207
|
|
757
1208
|
def print_recommendations
|
758
|
-
puts "\n" + '=' *
|
1209
|
+
puts "\n" + '=' * 20
|
759
1210
|
puts 'RECOMMENDATIONS'
|
760
|
-
puts '=' *
|
1211
|
+
puts '=' * 20
|
761
1212
|
|
762
1213
|
all_ratios = @results.values.flat_map(&:values).map { |data| data[:ratio] }
|
763
1214
|
avg_ratio = all_ratios.sum / all_ratios.size
|
764
1215
|
|
1216
|
+
basic = @results['Basic Patterns']
|
1217
|
+
hot_path_ratio = basic&.[]('Hot Path (after initialization)')&.[](:ratio)
|
1218
|
+
|
765
1219
|
puts "\nWhen to use LazyInit:"
|
766
|
-
|
767
|
-
|
768
|
-
puts '
|
769
|
-
puts '
|
1220
|
+
|
1221
|
+
if hot_path_ratio && hot_path_ratio < 1.5
|
1222
|
+
puts '✓ Hot path performance is excellent - safe for frequent access'
|
1223
|
+
puts '✓ Thread safety benefits outweigh minimal overhead'
|
1224
|
+
puts '✓ Recommended for most lazy initialization scenarios'
|
1225
|
+
elsif avg_ratio > 10
|
1226
|
+
puts '⚠ Only for very expensive initialization (>10ms)'
|
1227
|
+
puts '⚠ When thread safety is absolutely critical'
|
1228
|
+
puts '⚠ Complex dependency chains only'
|
770
1229
|
elsif avg_ratio > 5
|
771
1230
|
puts '• Expensive initialization (>1ms)'
|
772
1231
|
puts '• Multi-threaded applications'
|
@@ -777,9 +1236,21 @@ class LazyInitBenchmark
|
|
777
1236
|
puts '• Clean dependency management needed'
|
778
1237
|
end
|
779
1238
|
|
780
|
-
puts "\
|
781
|
-
puts
|
782
|
-
|
1239
|
+
puts "\nKey Findings:"
|
1240
|
+
puts "• Hot path overhead is minimal (#{hot_path_ratio}x)" if hot_path_ratio && hot_path_ratio < 2.0
|
1241
|
+
|
1242
|
+
thread_safety = @results['Thread Safety']
|
1243
|
+
if thread_safety
|
1244
|
+
concurrent_ratio = thread_safety['Concurrent Access (10 threads)']&.[](:ratio)
|
1245
|
+
puts '• Good performance under concurrent access' if concurrent_ratio && concurrent_ratio < 3.0
|
1246
|
+
end
|
1247
|
+
|
1248
|
+
puts '• Trade-off: initialization cost vs runtime safety'
|
1249
|
+
puts '• Consider cold start impact for short-lived processes'
|
1250
|
+
|
1251
|
+
puts "\n" + '=' * 50
|
1252
|
+
puts 'BENCHMARK COMPLETED (Fixed Methodology)'
|
1253
|
+
puts '=' * 50
|
783
1254
|
end
|
784
1255
|
end
|
785
1256
|
|
@@ -790,7 +1261,7 @@ if __FILE__ == $0
|
|
790
1261
|
benchmark.run_all
|
791
1262
|
rescue StandardError => e
|
792
1263
|
puts "Benchmark failed: #{e.message}"
|
793
|
-
puts e.backtrace.first(
|
1264
|
+
puts e.backtrace.first(10)
|
794
1265
|
exit 1
|
795
1266
|
end
|
796
1267
|
end
|