Disable the call arena (for now)

The call arena doesn't consider nested calls (like, from callbacks). Currently
when a "call" ends, the arena is cleared. But in a callback, if we do that,
the memory for the containing code is no longer valid, even though it's still
executing.

For now, use the existing scope_arena, instead of the call_arena. In the future
we need to track the call-depth, and only reset the call_arena when we're done
with a top-level statement.

Also:
-Properly handle callback errors
-Increase wpt file size
-Merge latest loop.zig from zig-js-runtime.
This commit is contained in:
Karl Seguin
2025-04-17 18:34:28 +08:00
parent 4e5fe5ae1a
commit 4e1659b98d
3 changed files with 68 additions and 19 deletions

View File

@@ -873,7 +873,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
self.scope = Scope{ self.scope = Scope{
.handle_scope = handle_scope, .handle_scope = handle_scope,
.arena = self.scope_arena.allocator(), .arena = self.scope_arena.allocator(),
.call_arena = self.call_arena.allocator(), .call_arena = self.scope_arena.allocator(),
}; };
_ = try self._mapZigInstanceToJs(self.context.getGlobal(), global); _ = try self._mapZigInstanceToJs(self.context.getGlobal(), global);
} }
@@ -1128,7 +1128,19 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
inline for (fields, 0..) |f, i| { inline for (fields, 0..) |f, i| {
js_args[i] = try executor.zigValueToJs(@field(aargs, f.name)); js_args[i] = try executor.zigValueToJs(@field(aargs, f.name));
} }
_ = self.func.castToFunction().call(executor.context, js_this, &js_args);
const result = self.func.castToFunction().call(executor.context, js_this, &js_args);
if (result == null) {
return error.JSExecCallback;
}
}
// debug/helper to print the source of the JS callback
fn printFunc(self: *const @This()) !void {
const executor = self.executor;
const value = self.func.castToFunction().toValue();
const src = try valueToString(executor.call_arena.allocator(), value, executor.isolate, executor.context);
std.debug.print("{s}\n", .{src});
} }
}; };
@@ -1405,7 +1417,8 @@ fn Caller(comptime E: type) type {
} }
fn deinit(self: *Self) void { fn deinit(self: *Self) void {
_ = self.executor.call_arena.reset(.{ .retain_with_limit = 4096 }); _ = self;
// _ = self.executor.call_arena.reset(.{ .retain_with_limit = 4096 });
} }
fn constructor(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void { fn constructor(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void {

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const MemoryPool = std.heap.MemoryPool; const MemoryPool = std.heap.MemoryPool;
pub const IO = @import("tigerbeetle-io").IO; pub const IO = @import("tigerbeetle-io").IO;
@@ -35,8 +36,12 @@ const log = std.log.scoped(.loop);
pub const Loop = struct { pub const Loop = struct {
alloc: std.mem.Allocator, // TODO: unmanaged version ? alloc: std.mem.Allocator, // TODO: unmanaged version ?
io: IO, io: IO,
// both events_nb are used to track how many callbacks are to be called.
// We use these counters to wait until all the events are finished.
js_events_nb: usize, js_events_nb: usize,
zig_events_nb: usize, zig_events_nb: usize,
cbk_error: bool = false, cbk_error: bool = false,
// js_ctx_id is incremented each time the loop is reset for JS. // js_ctx_id is incremented each time the loop is reset for JS.
@@ -51,6 +56,11 @@ pub const Loop = struct {
// This is a weak way to cancel all future Zig callbacks. // This is a weak way to cancel all future Zig callbacks.
zig_ctx_id: u32 = 0, zig_ctx_id: u32 = 0,
// The MacOS event loop doesn't support cancellation. We use this to track
// cancellation ids and, on the timeout callback, we can can check here
// to see if it's been cancelled.
cancelled: std.AutoHashMapUnmanaged(usize, void),
cancel_pool: MemoryPool(ContextCancel), cancel_pool: MemoryPool(ContextCancel),
timeout_pool: MemoryPool(ContextTimeout), timeout_pool: MemoryPool(ContextTimeout),
event_callback_pool: MemoryPool(EventCallbackContext), event_callback_pool: MemoryPool(EventCallbackContext),
@@ -65,6 +75,7 @@ pub const Loop = struct {
pub fn init(alloc: std.mem.Allocator) !Self { pub fn init(alloc: std.mem.Allocator) !Self {
return Self{ return Self{
.alloc = alloc, .alloc = alloc,
.cancelled = .{},
.io = try IO.init(32, 0), .io = try IO.init(32, 0),
.js_events_nb = 0, .js_events_nb = 0,
.zig_events_nb = 0, .zig_events_nb = 0,
@@ -75,6 +86,12 @@ pub const Loop = struct {
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
// first disable callbacks for existing events.
// We don't want a callback re-create a setTimeout, it could create an
// infinite loop on wait for events.
self.resetJS();
self.resetZig();
// run tail events. We do run the tail events to ensure all the // run tail events. We do run the tail events to ensure all the
// contexts are correcly free. // contexts are correcly free.
while (self.eventsNb(.js) > 0 or self.eventsNb(.zig) > 0) { while (self.eventsNb(.js) > 0 or self.eventsNb(.zig) > 0) {
@@ -83,11 +100,14 @@ pub const Loop = struct {
break; break;
}; };
} }
self.cancelAll(); if (comptime CANCEL_SUPPORTED) {
self.io.cancel_all();
}
self.io.deinit(); self.io.deinit();
self.cancel_pool.deinit(); self.cancel_pool.deinit();
self.timeout_pool.deinit(); self.timeout_pool.deinit();
self.event_callback_pool.deinit(); self.event_callback_pool.deinit();
self.cancelled.deinit(self.alloc);
} }
// Retrieve all registred I/O events completed by OS kernel, // Retrieve all registred I/O events completed by OS kernel,
@@ -131,9 +151,6 @@ pub const Loop = struct {
fn eventsNb(self: *Self, comptime event: Event) usize { fn eventsNb(self: *Self, comptime event: Event) usize {
return @atomicLoad(usize, self.eventsPtr(event), .seq_cst); return @atomicLoad(usize, self.eventsPtr(event), .seq_cst);
} }
fn resetEvents(self: *Self, comptime event: Event) void {
@atomicStore(usize, self.eventsPtr(event), 0, .unordered);
}
// JS callbacks APIs // JS callbacks APIs
// ----------------- // -----------------
@@ -158,6 +175,12 @@ pub const Loop = struct {
loop.alloc.destroy(completion); loop.alloc.destroy(completion);
} }
if (comptime CANCEL_SUPPORTED == false) {
if (loop.cancelled.remove(@intFromPtr(completion))) {
return;
}
}
// If the loop's context id has changed, don't call the js callback // If the loop's context id has changed, don't call the js callback
// function. The callback's memory has already be cleaned and the // function. The callback's memory has already be cleaned and the
// events nb reset. // events nb reset.
@@ -175,7 +198,7 @@ pub const Loop = struct {
// js callback // js callback
if (ctx.js_cbk) |*js_cbk| { if (ctx.js_cbk) |*js_cbk| {
js_cbk.call(null) catch { js_cbk.call(null) catch {
ctx.loop.cbk_error = true; loop.cbk_error = true;
}; };
} }
} }
@@ -234,19 +257,26 @@ pub const Loop = struct {
// js callback // js callback
if (ctx.js_cbk) |*js_cbk| { if (ctx.js_cbk) |*js_cbk| {
js_cbk.call(null) catch { js_cbk.call(null) catch {
ctx.loop.cbk_error = true; loop.cbk_error = true;
}; };
} }
} }
pub fn cancel(self: *Self, id: usize, js_cbk: ?JSCallback) !void { pub fn cancel(self: *Self, id: usize, js_cbk: ?JSCallback) !void {
if (IO.supports_cancel == false) { const alloc = self.alloc;
if (comptime CANCEL_SUPPORTED == false) {
try self.cancelled.put(alloc, id, {});
if (js_cbk) |cbk| {
cbk.call(null) catch {
self.cbk_error = true;
};
}
return; return;
} }
const comp_cancel: *IO.Completion = @ptrFromInt(id); const comp_cancel: *IO.Completion = @ptrFromInt(id);
const completion = try self.alloc.create(Completion); const completion = try alloc.create(Completion);
errdefer self.alloc.destroy(completion); errdefer alloc.destroy(completion);
completion.* = undefined; completion.* = undefined;
const ctx = self.alloc.create(ContextCancel) catch unreachable; const ctx = self.alloc.create(ContextCancel) catch unreachable;
@@ -260,18 +290,17 @@ pub const Loop = struct {
self.io.cancel_one(*ContextCancel, ctx, cancelCallback, completion, comp_cancel); self.io.cancel_one(*ContextCancel, ctx, cancelCallback, completion, comp_cancel);
} }
fn cancelAll(self: *Self) void {
self.resetEvents(.js);
self.resetEvents(.zig);
self.io.cancel_all();
}
// Reset all existing JS callbacks. // Reset all existing JS callbacks.
// The existing events will happen and their memory will be cleanup but the
// corresponding callbacks will not be called.
pub fn resetJS(self: *Self) void { pub fn resetJS(self: *Self) void {
self.js_ctx_id += 1; self.js_ctx_id += 1;
self.cancelled.clearRetainingCapacity();
} }
// Reset all existing Zig callbacks. // Reset all existing Zig callbacks.
// The existing events will happen and their memory will be cleanup but the
// corresponding callbacks will not be called.
pub fn resetZig(self: *Self) void { pub fn resetZig(self: *Self) void {
self.zig_ctx_id += 1; self.zig_ctx_id += 1;
} }
@@ -365,6 +394,7 @@ pub const Loop = struct {
const ContextZigTimeout = struct { const ContextZigTimeout = struct {
loop: *Self, loop: *Self,
zig_ctx_id: u32, zig_ctx_id: u32,
context: *anyopaque, context: *anyopaque,
callback: *const fn ( callback: *const fn (
context: ?*anyopaque, context: ?*anyopaque,
@@ -431,3 +461,9 @@ const EventCallbackContext = struct {
ctx: *anyopaque, ctx: *anyopaque,
loop: *Loop, loop: *Loop,
}; };
const CANCEL_SUPPORTED = switch (builtin.target.os.tag) {
.linux => true,
.macos, .tvos, .watchos, .ios => false,
else => @compileError("IO is not supported for platform"),
};

View File

@@ -35,7 +35,7 @@ pub fn run(arena: Allocator, comptime dir: []const u8, f: []const u8, loader: *F
const html = blk: { const html = blk: {
const file = try std.fs.cwd().openFile(f, .{}); const file = try std.fs.cwd().openFile(f, .{});
defer file.close(); defer file.close();
break :blk try file.readToEndAlloc(arena, 16 * 1024); break :blk try file.readToEndAlloc(arena, 128 * 1024);
}; };
const dirname = fspath.dirname(f[dir.len..]) orelse unreachable; const dirname = fspath.dirname(f[dir.len..]) orelse unreachable;