rework _element_attr_listeners

Merges caching and lazy evaluation logic in `_element_attr_listeners`; this introduce allocations for inline function values, which is a price we pay for future compat w/ modern browsers.

Spec also require us to keep a list of **internal raw uncompiled handler**, which is a fancy way to say store-as-bytes-evaluate-later.
This commit is contained in:
Halil Durak
2026-01-30 14:32:48 +03:00
parent ef1bb7f519
commit 11ac11496d
3 changed files with 55 additions and 34 deletions

View File

@@ -276,7 +276,6 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._arena_pool_leak_track.clearRetainingCapacity(); self._arena_pool_leak_track.clearRetainingCapacity();
} }
// We force a garbage collection between page navigations to keep v8 // We force a garbage collection between page navigations to keep v8
// memory usage as low as possible. // memory usage as low as possible.
self._session.browser.env.memoryPressureNotification(.moderate); self._session.browser.env.memoryPressureNotification(.moderate);
@@ -1173,7 +1172,10 @@ pub fn setAttrListener(
self: *Page, self: *Page,
element: *Element, element: *Element,
listener_type: Element.KnownListener, listener_type: Element.KnownListener,
listener_callback: JS.Function.Global, /// This can be;
/// []const u8 or []u8 (memory will be duped anyway) or,
/// JS function (persisted).
raw_or_function: anytype,
) !void { ) !void {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.event, "Page.setAttrListener", .{ log.debug(.event, "Page.setAttrListener", .{
@@ -1184,16 +1186,53 @@ pub fn setAttrListener(
const key = element.calcAttrListenerKey(listener_type); const key = element.calcAttrListenerKey(listener_type);
const gop = try self._element_attr_listeners.getOrPut(self.arena, key); const gop = try self._element_attr_listeners.getOrPut(self.arena, key);
gop.value_ptr.* = listener_callback; switch (@TypeOf(raw_or_function)) {
[]const u8, []u8 => gop.value_ptr.* = .{ .raw = try self.arena.dupe(u8, raw_or_function) },
JS.Function.Global => gop.value_ptr.* = .{ .function = raw_or_function },
else => |T| @panic("setAttrListener: unknown type " ++ @typeName(T)),
}
} }
/// Returns the inline event listener by an element and listener type. /// Returns the inline event listener function by an element and listener type.
pub fn getAttrListener( pub fn getAttrListener(
self: *const Page, self: *Page,
element: *Element, element: *Element,
listener_type: Element.KnownListener, listener_type: Element.KnownListener,
) ?JS.Function.Global { ) ?JS.Function.Global {
return self._element_attr_listeners.get(element.calcAttrListenerKey(listener_type)); const listeners = &self._element_attr_listeners;
// Check if there's such attr listener.
const key = element.calcAttrListenerKey(listener_type);
const listener = listeners.getPtr(key) orelse return null;
return switch (listener.*) {
// Fast path.
.function => |function| function,
// Lazy evaluation.
.raw => |untrusted| {
// First time access to this getter; try to compile as function and cache result.
const function = self.js.stringToPersistedFunction(untrusted) catch |err| {
// Not a valid expression; log this to find out if its something
// that we should be supporting.
log.warn(.unknown_prop, "Page.getAttrListener", .{
.expression = untrusted,
.err = err,
});
// We can remove this safely.
const result = listeners.remove(key);
lp.assert(result == true, "Page.getAttrListener: unexpected result", .{});
// Remove invalid bytes.
self.arena.free(untrusted);
return null;
};
// Now that we obtained a function; this has no use.
self.arena.free(untrusted);
// Cache the resulting function.
listener.* = .{ .function = function };
return function;
},
};
} }
pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void { pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void {
@@ -2242,14 +2281,12 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi
const has_on_prefix = @as(u16, @bitCast([2]u8{ name.ptr[0], name.ptr[1 % name.len] })) == asUint("on"); const has_on_prefix = @as(u16, @bitCast([2]u8{ name.ptr[0], name.ptr[1 % name.len] })) == asUint("on");
// We may have found an event handler. // We may have found an event handler.
if (has_on_prefix) { if (has_on_prefix) {
// Must be usable as function.
const func = self.js.stringToPersistedFunction(attr.value.slice()) catch continue;
// Longest known listener kind is 32 bytes long. // Longest known listener kind is 32 bytes long.
const remaining: u6 = @truncate(name.len -| 2); const remaining: u6 = @truncate(name.len -| 2);
const unsafe = name.ptr + 2; const unsafe = name.ptr + 2;
const Vec16x8 = @Vector(16, u8); const Vec16x8 = @Vector(16, u8);
const Vec32x8 = @Vector(32, u8); const Vec32x8 = @Vector(32, u8);
const func = attr.value.slice();
switch (remaining) { switch (remaining) {
3 => if (@as(u24, @bitCast(unsafe[0..3].*)) == asUint("cut")) { 3 => if (@as(u24, @bitCast(unsafe[0..3].*)) == asUint("cut")) {

View File

@@ -53,8 +53,15 @@ pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
/// ///
/// See `calcAttrListenerKey` to obtain one. /// See `calcAttrListenerKey` to obtain one.
const AttrListenerKey = u64; const AttrListenerKey = u64;
/// We lazily evaluate functions in order to not to create garbage.
const AttrListener = union(enum(u1)) {
/// Raw bytes; this may or may not be a valid JS expression.
raw: []const u8,
/// A valid JS function; can be executed directly.
function: js.Function.Global,
};
/// Use `getAttrListenerKey` to create a key. /// Use `getAttrListenerKey` to create a key.
pub const AttrListenerLookup = std.AutoHashMapUnmanaged(AttrListenerKey, js.Function.Global); pub const AttrListenerLookup = std.AutoHashMapUnmanaged(AttrListenerKey, AttrListener);
/// Enum of known event listeners; increasing the size of it (u7) /// Enum of known event listeners; increasing the size of it (u7)
/// can cause `AttrListenerKey` to behave incorrectly. /// can cause `AttrListenerKey` to behave incorrectly.

View File

@@ -339,30 +339,7 @@ fn getAttributeFunction(
listener_type: Element.KnownListener, listener_type: Element.KnownListener,
page: *Page, page: *Page,
) ?js.Function.Global { ) ?js.Function.Global {
const element = self.asElement(); return page.getAttrListener(self.asElement(), listener_type);
// Check if we've already cached this.
if (page.getAttrListener(element, listener_type)) |cached_func| {
return cached_func;
}
// Not found in cache; parse from attribute list.
const js_expression = element.getAttributeSafe(.wrap(@tagName(listener_type))) orelse {
return null;
};
const callback = page.js.stringToPersistedFunction(js_expression) catch {
// Not a valid expression; log this to find out if its something
// that we should be supporting.
log.warn(.unknown_prop, "Html.getAttributeFunction", .{ .expression = js_expression });
return null;
};
// Cache the function for future calls.
page.setAttrListener(element, listener_type, callback) catch {
// This is fine :tm: we likely hit out of memory.
};
return callback;
} }
pub fn setOnAbort(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { pub fn setOnAbort(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {