From adf474774ea30c4290defb9df6cb1a4c6d4dace2 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 3 Jul 2026 09:36:21 +0800 Subject: [PATCH] perf: used cachedParse for closest and waitForSelector closest was already hitting the cache, but it was doing it inside the loop. Moving it outside the loop avoids the cache lookup. Inversely, Runner.waitForSelector was parsing outside the loop, but it wasn't caching. Repeated calls to waitForSelector with the same selector would not leverage the cache. --- src/browser/Runner.zig | 9 ++-- src/browser/webapi/Element.zig | 6 ++- src/browser/webapi/selector/Selector.zig | 68 ++++++++++-------------- 3 files changed, 35 insertions(+), 48 deletions(-) diff --git a/src/browser/Runner.zig b/src/browser/Runner.zig index 111fe152e..1f08018d6 100644 --- a/src/browser/Runner.zig +++ b/src/browser/Runner.zig @@ -301,13 +301,10 @@ fn _tick(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa return .done; } -pub fn waitForSelector(self: *Runner, frame_id: u32, selector: [:0]const u8, timeout_ms: u32) !*Node.Element { +pub fn waitForSelector(self: *Runner, frame_id: u32, input: [:0]const u8, timeout_ms: u32) !*Node.Element { const session = self.session; - const arena = try session.getArena(.small, "Runner.waitForSelector"); - defer session.releaseArena(arena); - var timer = try std.time.Timer.start(); - const parsed_selector = try Selector.parseLeaky(arena, selector); + const selector = try Selector.cachedParse(session.browser, input); while (true) { if (session.isCancelled()) { @@ -319,7 +316,7 @@ pub fn waitForSelector(self: *Runner, frame_id: u32, selector: [:0]const u8, tim }; const frame = &page.frame; - if (try parsed_selector.query(frame.document.asNode(), frame)) |el| { + if (try Selector.query(selector, frame.document.asNode(), frame)) |el| { return el; } diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 0ec8a5d9c..effb8def1 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1143,11 +1143,13 @@ pub fn animate(_: *Element, _: ?js.Object, _: ?js.Object, frame: *Frame) !*Anima return Animation.init(frame); } -pub fn closest(self: *Element, selector: []const u8, frame: *Frame) !?*Element { - if (selector.len == 0) { +pub fn closest(self: *Element, input: []const u8, frame: *Frame) !?*Element { + if (input.len == 0) { return error.SyntaxError; } + const selector = try Selector.cachedParse(frame._session.browser, input); + var current: ?*Element = self; while (current) |el| { if (try Selector.matchesWithScope(el, selector, self, frame)) { diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index 96963bec4..ecdc82251 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -21,6 +21,7 @@ const lp = @import("lightpanda"); const Node = @import("../Node.zig"); const Frame = @import("../../Frame.zig"); +const Browser = @import("../../Browser.zig"); const Parser = @import("Parser.zig"); pub const List = @import("List.zig"); @@ -44,16 +45,9 @@ pub fn mapErrorToDOM(err: anyerror) anyerror { }; } -pub fn parseLeaky(arena: Allocator, input: []const u8) !Parsed { - if (input.len == 0) { - return error.SyntaxError; - } - return .{ .selectors = try Parser.parseList(arena, input) }; -} - /// One-off synthesized selectors use the `*Uncached` variants instead. -fn cachedParse(frame: *Frame, input: []const u8) ![]const Selector { - return frame._session.browser.selector_cache.parse(input); +pub fn cachedParse(browser: *Browser, input: []const u8) ![]const Selector { + return browser.selector_cache.parse(input); } /// On the Browser because a parsed selector references no Frame/Context, so @@ -136,24 +130,23 @@ fn matchesAny(selectors: []const Selector, el: *Node.Element, scope: *Node, fram } pub fn querySelector(root: *Node, input: []const u8, frame: *Frame) !?*Node.Element { - const parsed = Parsed{ .selectors = try cachedParse(frame, input) }; - return parsed.query(root, frame); + return query(try cachedParse(frame._session.browser, input), root, frame); } pub fn querySelectorAll(root: *Node, input: []const u8, frame: *Frame) !*List { const arena = try frame.getArena(.small, "querySelectorAll"); errdefer frame.releaseArena(arena); - return collectAll(arena, try cachedParse(frame, input), root, frame); + return collectAll(arena, try cachedParse(frame._session.browser, input), root, frame); } pub fn matches(el: *Node.Element, input: []const u8, frame: *Frame) !bool { - return matchesAny(try cachedParse(frame, input), el, el.asNode(), frame); + return matchesAny(try cachedParse(frame._session.browser, input), el, el.asNode(), frame); } // Like matches, but allows the caller to specify a scope node distinct from el. // Used by closest() so that :scope always refers to the original context element. -pub fn matchesWithScope(el: *Node.Element, input: []const u8, scope: *Node.Element, frame: *Frame) !bool { - return matchesAny(try cachedParse(frame, input), el, scope.asNode(), frame); +pub fn matchesWithScope(el: *Node.Element, selector: []const Selector, scope: *Node.Element, frame: *Frame) !bool { + return matchesAny(selector, el, scope.asNode(), frame); } /// Uncached counterparts for one-off selectors (SelectorPath): parse into @@ -163,8 +156,7 @@ pub fn querySelectorUncached(arena: Allocator, root: *Node, input: []const u8, f if (input.len == 0) { return error.SyntaxError; } - const parsed = Parsed{ .selectors = try Parser.parseList(arena, input) }; - return parsed.query(root, frame); + return query(try Parser.parseList(arena, input), root, frame); } pub fn querySelectorAllUncached(root: *Node, input: []const u8, frame: *Frame) !*List { @@ -363,31 +355,27 @@ pub const Selector = struct { } }; -pub const Parsed = struct { - selectors: []const Selector, - - pub fn query(self: Parsed, root: *Node, frame: *Frame) !?*Node.Element { - for (self.selectors) |selector| { - // Fast path: single compound with only an ID selector - if (selector.segments.len == 0 and selector.first.parts.len == 1) { - const first = selector.first.parts[0]; - if (first == .id) { - const el = frame.getElementByIdFromNode(root, first.id) orelse continue; - // Check if the element is within the root subtree - const node = el.asNode(); - if (node != root and root.contains(node)) { - return el; - } - continue; - } - } - - if (List.initOne(root, selector, frame)) |node| { - if (node.is(Node.Element)) |el| { +pub fn query(selectors: []const Selector, root: *Node, frame: *Frame) !?*Node.Element { + for (selectors) |selector| { + // Fast path: single compound with only an ID selector + if (selector.segments.len == 0 and selector.first.parts.len == 1) { + const first = selector.first.parts[0]; + if (first == .id) { + const el = frame.getElementByIdFromNode(root, first.id) orelse continue; + // Check if the element is within the root subtree + const node = el.asNode(); + if (node != root and root.contains(node)) { return el; } + continue; + } + } + + if (List.initOne(root, selector, frame)) |node| { + if (node.is(Node.Element)) |el| { + return el; } } - return null; } -}; + return null; +}