Merge pull request #2150 from lightpanda-io/eventcounts

Add EventCounts API
This commit is contained in:
Karl Seguin
2026-04-13 18:33:24 +08:00
committed by GitHub
5 changed files with 322 additions and 0 deletions

View File

@@ -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 });
}

View File

@@ -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"),

View File

@@ -393,3 +393,141 @@
}
}
</script>
<script id=eventCounts_exists>
{
const ec = performance.eventCounts;
testing.expectEqual('object', typeof ec);
testing.expectEqual(false, ec === null);
testing.expectEqual(false, ec === undefined);
}
</script>
<script id=eventCounts_same_object>
{
// performance.eventCounts should return the same object on each access
testing.expectEqual(true, performance.eventCounts === performance.eventCounts);
}
</script>
<script id=eventCounts_size>
{
// EventCounts tracks 36 event types per Event Timing spec
const ec = performance.eventCounts;
testing.expectEqual('number', typeof ec.size);
testing.expectEqual(36, ec.size);
}
</script>
<script id=eventCounts_has_tracked_events>
{
const ec = performance.eventCounts;
// These should be tracked
testing.expectEqual(true, ec.has("click"));
testing.expectEqual(true, ec.has("keydown"));
testing.expectEqual(true, ec.has("keyup"));
testing.expectEqual(true, ec.has("mousedown"));
testing.expectEqual(true, ec.has("mouseup"));
testing.expectEqual(true, ec.has("pointerdown"));
testing.expectEqual(true, ec.has("input"));
testing.expectEqual(true, ec.has("touchstart"));
}
</script>
<script id=eventCounts_has_non_tracked_events>
{
const ec = performance.eventCounts;
// These should NOT be tracked
testing.expectEqual(false, ec.has("load"));
testing.expectEqual(false, ec.has("scroll"));
testing.expectEqual(false, ec.has("resize"));
testing.expectEqual(false, ec.has("DOMContentLoaded"));
testing.expectEqual(false, ec.has("nonexistent"));
}
</script>
<script id=eventCounts_get_returns_number>
{
const ec = performance.eventCounts;
// get() should return a number for tracked events
testing.expectEqual('number', typeof ec.get("click"));
testing.expectEqual('number', typeof ec.get("keydown"));
}
</script>
<script id=eventCounts_get_returns_zero_for_non_tracked>
{
const ec = performance.eventCounts;
// get() should return 0 for non-tracked events (not undefined)
testing.expectEqual(0, ec.get("load"));
testing.expectEqual(0, ec.get("nonexistent"));
}
</script>
<script id=eventCounts_keys>
{
const ec = performance.eventCounts;
const keys = Array.from(ec.keys());
testing.expectEqual(36, keys.length);
testing.expectEqual(true, keys.includes("click"));
testing.expectEqual(true, keys.includes("keydown"));
testing.expectEqual(true, keys.includes("mousedown"));
}
</script>
<script id=eventCounts_values>
{
const ec = performance.eventCounts;
const values = Array.from(ec.values());
testing.expectEqual(36, values.length);
// All values should be numbers
for (const v of values) {
testing.expectEqual('number', typeof v);
}
}
</script>
<script id=eventCounts_entries>
{
const ec = performance.eventCounts;
const entries = Array.from(ec.entries());
testing.expectEqual(36, entries.length);
// Each entry should be [string, number]
for (const e of entries) {
testing.expectEqual(true, Array.isArray(e));
testing.expectEqual(2, e.length);
testing.expectEqual('string', typeof e[0]);
testing.expectEqual('number', typeof e[1]);
}
}
</script>
<script id=eventCounts_forEach>
{
const ec = performance.eventCounts;
let count = 0;
let sawClick = false;
ec.forEach((value, key, obj) => {
count++;
testing.expectEqual('number', typeof value);
testing.expectEqual('string', typeof key);
testing.expectEqual(ec, obj);
if (key === "click") sawClick = true;
});
testing.expectEqual(36, count);
testing.expectEqual(true, sawClick);
}
</script>
<script id=eventCounts_iterator>
{
const ec = performance.eventCounts;
let count = 0;
for (const [key, value] of ec) {
count++;
testing.expectEqual('string', typeof key);
testing.expectEqual('number', typeof value);
}
testing.expectEqual(36, count);
}
</script>

View File

@@ -0,0 +1,172 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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);

View File

@@ -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 {