moxml 0.5.43 → 0.5.44

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: 0c8fd5df5889694c9d7a3ee99c4caad866ddc40edd13ba46945f55e4e9f77a9b
4
- data.tar.gz: ce14f6e9199e22576c0c5149e716a672efb9e44ef3a9b68d1b78abad02754a92
3
+ metadata.gz: cd5c99b547004d2f6521f882b006da2b612ee74cabf92775a3b061d9f096bc20
4
+ data.tar.gz: 652bce88c043068ecd97ef7008044557f764661a2e792f1128d435d4e83d715a
5
5
  SHA512:
6
- metadata.gz: 93ac1cd541ed454ac89c10179e5e56f919a4cd309ae8f16abe02950ea5a6fedfdb03c53a7a8c23520616cd202a2b8f5384616ac8cd7b9e884d9f127c1d38bd98
7
- data.tar.gz: ed75757fec960bd181921f9562650f4f3d44f2261fa0eb41a09437906d26581ad07c8b5446d0719a1d26b5308de2e1e25c60d2f0013da6861d144692d53eada4
6
+ metadata.gz: ba8297809db361e15cac3cbfbc132aa5545ceee6fe4e3d8b7df9a6250e7585ce930eaf7ba7d15de155975848f0d2deb205dcf8b0ebbdd3e00d96cbfc8d758633
7
+ data.tar.gz: 982406a0ff5f858aeed1569fd4ca1d4536bbb2b9841d791b4f560475b19b77641db0831fac7efd6760c3493216a9db8635de55d5e146f158cc0f16297fc97895
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ gemspec
9
9
  gem "benchmark"
10
10
  gem "benchmark-ips"
11
11
  gem "get_process_mem"
12
- gem "leptris", "~> 1.9.32"
12
+ gem "leptris", "~> 1.9.174"
13
13
  gem "libxml-ruby", "~> 5.0"
14
14
  gem "nokogiri", "~> 1.18"
15
15
  gem "openssl", "~> 3.0"
@@ -42,6 +42,17 @@ module Moxml
42
42
  DIGEST_SUPPORTED =
43
43
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.99")
44
44
 
45
+ # 1.9.163.7 added the XPath seams (TODO.perf/15+16):
46
+ # Searchable#at_xpath materializes nodeset entry 0 and frees
47
+ # the handle in one C dispatch — no NodeSet container, no
48
+ # AutoPointer (2.8x on repeat at_xpath) — and
49
+ # CompiledXPath#eval_ptrs runs the compiled handle against
50
+ # raw pointers, skipping the per-call EvaluationContext
51
+ # (~2us on every multi-result query).
52
+ NATIVE_XPATH_SEAMS =
53
+ defined?(::Leptris::XML::Native) &&
54
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.7")
55
+
45
56
  # Opt-in native read layer (leptris-ruby#185, bindings >= 1.9.162.6):
46
57
  # TypedData node wrappers with C-bound hot reads and bulk
47
58
  # children construction. Minted from #root downward; the C
@@ -1303,6 +1314,9 @@ module Moxml
1303
1314
  # cached AST three times per xpath call cost ~23% of repeated
1304
1315
  # selective queries, so the boolean caches beside it.
1305
1316
  NATIVE_GATE_CACHE = XPath::Cache.new(100)
1317
+ # Root-context queries alone take the parent-axis guard; the
1318
+ # AST walk is per-expression, so the verdict caches too.
1319
+ PARENT_AXIS_CACHE = XPath::Cache.new(100)
1306
1320
 
1307
1321
  def xpath(node, expression, namespaces = {})
1308
1322
  node = to_binding(node) if NATIVE_READ_LAYER
@@ -1323,6 +1337,14 @@ module Moxml
1323
1337
  # @return [Array, Object, nil] native results, or nil when the
1324
1338
  # query must run on the Ruby engine
1325
1339
  def native_xpath(node, expression, namespaces, first_only: false)
1340
+ # Canonical natives (the #219 unification) are bare TypedData
1341
+ # wrappers — no Searchable methods, no document handle for
1342
+ # the gates below. The memoized bridge recovers the binding
1343
+ # identity; without it every element-context query fell to
1344
+ # the Ruby engine (a silent native-path loss since #219).
1345
+ if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
1346
+ node = to_binding(node)
1347
+ end
1326
1348
  return nil unless native_context_node?(node)
1327
1349
  return nil unless native_expression?(expression)
1328
1350
  # The binding reports the root element parentless while moxml
@@ -1330,27 +1352,70 @@ module Moxml
1330
1352
  # root element keep the Ruby engine.
1331
1353
  if node.is_a?(::Leptris::XML::Element) &&
1332
1354
  node.document&.root.equal?(node) &&
1333
- expression_uses_parent_axis?(expression)
1355
+ PARENT_AXIS_CACHE.get_or_set(expression) do
1356
+ expression_uses_parent_axis?(expression)
1357
+ end
1334
1358
  return nil
1335
1359
  end
1336
1360
 
1337
1361
  compiled = NATIVE_XPATH_CACHE.get_or_set(expression) do
1338
1362
  ::Leptris::XML::XPath.compile(expression)
1339
1363
  end
1340
- result = if namespaces && !namespaces.empty?
1341
- compiled.eval(node, namespaces)
1364
+ document = node.is_a?(::Leptris::XML::Document) ? node : node.document
1365
+
1366
+ if namespaces && !namespaces.empty?
1367
+ # Namespace-bound evaluation has no raw-pointer entry —
1368
+ # Searchable resolves the ns set for both result shapes.
1369
+ result = if first_only
1370
+ node.at_xpath(expression, namespaces)
1371
+ else
1372
+ compiled.eval(node, namespaces)
1373
+ end
1374
+ return case result
1375
+ when ::Leptris::XML::NodeSet
1376
+ first_only ? result.first : result.extend(Moxml::LazyNodeSet)
1342
1377
  else
1343
- compiled.eval(node)
1378
+ result
1344
1379
  end
1345
- case result
1346
- when ::Leptris::XML::NodeSet
1347
- # The binding's #to_a mints one Ruby wrapper per result
1348
- # node; hand the native set through so .size/.first stay
1349
- # native-side (LazyNodeSet holds it unmaterialized).
1350
- first_only ? result.first : result.extend(Moxml::LazyNodeSet)
1351
- else
1352
- result
1353
1380
  end
1381
+
1382
+ unless NATIVE_XPATH_SEAMS
1383
+ result = compiled.eval(node)
1384
+ return case result
1385
+ when ::Leptris::XML::NodeSet
1386
+ first_only ? result.first : result.extend(Moxml::LazyNodeSet)
1387
+ else
1388
+ result
1389
+ end
1390
+ end
1391
+
1392
+ # eval_ptrs against raw pointers — the per-call
1393
+ # EvaluationContext that #eval builds costs ~2us of Ruby
1394
+ # on every query (leptris-ruby TODO.perf/15).
1395
+ doc_ptr = node.is_a?(::Leptris::XML::Document) ? node.c_ptr : document.c_ptr
1396
+ context_ptr = node.is_a?(::Leptris::XML::Document) ? nil : node.c_ptr
1397
+ result_ptr = compiled.eval_ptrs(doc_ptr, context_ptr)
1398
+ return nil if result_ptr.null?
1399
+
1400
+ if first_only
1401
+ # The single-result seam: nodeset entry 0 materializes
1402
+ # and frees in one C dispatch — no NodeSet container, no
1403
+ # AutoPointer (leptris-ruby TODO.perf/16). The module
1404
+ # object back means a scalar, whose wrap (and free)
1405
+ # stays with wrap_xpath_first_result.
1406
+ return ::Leptris::XML::Searchable.wrap_xpath_first_result(
1407
+ document, result_ptr
1408
+ )
1409
+ end
1410
+
1411
+ # The binding's #to_a mints one Ruby wrapper per result
1412
+ # node; hand the native set through so .size/.first stay
1413
+ # native-side (LazyNodeSet holds it unmaterialized).
1414
+ # Scalars pass through unwrapped-and-unextended.
1415
+ result = ::Leptris::XML::Searchable.wrap_xpath_result(
1416
+ document, result_ptr
1417
+ )
1418
+ result.is_a?(::Leptris::XML::NodeSet) ? result.extend(Moxml::LazyNodeSet) : result
1354
1419
  rescue ::Leptris::XML::XPathError
1355
1420
  # Not supported by the native engine — the Ruby engine is a
1356
1421
  # full XPath 1.0 implementation, including Moxml's syntax
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.43"
4
+ VERSION = "0.5.44"
5
5
  end
@@ -11,8 +11,12 @@ module Moxml
11
11
  # @param [Integer] max_size Maximum number of entries to cache
12
12
  def initialize(max_size = DEFAULT_SIZE)
13
13
  @max_size = max_size
14
- @cache = {}
15
- @access_order = []
14
+ # One insertion-ordered hash: re-inserting a key on access
15
+ # makes the first entry the least-recently-used. An
16
+ # Array-backed order list cost ~600ns per hit in O(n)
17
+ # deletes — the adapter's xpath path hits three caches per
18
+ # call.
19
+ @entries = {}
16
20
  end
17
21
 
18
22
  # Gets a value from the cache or sets it using the provided block.
@@ -21,11 +25,9 @@ module Moxml
21
25
  # @yield Block to execute if key is not in cache
22
26
  # @return [Object] Cached or newly computed value
23
27
  def get_or_set(key)
24
- if @cache.key?(key)
25
- # Move to end (most recently used)
26
- @access_order.delete(key)
27
- @access_order.push(key)
28
- @cache[key]
28
+ if @entries.key?(key)
29
+ value = @entries.delete(key)
30
+ @entries[key] = value
29
31
  else
30
32
  value = yield
31
33
  set(key, value)
@@ -39,16 +41,9 @@ module Moxml
39
41
  # @param [Object] value
40
42
  # @return [Object] The value
41
43
  def set(key, value)
42
- if @cache.key?(key)
43
- @access_order.delete(key)
44
- elsif @cache.size >= @max_size
45
- # Remove least recently used
46
- lru_key = @access_order.shift
47
- @cache.delete(lru_key)
48
- end
49
-
50
- @cache[key] = value
51
- @access_order.push(key)
44
+ @entries.delete(key)
45
+ @entries[key] = value
46
+ @entries.shift if @entries.size > @max_size
52
47
  value
53
48
  end
54
49
 
@@ -57,26 +52,24 @@ module Moxml
57
52
  # @param [Object] key
58
53
  # @return [Object, nil]
59
54
  def get(key)
60
- return unless @cache.key?(key)
55
+ return unless @entries.key?(key)
61
56
 
62
- @access_order.delete(key)
63
- @access_order.push(key)
64
- @cache[key]
57
+ value = @entries.delete(key)
58
+ @entries[key] = value
65
59
  end
66
60
 
67
61
  # Clears the cache.
68
62
  #
69
63
  # @return [void]
70
64
  def clear
71
- @cache.clear
72
- @access_order.clear
65
+ @entries.clear
73
66
  end
74
67
 
75
68
  # Returns the current size of the cache.
76
69
  #
77
70
  # @return [Integer]
78
71
  def size
79
- @cache.size
72
+ @entries.size
80
73
  end
81
74
 
82
75
  # Checks if a key exists in the cache.
@@ -84,7 +77,7 @@ module Moxml
84
77
  # @param [Object] key
85
78
  # @return [Boolean]
86
79
  def key?(key)
87
- @cache.key?(key)
80
+ @entries.key?(key)
88
81
  end
89
82
  end
90
83
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.43
4
+ version: 0.5.44
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.