mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-30 09:16:07 -04:00
Merge pull request #3089 from lightpanda-io/zig-memory-report
mem: add metric for arena allocations
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user