mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
Rework MutationObserver callback.
Previously, MutationObserver callbacks where called using the `jsCallScopeEnd` mechanism. This was slow and resulted in records split in a way that callers might not expect. `jsCallScopeEnd` has been removed. The new approach uses the loop.timeout mechanism, much like a window.setTimeout and only registers a timeout when events have been handled. It should perform much better. Exactly how MutationRecords are supposed to be grouped is still a mystery to me. This new grouping is still wrong in many cases (according to WPT), but appears slightly less wrong; I'm pretty hopeful clients don't really have hard-coded expectations for this though. Also implement the attributeFilter option of MutationObserver. (Github)
This commit is contained in:
@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
const Loop = @import("../../runtime/loop.zig").Loop;
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
const Env = @import("../env.zig").Env;
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
const NodeList = @import("nodelist.zig").NodeList;
|
||||||
@@ -35,25 +36,37 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
|
|||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
||||||
pub const MutationObserver = struct {
|
pub const MutationObserver = struct {
|
||||||
|
loop: *Loop,
|
||||||
cbk: Env.Function,
|
cbk: Env.Function,
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
|
connected: bool,
|
||||||
|
scheduled: bool,
|
||||||
|
loop_node: Loop.CallbackNode,
|
||||||
|
|
||||||
// List of records which were observed. When the call scope ends, we need to
|
// List of records which were observed. When the call scope ends, we need to
|
||||||
// execute our callback with it.
|
// execute our callback with it.
|
||||||
observed: std.ArrayListUnmanaged(*MutationRecord),
|
observed: std.ArrayListUnmanaged(MutationRecord),
|
||||||
|
|
||||||
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
|
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
|
||||||
return .{
|
return .{
|
||||||
.cbk = cbk,
|
.cbk = cbk,
|
||||||
|
.loop = page.loop,
|
||||||
.observed = .{},
|
.observed = .{},
|
||||||
|
.connected = true,
|
||||||
|
.scheduled = false,
|
||||||
.arena = page.arena,
|
.arena = page.arena,
|
||||||
|
.loop_node = .{.func = callback},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?MutationObserverInit) !void {
|
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
|
||||||
const options = options_ orelse MutationObserverInit{};
|
const arena = self.arena;
|
||||||
|
var options = options_ orelse Options{};
|
||||||
|
if (options.attributeFilter.len > 0) {
|
||||||
|
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
const observer = try self.arena.create(Observer);
|
const observer = try arena.create(Observer);
|
||||||
observer.* = .{
|
observer.* = .{
|
||||||
.node = node,
|
.node = node,
|
||||||
.options = options,
|
.options = options,
|
||||||
@@ -102,30 +115,34 @@ pub const MutationObserver = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn jsCallScopeEnd(self: *MutationObserver) void {
|
fn callback(node: *Loop.CallbackNode, _: *?u63) void {
|
||||||
const record = self.observed.items;
|
const self: *MutationObserver = @fieldParentPtr("loop_node", node);
|
||||||
if (record.len == 0) {
|
if (self.connected == false) {
|
||||||
|
self.scheduled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.scheduled = false;
|
||||||
|
|
||||||
|
const records = self.observed.items;
|
||||||
|
if (records.len == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
defer self.observed.clearRetainingCapacity();
|
defer self.observed.clearRetainingCapacity();
|
||||||
|
|
||||||
for (record) |r| {
|
var result: Env.Function.Result = undefined;
|
||||||
const records = [_]MutationRecord{r.*};
|
self.cbk.tryCall(void, .{records}, &result) catch {
|
||||||
var result: Env.Function.Result = undefined;
|
log.debug(.user_script, "callback error", .{
|
||||||
self.cbk.tryCall(void, .{records}, &result) catch {
|
.err = result.exception,
|
||||||
log.debug(.user_script, "callback error", .{
|
.stack = result.stack,
|
||||||
.err = result.exception,
|
.source = "mutation observer",
|
||||||
.stack = result.stack,
|
});
|
||||||
.source = "mutation observer",
|
};
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
pub fn _disconnect(_: *MutationObserver) !void {
|
pub fn _disconnect(self: *MutationObserver) !void {
|
||||||
// TODO unregister listeners.
|
self.connected = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
@@ -182,31 +199,27 @@ pub const MutationRecord = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const MutationObserverInit = struct {
|
const Options = struct {
|
||||||
childList: bool = false,
|
childList: bool = false,
|
||||||
attributes: bool = false,
|
attributes: bool = false,
|
||||||
characterData: bool = false,
|
characterData: bool = false,
|
||||||
subtree: bool = false,
|
subtree: bool = false,
|
||||||
attributeOldValue: bool = false,
|
attributeOldValue: bool = false,
|
||||||
characterDataOldValue: bool = false,
|
characterDataOldValue: bool = false,
|
||||||
// TODO
|
attributeFilter: [][]const u8 = &.{},
|
||||||
// attributeFilter: [][]const u8,
|
|
||||||
|
|
||||||
fn attr(self: MutationObserverInit) bool {
|
fn attr(self: Options) bool {
|
||||||
return self.attributes or self.attributeOldValue;
|
return self.attributes or self.attributeOldValue or self.attributeFilter.len > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cdata(self: MutationObserverInit) bool {
|
fn cdata(self: Options) bool {
|
||||||
return self.characterData or self.characterDataOldValue;
|
return self.characterData or self.characterDataOldValue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Observer = struct {
|
const Observer = struct {
|
||||||
node: *parser.Node,
|
node: *parser.Node,
|
||||||
options: MutationObserverInit,
|
options: Options,
|
||||||
|
|
||||||
// record of the mutation, all observed changes in 1 call are batched
|
|
||||||
record: ?MutationRecord = null,
|
|
||||||
|
|
||||||
// reference back to the MutationObserver so that we can access the arena
|
// reference back to the MutationObserver so that we can access the arena
|
||||||
// and batch the mutation records.
|
// and batch the mutation records.
|
||||||
@@ -214,19 +227,29 @@ const Observer = struct {
|
|||||||
|
|
||||||
event_node: parser.EventNode,
|
event_node: parser.EventNode,
|
||||||
|
|
||||||
fn appliesTo(o: *const Observer, target: *parser.Node) bool {
|
fn appliesTo(self: *const Observer, target: *parser.Node, event_type: MutationEventType, event: *parser.MutationEvent,) !bool {
|
||||||
|
if (event_type == .DOMAttrModified and self.options.attributeFilter.len > 0) {
|
||||||
|
const attribute_name = try parser.mutationEventAttributeName(event);
|
||||||
|
for (self.options.attributeFilter) |needle| blk: {
|
||||||
|
if (std.mem.eql(u8, attribute_name, needle)) {
|
||||||
|
break :blk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// mutation on any target is always ok.
|
// mutation on any target is always ok.
|
||||||
if (o.options.subtree) {
|
if (self.options.subtree) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if target equals node, alway ok.
|
// if target equals node, alway ok.
|
||||||
if (target == o.node) {
|
if (target == self.node) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no subtree, no same target and no childlist, always noky.
|
// no subtree, no same target and no childlist, always noky.
|
||||||
if (!o.options.childList) {
|
if (!self.options.childList) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +257,7 @@ const Observer = struct {
|
|||||||
const walker = Walker{};
|
const walker = Walker{};
|
||||||
var next: ?*parser.Node = null;
|
var next: ?*parser.Node = null;
|
||||||
while (true) {
|
while (true) {
|
||||||
next = walker.get_next(o.node, next) catch break orelse break;
|
next = walker.get_next(self.node, next) catch break orelse break;
|
||||||
if (next.? == target) {
|
if (next.? == target) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -258,27 +281,22 @@ const Observer = struct {
|
|||||||
break :blk parser.eventTargetToNode(event_target);
|
break :blk parser.eventTargetToNode(event_target);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (self.appliesTo(node) == false) {
|
const mutation_event = parser.eventToMutationEvent(event);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const event_type = blk: {
|
const event_type = blk: {
|
||||||
const t = try parser.eventType(event);
|
const t = try parser.eventType(event);
|
||||||
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
|
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const arena = mutation_observer.arena;
|
if (try self.appliesTo(node, event_type, mutation_event) == false) {
|
||||||
if (self.record == null) {
|
return;
|
||||||
self.record = .{
|
|
||||||
.target = self.node,
|
|
||||||
.type = event_type.recordType(),
|
|
||||||
};
|
|
||||||
try mutation_observer.observed.append(arena, &self.record.?);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var record = &self.record.?;
|
var record = MutationRecord{
|
||||||
const mutation_event = parser.eventToMutationEvent(event);
|
.target = self.node,
|
||||||
|
.type = event_type.recordType(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const arena = mutation_observer.arena;
|
||||||
switch (event_type) {
|
switch (event_type) {
|
||||||
.DOMAttrModified => {
|
.DOMAttrModified => {
|
||||||
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
||||||
@@ -302,6 +320,13 @@ const Observer = struct {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try mutation_observer.observed.append(arena, record);
|
||||||
|
|
||||||
|
if (mutation_observer.scheduled == false) {
|
||||||
|
mutation_observer.scheduled = true;
|
||||||
|
_ = try mutation_observer.loop.timeout(0, &mutation_observer.loop_node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -341,10 +366,9 @@ test "Browser.DOM.MutationObserver" {
|
|||||||
\\ document.firstElementChild.setAttribute("foo", "bar");
|
\\ document.firstElementChild.setAttribute("foo", "bar");
|
||||||
\\ // ignored b/c it's about another target.
|
\\ // ignored b/c it's about another target.
|
||||||
\\ document.firstElementChild.firstChild.setAttribute("foo", "bar");
|
\\ document.firstElementChild.firstChild.setAttribute("foo", "bar");
|
||||||
\\ nb;
|
,null
|
||||||
,
|
|
||||||
"1",
|
|
||||||
},
|
},
|
||||||
|
.{ "nb", "1"},
|
||||||
.{ "mrs[0].type", "attributes" },
|
.{ "mrs[0].type", "attributes" },
|
||||||
.{ "mrs[0].target == document.firstElementChild", "true" },
|
.{ "mrs[0].target == document.firstElementChild", "true" },
|
||||||
.{ "mrs[0].target.getAttribute('foo')", "bar" },
|
.{ "mrs[0].target.getAttribute('foo')", "bar" },
|
||||||
@@ -362,10 +386,9 @@ test "Browser.DOM.MutationObserver" {
|
|||||||
\\ nb2++;
|
\\ nb2++;
|
||||||
\\ }).observe(node, { characterData: true, characterDataOldValue: true });
|
\\ }).observe(node, { characterData: true, characterDataOldValue: true });
|
||||||
\\ node.data = "foo";
|
\\ node.data = "foo";
|
||||||
\\ nb2;
|
, null
|
||||||
,
|
|
||||||
"1",
|
|
||||||
},
|
},
|
||||||
|
.{ "nb2", "1"},
|
||||||
.{ "mrs2[0].type", "characterData" },
|
.{ "mrs2[0].type", "characterData" },
|
||||||
.{ "mrs2[0].target == node", "true" },
|
.{ "mrs2[0].target == node", "true" },
|
||||||
.{ "mrs2[0].target.data", "foo" },
|
.{ "mrs2[0].target.data", "foo" },
|
||||||
@@ -382,8 +405,23 @@ test "Browser.DOM.MutationObserver" {
|
|||||||
\\ node.innerText = 'a';
|
\\ node.innerText = 'a';
|
||||||
\\ }).observe(document, { subtree:true,childList:true });
|
\\ }).observe(document, { subtree:true,childList:true });
|
||||||
\\ node.innerText = "2";
|
\\ node.innerText = "2";
|
||||||
,
|
, null
|
||||||
"2",
|
|
||||||
},
|
},
|
||||||
|
.{"node.innerText", "a"},
|
||||||
|
}, .{});
|
||||||
|
|
||||||
|
try runner.testCases(&.{
|
||||||
|
.{
|
||||||
|
\\ var node = document.getElementById("para");
|
||||||
|
\\ var attrWatch = 0;
|
||||||
|
\\ new MutationObserver(() => {
|
||||||
|
\\ attrWatch++;
|
||||||
|
\\ }).observe(document, { attributeFilter: ["name"], subtree: true });
|
||||||
|
\\ node.setAttribute("id", "1");
|
||||||
|
, null
|
||||||
|
},
|
||||||
|
.{"attrWatch", "0"},
|
||||||
|
.{ "node.setAttribute('name', 'other');", null},
|
||||||
|
.{ "attrWatch", "1"},
|
||||||
}, .{});
|
}, .{});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,10 +122,10 @@ pub const Page = struct {
|
|||||||
// load polyfills
|
// load polyfills
|
||||||
try polyfill.load(self.arena, self.main_context);
|
try polyfill.load(self.arena, self.main_context);
|
||||||
|
|
||||||
_ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node);
|
|
||||||
// message loop must run only non-test env
|
// message loop must run only non-test env
|
||||||
if (comptime !builtin.is_test) {
|
if (comptime !builtin.is_test) {
|
||||||
_ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.messageloop_node);
|
_ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node);
|
||||||
|
_ = try session.browser.app.loop.timeout(100 * std.time.ns_per_ms, &self.messageloop_node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -595,9 +595,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
// Some Zig types have code to execute to cleanup
|
// Some Zig types have code to execute to cleanup
|
||||||
destructor_callbacks: std.ArrayListUnmanaged(DestructorCallback) = .empty,
|
destructor_callbacks: std.ArrayListUnmanaged(DestructorCallback) = .empty,
|
||||||
|
|
||||||
// Some Zig types have code to execute when the call scope ends
|
|
||||||
call_scope_end_callbacks: std.ArrayListUnmanaged(CallScopeEndCallback) = .empty,
|
|
||||||
|
|
||||||
// Our module cache: normalized module specifier => module.
|
// Our module cache: normalized module specifier => module.
|
||||||
module_cache: std.StringHashMapUnmanaged(PersistentModule) = .empty,
|
module_cache: std.StringHashMapUnmanaged(PersistentModule) = .empty,
|
||||||
|
|
||||||
@@ -828,10 +825,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
try self.destructor_callbacks.append(context_arena, DestructorCallback.init(value));
|
try self.destructor_callbacks.append(context_arena, DestructorCallback.init(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comptime @hasDecl(ptr.child, "jsCallScopeEnd")) {
|
|
||||||
try self.call_scope_end_callbacks.append(context_arena, CallScopeEndCallback.init(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sometimes we're creating a new v8.Object, like when
|
// Sometimes we're creating a new v8.Object, like when
|
||||||
// we're returning a value from a function. In those cases
|
// we're returning a value from a function. In those cases
|
||||||
// we have the FunctionTemplate, and we can get an object
|
// we have the FunctionTemplate, and we can get an object
|
||||||
@@ -2639,10 +2632,6 @@ fn Caller(comptime E: type, comptime State: type) type {
|
|||||||
// Therefore, we keep a call_depth, and only reset the call_arena
|
// Therefore, we keep a call_depth, and only reset the call_arena
|
||||||
// when a top-level (call_depth == 0) function ends.
|
// when a top-level (call_depth == 0) function ends.
|
||||||
if (call_depth == 0) {
|
if (call_depth == 0) {
|
||||||
for (js_context.call_scope_end_callbacks.items) |cb| {
|
|
||||||
cb.callScopeEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
const arena: *ArenaAllocator = @alignCast(@ptrCast(js_context.call_arena.ptr));
|
const arena: *ArenaAllocator = @alignCast(@ptrCast(js_context.call_arena.ptr));
|
||||||
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
|
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user