From a06f8f2161f78b0aa05c20d3e7255abc258a2c74 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 30 Jul 2026 17:17:32 +0800 Subject: [PATCH] mem: add metric for arena allocations Report memory held by arenas which are waiting on v8 finalization to v8. This acts as a hint to the v8 GC, so that it knows about external memory which is being held up by it. This is opt-in, code needs to getArena() -> getPinnedArena() and then needs to report() where it makes sense (e.g. no point reporting XHR memory if the Zig- side still needs to instance). --- src/Arena.zig | 90 ++++++++++++++++ src/ArenaPool.zig | 125 +++++++++++++++++++++- src/Metrics.zig | 14 ++- src/browser/Browser.zig | 15 +++ src/browser/Frame.zig | 4 + src/browser/Page.zig | 4 + src/browser/Runner.zig | 1 + src/browser/Session.zig | 5 + src/browser/js/Execution.zig | 4 + src/browser/js/Isolate.zig | 6 ++ src/browser/webapi/Blob.zig | 28 ++++- src/browser/webapi/File.zig | 6 +- src/browser/webapi/net/Fetch.zig | 1 + src/browser/webapi/net/Request.zig | 6 +- src/browser/webapi/net/Response.zig | 15 ++- src/browser/webapi/net/XMLHttpRequest.zig | 5 +- 16 files changed, 313 insertions(+), 16 deletions(-) diff --git a/src/Arena.zig b/src/Arena.zig index 5f57db4c0..c40637eaf 100644 --- a/src/Arena.zig +++ b/src/Arena.zig @@ -23,6 +23,7 @@ // Never copy an Arena by value: the Allocator it hands out points back into it. const std = @import("std"); +const lp = @import("lightpanda"); const builtin = @import("builtin"); const ArenaPool = @import("ArenaPool.zig"); @@ -37,6 +38,12 @@ const IS_DEBUG = builtin.mode == .Debug; // In Debug, don't pool and don't retain capacity. Both can mask UAF. pub const SAFETY = IS_DEBUG == true and builtin.is_test == false; +// Amount of memory the arena hasn't reported to v8 yet. This is only tracked +// when account != null. +pub const Account = struct { + pending: i64 = 0, +}; + _arena: ArenaAllocator, pool: *ArenaPool, bucket: *ArenaPool.Bucket, @@ -44,12 +51,39 @@ bucket: *ArenaPool.Bucket, // Only meaningful while this arena sits in its bucket's free list. next: ?*Arena, +// Bytes _arena holds from the backing allocator, maintained by the vtable +// below. Changes only when the arena grows or frees a node, so this costs +// O(log n) updates over an arena's life, not one per allocation. +bytes: usize, + +// Set only for an arena pinned by a V8 finalizer: see ArenaPool.acquirePinned. +account: ?*Account, + +// `bytes` as of the last report() — what the account has already been told. +reported: usize, + debug: if (IS_DEBUG) []const u8 else void = if (IS_DEBUG) "" else {}, pub fn allocator(self: *Arena) Allocator { return self._arena.allocator(); } +// The allocator _arena grows from. Sits between the arena and the pool's +// allocator purely to keep `bytes` current. +pub fn backing(self: *Arena) Allocator { + return .{ .ptr = self, .vtable = &vtable }; +} + +// Attribute everything this arena is currently holding to V8's external memory +// counter. Only meaningful for a pinned arena, and only correct once nothing +// but the JS wrapper's finalizer is keeping the arena alive — before that a GC +// can't reclaim it and reporting is pure GC pressure. See ArenaPool.acquirePinned. +pub fn report(self: *Arena) void { + const account = self.account orelse return; + account.pending += @as(i64, @intCast(self.bytes)) - @as(i64, @intCast(self.reported)); + self.reported = self.bytes; +} + pub fn release(self: *Arena) void { self.pool.release(self); } @@ -77,3 +111,59 @@ pub fn dupe(self: *Arena, comptime T: type, m: []const T) ![]T { pub fn dupeZ(self: *Arena, comptime T: type, m: []const T) ![:0]T { return self.allocator().dupeZ(T, m); } + +// Arena is being released. Account goes back to 0 (everything is being released) +pub fn unpin(self: *Arena) void { + const account = self.account orelse return; + account.pending -= @intCast(self.reported); + self.reported = 0; + self.account = null; +} + +fn grew(self: *Arena, n: usize) void { + self.bytes += n; + lp.metrics.arena_memory_bytes.incrBy(n); +} + +fn shrank(self: *Arena, n: usize) void { + self.bytes -= n; + lp.metrics.arena_memory_bytes.decrBy(n); +} + +fn resized(self: *Arena, old_len: usize, new_len: usize) void { + if (new_len >= old_len) self.grew(new_len - old_len) else self.shrank(old_len - new_len); +} + +const vtable = Allocator.VTable{ + .alloc = rawAlloc, + .resize = rawResize, + .remap = rawRemap, + .free = rawFree, +}; + +fn rawAlloc(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ra: usize) ?[*]u8 { + const self: *Arena = @ptrCast(@alignCast(ctx)); + const buf = self.pool.allocator.rawAlloc(len, alignment, ra) orelse return null; + self.grew(len); + return buf; +} + +fn rawResize(ctx: *anyopaque, mem: []u8, alignment: std.mem.Alignment, new_len: usize, ra: usize) bool { + const self: *Arena = @ptrCast(@alignCast(ctx)); + if (!self.pool.allocator.rawResize(mem, alignment, new_len, ra)) return false; + self.resized(mem.len, new_len); + return true; +} + +fn rawRemap(ctx: *anyopaque, mem: []u8, alignment: std.mem.Alignment, new_len: usize, ra: usize) ?[*]u8 { + const self: *Arena = @ptrCast(@alignCast(ctx)); + const buf = self.pool.allocator.rawRemap(mem, alignment, new_len, ra) orelse return null; + self.resized(mem.len, new_len); + return buf; +} + +fn rawFree(ctx: *anyopaque, mem: []u8, alignment: std.mem.Alignment, ra: usize) void { + const self: *Arena = @ptrCast(@alignCast(ctx)); + self.pool.allocator.rawFree(mem, alignment, ra); + self.shrank(mem.len); +} diff --git a/src/ArenaPool.zig b/src/ArenaPool.zig index 50ada110d..6adbac564 100644 --- a/src/ArenaPool.zig +++ b/src/ArenaPool.zig @@ -104,6 +104,16 @@ pub fn deinit(self: *ArenaPool) void { // - Pass a BucketSize (.tiny, .small, .medium, .large) for explicit bucket selection // - Pass a usize for automatic bucket selection based on expected size pub fn acquire(self: *ArenaPool, size_or_bucket: anytype, debug: []const u8) !*Arena { + return self._acquire(null, size_or_bucket, debug); +} + +// An arena that, once its owner's last native reference is gone, is kept alive +// by nothing but a V8 finalizer. These are the only arenas wort telling v8 about.// +pub fn acquirePinned(self: *ArenaPool, account: *Arena.Account, size_or_bucket: anytype, debug: []const u8) !*Arena { + return self._acquire(account, size_or_bucket, debug); +} + +fn _acquire(self: *ArenaPool, account: ?*Arena.Account, size_or_bucket: anytype, debug: []const u8) !*Arena { const bucket_size: BucketSize = blk: { const T = @TypeOf(size_or_bucket); if (T == BucketSize or T == @TypeOf(.enum_literal)) { @@ -139,6 +149,7 @@ pub fn acquire(self: *ArenaPool, size_or_bucket: anytype, debug: []const u8) !*A } gop.value_ptr.* += 1; } + entry.account = account; lp.metrics.arena_hit.incr(bucket_size); return entry; } @@ -150,9 +161,15 @@ pub fn acquire(self: *ArenaPool, size_or_bucket: anytype, debug: []const u8) !*A .next = null, .pool = self, .bucket = bucket, + .bytes = 0, + .account = account, + .reported = 0, .debug = if (IS_DEBUG) debug else {}, - ._arena = ArenaAllocator.init(self.allocator), + ._arena = undefined, }; + // Routed through the entry so it sees every node the arena takes and gives + // back. entry is pool-allocated, so this pointer is stable. + entry._arena = ArenaAllocator.init(entry.backing()); if (IS_DEBUG) { const gop = try self._leak_track.getOrPut(self.allocator, debug); @@ -184,6 +201,7 @@ pub fn release(self: *ArenaPool, entry: *Arena) void { } } + entry.unpin(); _ = arena.reset(.{ .retain_with_limit = bucket.retain_bytes }); self.mutex.lockUncancelable(lp.io); @@ -362,3 +380,108 @@ test "ArenaPool: size-based acquire" { try testing.expectEqual(1, pool.medium.free_list_len); try testing.expectEqual(1, pool.large.free_list_len); } + +test "ArenaPool: bytes track the arena's backing allocations" { + var pool = ArenaPool.init(testing.allocator, .{}); + defer pool.deinit(); + + const arena = try pool.acquire(.large, "bytes"); + defer arena.release(); + try testing.expectEqual(0, arena.bytes); + + _ = try arena.alloc(u8, 256 * 1024); + try testing.expect(arena.bytes >= 256 * 1024); + + arena.reset(0); + try testing.expectEqual(0, arena.bytes); +} + +test "ArenaPool: only a pinned arena reports, and only when asked" { + var pool = ArenaPool.init(testing.allocator, .{}); + defer pool.deinit(); + var account: Arena.Account = .{}; + + // An unpinned arena never touches the account. + { + const arena = try pool.acquire(.large, "unpinned"); + defer arena.release(); + _ = try arena.alloc(u8, 256 * 1024); + arena.report(); + try testing.expectEqual(0, account.pending); + } + + const arena = try pool.acquirePinned(&account, .large, "pinned"); + _ = try arena.alloc(u8, 256 * 1024); + + // Growing is not enough: until the owner reports, V8 hears nothing. + try testing.expectEqual(0, account.pending); + + arena.report(); + const reported = account.pending; + try testing.expect(reported >= 256 * 1024); + + // report() is a delta, so repeating it is a no-op. + arena.report(); + try testing.expectEqual(reported, account.pending); + + // Release takes back exactly what was reported. + arena.release(); + try testing.expectEqual(0, account.pending); +} + +test "ArenaPool: release un-reports even when the owner never reported" { + var pool = ArenaPool.init(testing.allocator, .{}); + defer pool.deinit(); + var account: Arena.Account = .{}; + + const arena = try pool.acquirePinned(&account, .large, "unreported"); + _ = try arena.alloc(u8, 256 * 1024); + arena.release(); + try testing.expectEqual(0, account.pending); +} + +test "ArenaPool: a reused arena carries its retained bytes to the new owner" { + var pool = ArenaPool.init(testing.allocator, .{}); + defer pool.deinit(); + var first: Arena.Account = .{}; + var second: Arena.Account = .{}; + + const a = try pool.acquirePinned(&first, .tiny, "first"); + _ = try a.alloc(u8, 256); + a.report(); + try testing.expect(first.pending > 0); + a.release(); + try testing.expectEqual(0, first.pending); + + const retained = a.bytes; + try testing.expect(retained > 0); + + const b = try pool.acquirePinned(&second, .tiny, "second"); + try testing.expectEqual(a, b); + try testing.expectEqual(retained, b.bytes); + + // The new owner inherits the bytes but still has to report them itself. + try testing.expectEqual(0, second.pending); + b.report(); + try testing.expectEqual(@as(i64, @intCast(retained)), second.pending); + + b.release(); + try testing.expectEqual(0, second.pending); +} + +test "ArenaPool: bytes agrees with the arena's own view of its capacity" { + var pool = ArenaPool.init(testing.allocator, .{}); + defer pool.deinit(); + + const arena = try pool.acquire(.large, "capacity"); + defer arena.release(); + + // queryCapacity excludes each node's header, which `bytes` counts, so the + // two bracket each other rather than matching exactly. + for ([_]usize{ 64, 4096, 300 * 1024, 2 * 1024 * 1024 }) |n| { + _ = try arena.alloc(u8, n); + const capacity = arena._arena.queryCapacity(); + try testing.expect(arena.bytes >= capacity); + try testing.expect(arena.bytes - capacity < 1024); // header slop only + } +} diff --git a/src/Metrics.zig b/src/Metrics.zig index 602eb52be..53fbc9eef 100644 --- a/src/Metrics.zig +++ b/src/Metrics.zig @@ -31,6 +31,7 @@ script_errors: Counter = .{}, js_errors: CounterEnum("kind", enum { js_exception, other }) = .{}, arena_hit: CounterEnum("size", @import("ArenaPool.zig").BucketSize) = .{}, arena_miss: CounterEnum("size", @import("ArenaPool.zig").BucketSize) = .{}, +arena_memory_bytes: Gauge = .{}, navigate: CounterEnum("type", @import("telemetry/telemetry.zig").Event.Navigate.Context) = .{}, js_heap_size_bytes: Histogram(&.{ 4 * 1024 * 1024, @@ -86,6 +87,7 @@ const help = .{ .js_errors = "Uncaught JS errors (script exceptions, listener/callback throws, unhandled promise rejections); kind=js_exception is a thrown JS value, other is an internal failure (e.g. compilation error, terminated execution)", .arena_hit = "Arena pool acquisitions served from the free list", .arena_miss = "Arena pool acquisitions that had to allocate a new arena", + .arena_memory_bytes = "Backing memory held by pooled arenas, including capacity retained on the free list", .navigate = "Navigations by initiating frame type", .js_heap_size_bytes = "V8 heap physical size, sampled when a page is closed", .http_requests = "HTTP requests submitted, by dispatch mode (excludes internal requests like robots.txt)", @@ -141,11 +143,19 @@ const Gauge = struct { value: isize = 0, pub fn incr(self: *Gauge) void { - _ = @atomicRmw(isize, &self.value, .Add, 1, .monotonic); + self.incrBy(1); + } + + pub fn incrBy(self: *Gauge, n: usize) void { + _ = @atomicRmw(isize, &self.value, .Add, @intCast(n), .monotonic); } pub fn decr(self: *Gauge) void { - _ = @atomicRmw(isize, &self.value, .Sub, 1, .monotonic); + self.decrBy(1); + } + + pub fn decrBy(self: *Gauge, n: usize) void { + _ = @atomicRmw(isize, &self.value, .Sub, @intCast(n), .monotonic); } fn write(self: *const Gauge, comptime name: []const u8, comptime help_text: []const u8, writer: *std.Io.Writer) !void { diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig index ff11177d8..42a2b20d2 100644 --- a/src/browser/Browser.zig +++ b/src/browser/Browser.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const App = @import("../App.zig"); const CDP = @import("../cdp/CDP.zig"); @@ -49,6 +50,9 @@ http_client: HttpClient, // Shared across pages, survives navigation. See Selector.Cache. selector_cache: Selector.Cache, +// Pinned-arena bytes we haven't told v8 about yet +arena_account: lp.Arena.Account = .{}, + // Permission state set via CDP Browser.grantPermissions / setPermission / // resetPermissions, keyed by permission name (e.g. "geolocation"). Read back // by navigator.permissions.query(). Scoped to the Browser so it persists @@ -193,6 +197,17 @@ pub fn closeSession(self: *Browser) void { session.deinit(); self.session = null; } + self.flushArenaMemory(); +} + +// Tell v8 the net change in pinned native memory. This can start a GC. +pub fn flushArenaMemory(self: *Browser) void { + const delta = self.arena_account.pending; + if (delta == 0) { + return; + } + self.arena_account.pending = 0; + self.env.isolate.adjustAmountOfExternalAllocatedMemory(delta); } pub fn runMicrotasks(self: *Browser) void { diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 71865aec6..d4c3db33e 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -622,6 +622,10 @@ pub fn getArena(self: *Frame, size_or_bucket: anytype, debug: []const u8) !*lp.A return self._session.getArena(size_or_bucket, debug); } +pub fn getPinnedArena(self: *Frame, size_or_bucket: anytype, debug: []const u8) !*lp.Arena { + return self._session.getPinnedArena(size_or_bucket, debug); +} + pub fn isSameOrigin(self: *const Frame, url: [:0]const u8) bool { const current_origin = self.origin orelse return false; diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 647772f27..a0bbb3151 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -242,6 +242,10 @@ pub fn getArena(self: *Page, size_or_bucket: anytype, debug: []const u8) !*lp.Ar return self.session.getArena(size_or_bucket, debug); } +pub fn getPinnedArena(self: *Page, size_or_bucket: anytype, debug: []const u8) !*lp.Arena { + return self.session.getPinnedArena(size_or_bucket, debug); +} + pub fn getOrCreateOrigin(self: *Page, key_: ?[]const u8) !*js.Origin { const session = self.session; const key = key_ orelse { diff --git a/src/browser/Runner.zig b/src/browser/Runner.zig index 10fb9dcea..5c6edd94d 100644 --- a/src/browser/Runner.zig +++ b/src/browser/Runner.zig @@ -205,6 +205,7 @@ fn _tick(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa const session = self.session; const browser = self.browser; const http_client = self.http_client; + defer browser.flushArenaMemory(); // Arms the watchdog (and proves liveness): a stall anywhere in this tick // ages this stamp until the watchdog fires. diff --git a/src/browser/Session.zig b/src/browser/Session.zig index be676f250..9b318e217 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -402,6 +402,11 @@ pub fn getArena(self: *Session, size_or_bucket: anytype, debug: []const u8) !*lp return self.arena_pool.acquire(size_or_bucket, debug); } +// For an arena owned by a JS-exposed object, freed by its finalizer. +pub fn getPinnedArena(self: *Session, size_or_bucket: anytype, debug: []const u8) !*lp.Arena { + return self.arena_pool.acquirePinned(&self.browser.arena_account, size_or_bucket, debug); +} + // The live page for a top-level browsing context, by its root frame id. pub fn livePage(self: *Session, frame_id: u32) ?*Page { for (self.pages.items) |page| { diff --git a/src/browser/js/Execution.zig b/src/browser/js/Execution.zig index 96f641900..bcdfe04a6 100644 --- a/src/browser/js/Execution.zig +++ b/src/browser/js/Execution.zig @@ -80,6 +80,10 @@ pub fn getArena(self: *const Execution, size_or_bucket: anytype, debug: []const return self.page.getArena(size_or_bucket, debug); } +pub fn getPinnedArena(self: *const Execution, size_or_bucket: anytype, debug: []const u8) !*lp.Arena { + return self.page.getPinnedArena(size_or_bucket, debug); +} + pub fn headersForRequest(self: *const Execution, headers: *HttpClient.Headers) !void { return switch (self.js.global) { inline else => |g| g.headersForRequest(headers), diff --git a/src/browser/js/Isolate.zig b/src/browser/js/Isolate.zig index 61cf3cb43..b3ecb9c80 100644 --- a/src/browser/js/Isolate.zig +++ b/src/browser/js/Isolate.zig @@ -51,6 +51,12 @@ pub fn memoryPressureNotification(self: Isolate, level: MemoryPressureLevel) voi v8.v8__Isolate__MemoryPressureNotification(self.handle, @intFromEnum(level)); } +// Tells V8 how much native memory is hanging off objects in this isolate, so +// heap-growth heuristics account for it. May enter GC work. +pub fn adjustAmountOfExternalAllocatedMemory(self: Isolate, delta: i64) void { + _ = v8.v8__Isolate__AdjustAmountOfExternalAllocatedMemory(self.handle, delta); +} + pub fn notifyContextDisposed(self: Isolate) void { _ = v8.v8__Isolate__ContextDisposedNotification(self.handle); } diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index 85bfaa705..4f4494a6c 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -80,11 +80,12 @@ const InitOptions = struct { /// This is the JS Constructor pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, page: *Page) !*Blob { const session = page.session; - const arena = try session.getArena(.large, "Blob"); + const arena = try session.getPinnedArena(.large, "Blob"); errdefer arena.release(); const self = try arena.create(Blob); self.* = try buildValue(arena, parts_, opts_ orelse .{}); + arena.report(); return self; } @@ -129,11 +130,12 @@ pub fn buildValueFromBytes(arena: *lp.Arena, data: []const u8, content_type: []c /// Creates a new Blob from raw byte slices (for internal Zig use). pub fn initFromBytes(data: []const u8, content_type: []const u8, page: *Page) !*Blob { - const arena = try page.getArena(data.len + content_type.len + 256, "Blob"); + const arena = try page.getPinnedArena(data.len + content_type.len + 256, "Blob"); errdefer arena.release(); const self = try arena.create(Blob); self.* = try buildValueFromBytes(arena, data, content_type); + arena.report(); return self; } @@ -158,7 +160,7 @@ pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !*Blob { const mime = try reader.readBytes(); const data = try reader.readBytes(); - const arena = try page.getArena(data.len + mime.len + 256, "Blob.clone"); + const arena = try page.getPinnedArena(data.len + mime.len + 256, "Blob.clone"); errdefer arena.release(); const self = try arena.create(Blob); @@ -170,6 +172,7 @@ pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !*Blob { // the serialized mime is already in normalized form; copy it verbatim ._mime = try arena.dupe(u8, mime), }; + arena.report(); return self; } @@ -372,3 +375,22 @@ const testing = @import("../../testing.zig"); test "WebApi: Blob" { try testing.htmlRunner("blob.html", .{}); } + +test "Blob: a pinned arena reaches the browser's account and is given back" { + const frame = try testing.createFrame(); + defer testing.test_session.closeAllPages(); + + const page = frame._page; + const browser = frame._session.browser; + + browser.flushArenaMemory(); + try testing.expectEqual(0, browser.arena_account.pending); + + const data = [_]u8{'x'} ** (64 * 1024); + const blob = try Blob.initFromBytes(&data, "text/plain", page); + try testing.expect(browser.arena_account.pending >= data.len); + + // The finalizer path hands every reported byte back. + blob.deinit(page); + try testing.expectEqual(0, browser.arena_account.pending); +} diff --git a/src/browser/webapi/File.zig b/src/browser/webapi/File.zig index ff5b4fa50..29d14488f 100644 --- a/src/browser/webapi/File.zig +++ b/src/browser/webapi/File.zig @@ -46,7 +46,7 @@ pub fn init( ) !*File { const opts = opts_ orelse InitOptions{}; const session = page.session; - const arena = try session.getArena(.large, "Blob"); + const arena = try session.getPinnedArena(.large, "Blob"); errdefer arena.release(); const file = try Factory.chainedWithAllocator(arena.allocator(), .{ @@ -62,6 +62,7 @@ pub fn init( }); file._proto._type = .{ .file = file }; + arena.report(); return file; } @@ -89,7 +90,7 @@ pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !*File { const name = try reader.readBytes(); const last_modified = try reader.readUint64(); - const arena = try page.getArena(data.len + mime.len + name.len + 256, "Blob.clone"); + const arena = try page.getPinnedArena(data.len + mime.len + name.len + 256, "Blob.clone"); errdefer arena.release(); const file = try Factory.chainedWithAllocator(arena.allocator(), .{ @@ -108,6 +109,7 @@ pub fn structuredDeserialize(reader: *js.StructuredReader, page: *Page) !*File { }, }); file._proto._type = .{ .file = file }; + arena.report(); return file; } diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index fc16c3882..0664b50ac 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -240,6 +240,7 @@ fn httpDoneCallback(ctx: *anyopaque) !void { const js_val = try ls.local.zigValueToJs(self._response, .{}); self._owns_response = false; + response._arena.report(); return ls.toLocal(self._resolver).resolve("fetch done", js_val); } diff --git a/src/browser/webapi/net/Request.zig b/src/browser/webapi/net/Request.zig index dd923b830..7532f1471 100644 --- a/src/browser/webapi/net/Request.zig +++ b/src/browser/webapi/net/Request.zig @@ -92,7 +92,7 @@ const Cache = enum { }; pub fn init(input: Input, opts_: ?InitOpts, exec: *const Execution) !*Request { - const arena = try exec.getArena(.medium, "Request"); + const arena = try exec.getPinnedArena(.medium, "Request"); errdefer arena.release(); const url = switch (input) { @@ -160,6 +160,7 @@ pub fn init(input: Input, opts_: ?InitOpts, exec: *const Execution) !*Request { ._body = body, ._signal = signal, }; + arena.report(); return self; } @@ -343,7 +344,7 @@ pub fn formData(self: *Request, exec: *const Execution) !js.Promise { } pub fn clone(self: *const Request, exec: *const Execution) !*Request { - const arena = try exec.getArena(if (self._body) |b| b.len else 512, "Request.clone"); + const arena = try exec.getPinnedArena(if (self._body) |b| b.len else 512, "Request.clone"); errdefer arena.release(); const request = try arena.create(Request); @@ -358,6 +359,7 @@ pub fn clone(self: *const Request, exec: *const Execution) !*Request { ._body = if (self._body) |b| try arena.dupe(u8, b) else null, ._signal = self._signal, }; + arena.report(); return request; } diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index 7d6b5b118..4e9e0f0ee 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -75,7 +75,7 @@ pub const BodyInit = body_init.BodyInit; pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Response { const session = exec.session; - const arena = try session.getArena(.large, "Response"); + const arena = try session.getPinnedArena(.large, "Response"); errdefer arena.release(); const opts = opts_ orelse InitOpts{}; @@ -112,12 +112,13 @@ pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Respon ._is_redirected = false, ._headers = headers, }; + arena.report(); return self; } pub fn createError(exec: *const Execution) !*Response { const session = exec.session; - const arena = try session.getArena(.large, "Response.error"); + const arena = try session.getPinnedArena(.large, "Response.error"); errdefer arena.release(); const self = try arena.create(Response); @@ -131,6 +132,7 @@ pub fn createError(exec: *const Execution) !*Response { ._is_redirected = false, ._headers = try Headers.init(null, exec), }; + arena.report(); return self; } @@ -142,7 +144,7 @@ pub fn createRedirect(url_: []const u8, status_: ?u16, exec: *const Execution) ! } const session = exec.session; - const arena = try session.getArena(.large, "Response.redirect"); + const arena = try session.getPinnedArena(.large, "Response.redirect"); errdefer arena.release(); const location = try URL.resolve(arena.allocator(), exec.base(), url_, .{ .encoding = exec.charset.* }); @@ -161,12 +163,13 @@ pub fn createRedirect(url_: []const u8, status_: ?u16, exec: *const Execution) ! ._is_redirected = false, ._headers = headers, }; + arena.report(); return self; } pub fn createJson(data: js.Value, opts_: ?InitOpts, exec: *const Execution) !*Response { const session = exec.session; - const arena = try session.getArena(.medium, "Response.json"); + const arena = try session.getPinnedArena(.medium, "Response.json"); errdefer arena.release(); const json = data.toJson(arena.allocator()) catch |err| switch (err) { @@ -196,6 +199,7 @@ pub fn createJson(data: js.Value, opts_: ?InitOpts, exec: *const Execution) !*Re ._is_redirected = false, ._headers = headers, }; + arena.report(); return self; } @@ -508,7 +512,7 @@ pub fn clone(self: *const Response, exec: *const Execution) !*Response { .empty => 0, .stream => 0, }; - const arena = try session.getArena(body_len + self._url.len + 256, "Response.clone"); + const arena = try session.getPinnedArena(body_len + self._url.len + 256, "Response.clone"); errdefer arena.release(); const body: Body = switch (self._body) { @@ -531,6 +535,7 @@ pub fn clone(self: *const Response, exec: *const Execution) !*Response { ._headers = try Headers.init(.{ .obj = self._headers }, exec), ._http_transfer = null, }; + arena.report(); return cloned; } diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 53a2b9bb4..d3b2555e9 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -108,7 +108,7 @@ const ResponseType = enum { }; pub fn init(exec: *const Execution) !*XMLHttpRequest { - const arena = try exec.getArena(.large, "XMLHttpRequest"); + const arena = try exec.getPinnedArena(.large, "XMLHttpRequest"); errdefer arena.release(); const self = try exec._factory.xhrEventTarget(arena.allocator(), XMLHttpRequest{ ._exec = exec, @@ -141,6 +141,9 @@ fn releaseSelfRef(self: *XMLHttpRequest) void { return; } self._active_requests -= 1; + if (self._active_requests == 0) { + self._arena.report(); + } self.releaseRef(self._exec.page); }