mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 06:23:45 +00:00
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:
@@ -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")) {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user