lazy_init 0.1.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 +7 -0
- data/.gitignore +20 -0
- data/.rspec +4 -0
- data/CHANGELOG.md +0 -0
- data/GEMFILE +5 -0
- data/LICENSE +21 -0
- data/RAKEFILE +43 -0
- data/README.md +765 -0
- data/benchmarks/benchmark.rb +796 -0
- data/benchmarks/benchmark_performance.rb +250 -0
- data/benchmarks/benchmark_threads.rb +433 -0
- data/benchmarks/bottleneck_searcher.rb +381 -0
- data/benchmarks/thread_safety_verification.rb +376 -0
- data/lazy_init.gemspec +40 -0
- data/lib/lazy_init/class_methods.rb +549 -0
- data/lib/lazy_init/configuration.rb +57 -0
- data/lib/lazy_init/dependency_resolver.rb +226 -0
- data/lib/lazy_init/errors.rb +23 -0
- data/lib/lazy_init/instance_methods.rb +291 -0
- data/lib/lazy_init/lazy_value.rb +167 -0
- data/lib/lazy_init/version.rb +5 -0
- data/lib/lazy_init.rb +47 -0
- metadata +140 -0
@@ -0,0 +1,381 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'benchmark/ips'
|
5
|
+
require_relative '../lib/lazy_init'
|
6
|
+
|
7
|
+
puts '=== DOGŁĘBNA ANALIZA BOTTLENECKÓW LAZYINIT ==='
|
8
|
+
puts "Ruby version: #{RUBY_VERSION}"
|
9
|
+
|
10
|
+
# === STEP 1: MICRO-BENCHMARK OF EACH ELEMENT ===
|
11
|
+
|
12
|
+
def micro_benchmark(description, &block)
|
13
|
+
puts "\n--- #{description} ---"
|
14
|
+
|
15
|
+
result = Benchmark.ips { |x| x.report('test', &block) }.entries.first.ips
|
16
|
+
puts " #{description}: #{format_ips(result)}"
|
17
|
+
result
|
18
|
+
end
|
19
|
+
|
20
|
+
def format_ips(ips)
|
21
|
+
if ips >= 1_000_000
|
22
|
+
"#{(ips / 1_000_000.0).round(2)}M i/s"
|
23
|
+
elsif ips >= 1_000
|
24
|
+
"#{(ips / 1_000.0).round(1)}K i/s"
|
25
|
+
else
|
26
|
+
"#{ips.round(0)} i/s"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
puts "\n" + '=' * 80
|
31
|
+
puts 'STEP 1: ISOLATING INDIVIDUAL COMPONENTS'
|
32
|
+
puts '=' * 80
|
33
|
+
|
34
|
+
# 1.1 Direct instance variable access (baseline)
|
35
|
+
class DirectVar
|
36
|
+
def initialize
|
37
|
+
@value = 'computed_value'
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_value
|
41
|
+
@value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
direct = DirectVar.new
|
46
|
+
baseline = micro_benchmark('1.1 Direct @value access') { direct.get_value }
|
47
|
+
|
48
|
+
# 1.2 Manual ||= pattern
|
49
|
+
class ManualPattern
|
50
|
+
def get_value
|
51
|
+
@value ||= 'computed_value'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
manual = ManualPattern.new
|
56
|
+
manual.get_value # warm up
|
57
|
+
manual_result = micro_benchmark('1.2 Manual @value ||= (warm)') { manual.get_value }
|
58
|
+
|
59
|
+
# 1.3 LazyValue object access (isolated)
|
60
|
+
lazy_value = LazyInit::LazyValue.new { 'computed_value' }
|
61
|
+
lazy_value.value # warm up
|
62
|
+
lazy_value_result = micro_benchmark('1.3 LazyValue.value (warm)') { lazy_value.value }
|
63
|
+
|
64
|
+
# 1.4 Method dispatch przez generated method
|
65
|
+
class GeneratedMethodTest
|
66
|
+
extend LazyInit
|
67
|
+
lazy_attr_reader :test_value do
|
68
|
+
'computed_value'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
generated = GeneratedMethodTest.new
|
73
|
+
generated.test_value # warm up
|
74
|
+
generated_result = micro_benchmark('1.4 Generated method (warm)') { generated.test_value }
|
75
|
+
|
76
|
+
puts "\n" + '=' * 50
|
77
|
+
puts 'STEP 1 ANALYSIS:'
|
78
|
+
puts '=' * 50
|
79
|
+
puts "Baseline (direct @var): #{format_ips(baseline)}"
|
80
|
+
puts "Manual ||=: #{format_ips(manual_result)} (#{(baseline / manual_result).round(2)}x slower)"
|
81
|
+
puts "LazyValue only: #{format_ips(lazy_value_result)} (#{(baseline / lazy_value_result).round(2)}x slower)"
|
82
|
+
puts "Generated method: #{format_ips(generated_result)} (#{(baseline / generated_result).round(2)}x slower)"
|
83
|
+
|
84
|
+
# === STEP 2: DECOMPOSITION OF GENERATED METHOD ===
|
85
|
+
|
86
|
+
puts "\n" + '=' * 80
|
87
|
+
puts 'STEP 2: DECOMPOSITION GENERATED METHOD CALL'
|
88
|
+
puts '=' * 80
|
89
|
+
|
90
|
+
class DecompositionTest
|
91
|
+
extend LazyInit
|
92
|
+
|
93
|
+
lazy_attr_reader :full_method do
|
94
|
+
'computed_value'
|
95
|
+
end
|
96
|
+
|
97
|
+
def initialize
|
98
|
+
@manual_lazy_value = LazyInit::LazyValue.new { 'computed_value' }
|
99
|
+
@manual_lazy_value.value # warm up
|
100
|
+
end
|
101
|
+
|
102
|
+
def step1_config_lookup
|
103
|
+
self.class.lazy_initializers[:full_method]
|
104
|
+
end
|
105
|
+
|
106
|
+
def step2_dependency_check
|
107
|
+
config = self.class.lazy_initializers[:full_method]
|
108
|
+
config[:depends_on] # nil in this case
|
109
|
+
end
|
110
|
+
|
111
|
+
def step3_ivar_access
|
112
|
+
instance_variable_get(:@full_method_lazy_value)
|
113
|
+
end
|
114
|
+
|
115
|
+
def step4_lazy_value_call
|
116
|
+
@manual_lazy_value.value
|
117
|
+
end
|
118
|
+
|
119
|
+
def step5_full_manual_replication
|
120
|
+
config = self.class.lazy_initializers[:full_method]
|
121
|
+
|
122
|
+
# Skip dependency resolution if no dependencies
|
123
|
+
# if config[:depends_on]
|
124
|
+
# self.class.dependency_resolver.resolve_dependencies(name, self)
|
125
|
+
# end
|
126
|
+
|
127
|
+
ivar_name = :@full_method_lazy_value
|
128
|
+
lazy_value = instance_variable_get(ivar_name)
|
129
|
+
|
130
|
+
unless lazy_value
|
131
|
+
lazy_value = LazyInit::LazyValue.new(timeout: config[:timeout]) do
|
132
|
+
instance_eval(&config[:block])
|
133
|
+
end
|
134
|
+
instance_variable_set(ivar_name, lazy_value)
|
135
|
+
end
|
136
|
+
|
137
|
+
lazy_value.value
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
decomp = DecompositionTest.new
|
142
|
+
decomp.full_method # warm up
|
143
|
+
|
144
|
+
step1 = micro_benchmark('2.1 Config hash lookup') { decomp.step1_config_lookup }
|
145
|
+
step2 = micro_benchmark('2.2 Dependency check') { decomp.step2_dependency_check }
|
146
|
+
step3 = micro_benchmark('2.3 Instance var get') { decomp.step3_ivar_access }
|
147
|
+
step4 = micro_benchmark('2.4 LazyValue.value call') { decomp.step4_lazy_value_call }
|
148
|
+
step5 = micro_benchmark('2.5 Full manual replication') { decomp.step5_full_manual_replication }
|
149
|
+
full_method = micro_benchmark('2.6 Actual generated method') { decomp.full_method }
|
150
|
+
|
151
|
+
puts "\n" + '=' * 50
|
152
|
+
puts 'STEP 2 ANALYSIS:'
|
153
|
+
puts '=' * 50
|
154
|
+
puts "Config lookup: #{format_ips(step1)} (#{(baseline / step1).round(2)}x slower than baseline)"
|
155
|
+
puts "Dependency check: #{format_ips(step2)} (#{(baseline / step2).round(2)}x slower than baseline)"
|
156
|
+
puts "Instance var get: #{format_ips(step3)} (#{(baseline / step3).round(2)}x slower than baseline)"
|
157
|
+
puts "LazyValue.value: #{format_ips(step4)} (#{(baseline / step4).round(2)}x slower than baseline)"
|
158
|
+
puts "Manual replication: #{format_ips(step5)} (#{(baseline / step5).round(2)}x slower than baseline)"
|
159
|
+
puts "Generated method: #{format_ips(full_method)} (#{(baseline / full_method).round(2)}x slower than baseline)"
|
160
|
+
|
161
|
+
# === STEP 3: LAZYVALUE INTERNAL BOTTLENECKS ===
|
162
|
+
|
163
|
+
puts "\n" + '=' * 80
|
164
|
+
puts 'STEP 3: LAZYVALUE INTERNAL ANALYSIS'
|
165
|
+
puts '=' * 80
|
166
|
+
|
167
|
+
# Analiza LazyValue.value method
|
168
|
+
class LazyValueAnalysis
|
169
|
+
def initialize
|
170
|
+
@computed = true
|
171
|
+
@value = 'computed_value'
|
172
|
+
@mutex = Mutex.new
|
173
|
+
end
|
174
|
+
|
175
|
+
# Obecna implementacja (uproszczona)
|
176
|
+
def current_implementation
|
177
|
+
@mutex.synchronize do
|
178
|
+
return @value if @computed
|
179
|
+
# compute...
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Fast path with double-checked locking
|
184
|
+
def fast_path_implementation
|
185
|
+
return @value if @computed
|
186
|
+
|
187
|
+
@mutex.synchronize do
|
188
|
+
return @value if @computed
|
189
|
+
# compute...
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Ultra fast path (tylko check)
|
194
|
+
def ultra_fast_path
|
195
|
+
return @value if @computed
|
196
|
+
end
|
197
|
+
|
198
|
+
# Mutex overhead test
|
199
|
+
def mutex_overhead_test
|
200
|
+
@mutex.synchronize do
|
201
|
+
@value
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
lazy_analysis = LazyValueAnalysis.new
|
207
|
+
|
208
|
+
current = micro_benchmark('3.1 Current LazyValue impl') { lazy_analysis.current_implementation }
|
209
|
+
fast_path = micro_benchmark('3.2 Fast path impl') { lazy_analysis.fast_path_implementation }
|
210
|
+
ultra_fast = micro_benchmark('3.3 Ultra fast (just check)') { lazy_analysis.ultra_fast_path }
|
211
|
+
mutex_overhead = micro_benchmark('3.4 Mutex overhead') { lazy_analysis.mutex_overhead_test }
|
212
|
+
|
213
|
+
puts "\n" + '=' * 50
|
214
|
+
puts 'STEP 3 ANALYSIS:'
|
215
|
+
puts '=' * 50
|
216
|
+
puts "Current (always mutex): #{format_ips(current)} (#{(baseline / current).round(2)}x slower than baseline)"
|
217
|
+
puts "Fast path (skip mutex): #{format_ips(fast_path)} (#{(baseline / fast_path).round(2)}x slower than baseline)"
|
218
|
+
puts "Ultra fast (just check): #{format_ips(ultra_fast)} (#{(baseline / ultra_fast).round(2)}x slower than baseline)"
|
219
|
+
puts "Mutex overhead: #{format_ips(mutex_overhead)} (#{(baseline / mutex_overhead).round(2)}x slower than baseline)"
|
220
|
+
|
221
|
+
# === STEP 4: OPTIMIZED IMPLEMENTATIONS ===
|
222
|
+
|
223
|
+
puts "\n" + '=' * 80
|
224
|
+
puts 'STEP 4: OPTIMIZED IMPLEMENTATION PROTOTYPES'
|
225
|
+
puts '=' * 80
|
226
|
+
|
227
|
+
# Prototype 1: Cached config approach
|
228
|
+
class CachedConfigApproach
|
229
|
+
extend LazyInit
|
230
|
+
|
231
|
+
def self.lazy_attr_reader_optimized(name, &block)
|
232
|
+
config = { block: block, timeout: nil, depends_on: nil }
|
233
|
+
|
234
|
+
# Cache config in class for fast access
|
235
|
+
ivar_name = "@#{name}_lazy_value"
|
236
|
+
|
237
|
+
define_method(name) do
|
238
|
+
lazy_value = instance_variable_get(ivar_name)
|
239
|
+
|
240
|
+
unless lazy_value
|
241
|
+
lazy_value = LazyInit::LazyValue.new(&config[:block])
|
242
|
+
instance_variable_set(ivar_name, lazy_value)
|
243
|
+
end
|
244
|
+
|
245
|
+
lazy_value.value
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
lazy_attr_reader_optimized :optimized_value do
|
250
|
+
'computed_value'
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
cached_config = CachedConfigApproach.new
|
255
|
+
cached_config.optimized_value # warm up
|
256
|
+
cached_result = micro_benchmark('4.1 Cached config approach') { cached_config.optimized_value }
|
257
|
+
|
258
|
+
# Prototype 2: Inline fast path
|
259
|
+
class InlineFastPath
|
260
|
+
def initialize
|
261
|
+
@value = 'computed_value'
|
262
|
+
@computed = true
|
263
|
+
end
|
264
|
+
|
265
|
+
def inline_optimized
|
266
|
+
# Inline the most common path
|
267
|
+
return @value if @computed
|
268
|
+
|
269
|
+
# Fallback to slow path (would be method call)
|
270
|
+
slow_path_computation
|
271
|
+
end
|
272
|
+
|
273
|
+
def slow_path_computation
|
274
|
+
# This would be the full LazyValue logic
|
275
|
+
@value
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
inline_test = InlineFastPath.new
|
280
|
+
inline_result = micro_benchmark('4.2 Inline fast path') { inline_test.inline_optimized }
|
281
|
+
|
282
|
+
# Prototype 3: Specialized generated methods
|
283
|
+
class SpecializedGenerated
|
284
|
+
def initialize
|
285
|
+
@test_value = 'computed_value'
|
286
|
+
@test_value_computed = true
|
287
|
+
end
|
288
|
+
|
289
|
+
# This simulates a highly optimized generated method
|
290
|
+
def specialized_test_value
|
291
|
+
return @test_value if @test_value_computed
|
292
|
+
|
293
|
+
# Slow path would go here
|
294
|
+
@test_value
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
specialized = SpecializedGenerated.new
|
299
|
+
specialized_result = micro_benchmark('4.3 Specialized generated') { specialized.specialized_test_value }
|
300
|
+
|
301
|
+
puts "\n" + '=' * 50
|
302
|
+
puts 'STEP 4 ANALYSIS:'
|
303
|
+
puts '=' * 50
|
304
|
+
puts "Cached config: #{format_ips(cached_result)} (#{(baseline / cached_result).round(2)}x slower than baseline)"
|
305
|
+
puts "Inline fast path: #{format_ips(inline_result)} (#{(baseline / inline_result).round(2)}x slower than baseline)"
|
306
|
+
puts "Specialized generated: #{format_ips(specialized_result)} (#{(baseline / specialized_result).round(2)}x slower than baseline)"
|
307
|
+
|
308
|
+
# === STEP 5: FINAL COMPARISON ===
|
309
|
+
|
310
|
+
puts "\n" + '=' * 80
|
311
|
+
puts 'COMPREHENSIVE BOTTLENECK ANALYSIS'
|
312
|
+
puts '=' * 80
|
313
|
+
|
314
|
+
puts "\nBASELINE COMPARISON:"
|
315
|
+
puts "Direct @var access: #{format_ips(baseline)} (1.0x - baseline)"
|
316
|
+
puts "Manual ||=: #{format_ips(manual_result)} (#{(baseline / manual_result).round(2)}x slower)"
|
317
|
+
puts ''
|
318
|
+
puts 'CURRENT LAZYINIT:'
|
319
|
+
puts "Generated method: #{format_ips(generated_result)} (#{(baseline / generated_result).round(2)}x slower)"
|
320
|
+
puts ''
|
321
|
+
puts 'COMPONENT BREAKDOWN:'
|
322
|
+
puts "Config lookup: #{format_ips(step1)} (#{(baseline / step1).round(2)}x slower)"
|
323
|
+
puts "LazyValue.value: #{format_ips(step4)} (#{(baseline / step4).round(2)}x slower)"
|
324
|
+
puts "Mutex (always sync): #{format_ips(current)} (#{(baseline / current).round(2)}x slower)"
|
325
|
+
puts ''
|
326
|
+
puts 'OPTIMIZATION POTENTIAL:'
|
327
|
+
puts "Fast path LazyValue: #{format_ips(fast_path)} (#{(baseline / fast_path).round(2)}x slower)"
|
328
|
+
puts "Ultra fast path: #{format_ips(ultra_fast)} (#{(baseline / ultra_fast).round(2)}x slower)"
|
329
|
+
puts "Inline optimization: #{format_ips(inline_result)} (#{(baseline / inline_result).round(2)}x slower)"
|
330
|
+
puts "Specialized method: #{format_ips(specialized_result)} (#{(baseline / specialized_result).round(2)}x slower)"
|
331
|
+
|
332
|
+
# === IDENTIFIED BOTTLENECKS ===
|
333
|
+
|
334
|
+
puts "\n" + '=' * 80
|
335
|
+
puts 'IDENTIFIED BOTTLENECKS (RANKED BY IMPACT)'
|
336
|
+
puts '=' * 80
|
337
|
+
|
338
|
+
bottlenecks = [
|
339
|
+
{
|
340
|
+
name: 'Config Hash Lookup',
|
341
|
+
impact: baseline / step1,
|
342
|
+
description: 'lazy_initializers[name] lookup w każdym call',
|
343
|
+
fix: 'Cache config at method generation time'
|
344
|
+
},
|
345
|
+
{
|
346
|
+
name: 'Mutex Synchronization',
|
347
|
+
impact: baseline / current,
|
348
|
+
description: 'Always synchronize, even for computed values',
|
349
|
+
fix: 'Fast path with double-checked locking'
|
350
|
+
},
|
351
|
+
{
|
352
|
+
name: 'LazyValue Object Overhead',
|
353
|
+
impact: baseline / step4,
|
354
|
+
description: 'Extra object layer adds method dispatch cost',
|
355
|
+
fix: 'Inline critical path in generated methods'
|
356
|
+
},
|
357
|
+
{
|
358
|
+
name: 'Instance Variable Access Pattern',
|
359
|
+
impact: baseline / step3,
|
360
|
+
description: 'instance_variable_get instead of direct @var',
|
361
|
+
fix: 'Use direct @var access in generated methods'
|
362
|
+
}
|
363
|
+
]
|
364
|
+
|
365
|
+
bottlenecks.sort_by { |b| -b[:impact] }.each_with_index do |bottleneck, i|
|
366
|
+
puts "\n#{i + 1}. #{bottleneck[:name]} (#{bottleneck[:impact].round(2)}x impact)"
|
367
|
+
puts " Problem: #{bottleneck[:description]}"
|
368
|
+
puts " Fix: #{bottleneck[:fix]}"
|
369
|
+
end
|
370
|
+
|
371
|
+
theoretical_optimized = baseline / 1.5 # Theoretical 1.5x slower than baseline
|
372
|
+
current_gap = baseline / generated_result
|
373
|
+
|
374
|
+
puts "\nPERFORMANCE TARGETS:"
|
375
|
+
puts "Current performance: #{format_ips(generated_result)} (#{current_gap.round(2)}x slower than baseline)"
|
376
|
+
puts "Realistic target: #{format_ips(theoretical_optimized)} (1.5x slower than baseline)"
|
377
|
+
puts "Improvement needed: #{(generated_result / theoretical_optimized).round(2)}x faster"
|
378
|
+
|
379
|
+
puts "\n" + '=' * 80
|
380
|
+
puts 'ANALYSIS COMPLETED'
|
381
|
+
puts '=' * 80
|