mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 14:33:47 +00:00
Any object we return from Zig to V8 becomes a v8::Global that we track in our `ctx.identity_map`. V8 will not free such objects. On the flip side, on its own, our Zig code never knows if the underlying v8::Object of a global can still be used from JS. Imagine an XHR request where we fire the last readyStateChange event..we might think we no longer need that XHR instance, but nothing stops the JavaScript code from holding a reference to it and calling a property on it, e.g. `xhr.status`. What we can do is tell v8 that we're done with the global and register a callback. We make our reference to the global weak. When v8 determines that this object cannot be reached from JavaScript, it _may_ call our registered callback. We can then clean things up on our side and free the global (we actually _have_ to free the global). v8 makes no guarantee that our callback will ever be called, so we need to track these finalizable objects and free them ourselves on context shutdown. Furthermore there appears to be some possible timing issues, especially during context shutdown, so we need to be defensive and make sure we don't double-free (we can use the existing identity_map for this). An type like XMLHttpRequest can be re-used. After a request succeeds or fails, it can be re-opened and a new request sent. So we also need a way to revert a "weak" reference back into a "strong" reference. These are simple v8 calls on the v8::Global, but it highlights how sensitive all this is. We need to mark it as weak when we're 100% sure we're done with it, and we need to switch it to strong under any circumstances where we might need it again on our side. Finally, none of this makes sense if there isn't something to free. Of course, the finalizer lets us release the v8::Global, and we can free the memory for the object itself (i.e. the `*XMLHttpRequest`). This PR also adds an ArenaPool. This allows the XMLHTTPRequest to be self-contained and not need the `page.arena`. On init, the `XMLHTTPRequest` acquires an arena from the pool. On finalization it releases it back to the pool. So we now have: - page.call_arena: short, guaranteed for 1 v8 -> zig -> v8 flow - page.arena long: lives for the duration of the entire page - page.arena_pool: ideally lives for as long as needed by its instance (but no guarantees from v8 about this, or the script might leak a lot of global, so worst case, same as page.arena)
85 lines
2.4 KiB
Zig
85 lines
2.4 KiB
Zig
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
|
//
|
|
// Francis Bouvier <francis@lightpanda.io>
|
|
// Pierre Tachoire <pierre@lightpanda.io>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
const std = @import("std");
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
|
|
const ArenaPool = @This();
|
|
|
|
allocator: Allocator,
|
|
retain_bytes: usize,
|
|
free_list_len: u16 = 0,
|
|
free_list: ?*Entry = null,
|
|
free_list_max: u16,
|
|
entry_pool: std.heap.MemoryPool(Entry),
|
|
|
|
const Entry = struct {
|
|
next: ?*Entry,
|
|
arena: ArenaAllocator,
|
|
};
|
|
|
|
pub fn init(allocator: Allocator) ArenaPool {
|
|
return .{
|
|
.allocator = allocator,
|
|
.free_list_max = 512, // TODO make configurable
|
|
.retain_bytes = 1024 * 16, // TODO make configurable
|
|
.entry_pool = std.heap.MemoryPool(Entry).init(allocator),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *ArenaPool) void {
|
|
var entry = self.free_list;
|
|
while (entry) |e| {
|
|
entry = e.next;
|
|
e.arena.deinit();
|
|
}
|
|
self.entry_pool.deinit();
|
|
}
|
|
|
|
pub fn acquire(self: *ArenaPool) !Allocator {
|
|
if (self.free_list) |entry| {
|
|
self.free_list = entry.next;
|
|
return entry.arena.allocator();
|
|
}
|
|
|
|
const entry = try self.entry_pool.create();
|
|
entry.* = .{
|
|
.next = null,
|
|
.arena = ArenaAllocator.init(self.allocator),
|
|
};
|
|
|
|
return entry.arena.allocator();
|
|
}
|
|
|
|
pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
|
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
|
const entry: *Entry = @fieldParentPtr("arena", arena);
|
|
|
|
if (self.free_list_len == self.free_list_max) {
|
|
arena.deinit();
|
|
self.entry_pool.destroy(entry);
|
|
return;
|
|
}
|
|
|
|
_ = arena.reset(.{ .retain_with_limit = self.retain_bytes });
|
|
entry.next = self.free_list;
|
|
self.free_list = entry;
|
|
}
|