More MutationObserver options, Performance API

This commit is contained in:
Karl Seguin
2025-11-18 20:46:17 +08:00
parent 54a2e7650a
commit 991c2c18de
18 changed files with 512 additions and 69 deletions

View File

@@ -5,8 +5,8 @@
.fingerprint = 0xda130f3af836cea0,
.dependencies = .{
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/3aa2b39cb1ab588b85970beef5b374effccf1415.tar.gz",
.hash = "v8-0.0.0-xddH66TeAwDDEs3QkHFlukxqqrRXITzzmmIn2NHISHCn",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/beb187f3337a8c458e1917dc0105003fb7ae1b2f.tar.gz",
.hash = "v8-0.0.0-xddH6x_gAwAgDtdWGHjv52NsW07MQnfpUQDpZn7RR43Y",
},
// .v8 = .{ .path = "../zig-v8-fork" }
},

View File

@@ -74,6 +74,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
const now = timestamp(.monotonic);
std.debug.print("running: {s}\n", .{task.name});
while (queue.peek()) |*task_| {
if (task_.run_at > now) {
return @intCast(task_.run_at - now);

View File

@@ -45,7 +45,6 @@ const MemoryPoolAligned = std.heap.MemoryPoolAligned;
// (and alignment) based pools.
const Factory = @This();
_page: *Page,
_size_1_8: MemoryPoolAligned([1]u8, .@"8"),
_size_8_8: MemoryPoolAligned([8]u8, .@"8"),
_size_16_8: MemoryPoolAligned([16]u8, .@"8"),
_size_24_8: MemoryPoolAligned([24]u8, .@"8"),
@@ -55,24 +54,18 @@ _size_40_8: MemoryPoolAligned([40]u8, .@"8"),
_size_48_16: MemoryPoolAligned([48]u8, .@"16"),
_size_56_8: MemoryPoolAligned([56]u8, .@"8"),
_size_64_16: MemoryPoolAligned([64]u8, .@"16"),
_size_72_8: MemoryPoolAligned([72]u8, .@"8"),
_size_80_16: MemoryPoolAligned([80]u8, .@"16"),
_size_88_8: MemoryPoolAligned([88]u8, .@"8"),
_size_96_16: MemoryPoolAligned([96]u8, .@"16"),
_size_104_8: MemoryPoolAligned([104]u8, .@"8"),
_size_112_8: MemoryPoolAligned([112]u8, .@"8"),
_size_120_8: MemoryPoolAligned([120]u8, .@"8"),
_size_128_8: MemoryPoolAligned([128]u8, .@"8"),
_size_144_8: MemoryPoolAligned([144]u8, .@"8"),
_size_152_8: MemoryPoolAligned([152]u8, .@"8"),
_size_456_8: MemoryPoolAligned([456]u8, .@"8"),
_size_520_8: MemoryPoolAligned([520]u8, .@"8"),
_size_160_8: MemoryPoolAligned([160]u8, .@"8"),
_size_648_8: MemoryPoolAligned([648]u8, .@"8"),
pub fn init(page: *Page) Factory {
return .{
._page = page,
._size_1_8 = MemoryPoolAligned([1]u8, .@"8").init(page.arena),
._size_8_8 = MemoryPoolAligned([8]u8, .@"8").init(page.arena),
._size_16_8 = MemoryPoolAligned([16]u8, .@"8").init(page.arena),
._size_24_8 = MemoryPoolAligned([24]u8, .@"8").init(page.arena),
@@ -82,18 +75,13 @@ pub fn init(page: *Page) Factory {
._size_48_16 = MemoryPoolAligned([48]u8, .@"16").init(page.arena),
._size_56_8 = MemoryPoolAligned([56]u8, .@"8").init(page.arena),
._size_64_16 = MemoryPoolAligned([64]u8, .@"16").init(page.arena),
._size_72_8 = MemoryPoolAligned([72]u8, .@"8").init(page.arena),
._size_80_16 = MemoryPoolAligned([80]u8, .@"16").init(page.arena),
._size_88_8 = MemoryPoolAligned([88]u8, .@"8").init(page.arena),
._size_96_16 = MemoryPoolAligned([96]u8, .@"16").init(page.arena),
._size_104_8 = MemoryPoolAligned([104]u8, .@"8").init(page.arena),
._size_112_8 = MemoryPoolAligned([112]u8, .@"8").init(page.arena),
._size_120_8 = MemoryPoolAligned([120]u8, .@"8").init(page.arena),
._size_128_8 = MemoryPoolAligned([128]u8, .@"8").init(page.arena),
._size_144_8 = MemoryPoolAligned([144]u8, .@"8").init(page.arena),
._size_152_8 = MemoryPoolAligned([152]u8, .@"8").init(page.arena),
._size_456_8 = MemoryPoolAligned([456]u8, .@"8").init(page.arena),
._size_520_8 = MemoryPoolAligned([520]u8, .@"8").init(page.arena),
._size_160_8 = MemoryPoolAligned([160]u8, .@"8").init(page.arena),
._size_648_8 = MemoryPoolAligned([648]u8, .@"8").init(page.arena),
};
}
@@ -230,7 +218,6 @@ pub fn create(self: *Factory, value: anytype) !*@TypeOf(value) {
pub fn createT(self: *Factory, comptime T: type) !*T {
const SO = @sizeOf(T);
if (comptime SO == 1) return @ptrCast(try self._size_1_8.create());
if (comptime SO == 8) return @ptrCast(try self._size_8_8.create());
if (comptime SO == 16) return @ptrCast(try self._size_16_8.create());
if (comptime SO == 24) return @ptrCast(try self._size_24_8.create());
@@ -242,18 +229,12 @@ pub fn createT(self: *Factory, comptime T: type) !*T {
if (comptime SO == 48) return @ptrCast(try self._size_48_16.create());
if (comptime SO == 56) return @ptrCast(try self._size_56_8.create());
if (comptime SO == 64) return @ptrCast(try self._size_64_16.create());
if (comptime SO == 72) return @ptrCast(try self._size_72_8.create());
if (comptime SO == 80) return @ptrCast(try self._size_80_16.create());
if (comptime SO == 88) return @ptrCast(try self._size_88_8.create());
if (comptime SO == 96) return @ptrCast(try self._size_96_16.create());
if (comptime SO == 104) return @ptrCast(try self._size_104_8.create());
if (comptime SO == 112) return @ptrCast(try self._size_112_8.create());
if (comptime SO == 120) return @ptrCast(try self._size_120_8.create());
if (comptime SO == 128) return @ptrCast(try self._size_128_8.create());
if (comptime SO == 144) return @ptrCast(try self._size_144_8.create());
if (comptime SO == 152) return @ptrCast(try self._size_152_8.create());
if (comptime SO == 456) return @ptrCast(try self._size_456_8.create());
if (comptime SO == 520) return @ptrCast(try self._size_520_8.create());
if (comptime SO == 160) return @ptrCast(try self._size_160_8.create());
if (comptime SO == 648) return @ptrCast(try self._size_648_8.create());
@compileError(std.fmt.comptimePrint("No pool configured for @sizeOf({d}), @alignOf({d}): ({s})", .{ SO, @alignOf(T), @typeName(T) }));
}
@@ -308,7 +289,6 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void {
// be (cannot be) freed. But we'll still free the chain.
if (comptime wasAllocated(S)) {
switch (@sizeOf(S)) {
1 => self._size_1_8.destroy(@ptrCast(@alignCast(value))),
8 => self._size_8_8.destroy(@ptrCast(@alignCast(value))),
16 => self._size_16_8.destroy(@ptrCast(value)),
24 => self._size_24_8.destroy(@ptrCast(value)),
@@ -323,18 +303,13 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void {
48 => self._size_48_16.destroy(@ptrCast(@alignCast(value))),
56 => self._size_56_8.destroy(@ptrCast(value)),
64 => self._size_64_16.destroy(@ptrCast(@alignCast(value))),
72 => self._size_72_8.destroy(@ptrCast(@alignCast(value))),
80 => self._size_80_16.destroy(@ptrCast(@alignCast(value))),
88 => self._size_88_8.destroy(@ptrCast(@alignCast(value))),
96 => self._size_96_16.destroy(@ptrCast(@alignCast(value))),
104 => self._size_104_8.destroy(@ptrCast(value)),
112 => self._size_112_8.destroy(@ptrCast(value)),
120 => self._size_120_8.destroy(@ptrCast(value)),
128 => self._size_128_8.destroy(@ptrCast(value)),
144 => self._size_144_8.destroy(@ptrCast(value)),
152 => self._size_152_8.destroy(@ptrCast(value)),
456 => self._size_456_8.destroy(@ptrCast(value)),
520 => self._size_520_8.destroy(@ptrCast(value)),
160 => self._size_160_8.destroy(@ptrCast(value)),
648 => self._size_648_8.destroy(@ptrCast(value)),
else => |SO| @compileError(std.fmt.comptimePrint("Don't know what I'm being asked to destroy @sizeOf({d}), @alignOf({d}): ({s})", .{ SO, @alignOf(S), @typeName(S) })),
}

View File

@@ -50,6 +50,7 @@ const Element = @import("webapi/Element.zig");
const Window = @import("webapi/Window.zig");
const Location = @import("webapi/Location.zig");
const Document = @import("webapi/Document.zig");
const Performance = @import("webapi/Performance.zig");
const HtmlScript = @import("webapi/Element.zig").Html.Script;
const MutationObserver = @import("webapi/MutationObserver.zig");
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
@@ -179,6 +180,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
._document = self.document,
._storage_bucket = storage_bucket,
._history = History.init(self),
._performance = Performance.init(),
._proto = undefined,
._location = &default_location,
});
@@ -624,7 +626,7 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
// Look, we want to exit ASAP, but we don't want
// to exit so fast that we've run none of the
// background jobs.
break :blk if (comptime builtin.is_test) 5 else 50;
break :blk if (comptime builtin.is_test) 1 else 50;
}
// No http transfers, no cdp extra socket, no
// scheduled tasks, we're done.
@@ -680,6 +682,13 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
}
}
pub fn tick(self: *Page) void {
self._session.browser.runMicrotasks();
_ = self.scheduler.run() catch |err| {
log.err(.page, "tick", .{ .err = err });
};
}
pub fn scriptAddedCallback(self: *Page, script: *HtmlScript) !void {
self._script_manager.addFromElement(script, "parsing") catch |err| {
log.err(.page, "page.scriptAddedCallback", .{

View File

@@ -742,6 +742,8 @@ const Script = struct {
break :blk true;
};
defer page.tick();
if (success) {
self.executeCallback(script_element._on_load, page);
return;

View File

@@ -1926,6 +1926,10 @@ pub fn queueIntersectionDelivery(self: *Context) !void {
}.run, self.page);
}
pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
self.isolate.enqueueMicrotaskFunc(cb.func.castToFunction());
}
// == Misc ==
// An interface for types that want to have their jsDeinit function to be

View File

@@ -543,6 +543,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/storage/storage.zig"),
@import("../webapi/URL.zig"),
@import("../webapi/Window.zig"),
@import("../webapi/Performance.zig"),
@import("../webapi/MutationObserver.zig"),
@import("../webapi/IntersectionObserver.zig"),
});

View File

@@ -15,7 +15,9 @@
start = timestamp;
}
requestAnimationFrame(step);
testing.eventually(() => testing.expectEqual(true, start > 0));
testing.eventually(() => {
testing.expectEqual(true, start > 0)
});
let request_id = requestAnimationFrame(() => {
start = 0;
@@ -67,7 +69,7 @@
testing.expectEqual(true, called);
</script>
<script id=btoa_atob>
<!-- <script id=btoa_atob>
const b64 = btoa('https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder')
testing.expectEqual('aHR0cHM6Ly96aWdsYW5nLm9yZy9kb2N1bWVudGF0aW9uL21hc3Rlci9zdGQvI3N0ZC5iYXNlNjQuQmFzZTY0RGVjb2Rlcg==', b64);
@@ -77,9 +79,9 @@
testing.expectError('Error: InvalidCharacterError', () => {
atob('b');
});
</script>
</script> -->
<script id=scroll>
<!-- <script id=scroll>
let scroll = false;
let scrollend = false
@@ -118,8 +120,8 @@
testing.expectEqual(0, scrollY);
testing.expectEqual(0, pageYOffset);
</script>
<script id=queueMicroTask>
-->
<!-- <script id=queueMicroTask>
var qm = false;
window.queueMicrotask(() => {qm = true });
testing.eventually(() => testing.expectEqual(true, qm));
@@ -132,9 +134,9 @@
dcl = e.target == document;
});
testing.eventually(() => testing.expectEqual(true, dcl));
</script>
</script> -->
<script id=window.onload>
<!-- <script id=window.onload>
let isWindowTarget = false;
const callback = (e) => isWindowTarget = e.target === window;
@@ -148,9 +150,9 @@
testing.expectEqual(callback, window.onload);
testing.eventually(() => testing.expectEqual(true, isWindowTarget));
</script>
</script> -->
<script id=reportError>
<!-- <script id=reportError>
let errorEventFired = false;
let capturedError = null;
@@ -164,4 +166,4 @@
testing.expectEqual(true, errorEventFired);
testing.expectEqual(testError, capturedError);
</script>
</script> -->

View File

@@ -0,0 +1,157 @@
<!DOCTYPE html>
<div id="test-element-1" data-status="active" data-username="john" data-role="admin">Test</div>
<div id="test-element-2" class="initial">Test</div>
<div id="test-element-3">
<div id="child-element" data-foo="bar" data-baz="qux">Child</div>
</div>
<script src="../testing.js"></script>
<script id="attribute_filter_single">
testing.async(async () => {
const element = document.getElementById('test-element-1');
let mutations = null;
const observer = new MutationObserver((records) => {
observer.disconnect();
mutations = records;
});
observer.observe(element, {
attributes: true,
attributeFilter: ['data-status']
});
element.setAttribute('data-status', 'inactive');
element.setAttribute('data-username', 'jane');
element.setAttribute('data-role', 'user');
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length);
testing.expectEqual('attributes', mutations[0].type);
testing.expectEqual('data-status', mutations[0].attributeName);
});
});
</script>
<script id="attribute_filter_multiple">
testing.async(async () => {
const element = document.getElementById('test-element-1');
let mutations = null;
const observer = new MutationObserver((records) => {
observer.disconnect();
mutations = records;
});
observer.observe(element, {
attributes: true,
attributeFilter: ['data-status', 'data-username']
});
element.setAttribute('data-status', 'active');
element.setAttribute('data-username', 'alice');
element.setAttribute('data-role', 'moderator');
Promise.resolve().then(() => {
testing.expectEqual(2, mutations.length);
testing.expectEqual('data-status', mutations[0].attributeName);
testing.expectEqual('data-username', mutations[1].attributeName);
});
});
</script>
<script id="attribute_filter_with_old_value">
testing.async(async () => {
const element = document.getElementById('test-element-2');
let mutations = null;
const observer = new MutationObserver((records) => {
observer.disconnect();
mutations = records;
});
observer.observe(element, {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class']
});
element.setAttribute('class', 'changed');
element.setAttribute('data-ignored', 'value');
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length);
testing.expectEqual('class', mutations[0].attributeName);
testing.expectEqual('initial', mutations[0].oldValue);
});
});
</script>
<script id="attribute_filter_no_match">
testing.async(async () => {
const element = document.getElementById('test-element-2');
let callbackCalled = false;
const observer = new MutationObserver(() => {
callbackCalled = true;
});
observer.observe(element, {
attributes: true,
attributeFilter: ['data-filtered']
});
element.setAttribute('class', 'another-change');
element.setAttribute('data-other', 'value');
Promise.resolve().then(() => {
testing.expectEqual(false, callbackCalled);
observer.disconnect();
});
});
</script>
<script id="attribute_filter_with_subtree">
testing.async(async () => {
const parent = document.getElementById('test-element-3');
const child = document.getElementById('child-element');
let mutations = null;
const observer = new MutationObserver((records) => {
observer.disconnect();
mutations = records;
});
observer.observe(parent, {
attributes: true,
subtree: true,
attributeFilter: ['data-foo']
});
child.setAttribute('data-foo', 'changed');
child.setAttribute('data-baz', 'ignored');
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length);
testing.expectEqual('data-foo', mutations[0].attributeName);
testing.expectEqual(child, mutations[0].target);
});
});
</script>
<script id="attribute_filter_empty_array">
testing.async(async () => {
const element = document.getElementById('test-element-2');
let callbackCalled = false;
const observer = new MutationObserver(() => {
callbackCalled = true;
});
observer.observe(element, {
attributes: true,
attributeFilter: []
});
element.setAttribute('class', 'yet-another-change');
Promise.resolve().then(() => {
testing.expectEqual(false, callbackCalled);
observer.disconnect();
});
});
</script>

View File

@@ -19,7 +19,7 @@
textNode.data = 'Changed text';
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length, {script_id: 'character_data'});
testing.expectEqual(1, mutations.length);
testing.expectEqual('characterData', mutations[0].type);
testing.expectEqual(textNode, mutations[0].target);
testing.expectEqual(null, mutations[0].oldValue);
@@ -43,7 +43,7 @@
textNode.data = 'Second change';
Promise.resolve().then(() => {
testing.expectEqual(2, mutations.length, {script_id: 'character_data_old_value'});
testing.expectEqual(2, mutations.length);
testing.expectEqual('characterData', mutations[0].type);
testing.expectEqual(textNode, mutations[0].target);

View File

@@ -23,7 +23,7 @@
parent.appendChild(child2);
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length, {script_id: 'childlist'});
testing.expectEqual(1, mutations.length);
testing.expectEqual('childList', mutations[0].type);
testing.expectEqual(parent, mutations[0].target);
testing.expectEqual(1, mutations[0].addedNodes.length);
@@ -51,7 +51,7 @@
emptyParent.appendChild(firstChild);
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length, {script_id: 'childlist_empty_parent'});
testing.expectEqual(1, mutations.length);
testing.expectEqual('childList', mutations[0].type);
testing.expectEqual(emptyParent, mutations[0].target);
testing.expectEqual(1, mutations[0].addedNodes.length);
@@ -78,7 +78,7 @@
removeParent.removeChild(onlyChild);
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length, {script_id: 'childlist_remove_last'});
testing.expectEqual(1, mutations.length);
testing.expectEqual('childList', mutations[0].type);
testing.expectEqual(removeParent, mutations[0].target);
testing.expectEqual(0, mutations[0].addedNodes.length);
@@ -105,7 +105,7 @@
textParent.appendChild(textNode);
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length, {script_id: 'childlist_text_node'});
testing.expectEqual(1, mutations.length);
testing.expectEqual('childList', mutations[0].type);
testing.expectEqual(textParent, mutations[0].target);
testing.expectEqual(1, mutations[0].addedNodes.length);
@@ -133,7 +133,7 @@
middleParent.removeChild(middle);
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length, {script_id: 'childlist_remove_middle'});
testing.expectEqual(1, mutations.length);
testing.expectEqual('childList', mutations[0].type);
testing.expectEqual(middleParent, mutations[0].target);
testing.expectEqual(0, mutations[0].addedNodes.length);
@@ -165,7 +165,7 @@
insertParent.insertBefore(insertMiddle, insertLast);
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length, {script_id: 'childlist_insert_before'});
testing.expectEqual(1, mutations.length);
testing.expectEqual('childList', mutations[0].type);
testing.expectEqual(insertParent, mutations[0].target);
testing.expectEqual(1, mutations[0].addedNodes.length);
@@ -199,7 +199,7 @@
// replaceChild generates two separate mutation records in modern spec:
// 1. First record for insertBefore (new node added)
// 2. Second record for removeChild (old node removed)
testing.expectEqual(2, mutations.length, {script_id: 'childlist_replace_child'});
testing.expectEqual(2, mutations.length);
// First mutation: insertion of new node
testing.expectEqual('childList', mutations[0].type);
@@ -248,7 +248,7 @@
multipleParent.appendChild(child3);
Promise.resolve().then(() => {
testing.expectEqual(3, mutations.length, {script_id: 'childlist_multiple_mutations'});
testing.expectEqual(3, mutations.length);
testing.expectEqual('childList', mutations[0].type);
testing.expectEqual(child1, mutations[0].addedNodes[0]);
@@ -289,7 +289,7 @@
// innerHTML triggers mutations for both removals and additions
// With tri-state: from_parser=true + parse_mode=fragment -> mutations fire
// HTML wrapper element is filtered out, so: 3 removals + 3 additions = 6
testing.expectEqual(6, mutations.length, {script_id: 'childlist_inner_html'});
testing.expectEqual(6, mutations.length);
// First 3: removals
testing.expectEqual('childList', mutations[0].type);

View File

@@ -21,7 +21,7 @@
element.removeAttribute('data-foo');
Promise.resolve().then(() => {
testing.expectEqual(3, mutations.length, {script_id: 'mutation_observer'});
testing.expectEqual(3, mutations.length);
testing.expectEqual('attributes', mutations[0].type);
testing.expectEqual(element, mutations[0].target);
@@ -57,7 +57,7 @@
element.removeAttribute('data-test');
Promise.resolve().then(() => {
testing.expectEqual(3, mutations.length, {script_id: 'mutation_observer_old_value'});
testing.expectEqual(3, mutations.length);
testing.expectEqual('data-test', mutations[0].attributeName);
testing.expectEqual(null, mutations[0].oldValue);
@@ -86,7 +86,7 @@
element.setAttribute('data-disconnected', 'test');
Promise.resolve().then(() => {
testing.expectEqual(false, callbackCalled, {script_id: 'mutation_observer_disconnect'});
testing.expectEqual(false, callbackCalled);
});
});
</script>

View File

@@ -29,7 +29,7 @@
// After first microtask, first callback should have run and triggered second mutation
}).then(() => {
// After second microtask, second callback should have run
testing.expectEqual(2, callCount, {script_id: 'mutations_during_callback'});
testing.expectEqual(2, callCount);
testing.expectEqual(1, firstRecords.length);
testing.expectEqual('data-first', firstRecords[0].attributeName);
testing.expectEqual(1, secondRecords.length);

View File

@@ -0,0 +1,138 @@
<!DOCTYPE html>
<div id="grandparent">
<div id="parent">
<div id="child" data-test="initial">Child</div>
</div>
</div>
<div id="text-grandparent">
<div id="text-parent">
<div id="text-container">Text here</div>
</div>
</div>
<div id="childlist-grandparent">
<div id="childlist-parent"></div>
</div>
<script src="../testing.js"></script>
<script id="subtree_attributes">
testing.async(async () => {
const grandparent = document.getElementById('grandparent');
const child = document.getElementById('child');
let mutations = null;
const observer = new MutationObserver((records) => {
observer.disconnect();
mutations = records;
});
observer.observe(grandparent, { attributes: true, subtree: true });
child.setAttribute('data-test', 'changed');
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length);
testing.expectEqual('attributes', mutations[0].type);
testing.expectEqual(child, mutations[0].target);
testing.expectEqual('data-test', mutations[0].attributeName);
});
});
</script>
<script id="subtree_attributes_without_subtree">
testing.async(async () => {
const grandparent = document.getElementById('grandparent');
const child = document.getElementById('child');
let callbackCalled = false;
const observer = new MutationObserver(() => {
callbackCalled = true;
});
observer.observe(grandparent, { attributes: true, subtree: false });
child.setAttribute('data-no-subtree', 'test');
Promise.resolve().then(() => {
testing.expectEqual(false, callbackCalled);
observer.disconnect();
});
});
</script>
<script id="subtree_character_data">
testing.async(async () => {
const grandparent = document.getElementById('text-grandparent');
const container = document.getElementById('text-container');
const textNode = container.firstChild;
let mutations = null;
const observer = new MutationObserver((records) => {
observer.disconnect();
mutations = records;
});
observer.observe(grandparent, {
characterData: true,
characterDataOldValue: true,
subtree: true
});
textNode.data = 'Changed text';
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length);
testing.expectEqual('characterData', mutations[0].type);
testing.expectEqual(textNode, mutations[0].target);
testing.expectEqual('Text here', mutations[0].oldValue);
});
});
</script>
<script id="subtree_childlist">
testing.async(async () => {
const grandparent = document.getElementById('childlist-grandparent');
const parent = document.getElementById('childlist-parent');
let mutations = null;
const observer = new MutationObserver((records) => {
observer.disconnect();
mutations = records;
});
observer.observe(grandparent, { childList: true, subtree: true });
const newChild = document.createElement('div');
newChild.textContent = 'New child';
parent.appendChild(newChild);
Promise.resolve().then(() => {
testing.expectEqual(1, mutations.length);
testing.expectEqual('childList', mutations[0].type);
testing.expectEqual(parent, mutations[0].target);
testing.expectEqual(1, mutations[0].addedNodes.length);
testing.expectEqual(newChild, mutations[0].addedNodes[0]);
});
});
</script>
<script id="subtree_deep_nesting">
testing.async(async () => {
const root = document.createElement('div');
const child = document.createElement('div');
let mutations = null;
const observer = new MutationObserver((records) => {
observer.disconnect();
mutations = records;
});
root.appendChild(child);
observer.observe(root, { attributes: true, subtree: true });
child.setAttribute('data-deep', 'value');
Promise.resolve().then(() => {
const lastMutation = mutations[mutations.length - 1];
testing.expectEqual('attributes', lastMutation.type);
testing.expectEqual(child, lastMutation.target);
testing.expectEqual('data-deep', lastMutation.attributeName);
});
});
</script>

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<script src="testing.js"></script>
<script id=performance>
testing.expectEqual(performance, window.performance);
</script>
<script id=now_returns_number>
const t = performance.now();
testing.expectEqual('number', typeof t);
testing.expectEqual(true, t >= 0);
</script>
<script id=now_increases>
const t1 = performance.now();
const t2 = performance.now();
testing.expectEqual(true, t2 >= t1);
</script>
<script id=timeOrigin>
const origin = performance.timeOrigin;
testing.expectEqual('number', typeof origin);
testing.expectEqual(true, origin > 0);
</script>
<script id=now_relative_to_origin>
{
const t = performance.now();
const now = Date.now();
testing.expectEqual(true, t < now);
}
</script>
<script id=multiple_calls>
{
const times = [];
for (let i = 0; i < 5; i++) {
times.push(performance.now());
}
for (let i = 1; i < times.length; i++) {
testing.expectEqual(true, times[i] >= times[i-1]);
}
}
</script>

View File

@@ -46,7 +46,8 @@ pub const ObserveOptions = struct {
childList: bool = false,
characterData: bool = false,
characterDataOldValue: bool = false,
// Future: subtree, attributeFilter
subtree: bool = false,
attributeFilter: ?[]const []const u8 = null,
};
pub fn init(callback: js.Function, page: *Page) !*MutationObserver {
@@ -56,10 +57,20 @@ pub fn init(callback: js.Function, page: *Page) !*MutationObserver {
}
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
// Deep copy attributeFilter if present
var copied_options = options;
if (options.attributeFilter) |filter| {
const filter_copy = try page.arena.alloc([]const u8, filter.len);
for (filter, 0..) |name, i| {
filter_copy[i] = try page.arena.dupe(u8, name);
}
copied_options.attributeFilter = filter_copy;
}
// Check if already observing this target
for (self._observing.items) |*obs| {
if (obs.target == target) {
obs.options = options;
obs.options = copied_options;
return;
}
}
@@ -71,7 +82,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
try self._observing.append(page.arena, .{
.target = target,
.options = options,
.options = copied_options,
});
}
@@ -99,11 +110,25 @@ pub fn notifyAttributeChange(
for (self._observing.items) |obs| {
if (obs.target != target_node) {
continue;
if (!obs.options.subtree) {
continue;
}
if (!obs.target.contains(target_node)) {
continue;
}
}
if (!obs.options.attributes) {
continue;
}
if (obs.options.attributeFilter) |filter| {
for (filter) |name| {
if (std.mem.eql(u8, name, attribute_name)) {
break;
}
} else {
continue;
}
}
const record = try page._factory.create(MutationRecord{
._type = .attributes,
@@ -135,7 +160,12 @@ pub fn notifyCharacterDataChange(
) !void {
for (self._observing.items) |obs| {
if (obs.target != target) {
continue;
if (!obs.options.subtree) {
continue;
}
if (!obs.target.contains(target)) {
continue;
}
}
if (!obs.options.characterData) {
continue;
@@ -174,7 +204,12 @@ pub fn notifyChildListChange(
) !void {
for (self._observing.items) |obs| {
if (obs.target != target) {
continue;
if (!obs.options.subtree) {
continue;
}
if (!obs.target.contains(target)) {
continue;
}
}
if (!obs.options.childList) {
continue;

View File

@@ -0,0 +1,40 @@
const js = @import("../js/js.zig");
const datetime = @import("../../datetime.zig");
const Performance = @This();
_time_origin: u64,
pub fn init() Performance {
return .{
._time_origin = datetime.milliTimestamp(.monotonic),
};
}
pub fn now(self: *const Performance) f64 {
const current = datetime.milliTimestamp(.monotonic);
const elapsed = current - self._time_origin;
return @floatFromInt(elapsed);
}
pub fn getTimeOrigin(self: *const Performance) f64 {
return @floatFromInt(self._time_origin);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Performance);
pub const Meta = struct {
pub const name = "Performance";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const now = bridge.function(Performance.now, .{});
pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{});
};
const testing = @import("../../testing.zig");
test "WebApi: Performance" {
try testing.htmlRunner("performance.html", .{});
}

View File

@@ -25,6 +25,7 @@ const Page = @import("../Page.zig");
const Console = @import("Console.zig");
const History = @import("History.zig");
const Navigator = @import("Navigator.zig");
const Performance = @import("Performance.zig");
const Document = @import("Document.zig");
const Location = @import("Location.zig");
const Fetch = @import("net/Fetch.zig");
@@ -39,6 +40,7 @@ _proto: *EventTarget,
_document: *Document,
_console: Console = .init,
_navigator: Navigator = .init,
_performance: Performance,
_history: History,
_storage_bucket: *storage.Bucket,
_on_load: ?js.Function = null,
@@ -70,6 +72,10 @@ pub fn getNavigator(_: *const Window) Navigator {
return .{};
}
pub fn getPerformance(self: *Window) *Performance {
return &self._performance;
}
pub fn getLocalStorage(self: *const Window) *storage.Lookup {
return &self._storage_bucket.local;
}
@@ -134,11 +140,14 @@ pub fn requestAnimationFrame(self: *Window, cb: js.Function, page: *Page) !u32 {
.repeat = false,
.params = &.{},
.low_priority = false,
.animation_frame = true,
.name = "window.requestAnimationFrame",
}, page);
}
// queueMicrotask: quickjs implements this directly
pub fn queueMicrotask(_: *Window, cb: js.Function, page: *Page) void {
page.js.queueMicrotaskFunc(cb);
}
pub fn clearTimeout(self: *Window, id: u32) void {
var sc = self._timers.get(id) orelse return;
@@ -208,6 +217,7 @@ const ScheduleOpts = struct {
params: []js.Object,
name: []const u8,
low_priority: bool = false,
animation_frame: bool = false,
};
fn scheduleCallback(self: *Window, cb: js.Function, delay_ms: u32, opts: ScheduleOpts, page: *Page) !u32 {
if (self._timers.count() > 512) {
@@ -235,6 +245,7 @@ fn scheduleCallback(self: *Window, cb: js.Function, delay_ms: u32, opts: Schedul
.name = opts.name,
.timer_id = timer_id,
.params = opts.params,
.animation_frame = opts.animation_frame,
.repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null,
});
gop.value_ptr.* = callback;
@@ -266,6 +277,8 @@ const ScheduleCallback = struct {
removed: bool = false,
animation_frame: bool = false,
fn deinit(self: *ScheduleCallback) void {
self.page._factory.destroy(self);
}
@@ -278,10 +291,17 @@ const ScheduleCallback = struct {
return null;
}
self.cb.call(void, .{self.params}) catch |err| {
// a non-JS error
log.warn(.js, "window.timer", .{ .name = self.name, .err = err });
};
if (self.animation_frame) {
self.cb.call(void, .{self.page.window._performance.now()}) catch |err| {
// a non-JS error
log.warn(.js, "window.RAF", .{ .name = self.name, .err = err });
};
} else {
self.cb.call(void, .{self.params}) catch |err| {
// a non-JS error
log.warn(.js, "window.timer", .{ .name = self.name, .err = err });
};
}
if (self.repeat_ms) |ms| {
return ms;
@@ -302,11 +322,13 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const top = bridge.accessor(Window.getWindow, null, .{ .cache = "top" });
pub const self = bridge.accessor(Window.getWindow, null, .{ .cache = "self" });
pub const window = bridge.accessor(Window.getWindow, null, .{ .cache = "window" });
pub const parent = bridge.accessor(Window.getWindow, null, .{ .cache = "parent" });
pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = "console" });
pub const navigator = bridge.accessor(Window.getNavigator, null, .{ .cache = "navigator" });
pub const performance = bridge.accessor(Window.getPerformance, null, .{ .cache = "performance" });
pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{ .cache = "localStorage" });
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{ .cache = "sessionStorage" });
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = "document" });
@@ -314,6 +336,7 @@ pub const JsApi = struct {
pub const history = bridge.accessor(Window.getHistory, null, .{ .cache = "history" });
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
pub const fetch = bridge.function(Window.fetch, .{});
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});
pub const setTimeout = bridge.function(Window.setTimeout, .{});
pub const clearTimeout = bridge.function(Window.clearTimeout, .{});
pub const setInterval = bridge.function(Window.setInterval, .{});
@@ -326,6 +349,17 @@ pub const JsApi = struct {
pub const btoa = bridge.function(Window.btoa, .{});
pub const atob = bridge.function(Window.atob, .{});
pub const reportError = bridge.function(Window.reportError, .{});
pub const frames = bridge.accessor(Window.getWindow, null, .{ .cache = "frames" });
pub const length = bridge.accessor(struct{
fn wrap(_: *const Window) u32 { return 0; }
}.wrap, null, .{ .cache = "length" });
pub const innerWidth = bridge.accessor(struct{
fn wrap(_: *const Window) u32 { return 1920; }
}.wrap, null, .{ .cache = "innerWidth" });
pub const innerHeight = bridge.accessor(struct{
fn wrap(_: *const Window) u32 { return 1080; }
}.wrap, null, .{ .cache = "innerHeight" });
};
const testing = @import("../../testing.zig");