diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index e5ff22d5..97c0a9cc 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -107,6 +107,9 @@ pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, co event.acquireRef(); defer _ = event.releaseRef(self.page._session); + // Increment event count for Event Timing API + self.page.window._performance._event_counts.increment(event._type_string.str()); + if (comptime IS_DEBUG) { log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles }); } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 93c51f78..dc22a996 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -859,6 +859,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/URL.zig"), @import("../webapi/Window.zig"), @import("../webapi/Performance.zig"), + @import("../webapi/EventCounts.zig"), @import("../webapi/PluginArray.zig"), @import("../webapi/MutationObserver.zig"), @import("../webapi/IntersectionObserver.zig"), diff --git a/src/browser/tests/performance.html b/src/browser/tests/performance.html index d0383d90..069d011c 100644 --- a/src/browser/tests/performance.html +++ b/src/browser/tests/performance.html @@ -393,3 +393,141 @@ } } + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/EventCounts.zig b/src/browser/webapi/EventCounts.zig new file mode 100644 index 00000000..31b71628 --- /dev/null +++ b/src/browser/webapi/EventCounts.zig @@ -0,0 +1,172 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// 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 log = @import("../../log.zig"); + +pub fn registerTypes() []const type { + return &.{ + EventCounts, + KeyIterator, + ValueIterator, + EntryIterator, + }; +} + +const EventCounts = @This(); + +// Event types tracked per the Event Timing spec +// https://w3c.github.io/event-timing/#sec-events-exposed +const tracked_event_types = [_][]const u8{ + "auxclick", + "click", + "contextmenu", + "dblclick", + "mousedown", + "mouseenter", + "mouseleave", + "mouseout", + "mouseover", + "mouseup", + "pointerover", + "pointerenter", + "pointerdown", + "pointerup", + "pointercancel", + "pointerout", + "pointerleave", + "gotpointercapture", + "lostpointercapture", + "touchstart", + "touchend", + "touchcancel", + "keydown", + "keypress", + "keyup", + "beforeinput", + "input", + "compositionstart", + "compositionupdate", + "compositionend", + "dragstart", + "dragend", + "dragenter", + "dragleave", + "dragover", + "drop", +}; + +// Counts stored in a fixed array +_counts: [tracked_event_types.len]u32 = [_]u32{0} ** tracked_event_types.len, + +pub fn increment(self: *EventCounts, event_type: []const u8) void { + if (getIndex(event_type)) |idx| { + self._counts[idx] +|= 1; + } +} + +pub fn get(self: *const EventCounts, event_type: []const u8) u32 { + if (getIndex(event_type)) |idx| { + return self._counts[idx]; + } + return 0; +} + +pub fn has(_: *const EventCounts, event_type: []const u8) bool { + return getIndex(event_type) != null; +} + +pub fn getSize(_: *const EventCounts) u32 { + return tracked_event_types.len; +} + +pub fn keys(self: *EventCounts, page: *Page) !*KeyIterator { + return .init(.{ .event_counts = self }, page); +} + +pub fn values(self: *EventCounts, page: *Page) !*ValueIterator { + return .init(.{ .event_counts = self }, page); +} + +pub fn entries(self: *EventCounts, page: *Page) !*EntryIterator { + return .init(.{ .event_counts = self }, page); +} + +pub fn forEach(self: *EventCounts, cb_: js.Function, js_this_: ?js.Object) !void { + const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_; + + for (tracked_event_types, self._counts) |event_type, count| { + var caught: js.TryCatch.Caught = undefined; + cb.tryCall(void, .{ count, event_type, self }, &caught) catch { + log.debug(.js, "forEach callback", .{ .caught = caught, .source = "EventCounts" }); + }; + } +} + +fn getIndex(event_type: []const u8) ?usize { + for (tracked_event_types, 0..) |t, i| { + if (std.mem.eql(u8, t, event_type)) { + return i; + } + } + return null; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(EventCounts); + + pub const Meta = struct { + pub const name = "EventCounts"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const get = bridge.function(EventCounts.get, .{}); + pub const has = bridge.function(EventCounts.has, .{}); + pub const size = bridge.accessor(EventCounts.getSize, null, .{}); + pub const keys = bridge.function(EventCounts.keys, .{}); + pub const values = bridge.function(EventCounts.values, .{}); + pub const entries = bridge.function(EventCounts.entries, .{}); + pub const forEach = bridge.function(EventCounts.forEach, .{}); + pub const symbol_iterator = bridge.iterator(EventCounts.entries, .{}); +}; + +// Iterator implementation +pub const Iterator = struct { + index: u32 = 0, + event_counts: *EventCounts, + + pub const Entry = struct { []const u8, u32 }; + + pub fn next(self: *Iterator, _: *const Page) ?Iterator.Entry { + const index = self.index; + if (index >= tracked_event_types.len) { + return null; + } + self.index = index + 1; + + return .{ tracked_event_types[index], self.event_counts._counts[index] }; + } +}; + +const GenericIterator = @import("collections/iterator.zig").Entry; +pub const KeyIterator = GenericIterator(Iterator, "0"); +pub const ValueIterator = GenericIterator(Iterator, "1"); +pub const EntryIterator = GenericIterator(Iterator, null); diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig index 50c4457a..6880bed0 100644 --- a/src/browser/webapi/Performance.zig +++ b/src/browser/webapi/Performance.zig @@ -2,6 +2,8 @@ const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const datetime = @import("../../datetime.zig"); +const EventCounts = @import("EventCounts.zig"); + pub fn registerTypes() []const type { return &.{ Performance, Entry, Mark, Measure, PerformanceTiming, PerformanceNavigation }; } @@ -14,6 +16,7 @@ _time_origin: u64, _entries: std.ArrayList(*Entry) = .{}, _timing: PerformanceTiming = .{}, _navigation: PerformanceNavigation = .{}, +_event_counts: EventCounts = .{}, /// Get high-resolution timestamp in microseconds, rounded to 5μs increments /// to match browser behavior (prevents fingerprinting) @@ -54,6 +57,10 @@ pub fn getNavigation(self: *Performance) *PerformanceNavigation { return &self._navigation; } +pub fn getEventCounts(self: *Performance) *EventCounts { + return &self._event_counts; +} + pub fn mark( self: *Performance, name: []const u8, @@ -277,6 +284,7 @@ pub const JsApi = struct { pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{}); pub const timing = bridge.accessor(Performance.getTiming, null, .{}); pub const navigation = bridge.accessor(Performance.getNavigation, null, .{}); + pub const eventCounts = bridge.accessor(Performance.getEventCounts, null, .{}); }; pub const Entry = struct {