diff --git a/build.zig.zon b/build.zig.zon
index 036d9ba5..682f823c 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -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" }
},
diff --git a/src/Scheduler.zig b/src/Scheduler.zig
index 6ba8b6e1..98efb64f 100644
--- a/src/Scheduler.zig
+++ b/src/Scheduler.zig
@@ -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);
diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig
index 56d3eb98..f6bbc5d9 100644
--- a/src/browser/Factory.zig
+++ b/src/browser/Factory.zig
@@ -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) })),
}
diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index ff22f2b6..9a4a9f96 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -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", .{
diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig
index 29e9f683..0d8740c8 100644
--- a/src/browser/ScriptManager.zig
+++ b/src/browser/ScriptManager.zig
@@ -742,6 +742,8 @@ const Script = struct {
break :blk true;
};
+ defer page.tick();
+
if (success) {
self.executeCallback(script_element._on_load, page);
return;
diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig
index 4c7ff66b..d2d0640e 100644
--- a/src/browser/js/Context.zig
+++ b/src/browser/js/Context.zig
@@ -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
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 447ce3ef..1eab16c9 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -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"),
});
diff --git a/src/browser/tests/legacy/window/window.html b/src/browser/tests/legacy/window/window.html
index aac91171..2e49edd2 100644
--- a/src/browser/tests/legacy/window/window.html
+++ b/src/browser/tests/legacy/window/window.html
@@ -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);
-
+ -->
-
-
-
+ -->
-
+ -->
-
+ -->
diff --git a/src/browser/tests/mutation_observer/attribute_filter.html b/src/browser/tests/mutation_observer/attribute_filter.html
new file mode 100644
index 00000000..eb1c0605
--- /dev/null
+++ b/src/browser/tests/mutation_observer/attribute_filter.html
@@ -0,0 +1,157 @@
+
+
Test
+Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/mutation_observer/character_data.html b/src/browser/tests/mutation_observer/character_data.html
index 6afa299c..260d2822 100644
--- a/src/browser/tests/mutation_observer/character_data.html
+++ b/src/browser/tests/mutation_observer/character_data.html
@@ -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);
diff --git a/src/browser/tests/mutation_observer/childlist.html b/src/browser/tests/mutation_observer/childlist.html
index 915dbcf4..e15eaa35 100644
--- a/src/browser/tests/mutation_observer/childlist.html
+++ b/src/browser/tests/mutation_observer/childlist.html
@@ -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);
diff --git a/src/browser/tests/mutation_observer/mutation_observer.html b/src/browser/tests/mutation_observer/mutation_observer.html
index 8ced1815..76b95bb7 100644
--- a/src/browser/tests/mutation_observer/mutation_observer.html
+++ b/src/browser/tests/mutation_observer/mutation_observer.html
@@ -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);
});
});
diff --git a/src/browser/tests/mutation_observer/mutations_during_callback.html b/src/browser/tests/mutation_observer/mutations_during_callback.html
index 829f3d5a..938e4518 100644
--- a/src/browser/tests/mutation_observer/mutations_during_callback.html
+++ b/src/browser/tests/mutation_observer/mutations_during_callback.html
@@ -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);
diff --git a/src/browser/tests/mutation_observer/subtree.html b/src/browser/tests/mutation_observer/subtree.html
new file mode 100644
index 00000000..6492a20e
--- /dev/null
+++ b/src/browser/tests/mutation_observer/subtree.html
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/performance.html b/src/browser/tests/performance.html
new file mode 100644
index 00000000..5aed2cc1
--- /dev/null
+++ b/src/browser/tests/performance.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig
index ed34d80f..7d97a94b 100644
--- a/src/browser/webapi/MutationObserver.zig
+++ b/src/browser/webapi/MutationObserver.zig
@@ -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;
diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig
new file mode 100644
index 00000000..3ac87871
--- /dev/null
+++ b/src/browser/webapi/Performance.zig
@@ -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", .{});
+}
diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig
index b3ac4414..bafb12d4 100644
--- a/src/browser/webapi/Window.zig
+++ b/src/browser/webapi/Window.zig
@@ -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");