mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 06:23:45 +00:00
Merge pull request #1282 from lightpanda-io/nikneym/performance-observer
Support `PerformanceObserver` API
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id="observe_performance_mark">
|
||||
{
|
||||
const observer = new PerformanceObserver((list, observer) => {
|
||||
testing.expectEqual(true, list instanceof PerformanceObserverEntryList);
|
||||
testing.expectEqual(true, observer instanceof PerformanceObserver);
|
||||
|
||||
const entries = list.getEntries();
|
||||
testing.expectEqual(true, entries instanceof Array);
|
||||
testing.expectEqual(2, entries.length);
|
||||
|
||||
{
|
||||
const { name, startTime, duration, entryType } = entries[0];
|
||||
testing.expectEqual("operationStart", name);
|
||||
testing.expectEqual(20, startTime);
|
||||
testing.expectEqual(0, duration);
|
||||
testing.expectEqual("mark", entryType);
|
||||
}
|
||||
|
||||
{
|
||||
const { name, startTime, duration, entryType } = entries[1];
|
||||
testing.expectEqual("operationEnd", name);
|
||||
testing.expectEqual(34.0, startTime);
|
||||
testing.expectEqual(0, duration);
|
||||
testing.expectEqual("mark", entryType);
|
||||
}
|
||||
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
// Look for performance marks.
|
||||
observer.observe({ type: "mark" });
|
||||
performance.mark("operationStart", { startTime: 20.0 });
|
||||
performance.mark("operationEnd", { startTime: 34.0 });
|
||||
}
|
||||
</script>
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,41 +16,140 @@
|
||||
// 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 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", .{});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user