moxml 0.5.43 → 0.5.45

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: f552fee96842dca761f5e88ba1f5074e6cfdaef32411b012e2bf30a6736a16c4
4
+ data.tar.gz: 68c734aa63688134b897a1e29e8abfa292a02534daefd278b727122ad952e4e3
5
5
  SHA512:
6
- metadata.gz: 93ac1cd541ed454ac89c10179e5e56f919a4cd309ae8f16abe02950ea5a6fedfdb03c53a7a8c23520616cd202a2b8f5384616ac8cd7b9e884d9f127c1d38bd98
7
- data.tar.gz: ed75757fec960bd181921f9562650f4f3d44f2261fa0eb41a09437906d26581ad07c8b5446d0719a1d26b5308de2e1e25c60d2f0013da6861d144692d53eada4
6
+ metadata.gz: ff775d10b05c5e60fcbf59c66e49284b841e4148b7da7766393b351b47bb626697a34bb5f30cf0d697d8a578ed07c1a9b8b7987507364c436c7c274be380843f
7
+ data.tar.gz: 7bbaef0118ea13de356dc95041dca057d59373f96eec6a4621a38d1cce07b283ca682b3fd2db77eea34f8d9252aaca821cf2651cbbbebbf0e7e171dd0d51f0f5
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"
@@ -139,27 +139,43 @@ module Moxml
139
139
  # facade's canonical form (checked post-hoc: a source
140
140
  # declaration carrying standalone or another version would
141
141
  # diverge).
142
+ # 1.9.174 attached leptris_document_first_child (libleptris
143
+ # 1.9.174): pointer probe for the single-child document
144
+ # shape without wrapping the child chain.
145
+ DOCUMENT_CHILD_PTRS =
146
+ ::Leptris::XML::FFI.respond_to?(:leptris_document_first_child)
147
+
142
148
  def fast_document_output(doc, options)
143
149
  return nil unless LIBXML2_LAYOUT_PARITY
144
- return nil if attachments.get(doc, :declaration) ||
145
- attachments.get(doc, :doctype) ||
146
- attachments.get(doc, :document_text) ||
147
- attachments.get(doc, :entity_markers)
150
+ return nil unless attachments.none_set?(
151
+ doc, %i[declaration doctype document_text entity_markers]
152
+ )
148
153
 
149
154
  include_decl = !options[:no_declaration] && options.fetch(:declaration) do
150
155
  document_has_declaration?(doc)
151
156
  end
152
157
 
153
- root_seen = false
154
- doc.children.each do |child|
155
- if child.is_a?(::Leptris::XML::Element)
156
- return nil if root_seen
157
-
158
- root_seen = true
159
- elsif root_seen
160
- # Epilog parts glue directly to the root in the engine's
161
- # document output — compose those.
162
- return nil
158
+ # Exactly one document child — the root — is the common
159
+ # parsed shape; pointer probes decide it without wrapping
160
+ # the child chain. Anything else (prolog/epilog parts,
161
+ # multi-root) falls back to the scan.
162
+ root = doc.root
163
+ single_child = DOCUMENT_CHILD_PTRS && root &&
164
+ ::Leptris::XML::FFI.leptris_document_first_child(doc.c_ptr)
165
+ .address == root.c_ptr.address &&
166
+ root.next_sibling.nil?
167
+ unless single_child
168
+ root_seen = false
169
+ doc.children.each do |child|
170
+ if child.is_a?(::Leptris::XML::Element)
171
+ return nil if root_seen
172
+
173
+ root_seen = true
174
+ elsif root_seen
175
+ # Epilog parts glue directly to the root in the engine's
176
+ # document output — compose those.
177
+ return nil
178
+ end
163
179
  end
164
180
  end
165
181
 
@@ -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
@@ -43,6 +43,13 @@ module Moxml
43
43
  end
44
44
  end
45
45
 
46
+ def none_set?(native, keys)
47
+ h = @data[native.object_id]
48
+ return true if h.nil?
49
+
50
+ keys.none? { |key| h.key?(key) }
51
+ end
52
+
46
53
  def key?(native, key)
47
54
  h = @data[native.object_id]
48
55
  !h.nil? && h.key?(key)
@@ -13,6 +13,10 @@ module Moxml
13
13
  native.instance_variable_set(attachment_ivar_name(key), value)
14
14
  end
15
15
 
16
+ def none_set?(native, keys)
17
+ keys.none? { |key| native.instance_variable_defined?(attachment_ivar_name(key)) }
18
+ end
19
+
16
20
  def key?(native, key)
17
21
  native.instance_variable_defined?(attachment_ivar_name(key))
18
22
  end
@@ -20,6 +20,12 @@ module Moxml
20
20
  @backend.get(native, key)
21
21
  end
22
22
 
23
+ # One probe for "none of these keys are set" — the document
24
+ # serialize gate asks about four keys per call.
25
+ def none_set?(native, keys)
26
+ @backend.none_set?(native, keys)
27
+ end
28
+
23
29
  def set(native, key, value)
24
30
  @backend.set(native, key, value)
25
31
  end
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.45"
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.45
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.