From 63f489d39fb033ff4406eea4e1741ee1ae74a329 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 09:13:58 -0800 Subject: [PATCH 1/8] initial with full chain allocations --- src/browser/Factory.zig | 356 ++++++++++-------- src/browser/Page.zig | 30 +- src/browser/webapi/element/Html.zig | 44 +-- .../webapi/net/XMLHttpRequestEventTarget.zig | 2 +- 4 files changed, 229 insertions(+), 203 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 336924b6..26b30052 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const assert = std.debug.assert; const builtin = @import("builtin"); const reflect = @import("reflect.zig"); const IS_DEBUG = builtin.mode == .Debug; @@ -35,21 +36,113 @@ const EventTarget = @import("webapi/EventTarget.zig"); const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig"); const Blob = @import("webapi/Blob.zig"); -const MemoryPoolAligned = std.heap.MemoryPoolAligned; - -// 1. Generally, wrapping an ArenaAllocator within an ArenaAllocator doesn't make -// much sense. But wrapping a MemoryPool within an Arena does. Specifically, by -// doing so, we solve a major issue with Arena: freed memory can be re-used [for -// more of the same size]. -// 2. Normally, you have a MemoryPool(T) where T is a `User` or something. Then -// the MemoryPool can be used for creating users. But in reality, that memory -// created by that pool could be re-used for anything with the same size (or less) -// than a User (and a compatible alignment). So that's what we do - we have size -// (and alignment) based pools. const Factory = @This(); _page: *Page, _slab: SlabAllocator, +fn PrototypeChain(comptime types: []const type) type { + return struct { + const Self = @This(); + memory: []u8, + + fn totalSize() usize { + var size: usize = 0; + for (types) |T| { + size = std.mem.alignForward(usize, size, @alignOf(T)); + size += @sizeOf(T); + } + return size; + } + + fn maxAlign() std.mem.Alignment { + var alignment: std.mem.Alignment = .@"1"; + + for (types) |T| { + alignment = std.mem.Alignment.max(alignment, std.mem.Alignment.of(T)); + } + + return alignment; + } + + fn getType(comptime index: usize) type { + return types[index]; + } + + fn allocate(allocator: std.mem.Allocator) !Self { + const size = comptime Self.totalSize(); + const alignment = comptime Self.maxAlign(); + + const memory = try allocator.alignedAlloc(u8, alignment, size); + return .{ .memory = memory }; + } + + fn get(self: *const Self, comptime index: usize) *getType(index) { + var offset: usize = 0; + inline for (types, 0..) |T, i| { + offset = std.mem.alignForward(usize, offset, @alignOf(T)); + + if (i == index) { + return @as(*T, @ptrCast(@alignCast(self.memory.ptr + offset))); + } + offset += @sizeOf(T); + } + unreachable; + } + + fn set(self: *const Self, comptime index: usize, value: getType(index)) void { + const ptr = self.get(index); + ptr.* = value; + } + + fn setRoot(self: *const Self, comptime T: type) void { + const ptr = self.get(0); + ptr.* = .{ ._type = unionInit(T, self.get(1)) }; + } + + fn setMiddle(self: *const Self, comptime index: usize, comptime T: type) void { + assert(index >= 1); + assert(index < types.len); + + const ptr = self.get(index); + ptr.* = .{ ._proto = self.get(index - 1), ._type = unionInit(T, self.get(index + 1)) }; + } + + fn setMiddleWithValue(self: *const Self, comptime index: usize, comptime T: type, value: anytype) void { + assert(index >= 1); + + const ptr = self.get(index); + ptr.* = .{ ._proto = self.get(index - 1), ._type = unionInit(T, value) }; + } + + fn setLeaf(self: *const Self, comptime index: usize, value: anytype) void { + assert(index >= 1); + + const ptr = self.get(index); + ptr.* = value; + ptr._proto = self.get(index - 1); + } + }; +} + +fn AutoPrototypeChain(comptime types: []const type) type { + return struct { + fn create(allocator: std.mem.Allocator, leaf_value: anytype) !*@TypeOf(leaf_value) { + const chain = try PrototypeChain(types).allocate(allocator); + + const RootType = types[0]; + chain.setRoot(RootType.Type); + + inline for (1..types.len - 1) |i| { + const MiddleType = types[i]; + chain.setMiddle(i, MiddleType.Type); + } + + chain.setLeaf(types.len - 1, leaf_value); + return chain.get(types.len - 1); + } + }; +} + pub fn init(page: *Page) Factory { return .{ ._page = page, @@ -59,165 +152,127 @@ pub fn init(page: *Page) Factory { // this is a root object pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { - const child_ptr = try self.createT(@TypeOf(child)); - child_ptr.* = child; + const allocator = self._slab.allocator(); + const chain = try PrototypeChain( + &.{ EventTarget, @TypeOf(child) }, + ).allocate(allocator); - const et = try self.createT(EventTarget); - child_ptr._proto = et; - et.* = .{ ._type = unionInit(EventTarget.Type, child_ptr) }; - return child_ptr; + chain.setRoot(EventTarget.Type); + chain.setLeaf(1, child); + + return chain.get(1); } pub fn node(self: *Factory, child: anytype) !*@TypeOf(child) { - const child_ptr = try self.createT(@TypeOf(child)); - child_ptr.* = child; - child_ptr._proto = try self.eventTarget(Node{ - ._proto = undefined, - ._type = unionInit(Node.Type, child_ptr), - }); - return child_ptr; + const allocator = self._slab.allocator(); + return try AutoPrototypeChain( + &.{ EventTarget, Node, @TypeOf(child) }, + ).create(allocator, child); } pub fn document(self: *Factory, child: anytype) !*@TypeOf(child) { - const child_ptr = try self.createT(@TypeOf(child)); - child_ptr.* = child; - child_ptr._proto = try self.node(Document{ - ._proto = undefined, - ._type = unionInit(Document.Type, child_ptr), - }); - return child_ptr; + const allocator = self._slab.allocator(); + return try AutoPrototypeChain( + &.{ EventTarget, Node, Document, @TypeOf(child) }, + ).create(allocator, child); } pub fn documentFragment(self: *Factory, child: anytype) !*@TypeOf(child) { - const child_ptr = try self.createT(@TypeOf(child)); - child_ptr.* = child; - child_ptr._proto = try self.node(Node.DocumentFragment{ - ._proto = undefined, - ._type = unionInit(Node.DocumentFragment.Type, child_ptr), - }); - return child_ptr; + const allocator = self._slab.allocator(); + return try AutoPrototypeChain( + &.{ EventTarget, Node, Node.DocumentFragment, @TypeOf(child) }, + ).create(allocator, child); } pub fn element(self: *Factory, child: anytype) !*@TypeOf(child) { - const child_ptr = try self.createT(@TypeOf(child)); - child_ptr.* = child; - child_ptr._proto = try self.node(Element{ - ._proto = undefined, - ._type = unionInit(Element.Type, child_ptr), - }); - return child_ptr; + const allocator = self._slab.allocator(); + return try AutoPrototypeChain( + &.{ EventTarget, Node, Element, @TypeOf(child) }, + ).create(allocator, child); } pub fn htmlElement(self: *Factory, child: anytype) !*@TypeOf(child) { - if (comptime fieldIsPointer(Element.Html.Type, @TypeOf(child))) { - const child_ptr = try self.createT(@TypeOf(child)); - child_ptr.* = child; - child_ptr._proto = try self.element(Element.Html{ - ._proto = undefined, - ._type = unionInit(Element.Html.Type, child_ptr), - }); - return child_ptr; - } - - // Our union type fields are usually pointers. But, at the leaf, they - // can be struct (if all they contain is the `_proto` field, then we might - // as well store it directly in the struct). - - const html = try self.element(Element.Html{ - ._proto = undefined, - ._type = unionInit(Element.Html.Type, child), - }); - const field_name = comptime unionFieldName(Element.Html.Type, @TypeOf(child)); - var child_ptr = &@field(html._type, field_name); - child_ptr._proto = html; - return child_ptr; + const allocator = self._slab.allocator(); + return try AutoPrototypeChain( + &.{ EventTarget, Node, Element, Element.Html, @TypeOf(child) }, + ).create(allocator, child); } pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) { - if (@TypeOf(child) == Element.Svg) { - return self.element(child); - } + const allocator = self._slab.allocator(); // will never allocate, can't fail const tag_name_str = String.init(self._page.arena, tag_name, .{}) catch unreachable; - if (comptime fieldIsPointer(Element.Svg.Type, @TypeOf(child))) { - const child_ptr = try self.createT(@TypeOf(child)); - child_ptr.* = child; - child_ptr._proto = try self.element(Element.Svg{ - ._proto = undefined, - ._tag_name = tag_name_str, - ._type = unionInit(Element.Svg.Type, child_ptr), - }); - return child_ptr; - } + const chain = try PrototypeChain( + &.{ EventTarget, Node, Element, Element.Svg, @TypeOf(child) }, + ).allocate(allocator); - // Our union type fields are usually pointers. But, at the leaf, they - // can be struct (if all they contain is the `_proto` field, then we might - // as well store it directly in the struct). - const svg = try self.element(Element.Svg{ - ._proto = undefined, + chain.setRoot(EventTarget.Type); + chain.setMiddle(1, Node.Type); + chain.setMiddle(2, Element.Type); + + // Manually set Element.Svg with the tag_name + chain.set(3, .{ + ._proto = chain.get(2), ._tag_name = tag_name_str, - ._type = unionInit(Element.Svg.Type, child), + ._type = unionInit(Element.Svg.Type, chain.get(4)), }); - const field_name = comptime unionFieldName(Element.Svg.Type, @TypeOf(child)); - var child_ptr = &@field(svg._type, field_name); - child_ptr._proto = svg; - return child_ptr; + + chain.setLeaf(4, child); + return chain.get(4); } // this is a root object pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { - const child_ptr = try self.createT(@TypeOf(child)); - child_ptr.* = child; + const allocator = self._slab.allocator(); - const e = try self.createT(Event); - child_ptr._proto = e; - e.* = .{ - ._type = unionInit(Event.Type, child_ptr), + // Special case: Event has a _type_string field, so we need manual setup + const chain = try PrototypeChain( + &.{ Event, @TypeOf(child) }, + ).allocate(allocator); + + const event_ptr = chain.get(0); + event_ptr.* = .{ + ._type = unionInit(Event.Type, chain.get(1)), ._type_string = try String.init(self._page.arena, typ, .{}), }; - return child_ptr; + chain.setLeaf(1, child); + + return chain.get(1); } pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { - const et = try self.eventTarget(XMLHttpRequestEventTarget{ - ._proto = undefined, - ._type = unionInit(XMLHttpRequestEventTarget.Type, child), - }); - const field_name = comptime unionFieldName(XMLHttpRequestEventTarget.Type, @TypeOf(child)); - var child_ptr = &@field(et._type, field_name); - child_ptr._proto = et; - return child_ptr; + const allocator = self._slab.allocator(); + + return try AutoPrototypeChain( + &.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) }, + ).create(allocator, child); } pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { - const child_ptr = try self.createT(@TypeOf(child)); - child_ptr.* = child; + const allocator = self._slab.allocator(); - const b = try self.createT(Blob); - child_ptr._proto = b; - b.* = .{ - ._type = unionInit(Blob.Type, child_ptr), + // Special case: Blob has slice and mime fields, so we need manual setup + const chain = try PrototypeChain( + &.{ Blob, @TypeOf(child) }, + ).allocate(allocator); + + const blob_ptr = chain.get(0); + blob_ptr.* = .{ + ._type = unionInit(Blob.Type, chain.get(1)), .slice = "", .mime = "", }; - return child_ptr; -} + chain.setLeaf(1, child); -pub fn create(self: *Factory, value: anytype) !*@TypeOf(value) { - const ptr = try self.createT(@TypeOf(value)); - ptr.* = value; - return ptr; -} - -pub fn createT(self: *Factory, comptime T: type) !*T { - const allocator = self._slab.allocator(); - return try allocator.create(T); + return chain.get(1); } pub fn destroy(self: *Factory, value: anytype) void { const S = reflect.Struct(@TypeOf(value)); + // const allocator = self._slab.allocator(); + if (comptime IS_DEBUG) { // We should always destroy from the leaf down. if (@hasField(S, "_type") and @typeInfo(@TypeOf(value._type)) == .@"union") { @@ -231,12 +286,13 @@ pub fn destroy(self: *Factory, value: anytype) void { } } - self.destroyChain(value, true); + const root_ptr = self.destroyChain(value, true); + _ = root_ptr; + // allocator.destroy(root_ptr); } -fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void { +fn destroyChain(self: *Factory, value: anytype, comptime first: bool) *@TypeOf(value) { const S = reflect.Struct(@TypeOf(value)); - const allocator = self._slab.allocator(); // This is initially called from a deinit. We don't want to call that @@ -255,7 +311,7 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void { } if (@hasField(S, "_proto")) { - self.destroyChain(value._proto, false); + return self.destroyChain(value._proto, false); } else if (@hasDecl(S, "JsApi")) { // Doesn't have a _proto, but has a JsApi. if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| { @@ -263,36 +319,18 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void { } } - // Leaf types are allowed by be placed directly within their _proto - // (which makes sense when the @sizeOf(Leaf) == 8). These don't need to - // be (cannot be) freed. But we'll still free the chain. - if (comptime wasAllocated(S)) { - allocator.destroy(value); - } + return @ptrCast(value); } -fn wasAllocated(comptime S: type) bool { - // Whether it's heap allocate or not, we should have a pointer. - // (If it isn't heap allocated, it'll be a pointer from the proto's type - // e.g. &html._type.title) - if (!@hasField(S, "_proto")) { - // a root is always on the heap. - return true; - } +pub fn createT(self: *Factory, comptime T: type) !*T { + const allocator = self._slab.allocator(); + return try allocator.create(T); +} - // the _proto type - const P = reflect.Struct(std.meta.fieldInfo(S, ._proto).type); - - // the _proto._type type (the parent's _type union) - const U = std.meta.fieldInfo(P, ._type).type; - inline for (@typeInfo(U).@"union".fields) |field| { - if (field.type == S) { - // One of the types in the proto's _type union is this non-pointer - // structure, so it isn't heap allocted. - return false; - } - } - return true; +pub fn create(self: *Factory, value: anytype) !*@TypeOf(value) { + const ptr = try self.createT(@TypeOf(value)); + ptr.* = value; + return ptr; } fn unionInit(comptime T: type, value: anytype) T { @@ -316,15 +354,3 @@ fn unionFieldName(comptime T: type, comptime V: type) []const u8 { } @compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type"); } - -fn fieldIsPointer(comptime T: type, comptime V: type) bool { - inline for (@typeInfo(T).@"union".fields) |field| { - if (field.type == V) { - return false; - } - if (field.type == *V) { - return true; - } - } - @compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type"); -} diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 6b119843..a11b736a 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1175,21 +1175,22 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ else => {}, } - if (namespace == .svg) { - const tag_name = try String.init(self.arena, name, .{}); - if (std.ascii.eqlIgnoreCase(name, "svg")) { - return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{ - ._proto = undefined, - ._type = .svg, - ._tag_name = tag_name, - }); - } + // TODO: uncomment + // if (namespace == .svg) { + // const tag_name = try String.init(self.arena, name, .{}); + // if (std.ascii.eqlIgnoreCase(name, "svg")) { + // return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{ + // ._proto = undefined, + // ._type = .svg, + // ._tag_name = tag_name, + // }); + // } - // Other SVG elements (rect, circle, text, g, etc.) - const lower = std.ascii.lowerString(&self.buf, name); - const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown; - return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag }); - } + // // Other SVG elements (rect, circle, text, g, etc.) + // const lower = std.ascii.lowerString(&self.buf, name); + // const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown; + // return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag }); + // } const tag_name = try String.init(self.arena, name, .{}); @@ -1221,7 +1222,6 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ return node; }; - // After constructor runs, invoke attributeChangedCallback for initial attributes const element = node.as(Element); if (element._attributes) |attributes| { diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 4468b553..e6d748c8 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -67,36 +67,36 @@ pub fn construct(page: *Page) !*Element { } pub const Type = union(enum) { - anchor: Anchor, - body: Body, - br: BR, - button: Button, + anchor: *Anchor, + body: *Body, + br: *BR, + button: *Button, custom: *Custom, - dialog: Dialog, - div: Div, - form: Form, + dialog: *Dialog, + div: *Div, + form: *Form, generic: *Generic, heading: *Heading, - head: Head, - html: Html, - hr: HR, - img: Image, - iframe: IFrame, + head: *Head, + html: *Html, + hr: *HR, + img: *Image, + iframe: *IFrame, input: *Input, - li: LI, - link: Link, - meta: Meta, - ol: OL, + li: *LI, + link: *Link, + meta: *Meta, + ol: *OL, option: *Option, - p: Paragraph, + p: *Paragraph, script: *Script, - select: Select, - slot: Slot, - style: Style, + select: *Select, + slot: *Slot, + style: *Style, template: *Template, text_area: *TextArea, - title: Title, - ul: UL, + title: *Title, + ul: *UL, unknown: *Unknown, }; diff --git a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig index c5568a9a..4bc16b23 100644 --- a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig +++ b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig @@ -35,7 +35,7 @@ _on_progress: ?js.Function = null, _on_timeout: ?js.Function = null, pub const Type = union(enum) { - request: @import("XMLHttpRequest.zig"), + request: *@import("XMLHttpRequest.zig"), // TODO: xml_http_request_upload }; From 8348f2dcc84ffda4c544418fd1ea23acfeb65529 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 09:45:56 -0800 Subject: [PATCH 2/8] fix slot alignment in slab chunks --- src/slab.zig | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/slab.zig b/src/slab.zig index 02d10aa7..0e574fef 100644 --- a/src/slab.zig +++ b/src/slab.zig @@ -376,23 +376,25 @@ pub const SlabAllocator = struct { const self: *Self = @ptrCast(@alignCast(ctx)); _ = ret_addr; + const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits()); + const list_gop = self.slabs.getOrPut( self.child_allocator, - SlabKey{ .size = len, .alignment = alignment }, + SlabKey{ .size = aligned_len, .alignment = alignment }, ) catch return null; if (!list_gop.found_existing) { list_gop.value_ptr.* = Slab.init( self.child_allocator, alignment, - len, + aligned_len, self.max_slot_count, ) catch return null; } const list = list_gop.value_ptr; const buf = list.alloc(self.child_allocator) catch return null; - return buf.ptr; + return buf[0..len].ptr; } fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void { @@ -401,8 +403,9 @@ pub const SlabAllocator = struct { const ptr = memory.ptr; const len = memory.len; + const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits()); - const list = self.slabs.getPtr(.{ .size = len, .alignment = alignment }).?; + const list = self.slabs.getPtr(.{ .size = aligned_len, .alignment = alignment }).?; list.free(ptr); } }; @@ -822,3 +825,39 @@ test "slab allocator - different size classes don't interfere" { allocator.free(ptr_128); allocator.free(ptr_64_again); } + +test "slab allocator - 16-byte alignment" { + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); + defer slab_alloc.deinit(); + + const allocator = slab_alloc.allocator(); + + // Request 16-byte aligned memory + const ptr = try allocator.alignedAlloc(u8, .@"16", 152); + defer allocator.free(ptr); + + // Verify alignment + const addr = @intFromPtr(ptr.ptr); + try testing.expect(addr % 16 == 0); + + // Make sure we can use it + @memset(ptr, 0xFF); +} + +test "slab allocator - various alignments" { + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); + defer slab_alloc.deinit(); + + const allocator = slab_alloc.allocator(); + + const alignments = [_]std.mem.Alignment{ .@"1", .@"2", .@"4", .@"8", .@"16" }; + + inline for (alignments) |alignment| { + const ptr = try allocator.alignedAlloc(u8, alignment, 100); + defer allocator.free(ptr); + + const addr = @intFromPtr(ptr.ptr); + const align_value = alignment.toByteUnits(); + try testing.expect(addr % align_value == 0); + } +} From afe9ee5367a47e62b1f0e2a97a009bf50de89327 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 10:07:40 -0800 Subject: [PATCH 3/8] fix freeing with new combined chains --- src/browser/Factory.zig | 105 ++++++++++++++++------------- src/browser/Page.zig | 4 +- src/browser/webapi/Blob.zig | 6 ++ src/browser/webapi/Event.zig | 4 ++ src/browser/webapi/EventTarget.zig | 2 + 5 files changed, 72 insertions(+), 49 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 26b30052..4a7333f2 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -40,6 +40,13 @@ const Factory = @This(); _page: *Page, _slab: SlabAllocator, +pub const FactoryAllocationKind = union(enum) { + /// Allocated as part of a Factory PrototypeChain + chain: []u8, + /// Allocated standalone via factory.create() + standalone, +}; + fn PrototypeChain(comptime types: []const type) type { return struct { const Self = @This(); @@ -96,7 +103,7 @@ fn PrototypeChain(comptime types: []const type) type { fn setRoot(self: *const Self, comptime T: type) void { const ptr = self.get(0); - ptr.* = .{ ._type = unionInit(T, self.get(1)) }; + ptr.* = .{ ._type = unionInit(T, self.get(1)), ._allocation = FactoryAllocationKind{ .chain = self.memory } }; } fn setMiddle(self: *const Self, comptime index: usize, comptime T: type) void { @@ -163,6 +170,46 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { return chain.get(1); } +// this is a root object +pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { + const allocator = self._slab.allocator(); + + // Special case: Event has a _type_string field, so we need manual setup + const chain = try PrototypeChain( + &.{ Event, @TypeOf(child) }, + ).allocate(allocator); + + const event_ptr = chain.get(0); + event_ptr.* = .{ + ._type = unionInit(Event.Type, chain.get(1)), + ._type_string = try String.init(self._page.arena, typ, .{}), + ._allocation = FactoryAllocationKind{ .chain = chain.memory }, + }; + chain.setLeaf(1, child); + + return chain.get(1); +} + +pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { + const allocator = self._slab.allocator(); + + // Special case: Blob has slice and mime fields, so we need manual setup + const chain = try PrototypeChain( + &.{ Blob, @TypeOf(child) }, + ).allocate(allocator); + + const blob_ptr = chain.get(0); + blob_ptr.* = .{ + ._type = unionInit(Blob.Type, chain.get(1)), + ._allocation = FactoryAllocationKind{ .chain = chain.memory }, + .slice = "", + .mime = "", + }; + chain.setLeaf(1, child); + + return chain.get(1); +} + pub fn node(self: *Factory, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); return try AutoPrototypeChain( @@ -223,25 +270,6 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO return chain.get(4); } -// this is a root object -pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { - const allocator = self._slab.allocator(); - - // Special case: Event has a _type_string field, so we need manual setup - const chain = try PrototypeChain( - &.{ Event, @TypeOf(child) }, - ).allocate(allocator); - - const event_ptr = chain.get(0); - event_ptr.* = .{ - ._type = unionInit(Event.Type, chain.get(1)), - ._type_string = try String.init(self._page.arena, typ, .{}), - }; - chain.setLeaf(1, child); - - return chain.get(1); -} - pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); @@ -250,28 +278,9 @@ pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { ).create(allocator, child); } -pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { - const allocator = self._slab.allocator(); - - // Special case: Blob has slice and mime fields, so we need manual setup - const chain = try PrototypeChain( - &.{ Blob, @TypeOf(child) }, - ).allocate(allocator); - - const blob_ptr = chain.get(0); - blob_ptr.* = .{ - ._type = unionInit(Blob.Type, chain.get(1)), - .slice = "", - .mime = "", - }; - chain.setLeaf(1, child); - - return chain.get(1); -} - pub fn destroy(self: *Factory, value: anytype) void { const S = reflect.Struct(@TypeOf(value)); - // const allocator = self._slab.allocator(); + const allocator = self._slab.allocator(); if (comptime IS_DEBUG) { // We should always destroy from the leaf down. @@ -286,12 +295,14 @@ pub fn destroy(self: *Factory, value: anytype) void { } } - const root_ptr = self.destroyChain(value, true); - _ = root_ptr; - // allocator.destroy(root_ptr); + const allocation_kind = self.destroyChain(value, true) orelse return; + switch (allocation_kind) { + .chain => |buf| allocator.free(buf), + .standalone => {}, + } } -fn destroyChain(self: *Factory, value: anytype, comptime first: bool) *@TypeOf(value) { +fn destroyChain(self: *Factory, value: anytype, comptime first: bool) ?FactoryAllocationKind { const S = reflect.Struct(@TypeOf(value)); const allocator = self._slab.allocator(); @@ -317,9 +328,9 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) *@TypeOf(v if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| { allocator.destroy(tagged); } - } - - return @ptrCast(value); + } else if (@hasField(S, "_allocation")) { + return value._allocation; + } else return null; } pub fn createT(self: *Factory, comptime T: type) !*T { diff --git a/src/browser/Page.zig b/src/browser/Page.zig index a11b736a..37bb6d1a 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -176,8 +176,8 @@ pub fn deinit(self: *Page) void { log.debug(.page, "page.deinit", .{ .url = self.url }); // Uncomment if you want slab statistics to print. - // const stats = self._factory._slab.getStats(self.arena) catch unreachable; - // stats.print() catch unreachable; + const stats = self._factory._slab.getStats(self.arena) catch unreachable; + stats.print() catch unreachable; } self.js.deinit(); diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index 9abe6f29..14428085 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -21,12 +21,15 @@ const Writer = std.Io.Writer; const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; /// https://w3c.github.io/FileAPI/#blob-section /// https://developer.mozilla.org/en-US/docs/Web/API/Blob const Blob = @This(); _type: Type, +_allocation: FactoryAllocationKind, + /// Immutable slice of blob. /// Note that another blob may hold a pointer/slice to this, /// so its better to leave the deallocation of it to arena allocator. @@ -78,6 +81,7 @@ pub fn init( return page._factory.create(Blob{ ._type = .generic, + ._allocation = .standalone, .slice = slice, .mime = mime, }); @@ -267,6 +271,7 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, + ._allocation = .standalone, .slice = slice[start..end], .mime = mime, }); @@ -274,6 +279,7 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, + ._allocation = .standalone, .slice = slice, .mime = mime, }); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 70de6e07..c994004f 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -20,12 +20,15 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; const EventTarget = @import("EventTarget.zig"); const String = @import("../../string.zig").String; pub const Event = @This(); _type: Type, +_allocation: FactoryAllocationKind, + _bubbles: bool = false, _cancelable: bool = false, _type_string: String, @@ -65,6 +68,7 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event { return page._factory.create(Event{ ._type = .generic, + ._allocation = .standalone, ._bubbles = opts.bubbles, ._time_stamp = time_stamp, ._cancelable = opts.cancelable, diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 23ecdf98..fd2cefe7 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -21,12 +21,14 @@ const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const RegisterOptions = @import("../EventManager.zig").RegisterOptions; +const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; const Event = @import("Event.zig"); const EventTarget = @This(); _type: Type, +_allocation: FactoryAllocationKind, pub const Type = union(enum) { node: *@import("Node.zig"), From 2ddaa351abbd4f844a9c13a6f0b5e078a3b6475a Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 10:20:27 -0800 Subject: [PATCH 4/8] use stream for logging stats --- src/browser/Page.zig | 6 ++++-- src/slab.zig | 40 ++++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 37bb6d1a..4cc4e3e1 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -176,8 +176,10 @@ pub fn deinit(self: *Page) void { log.debug(.page, "page.deinit", .{ .url = self.url }); // Uncomment if you want slab statistics to print. - const stats = self._factory._slab.getStats(self.arena) catch unreachable; - stats.print() catch unreachable; + // const stats = self._factory._slab.getStats(self.arena) catch unreachable; + // var buffer: [256]u8 = undefined; + // var stream = std.fs.File.stderr().writer(&buffer).interface; + // stats.print(&stream) catch unreachable; } self.js.deinit(); diff --git a/src/slab.zig b/src/slab.zig index 0e574fef..dab2a0ef 100644 --- a/src/slab.zig +++ b/src/slab.zig @@ -258,47 +258,47 @@ pub const SlabAllocator = struct { utilization_ratio: f64, slabs: []const Slab.Stats, - pub fn print(self: *const Stats) !void { - std.debug.print("\n", .{}); - std.debug.print("\n=== Slab Allocator Statistics ===\n", .{}); - std.debug.print("Overall Memory:\n", .{}); - std.debug.print(" Total allocated: {} bytes ({d:.2} MB)\n", .{ + pub fn print(self: *const Stats, stream: *std.io.Writer) !void { + try stream.print("\n", .{}); + try stream.print("\n=== Slab Allocator Statistics ===\n", .{}); + try stream.print("Overall Memory:\n", .{}); + try stream.print(" Total allocated: {} bytes ({d:.2} MB)\n", .{ self.total_allocated_bytes, @as(f64, @floatFromInt(self.total_allocated_bytes)) / 1_048_576.0, }); - std.debug.print(" In use: {} bytes ({d:.2} MB)\n", .{ + try stream.print(" In use: {} bytes ({d:.2} MB)\n", .{ self.bytes_in_use, @as(f64, @floatFromInt(self.bytes_in_use)) / 1_048_576.0, }); - std.debug.print(" Free: {} bytes ({d:.2} MB)\n", .{ + try stream.print(" Free: {} bytes ({d:.2} MB)\n", .{ self.bytes_free, @as(f64, @floatFromInt(self.bytes_free)) / 1_048_576.0, }); - std.debug.print("\nOverall Structure:\n", .{}); - std.debug.print(" Slab Count: {}\n", .{self.slab_count}); - std.debug.print(" Total chunks: {}\n", .{self.total_chunks}); - std.debug.print(" Total slots: {}\n", .{self.total_slots}); - std.debug.print(" Slots in use: {}\n", .{self.slots_in_use}); - std.debug.print(" Slots free: {}\n", .{self.slots_free}); + try stream.print("\nOverall Structure:\n", .{}); + try stream.print(" Slab Count: {}\n", .{self.slab_count}); + try stream.print(" Total chunks: {}\n", .{self.total_chunks}); + try stream.print(" Total slots: {}\n", .{self.total_slots}); + try stream.print(" Slots in use: {}\n", .{self.slots_in_use}); + try stream.print(" Slots free: {}\n", .{self.slots_free}); - std.debug.print("\nOverall Efficiency:\n", .{}); - std.debug.print(" Utilization: {d:.1}%\n", .{self.utilization_ratio * 100.0}); - std.debug.print(" Fragmentation: {d:.1}%\n", .{self.fragmentation_ratio * 100.0}); + try stream.print("\nOverall Efficiency:\n", .{}); + try stream.print(" Utilization: {d:.1}%\n", .{self.utilization_ratio * 100.0}); + try stream.print(" Fragmentation: {d:.1}%\n", .{self.fragmentation_ratio * 100.0}); if (self.slabs.len > 0) { - std.debug.print("\nPer-Slab Breakdown:\n", .{}); - std.debug.print( + try stream.print("\nPer-Slab Breakdown:\n", .{}); + try stream.print( " {s:>5} | {s:>4} | {s:>6} | {s:>6} | {s:>6} | {s:>10} | {s:>6}\n", .{ "Size", "Algn", "Chunks", "Slots", "InUse", "Bytes", "Util%" }, ); - std.debug.print( + try stream.print( " {s:-<5}-+-{s:-<4}-+-{s:-<6}-+-{s:-<6}-+-{s:-<6}-+-{s:-<10}-+-{s:-<6}\n", .{ "", "", "", "", "", "", "" }, ); for (self.slabs) |slab| { - std.debug.print(" {d:5} | {d:4} | {d:6} | {d:6} | {d:6} | {d:10} | {d:5.1}%\n", .{ + try stream.print(" {d:5} | {d:4} | {d:6} | {d:6} | {d:6} | {d:10} | {d:5.1}%\n", .{ slab.key.size, @intFromEnum(slab.key.alignment), slab.chunk_count, From 45c7184fdeeae41a5d754394a293849c1aaf895f Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 11:14:30 -0800 Subject: [PATCH 5/8] use nullable slice for tracking chain allocations --- src/browser/Factory.zig | 28 +++++++++++----------------- src/browser/webapi/Blob.zig | 9 ++++----- src/browser/webapi/Event.zig | 5 ++--- src/browser/webapi/EventTarget.zig | 5 ++--- 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 4a7333f2..f986c895 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -40,13 +40,6 @@ const Factory = @This(); _page: *Page, _slab: SlabAllocator, -pub const FactoryAllocationKind = union(enum) { - /// Allocated as part of a Factory PrototypeChain - chain: []u8, - /// Allocated standalone via factory.create() - standalone, -}; - fn PrototypeChain(comptime types: []const type) type { return struct { const Self = @This(); @@ -103,7 +96,7 @@ fn PrototypeChain(comptime types: []const type) type { fn setRoot(self: *const Self, comptime T: type) void { const ptr = self.get(0); - ptr.* = .{ ._type = unionInit(T, self.get(1)), ._allocation = FactoryAllocationKind{ .chain = self.memory } }; + ptr.* = .{ ._type = unionInit(T, self.get(1)), ._allocation = self.memory }; } fn setMiddle(self: *const Self, comptime index: usize, comptime T: type) void { @@ -164,7 +157,11 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { &.{ EventTarget, @TypeOf(child) }, ).allocate(allocator); - chain.setRoot(EventTarget.Type); + const event_ptr = chain.get(0); + event_ptr.* = .{ + ._type = unionInit(EventTarget.Type, chain.get(1)), + ._allocation = chain.memory, + }; chain.setLeaf(1, child); return chain.get(1); @@ -183,7 +180,7 @@ pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { event_ptr.* = .{ ._type = unionInit(Event.Type, chain.get(1)), ._type_string = try String.init(self._page.arena, typ, .{}), - ._allocation = FactoryAllocationKind{ .chain = chain.memory }, + ._allocation = chain.memory, }; chain.setLeaf(1, child); @@ -201,7 +198,7 @@ pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { const blob_ptr = chain.get(0); blob_ptr.* = .{ ._type = unionInit(Blob.Type, chain.get(1)), - ._allocation = FactoryAllocationKind{ .chain = chain.memory }, + ._allocation = chain.memory, .slice = "", .mime = "", }; @@ -295,14 +292,11 @@ pub fn destroy(self: *Factory, value: anytype) void { } } - const allocation_kind = self.destroyChain(value, true) orelse return; - switch (allocation_kind) { - .chain => |buf| allocator.free(buf), - .standalone => {}, - } + const chain_memory = self.destroyChain(value, true) orelse return; + allocator.free(chain_memory); } -fn destroyChain(self: *Factory, value: anytype, comptime first: bool) ?FactoryAllocationKind { +fn destroyChain(self: *Factory, value: anytype, comptime first: bool) ?[]u8 { const S = reflect.Struct(@TypeOf(value)); const allocator = self._slab.allocator(); diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index 14428085..2b134e3f 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -21,14 +21,13 @@ const Writer = std.Io.Writer; const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); -const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; /// https://w3c.github.io/FileAPI/#blob-section /// https://developer.mozilla.org/en-US/docs/Web/API/Blob const Blob = @This(); _type: Type, -_allocation: FactoryAllocationKind, +_allocation: ?[]u8, /// Immutable slice of blob. /// Note that another blob may hold a pointer/slice to this, @@ -81,7 +80,7 @@ pub fn init( return page._factory.create(Blob{ ._type = .generic, - ._allocation = .standalone, + ._allocation = null, .slice = slice, .mime = mime, }); @@ -271,7 +270,7 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, - ._allocation = .standalone, + ._allocation = null, .slice = slice[start..end], .mime = mime, }); @@ -279,7 +278,7 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, - ._allocation = .standalone, + ._allocation = null, .slice = slice, .mime = mime, }); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index c994004f..21c4f83b 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -20,14 +20,13 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); -const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; const EventTarget = @import("EventTarget.zig"); const String = @import("../../string.zig").String; pub const Event = @This(); _type: Type, -_allocation: FactoryAllocationKind, +_allocation: ?[]u8, _bubbles: bool = false, _cancelable: bool = false, @@ -68,7 +67,7 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event { return page._factory.create(Event{ ._type = .generic, - ._allocation = .standalone, + ._allocation = null, ._bubbles = opts.bubbles, ._time_stamp = time_stamp, ._cancelable = opts.cancelable, diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index fd2cefe7..b9e584e1 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -21,14 +21,13 @@ const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const RegisterOptions = @import("../EventManager.zig").RegisterOptions; -const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; const Event = @import("Event.zig"); const EventTarget = @This(); _type: Type, -_allocation: FactoryAllocationKind, +_allocation: ?[]u8, pub const Type = union(enum) { node: *@import("Node.zig"), @@ -124,7 +123,7 @@ pub const JsApi = struct { const testing = @import("../../testing.zig"); test "WebApi: EventTarget" { // we create thousands of these per page. Nothing should bloat it. - try testing.expectEqual(16, @sizeOf(EventTarget)); + try testing.expectEqual(32, @sizeOf(EventTarget)); try testing.htmlRunner("events.html", .{}); } From 15dff342a6088614e5b8d1f5242443688a75f783 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 12:07:59 -0800 Subject: [PATCH 6/8] shrink EventTarget back to 16 --- src/browser/Factory.zig | 87 +++++++++++++++++++++++++----- src/browser/webapi/Blob.zig | 5 +- src/browser/webapi/Event.zig | 3 +- src/browser/webapi/EventTarget.zig | 4 +- 4 files changed, 79 insertions(+), 20 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index f986c895..8a0893e1 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -96,7 +96,7 @@ fn PrototypeChain(comptime types: []const type) type { fn setRoot(self: *const Self, comptime T: type) void { const ptr = self.get(0); - ptr.* = .{ ._type = unionInit(T, self.get(1)), ._allocation = self.memory }; + ptr.* = .{ ._type = unionInit(T, self.get(1)) }; } fn setMiddle(self: *const Self, comptime index: usize, comptime T: type) void { @@ -160,7 +160,6 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { const event_ptr = chain.get(0); event_ptr.* = .{ ._type = unionInit(EventTarget.Type, chain.get(1)), - ._allocation = chain.memory, }; chain.setLeaf(1, child); @@ -180,7 +179,6 @@ pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { event_ptr.* = .{ ._type = unionInit(Event.Type, chain.get(1)), ._type_string = try String.init(self._page.arena, typ, .{}), - ._allocation = chain.memory, }; chain.setLeaf(1, child); @@ -198,7 +196,6 @@ pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { const blob_ptr = chain.get(0); blob_ptr.* = .{ ._type = unionInit(Blob.Type, chain.get(1)), - ._allocation = chain.memory, .slice = "", .mime = "", }; @@ -275,9 +272,34 @@ pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { ).create(allocator, child); } +fn hasChainRoot(comptime T: type) bool { + // Check if this is a root + if (@hasDecl(T, "_prototype_root")) { + return true; + } + + // If no _proto field, we're at the top but not a recognized root + if (!@hasField(T, "_proto")) return false; + + // Get the _proto field's type and recurse + const fields = @typeInfo(T).@"struct".fields; + inline for (fields) |field| { + if (std.mem.eql(u8, field.name, "_proto")) { + const ProtoType = reflect.Struct(field.type); + return hasChainRoot(ProtoType); + } + } + + return false; +} + +fn isChainType(comptime T: type) bool { + if (@hasField(T, "_proto")) return false; + return comptime hasChainRoot(T); +} + pub fn destroy(self: *Factory, value: anytype) void { const S = reflect.Struct(@TypeOf(value)); - const allocator = self._slab.allocator(); if (comptime IS_DEBUG) { // We should always destroy from the leaf down. @@ -292,14 +314,48 @@ pub fn destroy(self: *Factory, value: anytype) void { } } - const chain_memory = self.destroyChain(value, true) orelse return; - allocator.free(chain_memory); + if (comptime isChainType(S)) { + self.destroyChain(value, true, 0, std.mem.Alignment.@"1"); + } else { + self.destroyStandalone(value); + } } -fn destroyChain(self: *Factory, value: anytype, comptime first: bool) ?[]u8 { +pub fn destroyStandalone(self: *Factory, value: anytype) void { + const S = reflect.Struct(@TypeOf(value)); + assert(!@hasDecl(S, "_prototype_root")); + + const allocator = self._slab.allocator(); + + if (@hasDecl(S, "deinit")) { + // And it has a deinit, we'll call it + switch (@typeInfo(@TypeOf(S.deinit)).@"fn".params.len) { + 1 => value.deinit(), + 2 => value.deinit(self._page), + else => @compileLog(@typeName(S) ++ " has an invalid deinit function"), + } + } + + allocator.destroy(value); +} + +fn destroyChain( + self: *Factory, + value: anytype, + comptime first: bool, + old_size: usize, + old_align: std.mem.Alignment, +) void { const S = reflect.Struct(@TypeOf(value)); const allocator = self._slab.allocator(); + // aligns the old size to the alignment of this element + const current_size = std.mem.alignForward(usize, old_size, @alignOf(S)); + const alignment = std.mem.Alignment.fromByteUnits(@alignOf(S)); + + const new_align = std.mem.Alignment.max(old_align, alignment); + const new_size = current_size + @sizeOf(S); + // This is initially called from a deinit. We don't want to call that // same deinit. So when this is the first time destroyChain is called // we don't call deinit (because we're in that deinit) @@ -316,15 +372,22 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) ?[]u8 { } if (@hasField(S, "_proto")) { - return self.destroyChain(value._proto, false); + self.destroyChain(value._proto, false, new_size, new_align); } else if (@hasDecl(S, "JsApi")) { // Doesn't have a _proto, but has a JsApi. if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| { allocator.destroy(tagged); } - } else if (@hasField(S, "_allocation")) { - return value._allocation; - } else return null; + } else { + // no proto so this is the head of the chain. + // we use this as the ptr to the start of the chain. + // and we have summed up the length. + assert(@hasDecl(S, "_prototype_root")); + + const memory_ptr: [*]const u8 = @ptrCast(value); + const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits()); + allocator.free(memory_ptr[0..len]); + } } pub fn createT(self: *Factory, comptime T: type) !*T { diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index 2b134e3f..a60f4b42 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -26,8 +26,8 @@ const Page = @import("../Page.zig"); /// https://developer.mozilla.org/en-US/docs/Web/API/Blob const Blob = @This(); +const _prototype_root = true; _type: Type, -_allocation: ?[]u8, /// Immutable slice of blob. /// Note that another blob may hold a pointer/slice to this, @@ -80,7 +80,6 @@ pub fn init( return page._factory.create(Blob{ ._type = .generic, - ._allocation = null, .slice = slice, .mime = mime, }); @@ -270,7 +269,6 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, - ._allocation = null, .slice = slice[start..end], .mime = mime, }); @@ -278,7 +276,6 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, - ._allocation = null, .slice = slice, .mime = mime, }); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 21c4f83b..b02357ba 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -25,8 +25,8 @@ const String = @import("../../string.zig").String; pub const Event = @This(); +const _prototype_root = true; _type: Type, -_allocation: ?[]u8, _bubbles: bool = false, _cancelable: bool = false, @@ -67,7 +67,6 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event { return page._factory.create(Event{ ._type = .generic, - ._allocation = null, ._bubbles = opts.bubbles, ._time_stamp = time_stamp, ._cancelable = opts.cancelable, diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index b9e584e1..4e5ab768 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -26,8 +26,8 @@ const Event = @import("Event.zig"); const EventTarget = @This(); +const _prototype_root = true; _type: Type, -_allocation: ?[]u8, pub const Type = union(enum) { node: *@import("Node.zig"), @@ -123,7 +123,7 @@ pub const JsApi = struct { const testing = @import("../../testing.zig"); test "WebApi: EventTarget" { // we create thousands of these per page. Nothing should bloat it. - try testing.expectEqual(32, @sizeOf(EventTarget)); + try testing.expectEqual(16, @sizeOf(EventTarget)); try testing.htmlRunner("events.html", .{}); } From 8ce8c7a0f35b20af5b022a0aa255e75186566e1d Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 27 Nov 2025 12:55:48 -0800 Subject: [PATCH 7/8] use _prototype_root decl everywhere --- src/browser/Factory.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 8a0893e1..a915d74a 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -303,7 +303,7 @@ pub fn destroy(self: *Factory, value: anytype) void { if (comptime IS_DEBUG) { // We should always destroy from the leaf down. - if (@hasField(S, "_type") and @typeInfo(@TypeOf(value._type)) == .@"union") { + if (@hasDecl(S, "_prototype_root")) { // A Event{._type == .generic} (or any other similar types) // _should_ be destoyed directly. The _type = .generic is a pseudo // child From 34c10e1e4889aae5511ea677e9d9ff557f9e8097 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 27 Nov 2025 13:10:35 -0800 Subject: [PATCH 8/8] fix svgElement + allow base tags --- src/browser/Factory.zig | 11 ++++++++--- src/browser/Page.zig | 29 ++++++++++++++--------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index a915d74a..2a4a0627 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -241,18 +241,23 @@ pub fn htmlElement(self: *Factory, child: anytype) !*@TypeOf(child) { pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); + const ChildT = @TypeOf(child); - // will never allocate, can't fail - const tag_name_str = String.init(self._page.arena, tag_name, .{}) catch unreachable; + if (ChildT == Element.Svg) { + return self.element(child); + } const chain = try PrototypeChain( - &.{ EventTarget, Node, Element, Element.Svg, @TypeOf(child) }, + &.{ EventTarget, Node, Element, Element.Svg, ChildT }, ).allocate(allocator); chain.setRoot(EventTarget.Type); chain.setMiddle(1, Node.Type); chain.setMiddle(2, Element.Type); + // will never allocate, can't fail + const tag_name_str = String.init(self._page.arena, tag_name, .{}) catch unreachable; + // Manually set Element.Svg with the tag_name chain.set(3, .{ ._proto = chain.get(2), diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 4cc4e3e1..4ca3241c 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1177,22 +1177,21 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ else => {}, } - // TODO: uncomment - // if (namespace == .svg) { - // const tag_name = try String.init(self.arena, name, .{}); - // if (std.ascii.eqlIgnoreCase(name, "svg")) { - // return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{ - // ._proto = undefined, - // ._type = .svg, - // ._tag_name = tag_name, - // }); - // } + if (namespace == .svg) { + const tag_name = try String.init(self.arena, name, .{}); + if (std.ascii.eqlIgnoreCase(name, "svg")) { + return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{ + ._proto = undefined, + ._type = .svg, + ._tag_name = tag_name, + }); + } - // // Other SVG elements (rect, circle, text, g, etc.) - // const lower = std.ascii.lowerString(&self.buf, name); - // const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown; - // return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag }); - // } + // Other SVG elements (rect, circle, text, g, etc.) + const lower = std.ascii.lowerString(&self.buf, name); + const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown; + return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag }); + } const tag_name = try String.init(self.arena, name, .{});