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.
81 lines
3.6 KiB
Swift
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)
|
|
}
|
|
}
|