mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Merge pull request #1417 from lightpanda-io/observer_arenas
Leverage finalizers and ArenaPool in Intersction and Mutation Observer
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
Reference in New Issue
Block a user