jekyll-minifier 0.2.0 → 0.2.1

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.
@@ -0,0 +1,238 @@
1
+ require 'spec_helper'
2
+ require 'benchmark'
3
+
4
+ describe "Jekyll::Minifier Caching Performance" do
5
+ let(:config) { Jekyll::Minifier::CompressionConfig.new({}) }
6
+ let(:factory) { Jekyll::Minifier::CompressorFactory }
7
+ let(:cache) { Jekyll::Minifier::CompressorCache }
8
+
9
+ before(:each) do
10
+ cache.clear_all
11
+ end
12
+
13
+ after(:all) do
14
+ Jekyll::Minifier::CompressorCache.clear_all
15
+ end
16
+
17
+ describe "compressor creation performance" do
18
+ it "demonstrates significant performance improvement with caching" do
19
+ iterations = 50
20
+
21
+ # Benchmark without caching (clear cache each time)
22
+ time_without_caching = Benchmark.realtime do
23
+ iterations.times do
24
+ cache.clear_all
25
+ factory.create_css_compressor(config)
26
+ factory.create_js_compressor(config)
27
+ factory.create_html_compressor(config)
28
+ end
29
+ end
30
+
31
+ # Benchmark with caching
32
+ cache.clear_all
33
+ time_with_caching = Benchmark.realtime do
34
+ iterations.times do
35
+ factory.create_css_compressor(config)
36
+ factory.create_js_compressor(config)
37
+ factory.create_html_compressor(config)
38
+ end
39
+ end
40
+
41
+ puts "\nCaching Performance Results:"
42
+ puts "Without caching: #{(time_without_caching * 1000).round(2)}ms (#{(time_without_caching * 1000 / iterations).round(2)}ms per iteration)"
43
+ puts "With caching: #{(time_with_caching * 1000).round(2)}ms (#{(time_with_caching * 1000 / iterations).round(2)}ms per iteration)"
44
+
45
+ improvement_ratio = time_without_caching / time_with_caching
46
+ puts "Performance improvement: #{improvement_ratio.round(2)}x faster"
47
+
48
+ # Cache should show high hit ratio
49
+ stats = cache.stats
50
+ puts "Cache hit ratio: #{(cache.hit_ratio * 100).round(1)}%"
51
+ puts "Cache statistics: #{stats}"
52
+
53
+ # Verify significant performance improvement
54
+ expect(improvement_ratio).to be > 2.0, "Caching should provide at least 2x performance improvement"
55
+ expect(cache.hit_ratio).to be > 0.8, "Cache hit ratio should be above 80%"
56
+ end
57
+
58
+ it "shows memory efficiency with reasonable cache size" do
59
+ # Create many different configurations
60
+ 20.times do |i|
61
+ test_config = Jekyll::Minifier::CompressionConfig.new({
62
+ 'jekyll-minifier' => {
63
+ 'terser_args' => { 'compress' => (i % 2 == 0), 'mangle' => (i % 3 == 0) }
64
+ }
65
+ })
66
+
67
+ factory.create_css_compressor(test_config)
68
+ factory.create_js_compressor(test_config)
69
+ factory.create_html_compressor(test_config)
70
+ end
71
+
72
+ sizes = cache.cache_sizes
73
+ puts "\nMemory Efficiency Results:"
74
+ puts "Cache sizes: #{sizes}"
75
+
76
+ # Verify cache size limits are respected
77
+ expect(sizes[:css]).to be <= Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE
78
+ expect(sizes[:js]).to be <= Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE
79
+ expect(sizes[:html]).to be <= Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE
80
+ expect(sizes[:total]).to be <= Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE * 3
81
+ end
82
+
83
+ it "demonstrates compression performance with cached compressors" do
84
+ css_content = "body { color: red; background-color: blue; margin: 10px; padding: 5px; }"
85
+ js_content = "function test() { var message = 'hello world'; console.log(message); return message; }"
86
+ html_content = "<html><head><title>Test</title></head><body><h1>Test</h1><p>Content</p></body></html>"
87
+
88
+ iterations = 30
89
+
90
+ # Benchmark compression performance with fresh compressors
91
+ cache.clear_all
92
+ time_without_cache = Benchmark.realtime do
93
+ iterations.times do
94
+ cache.clear_all
95
+ factory.compress_css(css_content, config, "test.css")
96
+ factory.compress_js(js_content, config, "test.js")
97
+ end
98
+ end
99
+
100
+ # Benchmark compression performance with cached compressors
101
+ cache.clear_all
102
+ time_with_cache = Benchmark.realtime do
103
+ iterations.times do
104
+ factory.compress_css(css_content, config, "test.css")
105
+ factory.compress_js(js_content, config, "test.js")
106
+ end
107
+ end
108
+
109
+ puts "\nCompression Performance Results:"
110
+ puts "Without cache: #{(time_without_cache * 1000).round(2)}ms"
111
+ puts "With cache: #{(time_with_cache * 1000).round(2)}ms"
112
+
113
+ improvement_ratio = time_without_cache / time_with_cache
114
+ puts "Compression improvement: #{improvement_ratio.round(2)}x faster"
115
+
116
+ # Verify compression performance improvement
117
+ expect(improvement_ratio).to be > 1.5, "Caching should improve compression performance by at least 50%"
118
+ end
119
+
120
+ it "maintains thread safety under concurrent load" do
121
+ threads = []
122
+ errors = []
123
+ iterations_per_thread = 10
124
+ thread_count = 5
125
+
126
+ cache.clear_all
127
+
128
+ # Create multiple threads performing compression
129
+ thread_count.times do |t|
130
+ threads << Thread.new do
131
+ begin
132
+ iterations_per_thread.times do |i|
133
+ config_data = {
134
+ 'jekyll-minifier' => {
135
+ 'terser_args' => { 'compress' => ((t + i) % 2 == 0) }
136
+ }
137
+ }
138
+ test_config = Jekyll::Minifier::CompressionConfig.new(config_data)
139
+
140
+ compressor = factory.create_js_compressor(test_config)
141
+ result = compressor.compile("function test() { return true; }")
142
+
143
+ Thread.current[:results] = (Thread.current[:results] || []) << result
144
+ end
145
+ rescue => e
146
+ errors << e
147
+ end
148
+ end
149
+ end
150
+
151
+ # Wait for completion
152
+ threads.each(&:join)
153
+
154
+ # Verify no errors occurred
155
+ expect(errors).to be_empty, "No thread safety errors should occur: #{errors.inspect}"
156
+
157
+ # Verify all threads got results
158
+ total_results = threads.sum { |t| (t[:results] || []).length }
159
+ expect(total_results).to eq(thread_count * iterations_per_thread)
160
+
161
+ puts "\nThread Safety Results:"
162
+ puts "Threads: #{thread_count}, Iterations per thread: #{iterations_per_thread}"
163
+ puts "Total operations: #{total_results}"
164
+ puts "Errors: #{errors.length}"
165
+ puts "Final cache stats: #{cache.stats}"
166
+ end
167
+ end
168
+
169
+ describe "cache behavior validation" do
170
+ it "properly limits cache size and demonstrates eviction capability" do
171
+ max_size = Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE
172
+
173
+ # Test cache size limiting by creating configurations we know will be different
174
+ # Use direct cache interface to verify behavior
175
+ test_objects = []
176
+ (1..(max_size + 3)).each do |i|
177
+ cache_key = "test_key_#{i}"
178
+ obj = cache.get_or_create(:css, cache_key) { "test_object_#{i}" }
179
+ test_objects << obj
180
+ end
181
+
182
+ puts "\nDirect Cache Test Results:"
183
+ puts "Created #{test_objects.length} objects"
184
+ puts "Cache sizes: #{cache.cache_sizes}"
185
+ puts "Cache stats: #{cache.stats}"
186
+
187
+ # Verify cache respects size limits
188
+ expect(cache.cache_sizes[:css]).to eq(max_size)
189
+ expect(cache.stats[:evictions]).to be > 0
190
+ expect(test_objects.length).to eq(max_size + 3)
191
+
192
+ # Test that early entries were evicted
193
+ first_key_result = cache.get_or_create(:css, "test_key_1") { "recreated_object_1" }
194
+ expect(first_key_result).to eq("recreated_object_1") # Should be recreated, not cached
195
+
196
+ puts "LRU Eviction confirmed: first entry was evicted and recreated"
197
+ end
198
+
199
+ it "correctly identifies cache hits vs misses" do
200
+ config1 = Jekyll::Minifier::CompressionConfig.new({
201
+ 'jekyll-minifier' => { 'terser_args' => { 'compress' => true } }
202
+ })
203
+ config2 = Jekyll::Minifier::CompressionConfig.new({
204
+ 'jekyll-minifier' => { 'terser_args' => { 'compress' => false } }
205
+ })
206
+
207
+ cache.clear_all
208
+
209
+ # First access - should be miss
210
+ factory.create_js_compressor(config1)
211
+ stats1 = cache.stats
212
+
213
+ # Second access same config - should be hit
214
+ factory.create_js_compressor(config1)
215
+ stats2 = cache.stats
216
+
217
+ # Third access different config - should be miss
218
+ factory.create_js_compressor(config2)
219
+ stats3 = cache.stats
220
+
221
+ # Fourth access first config - should be hit
222
+ factory.create_js_compressor(config1)
223
+ stats4 = cache.stats
224
+
225
+ puts "\nCache Hit/Miss Tracking:"
226
+ puts "After 1st call (config1): hits=#{stats1[:hits]}, misses=#{stats1[:misses]}"
227
+ puts "After 2nd call (config1): hits=#{stats2[:hits]}, misses=#{stats2[:misses]}"
228
+ puts "After 3rd call (config2): hits=#{stats3[:hits]}, misses=#{stats3[:misses]}"
229
+ puts "After 4th call (config1): hits=#{stats4[:hits]}, misses=#{stats4[:misses]}"
230
+
231
+ expect(stats1[:misses]).to eq(1)
232
+ expect(stats1[:hits]).to eq(0)
233
+ expect(stats2[:hits]).to eq(1)
234
+ expect(stats3[:misses]).to eq(2)
235
+ expect(stats4[:hits]).to eq(2)
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,326 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Jekyll::Minifier::CompressorCache" do
4
+ let(:cache) { Jekyll::Minifier::CompressorCache }
5
+
6
+ before(:each) do
7
+ # Clear cache before each test
8
+ cache.clear_all
9
+ end
10
+
11
+ after(:all) do
12
+ # Clean up after all tests
13
+ Jekyll::Minifier::CompressorCache.clear_all
14
+ end
15
+
16
+ describe "cache key generation" do
17
+ it "generates consistent keys for identical configurations" do
18
+ config1 = { terser_args: { compress: true, mangle: false } }
19
+ config2 = { terser_args: { compress: true, mangle: false } }
20
+
21
+ key1 = cache.generate_cache_key(config1)
22
+ key2 = cache.generate_cache_key(config2)
23
+
24
+ expect(key1).to eq(key2)
25
+ expect(key1).to be_a(String)
26
+ expect(key1.length).to eq(17) # SHA256 truncated to 16 chars + null terminator handling
27
+ end
28
+
29
+ it "generates different keys for different configurations" do
30
+ config1 = { terser_args: { compress: true, mangle: false } }
31
+ config2 = { terser_args: { compress: false, mangle: true } }
32
+
33
+ key1 = cache.generate_cache_key(config1)
34
+ key2 = cache.generate_cache_key(config2)
35
+
36
+ expect(key1).not_to eq(key2)
37
+ end
38
+
39
+ it "handles nil and empty configurations" do
40
+ expect(cache.generate_cache_key(nil)).to eq('default')
41
+ expect(cache.generate_cache_key({})).to eq('default')
42
+ end
43
+ end
44
+
45
+ describe "caching functionality" do
46
+ it "caches and retrieves compressor objects" do
47
+ call_count = 0
48
+
49
+ # First call should create new object
50
+ obj1 = cache.get_or_create(:js, "test_key") do
51
+ call_count += 1
52
+ "mock_compressor_#{call_count}"
53
+ end
54
+
55
+ # Second call should retrieve cached object
56
+ obj2 = cache.get_or_create(:js, "test_key") do
57
+ call_count += 1
58
+ "mock_compressor_#{call_count}"
59
+ end
60
+
61
+ expect(obj1).to eq(obj2)
62
+ expect(call_count).to eq(1) # Factory block called only once
63
+ expect(obj1).to eq("mock_compressor_1")
64
+ end
65
+
66
+ it "maintains separate caches for different types" do
67
+ css_obj = cache.get_or_create(:css, "key1") { "css_compressor" }
68
+ js_obj = cache.get_or_create(:js, "key1") { "js_compressor" }
69
+ html_obj = cache.get_or_create(:html, "key1") { "html_compressor" }
70
+
71
+ expect(css_obj).to eq("css_compressor")
72
+ expect(js_obj).to eq("js_compressor")
73
+ expect(html_obj).to eq("html_compressor")
74
+
75
+ # Each should be independent
76
+ expect(css_obj).not_to eq(js_obj)
77
+ expect(js_obj).not_to eq(html_obj)
78
+ end
79
+
80
+ it "implements LRU eviction when cache is full" do
81
+ # Fill cache to capacity
82
+ (1..Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE).each do |i|
83
+ cache.get_or_create(:js, "key_#{i}") { "compressor_#{i}" }
84
+ end
85
+
86
+ expect(cache.cache_sizes[:js]).to eq(Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE)
87
+
88
+ # Add one more - should evict oldest
89
+ cache.get_or_create(:js, "new_key") { "new_compressor" }
90
+
91
+ expect(cache.cache_sizes[:js]).to eq(Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE)
92
+ expect(cache.stats[:evictions]).to eq(1)
93
+
94
+ # First key should be evicted
95
+ call_count = 0
96
+ cache.get_or_create(:js, "key_1") do
97
+ call_count += 1
98
+ "recreated_compressor"
99
+ end
100
+
101
+ expect(call_count).to eq(1) # Had to recreate
102
+ end
103
+ end
104
+
105
+ describe "statistics tracking" do
106
+ it "tracks cache hits and misses" do
107
+ initial_stats = cache.stats
108
+ expect(initial_stats[:hits]).to eq(0)
109
+ expect(initial_stats[:misses]).to eq(0)
110
+
111
+ # First access - should be miss
112
+ cache.get_or_create(:css, "test") { "compressor" }
113
+ stats_after_miss = cache.stats
114
+ expect(stats_after_miss[:misses]).to eq(1)
115
+ expect(stats_after_miss[:hits]).to eq(0)
116
+
117
+ # Second access - should be hit
118
+ cache.get_or_create(:css, "test") { "compressor" }
119
+ stats_after_hit = cache.stats
120
+ expect(stats_after_hit[:misses]).to eq(1)
121
+ expect(stats_after_hit[:hits]).to eq(1)
122
+ end
123
+
124
+ it "calculates hit ratio correctly" do
125
+ expect(cache.hit_ratio).to eq(0.0) # No operations yet
126
+
127
+ # One miss
128
+ cache.get_or_create(:css, "test1") { "comp1" }
129
+ expect(cache.hit_ratio).to eq(0.0)
130
+
131
+ # One hit
132
+ cache.get_or_create(:css, "test1") { "comp1" }
133
+ expect(cache.hit_ratio).to eq(0.5)
134
+
135
+ # Another hit
136
+ cache.get_or_create(:css, "test1") { "comp1" }
137
+ expect(cache.hit_ratio).to be_within(0.01).of(0.67)
138
+ end
139
+ end
140
+
141
+ describe "thread safety" do
142
+ it "handles concurrent access safely" do
143
+ threads = []
144
+ results = {}
145
+
146
+ # Create multiple threads accessing cache concurrently
147
+ 10.times do |i|
148
+ threads << Thread.new do
149
+ key = "concurrent_key_#{i % 3}" # Use some duplicate keys
150
+ result = cache.get_or_create(:js, key) { "compressor_#{key}" }
151
+ Thread.current[:result] = result
152
+ end
153
+ end
154
+
155
+ # Wait for all threads to complete
156
+ threads.each(&:join)
157
+
158
+ # Collect results
159
+ threads.each_with_index do |thread, i|
160
+ results[i] = thread[:result]
161
+ end
162
+
163
+ # Verify no race conditions occurred
164
+ expect(results.values.uniq.length).to eq(3) # Should have 3 unique compressors
165
+ expect(cache.cache_sizes[:js]).to eq(3)
166
+ end
167
+ end
168
+
169
+ describe "memory management" do
170
+ it "limits cache size appropriately" do
171
+ # Add more than max cache size
172
+ (1..(Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE + 5)).each do |i|
173
+ cache.get_or_create(:css, "key_#{i}") { "compressor_#{i}" }
174
+ end
175
+
176
+ sizes = cache.cache_sizes
177
+ expect(sizes[:css]).to eq(Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE)
178
+ expect(sizes[:js]).to eq(0)
179
+ expect(sizes[:html]).to eq(0)
180
+ expect(sizes[:total]).to eq(Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE)
181
+ end
182
+
183
+ it "clears all caches completely" do
184
+ # Add some data to each cache
185
+ cache.get_or_create(:css, "css_key") { "css_comp" }
186
+ cache.get_or_create(:js, "js_key") { "js_comp" }
187
+ cache.get_or_create(:html, "html_key") { "html_comp" }
188
+
189
+ expect(cache.cache_sizes[:total]).to eq(3)
190
+
191
+ cache.clear_all
192
+
193
+ expect(cache.cache_sizes[:total]).to eq(0)
194
+ expect(cache.stats[:hits]).to eq(0)
195
+ expect(cache.stats[:misses]).to eq(0)
196
+ expect(cache.stats[:evictions]).to eq(0)
197
+ end
198
+ end
199
+ end
200
+
201
+ describe "Jekyll::Minifier::CompressorFactory with Caching" do
202
+ let(:config) { Jekyll::Minifier::CompressionConfig.new({}) }
203
+ let(:factory) { Jekyll::Minifier::CompressorFactory }
204
+ let(:cache) { Jekyll::Minifier::CompressorCache }
205
+
206
+ before(:each) do
207
+ cache.clear_all
208
+ end
209
+
210
+ after(:all) do
211
+ Jekyll::Minifier::CompressorCache.clear_all
212
+ end
213
+
214
+ describe "CSS compressor caching" do
215
+ it "caches CSS compressors based on configuration" do
216
+ initial_stats = cache.stats
217
+
218
+ # First call should create new compressor
219
+ comp1 = factory.create_css_compressor(config)
220
+ stats_after_first = cache.stats
221
+ expect(stats_after_first[:misses]).to eq(initial_stats[:misses] + 1)
222
+
223
+ # Second call with same config should return cached compressor
224
+ comp2 = factory.create_css_compressor(config)
225
+ stats_after_second = cache.stats
226
+ expect(stats_after_second[:hits]).to eq(initial_stats[:hits] + 1)
227
+
228
+ # Should be the same object
229
+ expect(comp1).to be(comp2)
230
+ end
231
+
232
+ it "creates different compressors for different configurations" do
233
+ config1 = Jekyll::Minifier::CompressionConfig.new({
234
+ 'jekyll-minifier' => { 'css_enhanced_mode' => false }
235
+ })
236
+ config2 = Jekyll::Minifier::CompressionConfig.new({
237
+ 'jekyll-minifier' => {
238
+ 'css_enhanced_mode' => true,
239
+ 'css_merge_duplicate_selectors' => true
240
+ }
241
+ })
242
+
243
+ comp1 = factory.create_css_compressor(config1)
244
+ comp2 = factory.create_css_compressor(config2)
245
+
246
+ # Should be different objects for different configurations
247
+ expect(comp1).not_to be(comp2)
248
+ end
249
+ end
250
+
251
+ describe "JavaScript compressor caching" do
252
+ it "caches JS compressors based on Terser configuration" do
253
+ initial_stats = cache.stats
254
+
255
+ comp1 = factory.create_js_compressor(config)
256
+ stats_after_first = cache.stats
257
+ expect(stats_after_first[:misses]).to be > initial_stats[:misses]
258
+
259
+ comp2 = factory.create_js_compressor(config)
260
+ stats_after_second = cache.stats
261
+ expect(stats_after_second[:hits]).to be > initial_stats[:hits]
262
+
263
+ expect(comp1).to be(comp2)
264
+ end
265
+
266
+ it "creates different compressors for different Terser configurations" do
267
+ config1 = Jekyll::Minifier::CompressionConfig.new({})
268
+ config2 = Jekyll::Minifier::CompressionConfig.new({
269
+ 'jekyll-minifier' => {
270
+ 'terser_args' => { 'compress' => false, 'mangle' => false }
271
+ }
272
+ })
273
+
274
+ comp1 = factory.create_js_compressor(config1)
275
+ comp2 = factory.create_js_compressor(config2)
276
+
277
+ expect(comp1).not_to be(comp2)
278
+ end
279
+ end
280
+
281
+ describe "HTML compressor caching" do
282
+ it "caches HTML compressors based on full configuration" do
283
+ initial_stats = cache.stats
284
+
285
+ comp1 = factory.create_html_compressor(config)
286
+ comp2 = factory.create_html_compressor(config)
287
+
288
+ final_stats = cache.stats
289
+ expect(final_stats[:hits]).to be > initial_stats[:hits]
290
+ expect(comp1).to be(comp2)
291
+ end
292
+ end
293
+
294
+ describe "integration with compression methods" do
295
+ it "benefits from caching in CSS compression" do
296
+ css_content = "body { color: red; background-color: blue; }"
297
+
298
+ cache.clear_all
299
+ initial_stats = cache.stats
300
+
301
+ # First compression
302
+ result1 = factory.compress_css(css_content, config, "test1.css")
303
+
304
+ # Second compression
305
+ result2 = factory.compress_css(css_content, config, "test2.css")
306
+
307
+ final_stats = cache.stats
308
+ expect(final_stats[:hits]).to be > initial_stats[:hits]
309
+ expect(result1).to eq(result2) # Same compression result
310
+ end
311
+
312
+ it "benefits from caching in JS compression" do
313
+ js_content = "function test() { return 'hello world'; }"
314
+
315
+ cache.clear_all
316
+ initial_stats = cache.stats
317
+
318
+ result1 = factory.compress_js(js_content, config, "test1.js")
319
+ result2 = factory.compress_js(js_content, config, "test2.js")
320
+
321
+ final_stats = cache.stats
322
+ expect(final_stats[:hits]).to be > initial_stats[:hits]
323
+ expect(result1).to eq(result2)
324
+ end
325
+ end
326
+ end