Merge pull request #3089 from lightpanda-io/zig-memory-report

mem: add metric for arena allocations
This commit is contained in:
Karl Seguin
2026-07-30 18:12:33 +08:00
committed by GitHub
16 changed files with 313 additions and 16 deletions

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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