core performance observer logic

Heavily based on MutationObserver and IntersectionObserver.
This commit is contained in:
Halil Durak
2025-12-19 20:34:10 +03:00
parent 9306adc786
commit 88de72a9ea
4 changed files with 221 additions and 33 deletions

View File

@@ -49,6 +49,8 @@ const Document = @import("webapi/Document.zig");
const ShadowRoot = @import("webapi/ShadowRoot.zig"); const ShadowRoot = @import("webapi/ShadowRoot.zig");
const Performance = @import("webapi/Performance.zig"); const Performance = @import("webapi/Performance.zig");
const Screen = @import("webapi/Screen.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 MutationObserver = @import("webapi/MutationObserver.zig");
const IntersectionObserver = @import("webapi/IntersectionObserver.zig"); const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
const CustomElementDefinition = @import("webapi/CustomElementDefinition.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) = .{}, _slots_pending_slotchange: std.AutoHashMapUnmanaged(*Element.Html.Slot, void) = .{},
_slotchange_delivery_scheduled: bool = false, _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. // Lookup for customized built-in elements. Maps element pointer to definition.
_customized_builtin_definitions: std.AutoHashMapUnmanaged(*Element, *CustomElementDefinition) = .{}, _customized_builtin_definitions: std.AutoHashMapUnmanaged(*Element, *CustomElementDefinition) = .{},
_customized_builtin_connected_callback_invoked: std.AutoHashMapUnmanaged(*Element, void) = .{}, _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_idle = .init;
self._notified_network_almost_idle = .init; self._notified_network_almost_idle = .init;
self._performance_observers = .{};
self._mutation_observers = .{}; self._mutation_observers = .{};
self._mutation_delivery_scheduled = false; self._mutation_delivery_scheduled = false;
self._mutation_delivery_depth = 0; self._mutation_delivery_depth = 0;
@@ -1025,6 +1031,32 @@ pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Elemen
return null; 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 { pub fn registerMutationObserver(self: *Page, observer: *MutationObserver) !void {
try self._mutation_observers.append(self.arena, observer); try self._mutation_observers.append(self.arena, observer);
} }

View File

@@ -2017,6 +2017,16 @@ fn zigJsonToJs(isolate: v8.Isolate, v8_context: v8.Context, value: std.json.Valu
} }
// Microtasks // 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 { pub fn queueMutationDelivery(self: *Context) !void {
self.isolate.enqueueMicrotask(struct { self.isolate.enqueueMicrotask(struct {
fn run(data: ?*anyopaque) callconv(.c) void { fn run(data: ?*anyopaque) callconv(.c) void {

View File

@@ -11,7 +11,7 @@ const std = @import("std");
const Performance = @This(); const Performance = @This();
_time_origin: u64, _time_origin: u64,
_entries: std.ArrayListUnmanaged(*Entry) = .{}, _entries: std.ArrayList(*Entry) = .{},
/// Get high-resolution timestamp in microseconds, rounded to 5μs increments /// Get high-resolution timestamp in microseconds, rounded to 5μs increments
/// to match browser behavior (prevents fingerprinting) /// 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; 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); const m = try Mark.init(name, _options, page);
try self._entries.append(page.arena, m._proto); try self._entries.append(page.arena, m._proto);
// Notify about the change.
try page.notifyPerformanceObservers(m._proto);
return m; return m;
} }
@@ -230,21 +237,40 @@ pub const Entry = struct {
_name: []const u8, _name: []const u8,
_start_time: f64 = 0.0, _start_time: f64 = 0.0,
const Type = union(enum) { pub const Type = union(Enum) {
element, element,
event, event,
first_input, first_input,
largest_contentful_paint, @"largest-contentful-paint",
layout_shift, @"layout-shift",
long_animation_frame, @"long-animation-frame",
longtask, longtask,
measure: *Measure, measure: *Measure,
navigation, navigation,
paint, paint,
resource, resource,
taskattribution, taskattribution,
visibility_state, @"visibility-state",
mark: *Mark, 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 { pub fn getDuration(self: *const Entry) f64 {
@@ -253,11 +279,6 @@ pub const Entry = struct {
pub fn getEntryType(self: *const Entry) []const u8 { pub fn getEntryType(self: *const Entry) []const u8 {
return switch (self._type) { 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), else => |t| @tagName(t),
}; };
} }

View File

@@ -16,41 +16,144 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig"); 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(); const PerformanceObserver = @This();
pub fn init(callback: js.Function) PerformanceObserver { /// Emitted when there are events with same interests.
_ = callback; _callback: js.Function,
return .{}; /// 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 { // We don't have to mark this as public but the declarations have to be public;
buffered: ?bool = null, // otherwise @typeInfo don't allow accessing them.
durationThreshold: ?f64 = null, //
// 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, entryTypes: ?[]const []const u8 = null,
type: ?[]const u8 = null, type: ?[]const u8 = null,
}; };
pub fn observe(self: *const PerformanceObserver, opts_: ?ObserverOptions) void { /// TODO: Support `buffered` option.
_ = self; pub fn observe(
_ = opts_; 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; 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 { pub fn disconnect(self: *PerformanceObserver) void {
_ = self; _ = self;
} }
pub fn takeRecords(_: *const PerformanceObserver) []const Entry { /// Returns the current list of PerformanceEntry objects
return &.{}; /// 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 { /// Returns true if observer interested with given entry.
return &.{}; 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 { pub const JsApi = struct {
@@ -60,13 +163,35 @@ pub const JsApi = struct {
pub const name = "PerformanceObserver"; pub const name = "PerformanceObserver";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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 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 }); 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, .{});
};
};
};