Merge pull request #1417 from lightpanda-io/observer_arenas

Leverage finalizers and ArenaPool in Intersction and Mutation Observer
This commit is contained in:
Karl Seguin
2026-02-06 07:42:38 +08:00
committed by GitHub
3 changed files with 150 additions and 38 deletions

View File

@@ -244,6 +244,18 @@ pub fn weakRef(self: *Context, obj: anytype) void {
v8.v8__Global__SetWeakFinalizer(global, obj, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter); v8.v8__Global__SetWeakFinalizer(global, obj, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
} }
pub fn safeWeakRef(self: *Context, obj: anytype) void {
const global = self.identity_map.getPtr(@intFromPtr(obj)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__ClearWeak(global);
v8.v8__Global__SetWeakFinalizer(global, obj, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
}
pub fn strongRef(self: *Context, obj: anytype) void { pub fn strongRef(self: *Context, obj: anytype) void {
const global = self.identity_map.getPtr(@intFromPtr(obj)) orelse { const global = self.identity_map.getPtr(@intFromPtr(obj)) orelse {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
// //
// Francis Bouvier <francis@lightpanda.io> // Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io> // Pierre Tachoire <pierre@lightpanda.io>
@@ -19,6 +19,10 @@ const std = @import("std");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator;
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Element = @import("Element.zig"); const Element = @import("Element.zig");
const DOMRect = @import("DOMRect.zig"); const DOMRect = @import("DOMRect.zig");
@@ -32,7 +36,9 @@ pub fn registerTypes() []const type {
const IntersectionObserver = @This(); const IntersectionObserver = @This();
_callback: js.Function.Global, _page: *Page,
_arena: Allocator,
_callback: js.Function.Temp,
_observing: std.ArrayList(*Element) = .{}, _observing: std.ArrayList(*Element) = .{},
_root: ?*Element = null, _root: ?*Element = null,
_root_margin: []const u8 = "0px", _root_margin: []const u8 = "0px",
@@ -59,25 +65,42 @@ pub const ObserverInit = struct {
}; };
}; };
pub fn init(callback: js.Function.Global, options: ?ObserverInit, page: *Page) !*IntersectionObserver { pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*IntersectionObserver {
const arena = try page.getArena(.{ .debug = "IntersectionObserver" });
errdefer page.releaseArena(arena);
const opts = options orelse ObserverInit{}; const opts = options orelse ObserverInit{};
const root_margin = if (opts.rootMargin) |rm| try page.arena.dupe(u8, rm) else "0px"; const root_margin = if (opts.rootMargin) |rm| try arena.dupe(u8, rm) else "0px";
const threshold = switch (opts.threshold) { const threshold = switch (opts.threshold) {
.scalar => |s| blk: { .scalar => |s| blk: {
const arr = try page.arena.alloc(f64, 1); const arr = try arena.alloc(f64, 1);
arr[0] = s; arr[0] = s;
break :blk arr; break :blk arr;
}, },
.array => |arr| try page.arena.dupe(f64, arr), .array => |arr| try arena.dupe(f64, arr),
}; };
return page._factory.create(IntersectionObserver{ const self = try arena.create(IntersectionObserver);
self.* = .{
._page = page,
._arena = arena,
._callback = callback, ._callback = callback,
._root = opts.root, ._root = opts.root,
._root_margin = root_margin, ._root_margin = root_margin,
._threshold = threshold, ._threshold = threshold,
}); };
return self;
}
pub fn deinit(self: *IntersectionObserver, shutdown: bool) void {
const page = self._page;
page.js.release(self._callback);
if ((comptime IS_DEBUG) and !shutdown) {
std.debug.assert(self._observing.items.len == 0);
}
page.releaseArena(self._arena);
} }
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void { pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
@@ -90,10 +113,11 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
// Register with page if this is our first observation // Register with page if this is our first observation
if (self._observing.items.len == 0) { if (self._observing.items.len == 0) {
page.js.strongRef(self);
try page.registerIntersectionObserver(self); try page.registerIntersectionObserver(self);
} }
try self._observing.append(page.arena, target); try self._observing.append(self._arena, target);
// Don't initialize previous state yet - let checkIntersection do it // Don't initialize previous state yet - let checkIntersection do it
// This ensures we get an entry on first observation // This ensures we get an entry on first observation
@@ -105,7 +129,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
} }
} }
pub fn unobserve(self: *IntersectionObserver, target: *Element) void { pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) void {
for (self._observing.items, 0..) |elem, i| { for (self._observing.items, 0..) |elem, i| {
if (elem == target) { if (elem == target) {
_ = self._observing.swapRemove(i); _ = self._observing.swapRemove(i);
@@ -115,21 +139,31 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element) void {
var j: usize = 0; var j: usize = 0;
while (j < self._pending_entries.items.len) { while (j < self._pending_entries.items.len) {
if (self._pending_entries.items[j]._target == target) { if (self._pending_entries.items[j]._target == target) {
_ = self._pending_entries.swapRemove(j); const entry = self._pending_entries.swapRemove(j);
entry.deinit(false);
} else { } else {
j += 1; j += 1;
} }
} }
return; break;
} }
} }
if (self._observing.items.len == 0) {
page.js.safeWeakRef(self);
}
} }
pub fn disconnect(self: *IntersectionObserver, page: *Page) void { pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
page.unregisterIntersectionObserver(self); page.unregisterIntersectionObserver(self);
self._observing.clearRetainingCapacity(); self._observing.clearRetainingCapacity();
self._previous_states.clearRetainingCapacity(); self._previous_states.clearRetainingCapacity();
for (self._pending_entries.items) |entry| {
entry.deinit(false);
}
self._pending_entries.clearRetainingCapacity(); self._pending_entries.clearRetainingCapacity();
page.js.safeWeakRef(self);
} }
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry { pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
@@ -206,8 +240,11 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
(was_intersecting_opt != null and was_intersecting_opt.? != is_now_intersecting); (was_intersecting_opt != null and was_intersecting_opt.? != is_now_intersecting);
if (should_report) { if (should_report) {
const entry = try page.arena.create(IntersectionObserverEntry); const arena = try page.getArena(.{ .debug = "IntersectionObserverEntry" });
const entry = try arena.create(IntersectionObserverEntry);
entry.* = .{ entry.* = .{
._page = page,
._arena = arena,
._target = target, ._target = target,
._time = 0.0, // TODO: Get actual timestamp ._time = 0.0, // TODO: Get actual timestamp
._bounding_client_rect = data.bounding_client_rect, ._bounding_client_rect = data.bounding_client_rect,
@@ -217,12 +254,12 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
._is_intersecting = is_now_intersecting, ._is_intersecting = is_now_intersecting,
}; };
try self._pending_entries.append(page.arena, entry); try self._pending_entries.append(self._arena, entry);
} }
// Always update the previous state, even if we didn't report // Always update the previous state, even if we didn't report
// This ensures we can detect state changes on subsequent checks // This ensures we can detect state changes on subsequent checks
try self._previous_states.put(page.arena, target, is_now_intersecting); try self._previous_states.put(self._arena, target, is_now_intersecting);
} }
pub fn checkIntersections(self: *IntersectionObserver, page: *Page) !void { pub fn checkIntersections(self: *IntersectionObserver, page: *Page) !void {
@@ -258,14 +295,20 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
} }
pub const IntersectionObserverEntry = struct { pub const IntersectionObserverEntry = struct {
_target: *Element, _page: *Page,
_arena: Allocator,
_time: f64, _time: f64,
_target: *Element,
_bounding_client_rect: *DOMRect, _bounding_client_rect: *DOMRect,
_intersection_rect: *DOMRect, _intersection_rect: *DOMRect,
_root_bounds: *DOMRect, _root_bounds: *DOMRect,
_intersection_ratio: f64, _intersection_ratio: f64,
_is_intersecting: bool, _is_intersecting: bool,
pub fn deinit(self: *const IntersectionObserverEntry, _: bool) void {
self._page.releaseArena(self._arena);
}
pub fn getTarget(self: *const IntersectionObserverEntry) *Element { pub fn getTarget(self: *const IntersectionObserverEntry) *Element {
return self._target; return self._target;
} }
@@ -301,6 +344,8 @@ pub const IntersectionObserverEntry = struct {
pub const name = "IntersectionObserverEntry"; pub const name = "IntersectionObserverEntry";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(IntersectionObserverEntry.deinit);
}; };
pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{}); pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{});
@@ -320,6 +365,8 @@ pub const JsApi = struct {
pub const name = "IntersectionObserver"; pub const name = "IntersectionObserver";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
}; };
pub const constructor = bridge.constructor(init, .{}); pub const constructor = bridge.constructor(init, .{});

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
// //
// Francis Bouvier <francis@lightpanda.io> // Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io> // Pierre Tachoire <pierre@lightpanda.io>
@@ -25,6 +25,10 @@ const Node = @import("Node.zig");
const Element = @import("Element.zig"); const Element = @import("Element.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator;
pub fn registerTypes() []const type { pub fn registerTypes() []const type {
return &.{ return &.{
MutationObserver, MutationObserver,
@@ -34,9 +38,12 @@ pub fn registerTypes() []const type {
const MutationObserver = @This(); const MutationObserver = @This();
_callback: js.Function.Global, _page: *Page,
_arena: Allocator,
_callback: js.Function.Temp,
_observing: std.ArrayList(Observing) = .{}, _observing: std.ArrayList(Observing) = .{},
_pending_records: std.ArrayList(*MutationRecord) = .{}, _pending_records: std.ArrayList(*MutationRecord) = .{},
/// Intrusively linked to next element (see Page.zig). /// Intrusively linked to next element (see Page.zig).
node: std.DoublyLinkedList.Node = .{}, node: std.DoublyLinkedList.Node = .{},
@@ -55,19 +62,38 @@ pub const ObserveOptions = struct {
attributeFilter: ?[]const []const u8 = null, attributeFilter: ?[]const []const u8 = null,
}; };
pub fn init(callback: js.Function.Global, page: *Page) !*MutationObserver { pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
return page._factory.create(MutationObserver{ const arena = try page.getArena(.{ .debug = "MutationObserver" });
errdefer page.releaseArena(arena);
const self = try arena.create(MutationObserver);
self.* = .{
._page = page,
._arena = arena,
._callback = callback, ._callback = callback,
}); };
return self;
}
pub fn deinit(self: *MutationObserver, shutdown: bool) void {
const page = self._page;
page.js.release(self._callback);
if ((comptime IS_DEBUG) and !shutdown) {
std.debug.assert(self._observing.items.len == 0);
}
page.releaseArena(self._arena);
} }
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void { pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
const arena = self._arena;
// Deep copy attributeFilter if present // Deep copy attributeFilter if present
var copied_options = options; var copied_options = options;
if (options.attributeFilter) |filter| { if (options.attributeFilter) |filter| {
const filter_copy = try page.arena.alloc([]const u8, filter.len); const filter_copy = try arena.alloc([]const u8, filter.len);
for (filter, 0..) |name, i| { for (filter, 0..) |name, i| {
filter_copy[i] = try page.arena.dupe(u8, name); filter_copy[i] = try arena.dupe(u8, name);
} }
copied_options.attributeFilter = filter_copy; copied_options.attributeFilter = filter_copy;
} }
@@ -86,10 +112,11 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
// Register with page if this is our first observation // Register with page if this is our first observation
if (self._observing.items.len == 0) { if (self._observing.items.len == 0) {
page.js.strongRef(self);
try page.registerMutationObserver(self); try page.registerMutationObserver(self);
} }
try self._observing.append(page.arena, .{ try self._observing.append(arena, .{
.target = target, .target = target,
.options = copied_options, .options = copied_options,
}); });
@@ -98,7 +125,11 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
pub fn disconnect(self: *MutationObserver, page: *Page) void { pub fn disconnect(self: *MutationObserver, page: *Page) void {
page.unregisterMutationObserver(self); page.unregisterMutationObserver(self);
self._observing.clearRetainingCapacity(); self._observing.clearRetainingCapacity();
for (self._pending_records.items) |record| {
record.deinit(false);
}
self._pending_records.clearRetainingCapacity(); self._pending_records.clearRetainingCapacity();
page.js.safeWeakRef(self);
} }
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord { pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
@@ -139,21 +170,25 @@ pub fn notifyAttributeChange(
} }
} }
const record = try page._factory.create(MutationRecord{ const arena = try self._page.getArena(.{ .debug = "MutationRecord" });
const record = try arena.create(MutationRecord);
record.* = .{
._page = page,
._arena = arena,
._type = .attributes, ._type = .attributes,
._target = target_node, ._target = target_node,
._attribute_name = try page.arena.dupe(u8, attribute_name.str()), ._attribute_name = try arena.dupe(u8, attribute_name.str()),
._old_value = if (obs.options.attributeOldValue and old_value != null) ._old_value = if (obs.options.attributeOldValue and old_value != null)
try page.arena.dupe(u8, old_value.?.str()) try arena.dupe(u8, old_value.?.str())
else else
null, null,
._added_nodes = &.{}, ._added_nodes = &.{},
._removed_nodes = &.{}, ._removed_nodes = &.{},
._previous_sibling = null, ._previous_sibling = null,
._next_sibling = null, ._next_sibling = null,
}); };
try self._pending_records.append(page.arena, record); try self._pending_records.append(self._arena, record);
try page.scheduleMutationDelivery(); try page.scheduleMutationDelivery();
break; break;
@@ -180,21 +215,25 @@ pub fn notifyCharacterDataChange(
continue; continue;
} }
const record = try page._factory.create(MutationRecord{ const arena = try self._page.getArena(.{ .debug = "MutationRecord" });
const record = try arena.create(MutationRecord);
record.* = .{
._page = page,
._arena = arena,
._type = .characterData, ._type = .characterData,
._target = target, ._target = target,
._attribute_name = null, ._attribute_name = null,
._old_value = if (obs.options.characterDataOldValue and old_value != null) ._old_value = if (obs.options.characterDataOldValue and old_value != null)
try page.arena.dupe(u8, old_value.?) try arena.dupe(u8, old_value.?)
else else
null, null,
._added_nodes = &.{}, ._added_nodes = &.{},
._removed_nodes = &.{}, ._removed_nodes = &.{},
._previous_sibling = null, ._previous_sibling = null,
._next_sibling = null, ._next_sibling = null,
}); };
try self._pending_records.append(page.arena, record); try self._pending_records.append(self._arena, record);
try page.scheduleMutationDelivery(); try page.scheduleMutationDelivery();
break; break;
@@ -224,18 +263,22 @@ pub fn notifyChildListChange(
continue; continue;
} }
const record = try page._factory.create(MutationRecord{ const arena = try self._page.getArena(.{ .debug = "MutationRecord" });
const record = try arena.create(MutationRecord);
record.* = .{
._page = page,
._arena = arena,
._type = .childList, ._type = .childList,
._target = target, ._target = target,
._attribute_name = null, ._attribute_name = null,
._old_value = null, ._old_value = null,
._added_nodes = try page.arena.dupe(*Node, added_nodes), ._added_nodes = try arena.dupe(*Node, added_nodes),
._removed_nodes = try page.arena.dupe(*Node, removed_nodes), ._removed_nodes = try arena.dupe(*Node, removed_nodes),
._previous_sibling = previous_sibling, ._previous_sibling = previous_sibling,
._next_sibling = next_sibling, ._next_sibling = next_sibling,
}); };
try self._pending_records.append(page.arena, record); try self._pending_records.append(self._arena, record);
try page.scheduleMutationDelivery(); try page.scheduleMutationDelivery();
break; break;
@@ -263,7 +306,9 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
pub const MutationRecord = struct { pub const MutationRecord = struct {
_type: Type, _type: Type,
_page: *Page,
_target: *Node, _target: *Node,
_arena: Allocator,
_attribute_name: ?[]const u8, _attribute_name: ?[]const u8,
_old_value: ?[]const u8, _old_value: ?[]const u8,
_added_nodes: []const *Node, _added_nodes: []const *Node,
@@ -277,6 +322,10 @@ pub const MutationRecord = struct {
characterData, characterData,
}; };
pub fn deinit(self: *const MutationRecord, _: bool) void {
self._page.releaseArena(self._arena);
}
pub fn getType(self: *const MutationRecord) []const u8 { pub fn getType(self: *const MutationRecord) []const u8 {
return switch (self._type) { return switch (self._type) {
.attributes => "attributes", .attributes => "attributes",
@@ -327,6 +376,8 @@ pub const MutationRecord = struct {
pub const name = "MutationRecord"; pub const name = "MutationRecord";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MutationRecord.deinit);
}; };
pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{}); pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{});
@@ -348,6 +399,8 @@ pub const JsApi = struct {
pub const name = "MutationObserver"; pub const name = "MutationObserver";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MutationObserver.deinit);
}; };
pub const constructor = bridge.constructor(MutationObserver.init, .{}); pub const constructor = bridge.constructor(MutationObserver.init, .{});