xpath: cache attribute axis nodes via frame lookup

The attribute axis was calling Entry.toAttribute on every visit,
materializing fresh *Attribute structs (plus duped name/value strings)
into page-lifetime storage. Repeated XPath queries — the Capybara/
Selenium polling pattern this PR targets — accumulated unbounded
copies for the same DOM entries. Route through frame._attribute_lookup
so each Entry resolves to a single cached *Attribute, matching
List.getAttribute and NamedNodeMap.getAtIndex.
This commit is contained in:
Navid EMAD
2026-04-29 00:37:12 +02:00
parent 33714a4dfd
commit a4abbb6d13

View File

@@ -277,11 +277,15 @@ fn appendAttributes(self: *Evaluator, node: *Node, out: *std.ArrayList(*Node)) E
const el = node.is(Element) orelse return;
var it = el.attributeIterator();
while (it.next()) |entry| {
// Materialize as full Attribute so the result is *Node-uniform.
// Allocates from frame.arena (long-lived); attribute axis is
// typically leaf, so churn is bounded.
const attr = try entry.toAttribute(el, self.frame);
try out.append(self.arena, attr._proto);
// Memoize via frame._attribute_lookup so repeated XPath queries
// (Capybara/Selenium polling) reuse the same *Attribute instead
// of leaking fresh ones into page-lifetime storage on every call.
// Same pattern as Attribute.List.getAttribute / NamedNodeMap.getAtIndex.
const gop = try self.frame._attribute_lookup.getOrPut(self.frame.arena, @intFromPtr(entry));
if (!gop.found_existing) {
gop.value_ptr.* = try entry.toAttribute(el, self.frame);
}
try out.append(self.arena, gop.value_ptr.*._proto);
}
}