From 9306adc786de8d040c9d19fb477ed56252a04610 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Thu, 18 Dec 2025 16:15:12 +0300 Subject: [PATCH 1/8] add an overwriting ring buffer implementation --- src/ring_buffer.zig | 80 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/ring_buffer.zig diff --git a/src/ring_buffer.zig b/src/ring_buffer.zig new file mode 100644 index 00000000..0ecb83d6 --- /dev/null +++ b/src/ring_buffer.zig @@ -0,0 +1,80 @@ +const std = @import("std"); + +/// Overwriting ring buffer implementation. +/// Useful if you're not interested with stale data. +pub fn Overwriting( + comptime T: type, + comptime backing: union(enum) { array: usize, slice }, +) type { + switch (comptime backing) { + .array => |size| if (size == 0) @panic("invalid ring buffer size"), + else => {}, + } + + return struct { + const Self = @This(); + + /// Storage. + buffer: switch (backing) { + .array => |size| [size]T, + .slice => []T, + }, + /// Next write index. + write_idx: usize = 0, + /// Length of items ring currently have. + count: usize = 0, + + fn initArray() Self { + return .{ .buffer = undefined }; + } + + fn initSlice(allocator: std.mem.Allocator, size: usize) !Self { + if (size == 0) return error.InvalidSize; + return .{ .buffer = try allocator.alloc(T, size) }; + } + + pub const init = switch (backing) { + .array => initArray, + .slice => initSlice, + }; + + /// Puts an item. + pub fn put(self: *Self, item: T) void { + self.buffer[self.write_idx] = item; + // Wrapping addition. + self.write_idx = (self.write_idx + 1) % self.buffer.len; + self.count = @min(self.count + 1, self.buffer.len); + } + + /// Returns the oldest item. + pub fn get(self: *Self) ?T { + // No items. + if (self.count == 0) { + return null; + } + + const read_idx = (self.write_idx + self.buffer.len - self.count) % self.buffer.len; + const item = self.buffer[read_idx]; + self.count -= 1; + return item; + } + + /// Returns slices to items in ring buffer. + /// In order to avoid memcpy at this level, this function returns + /// 2 slices. Second slice will be empty if items can be represented + /// in a contigious way. + pub fn slice(self: *const Self) struct { first: []const T, second: []const T } { + if (self.count == 0) { + return .{ .first = &.{}, .second = &.{} }; + } + + const read_idx = (self.write_idx + self.buffer.len - self.count) % self.buffer.len; + + if (read_idx < self.write_idx) { + return .{ .first = self.buffer[read_idx..self.write_idx], .second = &.{} }; + } + + return .{ .first = self.buffer[read_idx..], .second = self.buffer[0..self.write_idx] }; + } + }; +} From 88de72a9ea872ab1fda2202f99b45faf67df4c36 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 19 Dec 2025 20:34:10 +0300 Subject: [PATCH 2/8] core performance observer logic Heavily based on MutationObserver and IntersectionObserver. --- src/browser/Page.zig | 32 ++++ src/browser/js/Context.zig | 10 ++ src/browser/webapi/Performance.zig | 45 ++++-- src/browser/webapi/PerformanceObserver.zig | 167 ++++++++++++++++++--- 4 files changed, 221 insertions(+), 33 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 26d60b8c..52d41d86 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -49,6 +49,8 @@ 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 HtmlScript = @import("webapi/Element.zig").Html.Script; +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 +113,9 @@ _intersection_delivery_scheduled: bool = false, _slots_pending_slotchange: std.AutoHashMapUnmanaged(*Element.Html.Slot, void) = .{}, _slotchange_delivery_scheduled: bool = false, +// List of active PerformanceObservers. +_performance_observers: std.ArrayList(*PerformanceObserver) = .{}, + // 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 +267,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; @@ -1025,6 +1031,32 @@ 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; +/// microtask queue runs them periodically. +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 }); + }; + } + } +} + pub fn registerMutationObserver(self: *Page, observer: *MutationObserver) !void { try self._mutation_observers.append(self.arena, observer); } diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 0d2a7040..a51c3a55 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -2017,6 +2017,16 @@ fn zigJsonToJs(isolate: v8.Isolate, v8_context: v8.Context, value: std.json.Valu } // Microtasks +pub fn queuePerformanceDelivery(self: *Context) !void { + self.isolate.enqueueMicrotask(struct { + fn run(data: ?*anyopaque) callconv(.c) void { + const page: *Page = @ptrCast(@alignCast(data.?)); + _ = page; + @panic("TODO"); + } + }, self.page); +} + pub fn queueMutationDelivery(self: *Context) !void { self.isolate.enqueueMicrotask(struct { fn run(data: ?*anyopaque) callconv(.c) void { diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig index 5253373d..0bbab23b 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; } @@ -230,21 +237,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 +279,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..795c9f5b 100644 --- a/src/browser/webapi/PerformanceObserver.zig +++ b/src/browser/webapi/PerformanceObserver.zig @@ -16,41 +16,144 @@ // 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 +/// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver const PerformanceObserver = @This(); -pub fn init(callback: js.Function) PerformanceObserver { - _ = callback; - return .{}; +/// 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 ObserverOptions = struct { - buffered: ?bool = null, - durationThreshold: ?f64 = null, +// We don't have to mark this as public but the declarations have to be public; +// otherwise @typeInfo don't allow accessing them. +// +// Note that we also use this to report supported entry types. +//const Interest = struct { +// pub const element = 0x01; +// pub const event = 0x02; +// pub const @"first-input" = 0x04; +// pub const @"largest-contentful-paint" = 0x08; +// pub const @"layout-shift" = 0x010; +// pub const @"long-animation-frame" = 0x020; +// pub const longtask = 0x040; +// pub const mark = 0x080; +// pub const measure = 0x0100; +// pub const navigation = 0x0200; +// pub const pant = 0x0400; +// pub const resource = 0x0800; +// pub const @"visibility-state" = 0x01000; +//}; + +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 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 &.{}; +/// Returns true if observer interested with given entry. +pub fn interested( + self: *const PerformanceObserver, + entry: *const Performance.Entry, +) bool { + // TODO. + _ = self; + _ = entry; + return true; +} + +pub fn getSupportedEntryTypes(_: *const PerformanceObserver) []const []const u8 { + return &.{ "mark", "measure" }; } pub const JsApi = struct { @@ -60,13 +163,35 @@ 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 = "PerformanceEntryList"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + + pub const getEntries = bridge.function(EntryList.getEntries, .{}); + }; + }; +}; From 0264c94426d545d11d2adf1e79399478a2afd17b Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 22 Dec 2025 11:47:00 +0300 Subject: [PATCH 3/8] proper `interested` function --- src/browser/webapi/PerformanceObserver.zig | 27 +++------------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig index 795c9f5b..dbaed6d4 100644 --- a/src/browser/webapi/PerformanceObserver.zig +++ b/src/browser/webapi/PerformanceObserver.zig @@ -47,26 +47,6 @@ pub fn init(callback: js.Function, page: *Page) !*PerformanceObserver { }); } -// We don't have to mark this as public but the declarations have to be public; -// otherwise @typeInfo don't allow accessing them. -// -// Note that we also use this to report supported entry types. -//const Interest = struct { -// pub const element = 0x01; -// pub const event = 0x02; -// pub const @"first-input" = 0x04; -// pub const @"largest-contentful-paint" = 0x08; -// pub const @"layout-shift" = 0x010; -// pub const @"long-animation-frame" = 0x020; -// pub const longtask = 0x040; -// pub const mark = 0x080; -// pub const measure = 0x0100; -// pub const navigation = 0x0200; -// pub const pant = 0x0400; -// pub const resource = 0x0800; -// pub const @"visibility-state" = 0x01000; -//}; - const ObserveOptions = struct { buffered: bool = false, durationThreshold: f64 = DefaultDurationThreshold, @@ -146,10 +126,9 @@ pub fn interested( self: *const PerformanceObserver, entry: *const Performance.Entry, ) bool { - // TODO. - _ = self; - _ = entry; - return true; + const index = @as(u16, @intFromEnum(entry._type)); + const flag = @as(u16, 1) << index; + return self._interests & flag != 0; } pub fn getSupportedEntryTypes(_: *const PerformanceObserver) []const []const u8 { From acebbb90412a02e5ee4770648b31a74bd0b813b6 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 22 Dec 2025 13:19:00 +0300 Subject: [PATCH 4/8] don't prefer microtask queue for execution This still needs investigation. Spec doesn't refer usage of microtask queue for this, yet the current behavior doesn't match to Firefox and Chrome. --- src/browser/Page.zig | 19 ++++++++++++++++--- src/browser/js/Context.zig | 10 ---------- src/browser/webapi/PerformanceObserver.zig | 17 +++++++++++++---- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 52d41d86..03a93a01 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -113,8 +113,10 @@ _intersection_delivery_scheduled: bool = false, _slots_pending_slotchange: std.AutoHashMapUnmanaged(*Element.Html.Slot, void) = .{}, _slotchange_delivery_scheduled: bool = false, -// List of active PerformanceObservers. +/// 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) = .{}, @@ -951,6 +953,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; } @@ -1045,8 +1057,7 @@ pub fn unregisterPerformanceObserver(self: *Page, observer: *PerformanceObserver } /// Updates performance observers with the new entry. -/// This doesn't emit callbacks but rather fills the queues of observers; -/// microtask queue runs them periodically. +/// 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)) { @@ -1055,6 +1066,8 @@ pub fn notifyPerformanceObservers(self: *Page, entry: *Performance.Entry) !void }; } } + + self._performance_delivery_scheduled = true; } pub fn registerMutationObserver(self: *Page, observer: *MutationObserver) !void { diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index a51c3a55..0d2a7040 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -2017,16 +2017,6 @@ fn zigJsonToJs(isolate: v8.Isolate, v8_context: v8.Context, value: std.json.Valu } // Microtasks -pub fn queuePerformanceDelivery(self: *Context) !void { - self.isolate.enqueueMicrotask(struct { - fn run(data: ?*anyopaque) callconv(.c) void { - const page: *Page = @ptrCast(@alignCast(data.?)); - _ = page; - @panic("TODO"); - } - }, self.page); -} - pub fn queueMutationDelivery(self: *Context) !void { self.isolate.enqueueMicrotask(struct { fn run(data: ?*anyopaque) callconv(.c) void { diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig index dbaed6d4..be920c1a 100644 --- a/src/browser/webapi/PerformanceObserver.zig +++ b/src/browser/webapi/PerformanceObserver.zig @@ -121,18 +121,27 @@ pub fn takeRecords(self: *PerformanceObserver, page: *Page) ![]*Performance.Entr return records; } +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 index = @as(u16, @intFromEnum(entry._type)); - const flag = @as(u16, 1) << index; + const flag = @as(u16, 1) << @intCast(@intFromEnum(entry._type)); return self._interests & flag != 0; } -pub fn getSupportedEntryTypes(_: *const PerformanceObserver) []const []const u8 { - return &.{ "mark", "measure" }; +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, .{records}); } pub const JsApi = struct { From e103ee1ffad7556dc4a4fd16098a9a2abc6eaf8e Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 22 Dec 2025 16:20:14 +0300 Subject: [PATCH 5/8] prefer low priority queue to execute performance observer --- src/browser/Page.zig | 23 ++++++++++++++++++++++- src/browser/webapi/Performance.zig | 6 ++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 03a93a01..d6ca6347 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1067,7 +1067,28 @@ pub fn notifyPerformanceObservers(self: *Page, entry: *Performance.Entry) !void } } - self._performance_delivery_scheduled = true; + if (self._performance_delivery_scheduled == false) { + self._performance_delivery_scheduled = true; + try self.scheduler.add( + self, + struct { + fn run(_page: *anyopaque) anyerror!?u32 { + const page: *Page = @ptrCast(@alignCast(_page)); + page._performance_delivery_scheduled = true; + // 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 { diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig index 0bbab23b..9118b2f5 100644 --- a/src/browser/webapi/Performance.zig +++ b/src/browser/webapi/Performance.zig @@ -102,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| { @@ -125,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; } From 9a9f2ab94be59bc4b4657a2bb5942fd8692fcb1e Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 30 Dec 2025 11:46:58 +0300 Subject: [PATCH 6/8] rm `ring_buffer.zig` --- src/ring_buffer.zig | 80 --------------------------------------------- 1 file changed, 80 deletions(-) delete mode 100644 src/ring_buffer.zig diff --git a/src/ring_buffer.zig b/src/ring_buffer.zig deleted file mode 100644 index 0ecb83d6..00000000 --- a/src/ring_buffer.zig +++ /dev/null @@ -1,80 +0,0 @@ -const std = @import("std"); - -/// Overwriting ring buffer implementation. -/// Useful if you're not interested with stale data. -pub fn Overwriting( - comptime T: type, - comptime backing: union(enum) { array: usize, slice }, -) type { - switch (comptime backing) { - .array => |size| if (size == 0) @panic("invalid ring buffer size"), - else => {}, - } - - return struct { - const Self = @This(); - - /// Storage. - buffer: switch (backing) { - .array => |size| [size]T, - .slice => []T, - }, - /// Next write index. - write_idx: usize = 0, - /// Length of items ring currently have. - count: usize = 0, - - fn initArray() Self { - return .{ .buffer = undefined }; - } - - fn initSlice(allocator: std.mem.Allocator, size: usize) !Self { - if (size == 0) return error.InvalidSize; - return .{ .buffer = try allocator.alloc(T, size) }; - } - - pub const init = switch (backing) { - .array => initArray, - .slice => initSlice, - }; - - /// Puts an item. - pub fn put(self: *Self, item: T) void { - self.buffer[self.write_idx] = item; - // Wrapping addition. - self.write_idx = (self.write_idx + 1) % self.buffer.len; - self.count = @min(self.count + 1, self.buffer.len); - } - - /// Returns the oldest item. - pub fn get(self: *Self) ?T { - // No items. - if (self.count == 0) { - return null; - } - - const read_idx = (self.write_idx + self.buffer.len - self.count) % self.buffer.len; - const item = self.buffer[read_idx]; - self.count -= 1; - return item; - } - - /// Returns slices to items in ring buffer. - /// In order to avoid memcpy at this level, this function returns - /// 2 slices. Second slice will be empty if items can be represented - /// in a contigious way. - pub fn slice(self: *const Self) struct { first: []const T, second: []const T } { - if (self.count == 0) { - return .{ .first = &.{}, .second = &.{} }; - } - - const read_idx = (self.write_idx + self.buffer.len - self.count) % self.buffer.len; - - if (read_idx < self.write_idx) { - return .{ .first = self.buffer[read_idx..self.write_idx], .second = &.{} }; - } - - return .{ .first = self.buffer[read_idx..], .second = self.buffer[0..self.write_idx] }; - } - }; -} From 946b6d82263e23157145cfe7fc1314fc83115916 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Sun, 4 Jan 2026 18:09:28 +0300 Subject: [PATCH 7/8] `PerformanceObserver` changes --- src/browser/Page.zig | 47 ++++++++++++---------- src/browser/webapi/PerformanceObserver.zig | 25 +++++++++--- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index d6ca6347..11da60aa 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -49,7 +49,6 @@ 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 HtmlScript = @import("webapi/Element.zig").Html.Script; const PerformanceObserver = @import("webapi/PerformanceObserver.zig"); const MutationObserver = @import("webapi/MutationObserver.zig"); const IntersectionObserver = @import("webapi/IntersectionObserver.zig"); @@ -1067,28 +1066,32 @@ pub fn notifyPerformanceObservers(self: *Page, entry: *Performance.Entry) !void } } - if (self._performance_delivery_scheduled == false) { - self._performance_delivery_scheduled = true; - try self.scheduler.add( - self, - struct { - fn run(_page: *anyopaque) anyerror!?u32 { - const page: *Page = @ptrCast(@alignCast(_page)); - page._performance_delivery_scheduled = true; - // Dispatch performance observer events. - for (page._performance_observers.items) |observer| { - if (observer.hasRecords()) { - try observer.dispatch(page); - } - } - - return null; - } - }.run, - 0, - .{ .low_priority = true }, - ); + // 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 { diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig index be920c1a..7e4d9c5d 100644 --- a/src/browser/webapi/PerformanceObserver.zig +++ b/src/browser/webapi/PerformanceObserver.zig @@ -22,6 +22,10 @@ const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const Performance = @import("Performance.zig"); +pub fn registerTypes() []const type { + return &.{ PerformanceObserver, EntryList }; +} + /// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver const PerformanceObserver = @This(); @@ -109,8 +113,12 @@ pub fn observe( 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(); } /// Returns the current list of PerformanceEntry objects @@ -141,7 +149,7 @@ pub inline fn hasRecords(self: *const PerformanceObserver) bool { /// 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, .{records}); + _ = try self._callback.call(void, .{ EntryList{ ._entries = records }, self }); } pub const JsApi = struct { @@ -175,11 +183,16 @@ pub const EntryList = struct { pub const bridge = js.Bridge(EntryList); pub const Meta = struct { - pub const name = "PerformanceEntryList"; + pub const name = "PerformanceObserverEntryList"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - - pub const getEntries = bridge.function(EntryList.getEntries, .{}); }; + + pub const getEntries = bridge.function(EntryList.getEntries, .{}); }; }; + +const testing = @import("../../testing.zig"); +test "WebApi: PerformanceObserver" { + try testing.htmlRunner("performance_observer", .{}); +} From f7fe8d00fbdf37daf4407f30327c3f4bcacf3a15 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Sun, 4 Jan 2026 18:09:45 +0300 Subject: [PATCH 8/8] add a `PerformanceObserver` test --- .../performance_observer.html | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/browser/tests/performance_observer/performance_observer.html 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 @@ + + + +