Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/browser/Page.zig
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ 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");
Expand Down Expand Up @@ -108,6 +109,9 @@ _intersection_observers: std.ArrayList(*IntersectionObserver) = .{},
_intersection_check_scheduled: bool = false,
_intersection_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) = .{},
Expand Down Expand Up @@ -245,6 +249,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;
Expand Down Expand Up @@ -841,6 +846,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);
}
Expand Down
10 changes: 10 additions & 0 deletions src/browser/js/Context.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1964,6 +1964,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 {
Expand Down
45 changes: 33 additions & 12 deletions src/browser/webapi/Performance.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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),
};
}
Expand Down
167 changes: 146 additions & 21 deletions src/browser/webapi/PerformanceObserver.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,144 @@
// 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/>.

const js = @import("../js/js.zig");
const std = @import("std");

const Entry = @import("Performance.zig").Entry;
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Performance = @import("Performance.zig");

// 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 {
Expand All @@ -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, .{});
};
};
};
Loading