All frames must share the same Arena/Factory

This is a bit sad, but at least for now, all frames must share the same
page.arena and page.factory (they still get their own v8::Context and
call_arena).

Consider this case (from WPT /css/cssom-view/scrollingElement.html):

```
    let nonQuirksFrame = document.createElement("iframe");
    nonQuirksFrame.onload = this.step_func_done(function() {
      var nonQuirksDoc = nonQuirksFrame.contentDocument;
      nonQuirksDoc.removeChild(nonQuirksDoc.documentElement);
    });
    nonQuirksFrame.src = URL.createObjectURL(new Blob([`<!doctype html>`], { type:
"text/html" }));
    document.body.append(nonQuirksFrame);
```

We have the root page (p0) and the frame page (p1). When the frame (p1) is
created, it's [currently] given its own arena/factory and parses the page. Those
nodes are created with p1's factory.

The onload callback executes on p0, so when we call removeChild that's executing
with p0's arena/factory and tries to release memory using that factory - which
is NOT the factory that created them.

A better approach might be that _memory_ operations aren't tied to the current
calling context, but rather the owning document's page. But:
1 - That would mean we have 2 execution contexts: the v8 context where the code
    is running, and the memory context that owns the code
2 - Nodes can be disconnected, what page should we use?
3 - Some operations can behave across frames, p0 could adoptNode on p1, so we'd
    have to carefully use p1's factory when cleaning up and re-create the node
    in p0's factory.

So much hassle.

Using a shared factory/arena solves these problems at the cost of bloat - when
a frame goes away or navigates, we can't free its old memory. At some point, we
should fix that. But I don't have a quick and easy solution, and sharing the
arena/factory is _really_ quick and easy.
This commit is contained in:
Karl Seguin
2026-02-24 12:57:05 +08:00
parent 6b4db330d8
commit bb773c6c13
2 changed files with 94 additions and 105 deletions

View File

@@ -42,12 +42,93 @@ const Allocator = std.mem.Allocator;
const IS_DEBUG = builtin.mode == .Debug; const IS_DEBUG = builtin.mode == .Debug;
const assert = std.debug.assert; const assert = std.debug.assert;
// Shared across all frames of a Page.
const Factory = @This(); const Factory = @This();
_page: *Page,
_arena: Allocator, _arena: Allocator,
_slab: SlabAllocator, _slab: SlabAllocator,
pub fn init(arena: Allocator) !*Factory {
const self = try arena.create(Factory);
self.* = .{
._arena = arena,
._slab = SlabAllocator.init(arena, 128),
};
return self;
}
// this is a root object
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
const chain = try PrototypeChain(
&.{ EventTarget, @TypeOf(child) },
).allocate(allocator);
const event_ptr = chain.get(0);
event_ptr.* = .{
._type = unionInit(EventTarget.Type, chain.get(1)),
};
chain.setLeaf(1, child);
return chain.get(1);
}
pub fn standaloneEventTarget(self: *Factory, child: anytype) !*EventTarget {
const allocator = self._slab.allocator();
const et = try allocator.create(EventTarget);
et.* = .{ ._type = unionInit(EventTarget.Type, child) };
return et;
}
// this is a root object
pub fn event(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
const chain = try PrototypeChain(
&.{ Event, @TypeOf(child) },
).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0);
event_ptr.* = try eventInit(arena, typ, chain.get(1));
chain.setLeaf(1, child);
return chain.get(1);
}
pub fn uiEvent(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
const chain = try PrototypeChain(
&.{ Event, UIEvent, @TypeOf(child) },
).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0);
event_ptr.* = try eventInit(arena, typ, chain.get(1));
chain.setMiddle(1, UIEvent.Type);
chain.setLeaf(2, child);
return chain.get(2);
}
pub fn mouseEvent(_: *const Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
const chain = try PrototypeChain(
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0);
event_ptr.* = try eventInit(arena, typ, chain.get(1));
chain.setMiddle(1, UIEvent.Type);
// Set MouseEvent with all its fields
const mouse_ptr = chain.get(2);
mouse_ptr.* = mouse;
mouse_ptr._proto = chain.get(1);
mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3));
chain.setLeaf(3, child);
return chain.get(3);
}
fn PrototypeChain(comptime types: []const type) type { fn PrototypeChain(comptime types: []const type) type {
return struct { return struct {
const Self = @This(); const Self = @This();
@@ -151,86 +232,6 @@ fn AutoPrototypeChain(comptime types: []const type) type {
}; };
} }
pub fn init(arena: Allocator, page: *Page) Factory {
return .{
._page = page,
._arena = arena,
._slab = SlabAllocator.init(arena, 128),
};
}
// this is a root object
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
const chain = try PrototypeChain(
&.{ EventTarget, @TypeOf(child) },
).allocate(allocator);
const event_ptr = chain.get(0);
event_ptr.* = .{
._type = unionInit(EventTarget.Type, chain.get(1)),
};
chain.setLeaf(1, child);
return chain.get(1);
}
pub fn standaloneEventTarget(self: *Factory, child: anytype) !*EventTarget {
const allocator = self._slab.allocator();
const et = try allocator.create(EventTarget);
et.* = .{ ._type = unionInit(EventTarget.Type, child) };
return et;
}
// this is a root object
pub fn event(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
const chain = try PrototypeChain(
&.{ Event, @TypeOf(child) },
).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0);
event_ptr.* = try eventInit(arena, typ, chain.get(1));
chain.setLeaf(1, child);
return chain.get(1);
}
pub fn uiEvent(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
const chain = try PrototypeChain(
&.{ Event, UIEvent, @TypeOf(child) },
).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0);
event_ptr.* = try eventInit(arena, typ, chain.get(1));
chain.setMiddle(1, UIEvent.Type);
chain.setLeaf(2, child);
return chain.get(2);
}
pub fn mouseEvent(_: *const Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
const chain = try PrototypeChain(
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0);
event_ptr.* = try eventInit(arena, typ, chain.get(1));
chain.setMiddle(1, UIEvent.Type);
// Set MouseEvent with all its fields
const mouse_ptr = chain.get(2);
mouse_ptr.* = mouse;
mouse_ptr._proto = chain.get(1);
mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3));
chain.setLeaf(3, child);
return chain.get(3);
}
fn eventInit(arena: Allocator, typ: String, value: anytype) !Event { fn eventInit(arena: Allocator, typ: String, value: anytype) !Event {
// Round to 2ms for privacy (browsers do this) // Round to 2ms for privacy (browsers do this)
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic); const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
@@ -383,7 +384,7 @@ pub fn destroy(self: *Factory, value: anytype) void {
} }
if (comptime @hasField(S, "_proto")) { if (comptime @hasField(S, "_proto")) {
self.destroyChain(value, true, 0, std.mem.Alignment.@"1"); self.destroyChain(value, 0, std.mem.Alignment.@"1");
} else { } else {
self.destroyStandalone(value); self.destroyStandalone(value);
} }
@@ -397,7 +398,6 @@ pub fn destroyStandalone(self: *Factory, value: anytype) void {
fn destroyChain( fn destroyChain(
self: *Factory, self: *Factory,
value: anytype, value: anytype,
comptime first: bool,
old_size: usize, old_size: usize,
old_align: std.mem.Alignment, old_align: std.mem.Alignment,
) void { ) void {
@@ -409,23 +409,8 @@ fn destroyChain(
const new_size = current_size + @sizeOf(S); const new_size = current_size + @sizeOf(S);
const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(S)); const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(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)
if (!comptime first) {
// But if it isn't the first time
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"),
}
}
}
if (@hasField(S, "_proto")) { if (@hasField(S, "_proto")) {
self.destroyChain(value._proto, false, new_size, new_align); self.destroyChain(value._proto, new_size, new_align);
} else { } else {
// no proto so this is the head of the chain. // no proto so this is the head of the chain.
// we use this as the ptr to the start of the chain. // we use this as the ptr to the start of the chain.

View File

@@ -173,7 +173,7 @@ _upgrading_element: ?*Node = null,
_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{}, _undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{},
// for heap allocations and managing WebAPI objects // for heap allocations and managing WebAPI objects
_factory: Factory, _factory: *Factory,
_load_state: LoadState = .waiting, _load_state: LoadState = .waiting,
@@ -247,14 +247,15 @@ pub fn init(self: *Page, id: u32, session: *Session, parent: ?*Page) !void {
} }
const browser = session.browser; const browser = session.browser;
const arena_pool = browser.arena_pool; const arena_pool = browser.arena_pool;
const page_arena = try arena_pool.acquire();
errdefer arena_pool.release(page_arena); const page_arena = if (parent) |p| p.arena else try arena_pool.acquire();
errdefer if (parent == null) arena_pool.release(page_arena);
var factory = if (parent) |p| p._factory else try Factory.init(page_arena);
const call_arena = try arena_pool.acquire(); const call_arena = try arena_pool.acquire();
errdefer arena_pool.release(call_arena); errdefer arena_pool.release(call_arena);
var factory = Factory.init(page_arena, self);
const document = (try factory.document(Node.Document.HTMLDocument{ const document = (try factory.document(Node.Document.HTMLDocument{
._proto = undefined, ._proto = undefined,
})).asDocument(); })).asDocument();
@@ -355,7 +356,10 @@ pub fn deinit(self: *Page) void {
} }
self.arena_pool.release(self.call_arena); self.arena_pool.release(self.call_arena);
self.arena_pool.release(self.arena);
if (self.parent == null) {
self.arena_pool.release(self.arena);
}
} }
pub fn base(self: *const Page) [:0]const u8 { pub fn base(self: *const Page) [:0]const u8 {