PureXML/Tests/XPathDocumentOrderScaleTests.swift
Mihaela Mihaljevic b672c49a84
perf(xpath): share value-Node projections across a result set (#349)
Mapping an ordered node-set to selections rebuilt each result as an
independent value Node through TreeNode.node, projecting that node's whole
subtree. For a result set whose nodes nest within one another (`//a` over a
deep chain, where every `a` contains all deeper ones) this materialized the
sum of their subtrees: quadratic. `//a` over an 8000-deep chain took ~19s.

The projection now threads a memo keyed by tree-node identity across one
result set. A tree node is projected once; a result that nests a
deeper one reuses its already-built value Node by reference, which the
copy-on-write Element storage (from the value-Node depth-safety work) makes
a shared, not copied, subtree. The set is built in one pass over the
distinct nodes. The same query now returns in ~0.04s and scales linearly.

The non-memoized `node` projection (serialization, value comparison) is
unchanged. Results are identical: 1787 tests pass, including the XPath and
XSLT value corpora. Closes #348.
2026-06-26 18:43:27 +02:00

81 lines
3.6 KiB
Swift

import Testing
@testable import PureXML
/// XPath result ordering must not be quadratic in document depth. Sorting a
/// node-set into document order keyed each node by its root path, an array as
/// long as the node's depth, so ordering the descendants of a deeply-nested
/// document was O(n log n x depth). The order key is now a single O(1) index
/// from a per-root pre-order numbering, so the sort is linear in the document.
/// The time limit turns a regression back to the path-key form into a failure.
@Suite("XPath document-order scale")
struct XPathDocumentOrderScaleTests {
/// A document holding a chain of `depth` nested elements with a text leaf, as
/// a tree node. The document wrapper makes the top element a child, so `//a`
/// selects all `depth` of them.
private func deepTree(_ depth: Int) -> PureXML.Model.TreeNode {
var node: PureXML.Model.Node = .text("x")
for _ in 0 ..< depth {
node = .element(.init("a", children: [node]))
}
return PureXML.Model.TreeNode(.document([node]))
}
#if os(WASI)
@Test("ordering the descendants of a deep document is linear")
#else
@Test("ordering the descendants of a deep document is linear", .timeLimit(.minutes(1)))
#endif
func test_deepDescendantOrderingIsLinear() throws {
let depth = 50000
let tree = deepTree(depth)
let matches = try PureXML.XPath.Query("//a").nodes(over: tree)
#expect(matches.count == depth)
}
@Test("document order is preserved for mixed element, attribute, and child results")
func test_documentOrderUnchanged() throws {
// `//node()|//@*` over a small tree exercises the element/attribute bands
// of the order key; the result must be in document order.
let xml = "<r><a x=\"1\" y=\"2\"><b/></a><c/></r>"
let strings = try PureXML.XPath.Query("//*").strings(over: PureXML.parse(xml))
// Document order of the elements: r, a, b, c (by their start tags).
let names = try PureXML.XPath.Query("//*").elements(over: PureXML.parse(xml)).map(\.name.localName)
#expect(names == ["r", "a", "b", "c"])
#expect(strings.count == 4)
}
/// A value `Node` nesting `depth` `a` elements inside one another, as a document.
private func deepValueDocument(_ depth: Int) -> PureXML.Model.Node {
var node: PureXML.Model.Node = .text("leaf")
for _ in 0 ..< depth {
node = .element(.init("a", children: [node]))
}
return .document([node])
}
#if os(WASI)
@Test("the value-Node result API is linear for nested results")
#else
@Test("the value-Node result API is linear for nested results", .timeLimit(.minutes(1)))
#endif
func test_nestedValueResultsAreLinear() throws {
// `//a` over a deep chain selects every `a`, each containing all deeper
// ones. Without projection sharing, materializing them as value `Node`s is
// the sum of their subtree sizes, which is quadratic (#348).
let depth = 30000
let document = deepValueDocument(depth)
let results = try PureXML.XPath.Query("//a").evaluate(over: document)
#expect(results.count == depth)
// The outermost result still carries the full nesting, proving the shared
// projection did not truncate any subtree: descending the first child from
// the outermost `a` reaches all `depth` `a` elements.
var element = results.first?.element
var reachable = 0
while let current = element {
reachable += 1
element = current.children.first?.element
}
#expect(reachable == depth)
}
}