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.
- checksums.yaml +7 -0
- data/.gitignore +104 -0
- data/CHANGELOG.md +55 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +81 -0
- data/LICENSE +21 -0
- data/README.md +275 -0
- data/Rakefile +58 -0
- data/bin/sk +9 -0
- data/bin/snakommit +10 -0
- data/lib/snakommit/cli.rb +371 -0
- data/lib/snakommit/config.rb +154 -0
- data/lib/snakommit/git.rb +212 -0
- data/lib/snakommit/hooks.rb +258 -0
- data/lib/snakommit/performance.rb +328 -0
- data/lib/snakommit/prompt.rb +472 -0
- data/lib/snakommit/templates.rb +146 -0
- data/lib/snakommit/version.rb +5 -0
- data/lib/snakommit.rb +35 -0
- data/snakommit.gemspec +38 -0
- metadata +194 -0
@@ -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
|