diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index 80bae526..7a75ddc6 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -49,6 +49,7 @@ const Document = @import("webapi/Document.zig");
const ShadowRoot = @import("webapi/ShadowRoot.zig");
const Performance = @import("webapi/Performance.zig");
const Screen = @import("webapi/Screen.zig");
+const PerformanceObserver = @import("webapi/PerformanceObserver.zig");
const MutationObserver = @import("webapi/MutationObserver.zig");
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
@@ -111,6 +112,11 @@ _intersection_delivery_scheduled: bool = false,
_slots_pending_slotchange: std.AutoHashMapUnmanaged(*Element.Html.Slot, void) = .{},
_slotchange_delivery_scheduled: bool = false,
+/// List of active PerformanceObservers.
+/// Contrary to MutationObserver and IntersectionObserver, these are regular tasks.
+_performance_observers: std.ArrayList(*PerformanceObserver) = .{},
+_performance_delivery_scheduled: bool = false,
+
// Lookup for customized built-in elements. Maps element pointer to definition.
_customized_builtin_definitions: std.AutoHashMapUnmanaged(*Element, *CustomElementDefinition) = .{},
_customized_builtin_connected_callback_invoked: std.AutoHashMapUnmanaged(*Element, void) = .{},
@@ -262,6 +268,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._notified_network_idle = .init;
self._notified_network_almost_idle = .init;
+ self._performance_observers = .{};
self._mutation_observers = .{};
self._mutation_delivery_scheduled = false;
self._mutation_delivery_depth = 0;
@@ -945,6 +952,16 @@ fn printWaitAnalysis(self: *Page) void {
}
}
+pub fn tick(self: *Page) void {
+ if (comptime IS_DEBUG) {
+ log.debug(.page, "tick", .{});
+ }
+ _ = self.scheduler.run() catch |err| {
+ log.err(.page, "tick", .{ .err = err });
+ };
+ self.js.runMicrotasks();
+}
+
pub fn isGoingAway(self: *const Page) bool {
return self._queued_navigation != null;
}
@@ -1025,6 +1042,58 @@ pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Elemen
return null;
}
+pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void {
+ return self._performance_observers.append(self.arena, observer);
+}
+
+pub fn unregisterPerformanceObserver(self: *Page, observer: *PerformanceObserver) void {
+ for (self._performance_observers.items, 0..) |perf_observer, i| {
+ if (perf_observer == observer) {
+ _ = self._performance_observers.swapRemove(i);
+ return;
+ }
+ }
+}
+
+/// Updates performance observers with the new entry.
+/// This doesn't emit callbacks but rather fills the queues of observers.
+pub fn notifyPerformanceObservers(self: *Page, entry: *Performance.Entry) !void {
+ for (self._performance_observers.items) |observer| {
+ if (observer.interested(entry)) {
+ observer._entries.append(self.arena, entry) catch |err| {
+ log.err(.page, "notifyPerformanceObservers", .{ .err = err });
+ };
+ }
+ }
+
+ // Already scheduled.
+ if (self._performance_delivery_scheduled) {
+ return;
+ }
+ self._performance_delivery_scheduled = true;
+
+ return self.scheduler.add(
+ self,
+ struct {
+ fn run(_page: *anyopaque) anyerror!?u32 {
+ const page: *Page = @ptrCast(@alignCast(_page));
+ page._performance_delivery_scheduled = false;
+
+ // Dispatch performance observer events.
+ for (page._performance_observers.items) |observer| {
+ if (observer.hasRecords()) {
+ try observer.dispatch(page);
+ }
+ }
+
+ return null;
+ }
+ }.run,
+ 0,
+ .{ .low_priority = true },
+ );
+}
+
pub fn registerMutationObserver(self: *Page, observer: *MutationObserver) !void {
self._mutation_observers.append(&observer.node);
}
diff --git a/src/browser/tests/performance_observer/performance_observer.html b/src/browser/tests/performance_observer/performance_observer.html
new file mode 100644
index 00000000..11c82f23
--- /dev/null
+++ b/src/browser/tests/performance_observer/performance_observer.html
@@ -0,0 +1,38 @@
+
+
+
+
diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig
index 5253373d..9118b2f5 100644
--- a/src/browser/webapi/Performance.zig
+++ b/src/browser/webapi/Performance.zig
@@ -11,7 +11,7 @@ const std = @import("std");
const Performance = @This();
_time_origin: u64,
-_entries: std.ArrayListUnmanaged(*Entry) = .{},
+_entries: std.ArrayList(*Entry) = .{},
/// Get high-resolution timestamp in microseconds, rounded to 5μs increments
/// to match browser behavior (prevents fingerprinting)
@@ -42,9 +42,16 @@ pub fn getTimeOrigin(self: *const Performance) f64 {
return @as(f64, @floatFromInt(self._time_origin)) / 1000.0;
}
-pub fn mark(self: *Performance, name: []const u8, _options: ?Mark.Options, page: *Page) !*Mark {
+pub fn mark(
+ self: *Performance,
+ name: []const u8,
+ _options: ?Mark.Options,
+ page: *Page,
+) !*Mark {
const m = try Mark.init(name, _options, page);
try self._entries.append(page.arena, m._proto);
+ // Notify about the change.
+ try page.notifyPerformanceObservers(m._proto);
return m;
}
@@ -95,6 +102,8 @@ pub fn measure(
page,
);
try self._entries.append(page.arena, m._proto);
+ // Notify about the change.
+ try page.notifyPerformanceObservers(m._proto);
return m;
},
.start_mark => |start_mark| {
@@ -118,12 +127,16 @@ pub fn measure(
page,
);
try self._entries.append(page.arena, m._proto);
+ // Notify about the change.
+ try page.notifyPerformanceObservers(m._proto);
return m;
},
};
const m = try Measure.init(name, null, 0.0, self.now(), null, page);
try self._entries.append(page.arena, m._proto);
+ // Notify about the change.
+ try page.notifyPerformanceObservers(m._proto);
return m;
}
@@ -230,21 +243,40 @@ pub const Entry = struct {
_name: []const u8,
_start_time: f64 = 0.0,
- const Type = union(enum) {
+ pub const Type = union(Enum) {
element,
event,
first_input,
- largest_contentful_paint,
- layout_shift,
- long_animation_frame,
+ @"largest-contentful-paint",
+ @"layout-shift",
+ @"long-animation-frame",
longtask,
measure: *Measure,
navigation,
paint,
resource,
taskattribution,
- visibility_state,
+ @"visibility-state",
mark: *Mark,
+
+ pub const Enum = enum(u8) {
+ element = 1, // Changing this affect PerformanceObserver's behavior.
+ event = 2,
+ first_input = 3,
+ @"largest-contentful-paint" = 4,
+ @"layout-shift" = 5,
+ @"long-animation-frame" = 6,
+ longtask = 7,
+ measure = 8,
+ navigation = 9,
+ paint = 10,
+ resource = 11,
+ taskattribution = 12,
+ @"visibility-state" = 13,
+ mark = 14,
+ // If we ever have types more than 16, we have to update entry
+ // table of PerformanceObserver too.
+ };
};
pub fn getDuration(self: *const Entry) f64 {
@@ -253,11 +285,6 @@ pub const Entry = struct {
pub fn getEntryType(self: *const Entry) []const u8 {
return switch (self._type) {
- .first_input => "first-input",
- .largest_contentful_paint => "largest-contentful-paint",
- .layout_shift => "layout-shift",
- .long_animation_frame => "long-animation-frame",
- .visibility_state => "visibility-state",
else => |t| @tagName(t),
};
}
diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig
index cd77ad18..7e4d9c5d 100644
--- a/src/browser/webapi/PerformanceObserver.zig
+++ b/src/browser/webapi/PerformanceObserver.zig
@@ -16,41 +16,140 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+const std = @import("std");
+
const js = @import("../js/js.zig");
+const Page = @import("../Page.zig");
+const Performance = @import("Performance.zig");
-const Entry = @import("Performance.zig").Entry;
-
-// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
-const PerformanceObserver = @This();
-
-pub fn init(callback: js.Function) PerformanceObserver {
- _ = callback;
- return .{};
+pub fn registerTypes() []const type {
+ return &.{ PerformanceObserver, EntryList };
}
-const ObserverOptions = struct {
- buffered: ?bool = null,
- durationThreshold: ?f64 = null,
+/// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
+const PerformanceObserver = @This();
+
+/// Emitted when there are events with same interests.
+_callback: js.Function,
+/// The threshold to deliver `PerformanceEventTiming` entries.
+_duration_threshold: f64,
+/// Entry types we're looking for are encoded as bit flags.
+_interests: u16,
+/// Entries this observer hold.
+/// Don't mutate these; other observers may hold pointers to them.
+_entries: std.ArrayList(*Performance.Entry),
+
+const DefaultDurationThreshold: f64 = 104;
+
+/// Creates a new PerformanceObserver object with the given observer callback.
+pub fn init(callback: js.Function, page: *Page) !*PerformanceObserver {
+ return page._factory.create(PerformanceObserver{
+ ._callback = callback,
+ ._duration_threshold = DefaultDurationThreshold,
+ ._interests = 0,
+ ._entries = .{},
+ });
+}
+
+const ObserveOptions = struct {
+ buffered: bool = false,
+ durationThreshold: f64 = DefaultDurationThreshold,
entryTypes: ?[]const []const u8 = null,
type: ?[]const u8 = null,
};
-pub fn observe(self: *const PerformanceObserver, opts_: ?ObserverOptions) void {
- _ = self;
- _ = opts_;
- return;
+/// TODO: Support `buffered` option.
+pub fn observe(
+ self: *PerformanceObserver,
+ maybe_options: ?ObserveOptions,
+ page: *Page,
+) !void {
+ const options: ObserveOptions = maybe_options orelse .{};
+ // Update threshold.
+ self._duration_threshold = @max(@floor(options.durationThreshold / 8) * 8, 16);
+
+ const entry_types: []const []const u8 = blk: {
+ // More likely.
+ if (options.type) |entry_type| {
+ // Can't have both.
+ if (options.entryTypes != null) {
+ return error.TypeError;
+ }
+
+ break :blk &.{entry_type};
+ }
+
+ if (options.entryTypes) |entry_types| {
+ break :blk entry_types;
+ }
+
+ return error.TypeError;
+ };
+
+ // Update entries.
+ var interests: u16 = 0;
+ for (entry_types) |entry_type| {
+ const fields = @typeInfo(Performance.Entry.Type.Enum).@"enum".fields;
+ inline for (fields) |field| {
+ if (std.mem.eql(u8, field.name, entry_type)) {
+ const flag = @as(u16, 1) << @as(u16, field.value);
+ interests |= flag;
+ }
+ }
+ }
+
+ // Nothing has updated; no need to go further.
+ if (interests == 0) {
+ return;
+ }
+
+ // If we had no interests before, it means Page is not aware of
+ // this observer.
+ if (self._interests == 0) {
+ try page.registerPerformanceObserver(self);
+ }
+
+ // Update interests.
+ self._interests = interests;
}
-pub fn disconnect(self: *PerformanceObserver) void {
- _ = self;
+pub fn disconnect(self: *PerformanceObserver, page: *Page) void {
+ page.unregisterPerformanceObserver(self);
+ // Reset observer.
+ self._duration_threshold = DefaultDurationThreshold;
+ self._interests = 0;
+ self._entries.clearRetainingCapacity();
}
-pub fn takeRecords(_: *const PerformanceObserver) []const Entry {
- return &.{};
+/// Returns the current list of PerformanceEntry objects
+/// stored in the performance observer, emptying it out.
+pub fn takeRecords(self: *PerformanceObserver, page: *Page) ![]*Performance.Entry {
+ const records = try page.call_arena.dupe(*Performance.Entry, self._entries.items);
+ self._entries.clearRetainingCapacity();
+ return records;
}
-pub fn getSupportedEntryTypes(_: *const PerformanceObserver) [][]const u8 {
- return &.{};
+pub fn getSupportedEntryTypes(_: *const PerformanceObserver) []const []const u8 {
+ return &.{ "mark", "measure" };
+}
+
+/// Returns true if observer interested with given entry.
+pub fn interested(
+ self: *const PerformanceObserver,
+ entry: *const Performance.Entry,
+) bool {
+ const flag = @as(u16, 1) << @intCast(@intFromEnum(entry._type));
+ return self._interests & flag != 0;
+}
+
+pub inline fn hasRecords(self: *const PerformanceObserver) bool {
+ return self._entries.items.len > 0;
+}
+
+/// Runs the PerformanceObserver's callback with records; emptying it out.
+pub fn dispatch(self: *PerformanceObserver, page: *Page) !void {
+ const records = try self.takeRecords(page);
+ _ = try self._callback.call(void, .{ EntryList{ ._entries = records }, self });
}
pub const JsApi = struct {
@@ -60,13 +159,40 @@ pub const JsApi = struct {
pub const name = "PerformanceObserver";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
- pub const empty_with_no_proto = true;
};
- pub const constructor = bridge.constructor(PerformanceObserver.init, .{});
+ pub const constructor = bridge.constructor(PerformanceObserver.init, .{ .dom_exception = true });
- pub const observe = bridge.function(PerformanceObserver.observe, .{});
+ pub const observe = bridge.function(PerformanceObserver.observe, .{ .dom_exception = true });
pub const disconnect = bridge.function(PerformanceObserver.disconnect, .{});
- pub const takeRecords = bridge.function(PerformanceObserver.takeRecords, .{});
+ pub const takeRecords = bridge.function(PerformanceObserver.takeRecords, .{ .dom_exception = true });
pub const supportedEntryTypes = bridge.accessor(PerformanceObserver.getSupportedEntryTypes, null, .{ .static = true });
};
+
+/// List of performance events that were explicitly
+/// observed via the observe() method.
+/// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserverEntryList
+pub const EntryList = struct {
+ _entries: []*Performance.Entry,
+
+ pub fn getEntries(self: *const EntryList) []*Performance.Entry {
+ return self._entries;
+ }
+
+ pub const JsApi = struct {
+ pub const bridge = js.Bridge(EntryList);
+
+ pub const Meta = struct {
+ pub const name = "PerformanceObserverEntryList";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const getEntries = bridge.function(EntryList.getEntries, .{});
+ };
+};
+
+const testing = @import("../../testing.zig");
+test "WebApi: PerformanceObserver" {
+ try testing.htmlRunner("performance_observer", .{});
+}