Skip to content

Change namespace cache strategy#333

Open
tompng wants to merge 4 commits into
ruby:masterfrom
tompng:xpath_namespace_lookup_cache
Open

Change namespace cache strategy#333
tompng wants to merge 4 commits into
ruby:masterfrom
tompng:xpath_namespace_lookup_cache

Conversation

@tompng

@tompng tompng commented Jun 14, 2026

Copy link
Copy Markdown
Member

Builds on top of #335

Even if namespace lookup is cached, namespace lookup was slow for deeply nested XML nodes.
Namespaces are cached in document, but document lookup from a deep node costs O(n).

We can't cache it in a document or root of the node, because we also need to cache document lookup result.
It's not good to cache to node itself, for maintainability reason, cache invalidation, etc.
In this PR, Hash for caching node's namespace/namespaces, and cached namespace attributes defined in attlist decl can be injected through method parameters.

Benchmark

irb> doc = REXML::Document.new('<a attr="1">'*1000+'</a>'*1000)
irb> measure
irb> REXML::XPath.match(doc, '//a');
# Master:                             0.2791398s
# Master(XPathParser#sort disabled):  0.1138055s
# This PR:                            0.1743505s
# This PR(XPathParser#sort disabled): 0.0046388s

Time consumption was: Scan: 0.0046s (1.8%), Sort: 0.17s (60%), Namespace lookup: 0.11s (38%). This PR removes the 38% cost.

Cache through parameters also speeds up bare namespace call on deeply nested element.

irb> doc = REXML::Document.new('<a>'*1000+'</a>'*1000)
irb> node = doc; node = node.first while node.first
irb> measure
irb> node.namespace
# Master:  0.079568s O(N^2)
# This PR: 0.003091s O(N)

Copilot AI review requested due to automatic review settings June 14, 2026 17:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR refactors REXML namespace resolution to incorporate ATTLIST-declared xmlns values (and adds caching in the XPath parser), along with a new regression test that validates namespace precedence.

Changes:

  • Add support for applying ATTLIST-declared xmlns values into element/attribute namespace resolution.
  • Introduce per-parser caching for element namespace computations in XPathParser.
  • Add a new test for ATTLIST vs element-declared namespace precedence; remove the previous namespaces cache regression test.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/xpath/test_base.rb Removes a regression test that previously exercised namespace cache invalidation behavior.
test/test_core.rb Adds a test asserting ATTLIST-declared xmlns precedence vs inherited/locally-declared namespaces.
lib/rexml/xpath_parser.rb Adds namespace caching and routes namespace comparisons through cached lookup helpers.
lib/rexml/element.rb Refactors namespace calculation/lookup internals and integrates ATTLIST-declared namespaces.
lib/rexml/document.rb Adds extraction of ATTLIST-declared xmlns mappings per element name.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rexml/xpath_parser.rb Outdated
Comment thread lib/rexml/xpath_parser.rb Outdated
Comment thread lib/rexml/xpath_parser.rb
Comment thread lib/rexml/xpath_parser.rb
Comment thread lib/rexml/element.rb Outdated
Comment thread lib/rexml/element.rb Outdated
Comment thread test/test_core.rb Outdated
Comment thread lib/rexml/xpath_parser.rb
@tompng tompng force-pushed the xpath_namespace_lookup_cache branch from 8d141ec to 8a7840a Compare June 14, 2026 17:58
Comment thread lib/rexml/element.rb
@@ -2424,19 +2447,12 @@ def prefixes
# d.root.attributes.namespaces # => {"xmlns"=>"foo", "x"=>"bar", "y"=>"twee"}
#
def namespaces

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is no longer called except from the added test.
We need to keep this if it's a public API, but if not, we can remove it (perhaps with deprecation first)

Comment thread lib/rexml/element.rb Outdated
Comment on lines +1527 to +1528
namespaces = namespaces.merge(attlist_namespaces) if attlist_namespaces
namespaces = namespaces.merge(own_namespaces) if own_namespaces.any?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a small bug in master: precedence of attlist attributes and own attirbutes was reversed. Test is added for this

Copilot AI review requested due to automatic review settings June 15, 2026 10:50
@tompng tompng force-pushed the xpath_namespace_lookup_cache branch from 8a7840a to 95891d1 Compare June 15, 2026 10:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Comment thread lib/rexml/xpath_parser.rb Outdated
Comment thread lib/rexml/xpath_parser.rb Outdated
Comment thread lib/rexml/element.rb Outdated
Comment thread lib/rexml/document.rb Outdated
Comment thread test/xpath/test_base.rb
@tompng tompng force-pushed the xpath_namespace_lookup_cache branch 2 times, most recently from 2856f4a to 58fa153 Compare June 16, 2026 18:06
Copilot AI review requested due to automatic review settings June 16, 2026 18:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.

Comment thread lib/rexml/xpath_parser.rb
Comment thread lib/rexml/xpath_parser.rb Outdated
Comment thread lib/rexml/xpath_parser.rb
Comment thread lib/rexml/xpath_parser.rb Outdated
Comment thread lib/rexml/element.rb
Comment thread lib/rexml/doctype.rb Outdated
Comment on lines 126 to 136
def _attlist_mappings
mapping = {}
grep(AttlistDecl).each do |child|
raw_attributes = mapping[child.element_name] ||= {}
child.each do |key, val|
# First declaration wins
raw_attributes[key] = val unless raw_attributes.key? key
end
end
return nil unless att_decl
att_decl[attribute]
mapping
end
Comment thread lib/rexml/element.rb
Comment thread test/test_namespace.rb
Comment thread test/xpath/test_base.rb
Comment thread lib/rexml/element.rb Outdated
@tompng tompng force-pushed the xpath_namespace_lookup_cache branch from 58fa153 to 5d35322 Compare June 16, 2026 18:28
Comment thread lib/rexml/doctype.rb Outdated
Comment thread lib/rexml/element.rb
Comment thread lib/rexml/xpath_parser.rb
Comment thread test/xpath/test_base.rb
Comment thread lib/rexml/xpath_parser.rb Outdated
else
match( path_stack, node )
end
@document = node.document

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • before (expect)
> doc = REXML::Document.new '<!DOCTYPE a [<!ATTLIST b xmlns:p CDATA "urn:x">]><a><b><p:c/></b></a>'
> parser = REXML::XPathParser.new
> parser.predicate("//p:c", doc) #=> [<p:c/>]
  • after (this PR)
> doc = REXML::Document.new '<!DOCTYPE a [<!ATTLIST b xmlns:p CDATA "urn:x">]><a><b><p:c/></b></a>'
> parser = REXML::XPathParser.new
> parser.predicate("//p:c", doc) # => []

Although @document is set in parse, it is not set in predicate or get_first, so @attlist_mappings becomes {} in those cases, and the xmlns declaration derived from ATTLIST does not take effect.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. But I can't confidently say that it works because predicate is not tested at all, and first (called from get_first) is an incomplete function.

# FIXME: This method is incomplete!
def first( path_stack, node )
  ...
end

Maybe it's worth deleting them in a separate pull request.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Thanks

Maybe it's worth deleting them in a separate pull request.

We might want to consider deleting it.

Comment thread lib/rexml/element.rb Outdated
Comment thread lib/rexml/element.rb
Copilot AI review requested due to automatic review settings July 3, 2026 17:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread lib/rexml/xpath_parser.rb Outdated
Comment thread lib/rexml/element.rb
@tompng tompng force-pushed the xpath_namespace_lookup_cache branch from 3e780d9 to a83e31a Compare July 3, 2026 17:45
Comment thread lib/rexml/attribute.rb
Copilot AI review requested due to automatic review settings July 4, 2026 15:39
@tompng tompng force-pushed the xpath_namespace_lookup_cache branch from a83e31a to 20c7dbc Compare July 4, 2026 15:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comment thread lib/rexml/xpath_parser.rb
Comment thread lib/rexml/xpath_parser.rb
Comment thread lib/rexml/element.rb
@tompng tompng force-pushed the xpath_namespace_lookup_cache branch from 20c7dbc to 032ab99 Compare July 4, 2026 17:51
Comment thread lib/rexml/xpath_parser.rb


def match(path_stack, node)
@document = node.document

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reusing an XPathParser, old cache entries remain.

  • before (expect)
> doc = REXML::Document.new("<a xmlns='u1'><b/></a>")
> parser = REXML::XPathParser.new
> parser.namespaces = {"p" => "u1"}
> parser.parse("//p:b", doc)
=> [<b/>]
> doc.root
=> <a xmlns='u1'> ... </>
> doc.root.delete_namespace
=> <a> ... </>
> doc.root
=> <a> ... </>
> parser.parse("//p:b", doc)
=> []
  • after (this PR)
> doc = REXML::Document.new("<a xmlns='u1'><b/></a>")
> parser = REXML::XPathParser.new
> parser.namespaces = {"p" => "u1"}
> parser.parse("//p:b", doc)
=> [<b/>]
> doc.root.delete_namespace
=> <a> ... </>
> doc.root
=> <a> ... </>
> parser.parse("//p:b", doc)
=> [<b/>]
Suggested change
@document = node.document
@document = node.document
@attlist_mappings = nil
@element_namespaces_cache = {}

Comment thread lib/rexml/xpath_parser.rb Outdated
else
match( path_stack, node )
end
@document = node.document

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Thanks

Maybe it's worth deleting them in a separate pull request.

We might want to consider deleting it.

tompng and others added 4 commits July 5, 2026 21:15
Namespace calculation for each node can't be cached to document because document lookup is slow for deeply nested nodes.
Change namespace cache strategy, inject cached hash as an argument to retrieve namespace/namespaces from XPath match operation and also for bare namespace call.
Co-authored-by: NAITOH Jun <naitoh@gmail.com>
Copilot AI review requested due to automatic review settings July 5, 2026 12:15
@tompng tompng force-pushed the xpath_namespace_lookup_cache branch from 032ab99 to b9dcebe Compare July 5, 2026 12:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comment thread lib/rexml/element.rb
Comment on lines 590 to 592
def namespaces
namespaces_cache = document&.__send__(:namespaces_cache)
if namespaces_cache
namespaces_cache[self] ||= calculate_namespaces
else
calculate_namespaces
end
_calculate_namespaces
end
Comment thread lib/rexml/xpath_parser.rb
Comment on lines 94 to 98
def get_first path, node
path_stack = @parser.parse( path )
@document = node.document
first( path_stack, node )
end
Comment thread lib/rexml/xpath_parser.rb
Comment on lines 151 to 154
def match(path_stack, node)
@document = node.document
nodeset = [node]
result = expr(path_stack, nodeset)
Comment thread lib/rexml/doctype.rb
Comment on lines 122 to +126
def attribute_of element, attribute
attribute_declarations_of(element)[attribute]
_attlist_mappings[element]&.[](attribute)
end

def attribute_declarations_of(name)
raw_attributes = {}
decls = select do |child|
child.kind_of?(AttlistDecl) && child.element_name == name
end
decls.each do |child|
def _attlist_mappings # :nodoc:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants