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);
}
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 {
const global = self.identity_map.getPtr(@intFromPtr(obj)) orelse {
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>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -19,6 +19,10 @@ const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator;
const Page = @import("../Page.zig");
const Element = @import("Element.zig");
const DOMRect = @import("DOMRect.zig");
@@ -32,7 +36,9 @@ pub fn registerTypes() []const type {
const IntersectionObserver = @This();
_callback: js.Function.Global,
_page: *Page,
_arena: Allocator,
_callback: js.Function.Temp,
_observing: std.ArrayList(*Element) = .{},
_root: ?*Element = null,
_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 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) {
.scalar => |s| blk: {
const arr = try page.arena.alloc(f64, 1);
const arr = try arena.alloc(f64, 1);
arr[0] = s;
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,
._root = opts.root,
._root_margin = root_margin,
._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 {
@@ -90,10 +113,11 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
// Register with page if this is our first observation
if (self._observing.items.len == 0) {
page.js.strongRef(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
// 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| {
if (elem == target) {
_ = self._observing.swapRemove(i);
@@ -115,21 +139,31 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element) void {
var j: usize = 0;
while (j < self._pending_entries.items.len) {
if (self._pending_entries.items[j]._target == target) {
_ = self._pending_entries.swapRemove(j);
const entry = self._pending_entries.swapRemove(j);
entry.deinit(false);
} else {
j += 1;
}
}
return;
break;
}
}
if (self._observing.items.len == 0) {
page.js.safeWeakRef(self);
}
}
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
page.unregisterIntersectionObserver(self);
self._observing.clearRetainingCapacity();
self._previous_states.clearRetainingCapacity();
for (self._pending_entries.items) |entry| {
entry.deinit(false);
}
self._pending_entries.clearRetainingCapacity();
page.js.safeWeakRef(self);
}
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);
if (should_report) {
const entry = try page.arena.create(IntersectionObserverEntry);
const arena = try page.getArena(.{ .debug = "IntersectionObserverEntry" });
const entry = try arena.create(IntersectionObserverEntry);
entry.* = .{
._page = page,
._arena = arena,
._target = target,
._time = 0.0, // TODO: Get actual timestamp
._bounding_client_rect = data.bounding_client_rect,
@@ -217,12 +254,12 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
._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
// 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 {
@@ -258,14 +295,20 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
}
pub const IntersectionObserverEntry = struct {
_target: *Element,
_page: *Page,
_arena: Allocator,
_time: f64,
_target: *Element,
_bounding_client_rect: *DOMRect,
_intersection_rect: *DOMRect,
_root_bounds: *DOMRect,
_intersection_ratio: f64,
_is_intersecting: bool,
pub fn deinit(self: *const IntersectionObserverEntry, _: bool) void {
self._page.releaseArena(self._arena);
}
pub fn getTarget(self: *const IntersectionObserverEntry) *Element {
return self._target;
}
@@ -301,6 +344,8 @@ pub const IntersectionObserverEntry = struct {
pub const name = "IntersectionObserverEntry";
pub const prototype_chain = bridge.prototypeChain();
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, .{});
@@ -320,6 +365,8 @@ pub const JsApi = struct {
pub const name = "IntersectionObserver";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
};
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>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -25,6 +25,10 @@ const Node = @import("Node.zig");
const Element = @import("Element.zig");
const log = @import("../../log.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator;
pub fn registerTypes() []const type {
return &.{
MutationObserver,
@@ -34,9 +38,12 @@ pub fn registerTypes() []const type {
const MutationObserver = @This();
_callback: js.Function.Global,
_page: *Page,
_arena: Allocator,
_callback: js.Function.Temp,
_observing: std.ArrayList(Observing) = .{},
_pending_records: std.ArrayList(*MutationRecord) = .{},
/// Intrusively linked to next element (see Page.zig).
node: std.DoublyLinkedList.Node = .{},
@@ -55,19 +62,38 @@ pub const ObserveOptions = struct {
attributeFilter: ?[]const []const u8 = null,
};
pub fn init(callback: js.Function.Global, page: *Page) !*MutationObserver {
return page._factory.create(MutationObserver{
pub fn init(callback: js.Function.Temp, page: *Page) !*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,
});
};
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 {
const arena = self._arena;
// Deep copy attributeFilter if present
var copied_options = options;
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| {
filter_copy[i] = try page.arena.dupe(u8, name);
filter_copy[i] = try arena.dupe(u8, name);
}
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
if (self._observing.items.len == 0) {
page.js.strongRef(self);
try page.registerMutationObserver(self);
}
try self._observing.append(page.arena, .{
try self._observing.append(arena, .{
.target = target,
.options = copied_options,
});
@@ -98,7 +125,11 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
pub fn disconnect(self: *MutationObserver, page: *Page) void {
page.unregisterMutationObserver(self);
self._observing.clearRetainingCapacity();
for (self._pending_records.items) |record| {
record.deinit(false);
}
self._pending_records.clearRetainingCapacity();
page.js.safeWeakRef(self);
}
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,
._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)
try page.arena.dupe(u8, old_value.?.str())
try arena.dupe(u8, old_value.?.str())
else
null,
._added_nodes = &.{},
._removed_nodes = &.{},
._previous_sibling = null,
._next_sibling = null,
});
};
try self._pending_records.append(page.arena, record);
try self._pending_records.append(self._arena, record);
try page.scheduleMutationDelivery();
break;
@@ -180,21 +215,25 @@ pub fn notifyCharacterDataChange(
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,
._target = target,
._attribute_name = 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
null,
._added_nodes = &.{},
._removed_nodes = &.{},
._previous_sibling = null,
._next_sibling = null,
});
};
try self._pending_records.append(page.arena, record);
try self._pending_records.append(self._arena, record);
try page.scheduleMutationDelivery();
break;
@@ -224,18 +263,22 @@ pub fn notifyChildListChange(
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,
._target = target,
._attribute_name = null,
._old_value = null,
._added_nodes = try page.arena.dupe(*Node, added_nodes),
._removed_nodes = try page.arena.dupe(*Node, removed_nodes),
._added_nodes = try arena.dupe(*Node, added_nodes),
._removed_nodes = try arena.dupe(*Node, removed_nodes),
._previous_sibling = previous_sibling,
._next_sibling = next_sibling,
});
};
try self._pending_records.append(page.arena, record);
try self._pending_records.append(self._arena, record);
try page.scheduleMutationDelivery();
break;
@@ -263,7 +306,9 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
pub const MutationRecord = struct {
_type: Type,
_page: *Page,
_target: *Node,
_arena: Allocator,
_attribute_name: ?[]const u8,
_old_value: ?[]const u8,
_added_nodes: []const *Node,
@@ -277,6 +322,10 @@ pub const MutationRecord = struct {
characterData,
};
pub fn deinit(self: *const MutationRecord, _: bool) void {
self._page.releaseArena(self._arena);
}
pub fn getType(self: *const MutationRecord) []const u8 {
return switch (self._type) {
.attributes => "attributes",
@@ -327,6 +376,8 @@ pub const MutationRecord = struct {
pub const name = "MutationRecord";
pub const prototype_chain = bridge.prototypeChain();
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, .{});
@@ -348,6 +399,8 @@ pub const JsApi = struct {
pub const name = "MutationObserver";
pub const prototype_chain = bridge.prototypeChain();
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, .{});