vers 1.2.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ad669b5ca951e8a64ddd205d39e083b3a9836a67ca9ff25d99872a57658a560
4
- data.tar.gz: d1aefb9f16a5a842ad05d4e427b538a565240cf3c21969542bf614199e8e1955
3
+ metadata.gz: 13ef83ab41f6ecd9046acb7a33190abc7648805878ffb1c5cf1e3741fa3e7ae5
4
+ data.tar.gz: 5c8982aeb33abb1e1dec9b17a04693f6dfb7395ec29761680cdd3ca5c85df20e
5
5
  SHA512:
6
- metadata.gz: 6b87591c17a303bf820c2667f25ac802569173737c8bd63d13c86bc5aedf6e606cc50d3906c08d526dd8c9e934559f0cdc7717e8e6c4cfbd8ae5473ebc53bb9c
7
- data.tar.gz: 9a6cafa4929f8a71b699d6ce1171e574bb6c99aca17b89850e7bf232d1269727d00f6a15f1b9a870f8319913e77b0ec865fc5633866161cb989425c9eefc76ce
6
+ metadata.gz: c5375be69c79738814d42123b157392448603c12c4b5a7f54bde3107bbf12b9f885d08efbe44cddf1c5f2d78b48394eefa73cd720e77928259227a883db273aa
7
+ data.tar.gz: a6aa16f032331174794e130096751ae08b37cf04ebae8da23ac07a022959ee493c766b98a873908e436d2d69d234623352857b4fd04dce4c71168c6ca4255317
data/Rakefile CHANGED
@@ -187,74 +187,122 @@ namespace :benchmark do
187
187
  task :memory do
188
188
  require "benchmark"
189
189
  require "json"
190
+ require "objspace"
190
191
  require_relative "lib/vers"
191
-
192
- puts "💾 VERS Memory Usage Benchmarks"
192
+
193
+ puts "VERS Memory & Allocation Benchmarks"
193
194
  puts "=" * 50
194
-
195
- # Load sample ranges
195
+
196
196
  test_data_file = File.join(__dir__, "test-suite-data.json")
197
-
197
+
198
198
  unless File.exist?(test_data_file)
199
- puts "test-suite-data.json not found. Using fallback examples."
199
+ puts "test-suite-data.json not found. Using fallback examples."
200
200
  sample_ranges = [
201
- { input: "^1.2.3", scheme: "npm" },
202
- { input: "~> 1.2", scheme: "gem" },
203
- { input: ">=1.0,<2.0", scheme: "pypi" }
201
+ { "input" => "^1.2.3", "scheme" => "npm" },
202
+ { "input" => "~> 1.2", "scheme" => "gem" },
203
+ { "input" => ">=1.0,<2.0", "scheme" => "pypi" }
204
204
  ]
205
205
  else
206
206
  test_data = JSON.parse(File.read(test_data_file))
207
207
  sample_ranges = test_data.select { |data| !data["is_invalid"] }.first(100)
208
208
  end
209
-
210
- puts "📊 Testing with #{sample_ranges.length} version ranges"
209
+
210
+ puts "Sample size: #{sample_ranges.length} version ranges"
211
211
  puts
212
-
213
- # Parse all ranges and store objects
214
- puts "🔍 Parsing and storing #{sample_ranges.length} VersionRange objects..."
212
+
213
+ # Measure object allocations during cold parsing (no cache)
214
+ Vers::Version.class_variable_set(:@@version_cache, {})
215
+ Vers::Constraint.class_variable_set(:@@constraint_cache, {})
216
+ Vers::Parser.class_variable_set(:@@parser_cache, {})
217
+
218
+ GC.start
219
+ GC.disable
220
+ before = ObjectSpace.count_objects.dup
221
+
215
222
  version_ranges = []
216
-
217
- parsing_time = Benchmark.realtime do
218
- sample_ranges.each do |range|
219
- begin
220
- parsed = Vers.parse_native(range['input'], range['scheme'])
221
- version_ranges << parsed
222
- rescue
223
- # Skip invalid ranges
224
- end
225
- end
223
+ sample_ranges.each do |range|
224
+ parsed = Vers.parse_native(range['input'], range['scheme']) rescue nil
225
+ version_ranges << parsed if parsed
226
226
  end
227
-
228
- puts " Parsing completed in #{(parsing_time * 1000).round(2)}ms"
229
- puts " Successfully parsed #{version_ranges.length} ranges"
227
+
228
+ after = ObjectSpace.count_objects
229
+ GC.enable
230
+
231
+ string_alloc = after[:T_STRING] - before[:T_STRING]
232
+ array_alloc = after[:T_ARRAY] - before[:T_ARRAY]
233
+ hash_alloc = after[:T_HASH] - before[:T_HASH]
234
+ object_alloc = after[:T_OBJECT] - before[:T_OBJECT]
235
+ match_alloc = (after[:T_MATCH] || 0) - (before[:T_MATCH] || 0)
236
+ total_alloc = string_alloc + array_alloc + hash_alloc + object_alloc + match_alloc
237
+
238
+ puts "Cold parse allocations (#{version_ranges.length} ranges, no cache):"
239
+ puts " Total objects: #{total_alloc}"
240
+ puts " Strings: #{string_alloc}"
241
+ puts " Arrays: #{array_alloc}"
242
+ puts " Hashes: #{hash_alloc}"
243
+ puts " Objects: #{object_alloc}"
244
+ puts " MatchData: #{match_alloc}"
245
+ puts " Per range: #{(total_alloc.to_f / version_ranges.length).round(1)}"
230
246
  puts
231
-
232
- # Estimate memory usage
233
- estimated_memory = version_ranges.length * 300 # ~300 bytes per VersionRange object estimate
234
- puts "💾 Memory Usage Estimation:"
235
- puts " #{version_ranges.length} VersionRange objects: ~#{estimated_memory} bytes"
236
- puts " Average per object: ~300 bytes"
247
+
248
+ # Measure cached parse allocations (everything already cached)
249
+ GC.start
250
+ GC.disable
251
+ before = ObjectSpace.count_objects.dup
252
+
253
+ sample_ranges.each do |range|
254
+ Vers.parse_native(range['input'], range['scheme']) rescue nil
255
+ end
256
+
257
+ after = ObjectSpace.count_objects
258
+ GC.enable
259
+
260
+ cached_strings = after[:T_STRING] - before[:T_STRING]
261
+ cached_arrays = after[:T_ARRAY] - before[:T_ARRAY]
262
+ cached_objects = after[:T_OBJECT] - before[:T_OBJECT]
263
+ cached_match = (after[:T_MATCH] || 0) - (before[:T_MATCH] || 0)
264
+ cached_alloc = cached_strings + cached_arrays + cached_objects + cached_match
265
+
266
+ puts "Cached parse allocations (#{version_ranges.length} ranges, warm cache):"
267
+ puts " Total objects: #{cached_alloc}"
268
+ puts " Strings: #{cached_strings}"
269
+ puts " MatchData: #{cached_match}"
270
+ puts " Per range: #{(cached_alloc.to_f / version_ranges.length).round(1)}"
237
271
  puts
238
-
239
- # Test repeated operations
240
- puts "🔄 Repeated Operations Test:"
241
-
272
+
273
+ # Measure memory size of retained objects
274
+ total_memsize = version_ranges.sum { |r| ObjectSpace.memsize_of(r) }
275
+ puts "Retained memory:"
276
+ puts " #{version_ranges.length} VersionRange objects: #{total_memsize} bytes"
277
+ puts " Average per object: #{(total_memsize.to_f / version_ranges.length).round(0)} bytes"
278
+ puts
279
+
280
+ # Repeated operation allocations
281
+ puts "Repeated operation allocations (#{version_ranges.length} calls each):"
282
+
242
283
  operations = {
243
- "to_s conversion" => proc { version_ranges.each(&:to_s) },
244
- "contains? check" => proc { version_ranges.each { |r| r.contains?("1.5.0") } },
245
- "empty? check" => proc { version_ranges.each(&:empty?) },
246
- "unbounded? check" => proc { version_ranges.each(&:unbounded?) }
284
+ "to_s" => proc { version_ranges.each(&:to_s) },
285
+ "contains?" => proc { version_ranges.each { |r| r.contains?("1.5.0") } },
286
+ "empty?" => proc { version_ranges.each(&:empty?) },
247
287
  }
248
-
249
- operations.each do |op_name, op_proc|
250
- time = Benchmark.realtime { op_proc.call }
251
- ops_per_second = version_ranges.length / time
252
-
253
- puts " #{op_name.ljust(20)}: #{(time * 1000).round(2)}ms (#{ops_per_second.round(0)} ops/sec)"
288
+
289
+ operations.each do |name, op|
290
+ # Warm up
291
+ op.call
292
+
293
+ GC.start
294
+ GC.disable
295
+ before = ObjectSpace.count_objects.dup
296
+ op.call
297
+ after = ObjectSpace.count_objects
298
+ GC.enable
299
+
300
+ allocs = [:T_STRING, :T_ARRAY, :T_OBJECT, :T_MATCH, :T_HASH].sum { |k| (after[k] || 0) - (before[k] || 0) }
301
+ puts " #{name.ljust(15)}: #{allocs} allocations"
254
302
  end
255
-
303
+
256
304
  puts
257
- puts "Memory benchmark completed!"
305
+ puts "Memory benchmark completed!"
258
306
  end
259
307
 
260
308
  desc "Run complexity stress tests"
@@ -23,7 +23,7 @@ module Vers
23
23
 
24
24
  # Cache for parsed constraints
25
25
  @@constraint_cache = {}
26
- @@cache_size_limit = 500
26
+ @@cache_size_limit = 1000
27
27
 
28
28
  attr_reader :operator, :version
29
29
 
@@ -54,14 +54,13 @@ module Vers
54
54
  # Vers::Constraint.parse("!=2.0.0") # => #<Vers::Constraint:0x... @operator="!=", @version="2.0.0">
55
55
  #
56
56
  def self.parse(constraint_string)
57
- # Limit cache size to prevent memory bloat
57
+ return @@constraint_cache[constraint_string] if @@constraint_cache.key?(constraint_string)
58
+
58
59
  if @@constraint_cache.size >= @@cache_size_limit
59
- @@constraint_cache.clear
60
+ keys = @@constraint_cache.keys
61
+ keys.first(keys.size / 2).each { |k| @@constraint_cache.delete(k) }
60
62
  end
61
-
62
- # Return cached constraint if available
63
- return @@constraint_cache[constraint_string] if @@constraint_cache.key?(constraint_string)
64
-
63
+
65
64
  constraint = parse_uncached(constraint_string)
66
65
  @@constraint_cache[constraint_string] = constraint
67
66
  constraint
data/lib/vers/interval.rb CHANGED
@@ -12,8 +12,7 @@ module Vers
12
12
  @min_inclusive = min_inclusive
13
13
  @max_inclusive = max_inclusive
14
14
  @scheme = scheme
15
-
16
- validate_bounds!
15
+ @empty = compute_empty
17
16
  end
18
17
 
19
18
  def self.empty(scheme: nil)
@@ -37,9 +36,7 @@ module Vers
37
36
  end
38
37
 
39
38
  def empty?
40
- return true if min && max && version_compare(min, max) > 0
41
- return true if min && max && version_compare(min, max) == 0 && (!min_inclusive || !max_inclusive)
42
- false
39
+ @empty
43
40
  end
44
41
 
45
42
  def unbounded?
@@ -180,7 +177,22 @@ module Vers
180
177
 
181
178
  def overlaps?(other)
182
179
  return false if empty? || other.empty?
183
- !intersect(other).empty?
180
+ return true if unbounded? || other.unbounded?
181
+
182
+ # Check if the intervals can't overlap by comparing bounds directly
183
+ if max && other.min
184
+ cmp = version_compare(max, other.min)
185
+ return false if cmp < 0
186
+ return false if cmp == 0 && (!max_inclusive || !other.min_inclusive)
187
+ end
188
+
189
+ if min && other.max
190
+ cmp = version_compare(min, other.max)
191
+ return false if cmp > 0
192
+ return false if cmp == 0 && (!min_inclusive || !other.max_inclusive)
193
+ end
194
+
195
+ true
184
196
  end
185
197
 
186
198
  def adjacent?(other)
@@ -211,14 +223,12 @@ module Vers
211
223
 
212
224
  private
213
225
 
214
- def validate_bounds!
215
- return unless min && max
216
-
217
- comparison = version_compare(min, max)
218
- if comparison > 0
219
- return
220
- elsif comparison == 0 && (!min_inclusive || !max_inclusive)
221
- return
226
+ def compute_empty
227
+ if min && max
228
+ cmp = version_compare(min, max)
229
+ cmp > 0 || (cmp == 0 && (!min_inclusive || !max_inclusive))
230
+ else
231
+ false
222
232
  end
223
233
  end
224
234
 
data/lib/vers/parser.rb CHANGED
@@ -26,10 +26,11 @@ module Vers
26
26
  NPM_HYPHEN_REGEX = /\A(.+?)\s+-\s+(.+)\z/
27
27
  NPM_X_RANGE_MAJOR_REGEX = /\A(\d+)\.x\z/
28
28
  NPM_X_RANGE_MINOR_REGEX = /\A(\d+)\.(\d+)\.x\z/
29
+ OPERATOR_PREFIX_REGEX = /\A[><=!]+/
29
30
 
30
31
  # Cache for parsed ranges to improve performance
31
32
  @@parser_cache = {}
32
- @@cache_size_limit = 200
33
+ @@cache_size_limit = 500
33
34
 
34
35
  ##
35
36
  # Parses a vers URI string into a VersionRange
@@ -151,7 +152,7 @@ module Vers
151
152
  private
152
153
 
153
154
  def sort_key_for_constraint(constraint)
154
- version = constraint.sub(/\A[><=!]+/, '')
155
+ version = constraint.sub(OPERATOR_PREFIX_REGEX, '')
155
156
  v = Version.cached_new(version)
156
157
  [v, constraint]
157
158
  end
@@ -231,15 +232,12 @@ module Vers
231
232
  end
232
233
 
233
234
  def parse_npm_single_range(range_string)
234
- # Check cache first
235
235
  cache_key = "npm:#{range_string}"
236
- if @@parser_cache.key?(cache_key)
237
- return @@parser_cache[cache_key]
238
- end
239
-
240
- # Limit cache size
236
+ return @@parser_cache[cache_key] if @@parser_cache.key?(cache_key)
237
+
241
238
  if @@parser_cache.size >= @@cache_size_limit
242
- @@parser_cache.clear
239
+ keys = @@parser_cache.keys
240
+ keys.first(keys.size / 2).each { |k| @@parser_cache.delete(k) }
243
241
  end
244
242
 
245
243
  result = case range_string
data/lib/vers/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vers
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
 
6
6
  ##
7
7
  # Handles version comparison and normalization across different package ecosystems.
@@ -18,7 +18,7 @@ module Vers
18
18
  class Version
19
19
  # Cache for parsed versions to avoid repeated parsing
20
20
  @@version_cache = {}
21
- @@cache_size_limit = 1000
21
+ @@cache_size_limit = 2000
22
22
  # Regex for parsing semantic version components including build metadata
23
23
  SEMANTIC_VERSION_REGEX = /\A(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([^+]+))?(?:\+(.+))?\z/
24
24
 
@@ -41,11 +41,12 @@ module Vers
41
41
  # @return [Version] Cached or new Version object
42
42
  #
43
43
  def self.cached_new(version_string)
44
- # Limit cache size to prevent memory bloat
45
44
  if @@version_cache.size >= @@cache_size_limit
46
- @@version_cache.clear
45
+ # Keep the most recent half instead of clearing everything
46
+ keys = @@version_cache.keys
47
+ keys.first(keys.size / 2).each { |k| @@version_cache.delete(k) }
47
48
  end
48
-
49
+
49
50
  @@version_cache[version_string] ||= new(version_string)
50
51
  end
51
52
 
@@ -145,15 +146,17 @@ module Vers
145
146
  # @return [String] The normalized version string
146
147
  #
147
148
  def to_s
148
- version = "#{major || 0}"
149
- version += ".#{minor || 0}"
150
- version += ".#{patch || 0}"
151
- version += "-#{prerelease}" if prerelease
152
- version
149
+ @to_s ||= begin
150
+ version = "#{major || 0}"
151
+ version += ".#{minor || 0}"
152
+ version += ".#{patch || 0}"
153
+ version += "-#{prerelease}" if prerelease
154
+ version.freeze
155
+ end
153
156
  end
154
157
 
155
158
  def ==(other)
156
- other.is_a?(Version) && self <=> other == 0
159
+ other.is_a?(Version) && (self <=> other) == 0
157
160
  end
158
161
 
159
162
  def <(other)
@@ -320,7 +323,7 @@ module Vers
320
323
  @original = @original.sub(/\Av/i, '')
321
324
 
322
325
  # Handle simple numeric versions (optimized case)
323
- if @original.match(/^\d+$/)
326
+ if @original.match?(/^\d+$/)
324
327
  @major = @original.to_i
325
328
  return
326
329
  end
@@ -382,7 +385,7 @@ module Vers
382
385
  return 1 if part_b.nil?
383
386
 
384
387
  # Try numeric comparison first
385
- if part_a.match(/^\d+$/) && part_b.match(/^\d+$/)
388
+ if part_a.match?(/^\d+$/) && part_b.match?(/^\d+$/)
386
389
  numeric_cmp = part_a.to_i <=> part_b.to_i
387
390
  return numeric_cmp unless numeric_cmp == 0
388
391
  else
@@ -9,7 +9,7 @@ module Vers
9
9
 
10
10
  def initialize(intervals = [], raw_constraints: nil, scheme: nil)
11
11
  @scheme = scheme
12
- @intervals = intervals.compact.reject(&:empty?)
12
+ @intervals = intervals.select { |i| i && !i.empty? }
13
13
  if @scheme
14
14
  @intervals.sort! { |a, b| compare_interval_bounds(a, b) }
15
15
  else
data/lib/vers.rb CHANGED
@@ -126,7 +126,7 @@ module Vers
126
126
  else
127
127
  parse(sub_range)
128
128
  end
129
- range.contains?(version)
129
+ range&.contains?(version) || false
130
130
  end
131
131
  end
132
132
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vers
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt