From a4abbb6d13fcae99fed3ba2fabebf1253820234d Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Wed, 29 Apr 2026 00:37:12 +0200 Subject: [PATCH] xpath: cache attribute axis nodes via frame lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/browser/xpath/Evaluator.zig | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/browser/xpath/Evaluator.zig b/src/browser/xpath/Evaluator.zig index a16d7b37..11f7a6c1 100644 --- a/src/browser/xpath/Evaluator.zig +++ b/src/browser/xpath/Evaluator.zig @@ -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); } }