mem: Swith EventTarget's _type to a bare tag

Same change as https://github.com/lightpanda-io/browser/pull/3128 but for
EventTarget. This shrinks all EventTargets (i.e. most WebAPI interfaces) by 8
bytes.
This commit is contained in:
Karl Seguin committed 2026-08-12 08:01:12 +08:00
1 parent efc7613768
commit 0f6ea46b2e
7 files changed
+165 -79

No files matched your search

+26 -28
View File
@@ -25,9 +25,11 @@ const EventManagerBase = @import("EventManagerBase.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
const Window = @import("webapi/Window.zig");
const EventTarget = @import("webapi/EventTarget.zig");
const Element = @import("webapi/Element.zig");
const ShadowRoot = @import("webapi/ShadowRoot.zig");
const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig");
const log = lp.log;
const Allocator = std.mem.Allocator;
@@ -83,15 +85,15 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
}
switch (target._type) {
.node => |node| try self.dispatchNode(node, event),
.xhr => |xhr| try self.dispatchDirect(target, event, xhr.inlineHandler(event._type_string), .{ .context = "dispatch" }),
.window => |w| try self.dispatchDirect(target, event, windowInlineHandler(w, event._type_string), .{ .context = "dispatch" }),
.node => try self.dispatchNode(target.subtype(Node), event),
.xhr => try self.dispatchDirect(target, event, target.subtype(XMLHttpRequestEventTarget).inlineHandler(event._type_string), .{ .context = "dispatch" }),
.window => try self.dispatchDirect(target, event, windowInlineHandler(target.subtype(Window), event._type_string), .{ .context = "dispatch" }),
else => try self.dispatchDirect(target, event, null, .{ .context = "dispatch" }),
}
}
// Resolves the Window's property event handler for the given event type.
fn windowInlineHandler(window: *@import("webapi/Window.zig"), typ: lp.String) ?js.Function.Global {
fn windowInlineHandler(window: *Window, typ: lp.String) ?js.Function.Global {
const global_event_handlers = @import("webapi/global_event_handlers.zig");
const handler_type = global_event_handlers.fromEventType(typ.str()) orelse return null;
return switch (handler_type) {
@@ -244,9 +246,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
// root is the document (not for detached trees, and not when propagation
// stopped at a shadow boundary). The only explicit exception is "load".
if (event._type_string.eql(comptime .wrap("load")) == false and path_len < path_buffer.len) {
const root_is_document = path_len > 0 and switch (path_buffer[path_len - 1]._type) {
.node => |n| n._type == .document,
else => false,
const root_is_document = blk: {
if (path_len == 0) break :blk false;
const root = path_buffer[path_len - 1].is(Node) orelse break :blk false;
break :blk root._type == .document;
};
if (root_is_document) {
path_buffer[path_len] = frame.window.asEventTarget();
@@ -508,17 +511,20 @@ fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?j
// Look up the inline handler for this target
const html_element = switch (target._type) {
.node => |n| n.is(Element.Html) orelse return null,
.node => target.subtype(Node).is(Element.Html) orelse return null,
// The Window stores its event handlers in dedicated fields; an event
// propagating to the window must fire them too.
.window => |w| return switch (handler_type) {
.onerror => w._on_error,
.onload => w._on_load,
.onblur => w._on_blur,
.onfocus => w._on_focus,
.onresize => w._on_resize,
.onscroll => w._on_scroll,
else => null,
.window => {
const w = target.subtype(Window);
return switch (handler_type) {
.onerror => w._on_error,
.onload => w._on_load,
.onblur => w._on_blur,
.onfocus => w._on_focus,
.onresize => w._on_resize,
.onscroll => w._on_scroll,
else => null,
};
},
else => return null,
};
@@ -639,14 +645,8 @@ fn eventPathParent(node: *Node, event: *Event, target_root: *Node, frame: ?*Fram
// DOM spec "retarget": walk original_target out of shadow trees until the
// node is visible from current_target's tree.
fn getAdjustedTarget(original_target: ?*EventTarget, current_target: *EventTarget) ?*EventTarget {
const orig_node = switch ((original_target orelse return null)._type) {
.node => |n| n,
else => return original_target,
};
const curr_node = switch (current_target._type) {
.node => |n| n,
else => return original_target,
};
const orig_node = (original_target orelse return null).is(Node) orelse return original_target;
const curr_node = current_target.is(Node) orelse return original_target;
var node = orig_node;
while (true) {
@@ -678,10 +678,8 @@ fn isShadowIncludingInclusiveAncestor(ancestor: *Node, node: *Node) bool {
// shadow root. Used for the spec's post-dispatch "clear targets" step.
fn rootIsShadowRoot(target_: ?*EventTarget) bool {
const target = target_ orelse return false;
return switch (target._type) {
.node => |n| n.containingShadowRoot() != null,
else => false,
};
const node = target.is(Node) orelse return false;
return node.containingShadowRoot() != null;
}
// Check if ancestor is an ancestor of (or the same as) node
+4 -1
View File
@@ -68,7 +68,7 @@ pub fn eventTargetWithAllocator(_: *const Factory, allocator: Allocator, child:
const event_ptr = chain.get(0);
event_ptr.* = .{
._type = unionInit(EventTarget.Type, chain.get(1)),
._type = typeInit(EventTarget, chain.get(1)),
};
chain.setLeaf(1, child);
@@ -621,3 +621,6 @@ fn unionFieldName(comptime T: type, comptime V: type) []const u8 {
}
@compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type");
}
+1 -2
View File
@@ -161,10 +161,9 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
errdefer arena.release();
const navigation = try Factory.chainedWithAllocator(arena.allocator(), .{
EventTarget{ ._type = undefined },
EventTarget{ ._type = .navigation },
Navigation{ ._proto = undefined },
});
navigation._proto._type = .{ .navigation = navigation };
self.* = .{
.arena = arena,
+5 -11
View File
@@ -322,10 +322,7 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget {
const target = self._dispatch_target orelse self._target orelse return &.{};
// Only nodes have a propagation path
const target_node = switch (target._type) {
.node => |n| n,
else => return &.{},
};
const target_node = target.is(Node) orelse return &.{};
const frame_ = switch (exec.js.global) {
.frame => |frame| frame,
@@ -341,9 +338,9 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget {
// Window follows the document at the end of the path. A path that stopped
// early — at a shadow boundary, or at the relatedTarget — doesn't end on
// the document and so doesn't reach it.
const root_is_document = switch (path_buffer[path_len - 1]._type) {
.node => |n| n._type == .document,
else => false,
const root_is_document = blk: {
const root = path_buffer[path_len - 1].is(Node) orelse break :blk false;
break :blk root._type == .document;
};
if (root_is_document and path_len < path_buffer.len) {
if (frame_) |frame| {
@@ -356,10 +353,7 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget {
// it is inside that root and hidden from a currentTarget outside it.
var closed_host_index: ?usize = null;
for (path_buffer[0..path_len], 0..) |entry, i| {
const node = switch (entry._type) {
.node => |n| n,
else => continue,
};
const node = entry.is(Node) orelse continue;
const shadow = node.is(Node.ShadowRoot) orelse continue;
if (shadow._mode == .closed) {
closed_host_index = i + 1;
+126 -31
View File
@@ -17,50 +17,142 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Factory = @import("../Factory.zig");
const EventManager = @import("../EventManager.zig");
const Node = @import("Node.zig");
const Event = @import("Event.zig");
const Screen = @import("Screen.zig");
const Worker = @import("Worker.zig");
const Window = @import("Window.zig");
const AbortSignal = @import("AbortSignal.zig");
const MessagePort = @import("MessagePort.zig");
const FileReader = @import("FileReader.zig");
const WebSocket = @import("net/WebSocket.zig");
const Navigation = @import("navigation/Navigation.zig");
const Notification = @import("Notification.zig");
const EventSource = @import("net/EventSource.zig");
const CookieStore = @import("storage/CookieStore.zig");
const IDBRequest = @import("storage/idb/IDBRequest.zig");
const SharedWorker = @import("SharedWorker.zig");
const FontFaceSet = @import("css/FontFaceSet.zig");
const IDBDatabase = @import("storage/idb/IDBDatabase.zig");
const TextTrackCue = @import("media/TextTrackCue.zig");
const VisualViewport = @import("VisualViewport.zig");
const MediaQueryList = @import("css/MediaQueryList.zig");
const IDBTransaction = @import("storage/idb/IDBTransaction.zig");
const BroadcastChannel = @import("BroadcastChannel.zig");
const WorkerGlobalScope = @import("WorkerGlobalScope.zig");
const NavigationHistoryEntry = @import("navigation/NavigationHistoryEntry.zig");
const XMLHttpRequestEventTarget = @import("net/XMLHttpRequestEventTarget.zig");
const RegisterOptions = EventManager.RegisterOptions;
const EventTarget = @This();
pub const _prototype_root = true;
_type: Type,
pub const Type = union(enum) {
generic: void,
node: *@import("Node.zig"),
window: *@import("Window.zig"),
worker: *@import("Worker.zig"),
shared_worker: *@import("SharedWorker.zig"),
worker_global_scope: *@import("WorkerGlobalScope.zig"),
xhr: *@import("net/XMLHttpRequestEventTarget.zig"),
abort_signal: *@import("AbortSignal.zig"),
media_query_list: *@import("css/MediaQueryList.zig"),
message_port: *@import("MessagePort.zig"),
broadcast_channel: *@import("BroadcastChannel.zig"),
text_track_cue: *@import("media/TextTrackCue.zig"),
navigation: *@import("navigation/Navigation.zig"),
navigation_history_entry: *@import("navigation/NavigationHistoryEntry.zig"),
screen: *@import("Screen.zig"),
screen_orientation: *@import("Screen.zig").Orientation,
visual_viewport: *@import("VisualViewport.zig"),
file_reader: *@import("FileReader.zig"),
font_face_set: *@import("css/FontFaceSet.zig"),
websocket: *@import("net/WebSocket.zig"),
event_source: *@import("net/EventSource.zig"),
cookie_store: *@import("storage/CookieStore.zig"),
idb_request: *@import("storage/idb/IDBRequest.zig"),
idb_database: *@import("storage/idb/IDBDatabase.zig"),
idb_transaction: *@import("storage/idb/IDBTransaction.zig"),
notification: *@import("Notification.zig"),
// `global_event_handlers.Key` reuses the low 3 bits of an EventTarget pointer,
// so the type has to stay 8-byte aligned even though the tag is a single byte.
// This costs nothing in a chain: EventTarget is always at offset 0 and every
// subtype that follows it is itself 8-aligned.
_type: Type align(8),
pub const Type = enum(u8) {
generic,
node,
window,
worker,
shared_worker,
worker_global_scope,
xhr,
abort_signal,
media_query_list,
message_port,
broadcast_channel,
text_track_cue,
navigation,
navigation_history_entry,
screen,
screen_orientation,
visual_viewport,
file_reader,
font_face_set,
websocket,
event_source,
cookie_store,
idb_request,
idb_database,
idb_transaction,
notification,
};
// `.generic` maps to EventTarget itself: a standalone `new EventTarget()` has
// no chain member of its own.
pub fn Subtype(comptime tag: Type) type {
return switch (tag) {
.generic => EventTarget,
.node => Node,
.window => Window,
.worker => Worker,
.shared_worker => SharedWorker,
.worker_global_scope => WorkerGlobalScope,
.xhr => XMLHttpRequestEventTarget,
.abort_signal => AbortSignal,
.media_query_list => MediaQueryList,
.message_port => MessagePort,
.broadcast_channel => BroadcastChannel,
.text_track_cue => TextTrackCue,
.navigation => Navigation,
.navigation_history_entry => NavigationHistoryEntry,
.screen => Screen,
.screen_orientation => Screen.Orientation,
.visual_viewport => VisualViewport,
.file_reader => FileReader,
.font_face_set => FontFaceSet,
.websocket => WebSocket,
.event_source => EventSource,
.cookie_store => CookieStore,
.idb_request => IDBRequest,
.idb_database => IDBDatabase,
.idb_transaction => IDBTransaction,
.notification => Notification,
};
}
pub fn subtype(self: *const EventTarget, comptime T: type) *T {
const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, EventTarget);
const sub: *T = @ptrFromInt(@intFromPtr(self) + offset);
if (comptime lp.IS_DEBUG) {
// This pointer dance only works because the factory allocates the chain
// in a contiguous block of memory. In debug, we assert this holds via
// the _proto_canary back pointer.
std.debug.assert(Factory.protoOf(sub) == self);
}
return sub;
}
// Returns the target as a more specific type, or null if it isn't a `T`.
pub fn is(self: *EventTarget, comptime T: type) ?*T {
switch (self._type) {
.generic => {},
inline else => |tag| {
if (Subtype(tag) == T) {
return self.subtype(T);
}
},
}
return null;
}
pub fn as(self: *EventTarget, comptime T: type) *T {
return self.is(T).?;
}
pub fn init(page: *Page) !*EventTarget {
return page.factory.create(EventTarget{
._type = .generic,
@@ -121,8 +213,9 @@ fn defaultPassiveValue(self: *EventTarget, typ: []const u8) bool {
switch (self._type) {
.window => return true,
.node => |n| {
.node => {
const Element = @import("Element.zig");
const n = self.subtype(Node);
if (n._type == .document) {
return true;
}
@@ -212,7 +305,7 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: js.Nu
pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
return switch (self._type) {
.node => |n| n.format(writer),
.node => self.subtype(Node).format(writer),
.generic => writer.writeAll("<EventTarget>"),
.window => writer.writeAll("<Window>"),
.worker => writer.writeAll("<Worker>"),
@@ -293,6 +386,8 @@ test "WebApi: EventTarget" {
testing.silenceLog(&.{ .js, .event });
// we create thousands of these per frame. Nothing should bloat it.
try testing.expectEqual(16, @sizeOf(EventTarget));
// The tag is 1 byte; the rest is the align(8) that `Key.fuse` depends on.
try testing.expectEqual(8, @sizeOf(EventTarget));
try testing.expectEqual(8, @alignOf(EventTarget));
try testing.htmlRunner("events.html", .{});
}
+1 -2
View File
@@ -143,7 +143,7 @@ pub fn init(
const factory = frame._factory;
const leaf = try Factory.chainedWithAllocator(arena, .{
EventTarget{ ._type = undefined },
EventTarget{ ._type = .worker_global_scope },
WorkerGlobalScope{
.url = url,
.arena = arena,
@@ -173,7 +173,6 @@ pub fn init(
});
const self = leaf._proto;
self._type = @unionInit(Type, @tagName(tag), leaf);
self._proto._type = .{ .worker_global_scope = self };
self._http_owner = .init(&frame._page.blob_urls, &self.origin);
+2 -4
View File
@@ -216,7 +216,7 @@ pub fn pushEntry(
const id_str = try std.fmt.allocPrint(arena.allocator(), "{d}", .{id});
const entry = try Factory.chainedWithAllocator(arena.allocator(), .{
EventTarget{ ._type = undefined },
EventTarget{ ._type = .navigation_history_entry },
NavigationHistoryEntry{
._proto = undefined,
._id = id_str,
@@ -225,7 +225,6 @@ pub fn pushEntry(
._state = state,
},
});
entry._proto._type = .{ .navigation_history_entry = entry };
// we don't always have a current entry...
const previous = if (self._entries.items.len > 0) self.getCurrentEntry() else null;
@@ -263,7 +262,7 @@ pub fn replaceEntry(
const id_str = try std.fmt.allocPrint(arena.allocator(), "{d}", .{id});
const entry = try Factory.chainedWithAllocator(arena.allocator(), .{
EventTarget{ ._type = undefined },
EventTarget{ ._type = .navigation_history_entry },
NavigationHistoryEntry{
._proto = undefined,
._id = id_str,
@@ -272,7 +271,6 @@ pub fn replaceEntry(
._state = state,
},
});
entry._proto._type = .{ .navigation_history_entry = entry };
const old_entry = self._entries.items[self._index];
self._entries.items[self._index] = entry;