snakommit 0.1.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,328 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+
5
+ module Snakommit
6
+ # Performance optimization utilities for Snakommit
7
+ class Performance
8
+ # Cache for expensive Git operations
9
+ class Cache
10
+ # Initialize a new cache
11
+ # @param max_size [Integer] Maximum number of items to cache
12
+ # @param ttl [Integer] Time-to-live in seconds for cached items
13
+ def initialize(max_size = 100, ttl = 300) # 5 minutes TTL by default
14
+ @cache = {}
15
+ @max_size = max_size
16
+ @ttl = ttl
17
+ @hits = 0
18
+ @misses = 0
19
+ end
20
+
21
+ # Get a value from the cache
22
+ # @param key [Object] Cache key
23
+ # @return [Object, nil] Cached value or nil if not found or expired
24
+ def get(key)
25
+ return nil unless @cache.key?(key)
26
+ entry = @cache[key]
27
+
28
+ if Time.now - entry[:timestamp] > @ttl
29
+ @misses += 1
30
+ return nil
31
+ end
32
+
33
+ @hits += 1
34
+ entry[:value]
35
+ end
36
+
37
+ # Set a value in the cache
38
+ # @param key [Object] Cache key
39
+ # @param value [Object] Value to cache
40
+ # @return [Object] The value that was cached
41
+ def set(key, value)
42
+ cleanup if @cache.size >= @max_size
43
+ @cache[key] = { value: value, timestamp: Time.now }
44
+ @misses += 1
45
+ value
46
+ end
47
+
48
+ # Remove a specific key from the cache
49
+ # @param key [Object] Cache key to invalidate
50
+ # @return [nil]
51
+ def invalidate(key)
52
+ @cache.delete(key)
53
+ nil
54
+ end
55
+
56
+ # Clear the entire cache
57
+ # @return [Hash] Empty hash
58
+ def clear
59
+ @cache = {}
60
+ end
61
+
62
+ # Get cache stats
63
+ # @return [Hash] Cache statistics
64
+ def stats
65
+ {
66
+ size: @cache.size,
67
+ max_size: @max_size,
68
+ ttl: @ttl,
69
+ hits: @hits,
70
+ misses: @misses,
71
+ hit_rate: hit_rate
72
+ }
73
+ end
74
+
75
+ # Calculate cache hit rate
76
+ # @return [Float] Cache hit rate as a percentage
77
+ def hit_rate
78
+ total = @hits + @misses
79
+ return 0.0 if total.zero?
80
+ (@hits.to_f / total) * 100
81
+ end
82
+
83
+ private
84
+
85
+ # Clean up expired or oldest entries when cache is full
86
+ # @return [nil]
87
+ def cleanup
88
+ # Remove expired entries first
89
+ expired_keys = @cache.select { |_, v| Time.now - v[:timestamp] > @ttl }.keys
90
+ @cache.delete_if { |k, _| expired_keys.include?(k) }
91
+
92
+ # If still too large, remove oldest entries
93
+ if @cache.size >= @max_size
94
+ sorted_keys = @cache.sort_by { |_, v| v[:timestamp] }.map(&:first)
95
+ sorted_keys[0...(@cache.size - @max_size / 2)].each { |k| @cache.delete(k) }
96
+ end
97
+
98
+ nil
99
+ end
100
+ end
101
+
102
+ # Batch processing for Git operations
103
+ class BatchProcessor
104
+ # Initialize a new batch processor
105
+ # @param batch_size [Integer] Default batch size for processing
106
+ def initialize(batch_size = 100)
107
+ @batch_size = batch_size
108
+ @total_processed = 0
109
+ @batch_count = 0
110
+ end
111
+
112
+ # Process files in batches
113
+ # @param files [Array<String>] List of files to process
114
+ # @param batch_size [Integer, nil] Optional override for batch size
115
+ # @yield [batch] Yields each batch of files for processing
116
+ # @yieldparam batch [Array<String>] A batch of files
117
+ # @return [Array] Combined results from all batches
118
+ def process_files(files, batch_size = nil, &block)
119
+ size = batch_size || @batch_size
120
+ results = []
121
+
122
+ files.each_slice(size).each_with_index do |batch, index|
123
+ @batch_count += 1
124
+ batch_result = block.call(batch)
125
+ @total_processed += batch.size
126
+ results.concat(Array(batch_result))
127
+ end
128
+
129
+ results
130
+ end
131
+
132
+ # Get batch processing stats
133
+ # @return [Hash] Batch processing statistics
134
+ def stats
135
+ {
136
+ batch_size: @batch_size,
137
+ total_processed: @total_processed,
138
+ batch_count: @batch_count,
139
+ average_batch_size: average_batch_size
140
+ }
141
+ end
142
+
143
+ # Calculate average batch size
144
+ # @return [Float] Average batch size
145
+ def average_batch_size
146
+ return 0.0 if @batch_count.zero?
147
+ @total_processed.to_f / @batch_count
148
+ end
149
+ end
150
+
151
+ # Helper for parallel processing where appropriate
152
+ class ParallelHelper
153
+ # Check if parallel processing is available
154
+ # @return [Boolean] True if the parallel gem is available
155
+ def self.available?
156
+ begin
157
+ require 'parallel'
158
+ true
159
+ rescue LoadError
160
+ false
161
+ end
162
+ end
163
+
164
+ # Process items in parallel if possible, otherwise sequentially
165
+ # @param items [Array] Items to process
166
+ # @param options [Hash] Options for parallel processing
167
+ # @option options [Integer] :threshold Minimum number of items to use parallel processing
168
+ # @option options [Integer] :workers Number of workers to use (defaults to processor count)
169
+ # @yield [item] Block to process each item
170
+ # @yieldparam item [Object] An item to process
171
+ # @return [Array] Results of processing all items
172
+ def self.process(items, options = {}, &block)
173
+ threshold = options.delete(:threshold) || 10
174
+ workers = options.delete(:workers) || processor_count
175
+
176
+ if available? && items.size > threshold
177
+ require 'parallel'
178
+ Parallel.map(items, { in_processes: workers }.merge(options)) { |item| block.call(item) }
179
+ else
180
+ items.map(&block)
181
+ end
182
+ end
183
+
184
+ # Get number of available processors
185
+ # @return [Integer] Number of processors available
186
+ def self.processor_count
187
+ if defined?(Etc) && Etc.respond_to?(:nprocessors)
188
+ Etc.nprocessors
189
+ else
190
+ 2 # Conservative default
191
+ end
192
+ end
193
+ end
194
+
195
+ # Performance monitoring and reporting
196
+ class Monitor
197
+ # Initialize a new monitor
198
+ def initialize
199
+ @timings = {}
200
+ @counts = {}
201
+ end
202
+
203
+ # Measure execution time of a block
204
+ # @param label [String, Symbol] Label for the measurement
205
+ # @yield Block to measure
206
+ # @return [Object] Result of the block
207
+ def measure(label)
208
+ start_time = Time.now
209
+ result = yield
210
+ duration = Time.now - start_time
211
+
212
+ @timings[label] ||= 0
213
+ @timings[label] += duration
214
+
215
+ @counts[label] ||= 0
216
+ @counts[label] += 1
217
+
218
+ result
219
+ end
220
+
221
+ # Get a report of all timings
222
+ # @return [Array<String>] Formatted timing report lines
223
+ def report
224
+ @timings.sort_by { |_, v| -v }.map do |k, v|
225
+ count = @counts[k]
226
+ avg = count > 0 ? v / count : 0
227
+ "#{k}: #{v.round(3)}s total, #{count} calls, #{avg.round(3)}s avg"
228
+ end
229
+ end
230
+
231
+ # Reset all timings
232
+ # @return [nil]
233
+ def reset
234
+ @timings.clear
235
+ @counts.clear
236
+ nil
237
+ end
238
+ end
239
+
240
+ # Benchmarking utility for snakommit operations
241
+ class Benchmark
242
+ # Run a benchmark test
243
+ # @param label [String] Label for the benchmark
244
+ # @param iterations [Integer] Number of iterations to run
245
+ # @yield Block to benchmark
246
+ # @return [Hash] Benchmark results
247
+ def self.run(label, iterations = 1)
248
+ results = {}
249
+
250
+ # Warm up
251
+ yield
252
+
253
+ # Run the benchmark
254
+ results[:real] = ::Benchmark.realtime do
255
+ iterations.times { yield }
256
+ end
257
+
258
+ results[:avg] = results[:real] / iterations
259
+ results[:label] = label
260
+ results[:iterations] = iterations
261
+
262
+ results
263
+ end
264
+
265
+ # Compare performance of multiple implementations
266
+ # @param options [Hash] Options for comparison
267
+ # @option options [Integer] :iterations Number of iterations
268
+ # @option options [Boolean] :verbose Print results
269
+ # @yield Block that returns a hash of callable objects to compare
270
+ # @return [Hash] Comparison results
271
+ def self.compare(options = {})
272
+ iterations = options[:iterations] || 100
273
+ verbose = options[:verbose] || false
274
+
275
+ implementations = yield
276
+ results = {}
277
+
278
+ implementations.each do |name, callable|
279
+ results[name] = run(name, iterations) { callable.call }
280
+ end
281
+
282
+ if verbose
283
+ puts "Performance comparison (#{iterations} iterations):"
284
+ results.sort_by { |_, v| v[:avg] }.each do |name, result|
285
+ puts " #{name}: #{result[:avg].round(6)}s avg (total: #{result[:real].round(3)}s)"
286
+ end
287
+ end
288
+
289
+ results
290
+ end
291
+ end
292
+
293
+ # Memory usage tracking
294
+ class Memory
295
+ # Get current memory usage in KB
296
+ # @return [Integer] Memory usage in KB
297
+ def self.usage
298
+ case RbConfig::CONFIG['host_os']
299
+ when /linux/
300
+ `ps -o rss= -p #{Process.pid}`.to_i
301
+ when /darwin/
302
+ `ps -o rss= -p #{Process.pid}`.to_i
303
+ when /windows|mswin|mingw/
304
+ # Not implemented for Windows
305
+ 0
306
+ else
307
+ 0
308
+ end
309
+ end
310
+
311
+ # Measure memory usage before and after a block execution
312
+ # @yield Block to measure
313
+ # @return [Hash] Memory usage statistics
314
+ def self.measure
315
+ before = usage
316
+ result = yield
317
+ after = usage
318
+
319
+ {
320
+ before: before,
321
+ after: after,
322
+ diff: after - before,
323
+ result: result
324
+ }
325
+ end
326
+ end
327
+ end
328
+ end