Skip to content

Commit 9727a9d

Browse files
Merge pull request #547 from lightpanda-io/jsruntime_arenas
Re-introduce call_arena
2 parents 1b74289 + 114e11f commit 9727a9d

File tree

2 files changed

+73
-28
lines changed

2 files changed

+73
-28
lines changed

src/browser/dom/mutation_observer.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ const EventHandler = struct {
284284
const muevt = parser.eventToMutationEvent(evt.?);
285285

286286
// TODO get the allocator by another way?
287-
const alloc = data.cbk.executor.call_arena.allocator();
287+
const alloc = data.cbk.executor.scope_arena;
288288

289289
if (std.mem.eql(u8, t, "DOMAttrModified")) {
290290
mrs.first = .{

src/runtime/js.zig

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const ArenaAllocator = std.heap.ArenaAllocator;
2727

2828
const log = std.log.scoped(.js);
2929

30+
const CALL_ARENA_RETAIN = 1024 * 16;
31+
const SCOPE_ARENA_RETAIN = 1024 * 64;
32+
3033
// Global, should only be initialized once.
3134
pub const Platform = struct {
3235
inner: v8.Platform,
@@ -177,6 +180,9 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
177180
// Send a lowMemoryNotification whenever we stop an executor
178181
gc_hints: bool,
179182

183+
// Used in debug mode to assert that we only have one executor at a time
184+
has_executor: if (builtin.mode == .Debug) bool else void = if (builtin.mode == .Debug) false else {},
185+
180186
const Self = @This();
181187

182188
const State = S;
@@ -260,6 +266,10 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
260266
}
261267

262268
pub fn startExecutor(self: *Self, comptime Global: type, state: State, module_loader: anytype) !*Executor {
269+
if (comptime builtin.mode == .Debug) {
270+
std.debug.assert(self.has_executor == false);
271+
self.has_executor = true;
272+
}
263273
const isolate = self.isolate;
264274
const templates = &self.templates;
265275

@@ -332,22 +342,28 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
332342
context.setEmbedderData(1, data);
333343
}
334344

335-
const allocator = self.allocator;
336-
337345
executor.* = .{
338346
.state = state,
339347
.context = context,
340348
.isolate = isolate,
341349
.templates = templates,
342350
.handle_scope = handle_scope,
343-
.call_arena = ArenaAllocator.init(allocator),
344-
.scope_arena = ArenaAllocator.init(allocator),
351+
.call_arena = undefined,
352+
.scope_arena = undefined,
353+
._call_arena_instance = std.heap.ArenaAllocator.init(self.allocator),
354+
._scope_arena_instance = std.heap.ArenaAllocator.init(self.allocator),
345355
.module_loader = .{
346356
.ptr = @ptrCast(module_loader),
347357
.func = @TypeOf(module_loader.*).fetchModuleSource,
348358
},
349359
};
350360

361+
// We do it this way, just to present a slightly nicer API. Many
362+
// things want one of these two allocator from the executor. Now they
363+
// can do `executor.call_arena` instead of `executor.call_arena.allocator`.
364+
executor.call_arena = executor._call_arena_instance.allocator();
365+
executor.scope_arena = executor._scope_arena_instance.allocator();
366+
351367
errdefer self.stopExecutor(executor);
352368

353369
// Custom exception
@@ -375,6 +391,11 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
375391
if (self.gc_hints) {
376392
self.isolate.lowMemoryNotification();
377393
}
394+
395+
if (comptime builtin.mode == .Debug) {
396+
std.debug.assert(self.has_executor == true);
397+
self.has_executor = false;
398+
}
378399
}
379400

380401
// Give it a Zig struct, get back a v8.FunctionTemplate.
@@ -755,19 +776,27 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
755776
// a context, we can always get the Executor back.
756777
context: v8.Context,
757778

779+
// Because calls can be nested (i.e.a function calling a callback),
780+
// we can only reset the call_arena when call_depth == 0. If we were
781+
// to reset it within a callback, it would invalidate the data of
782+
// the call which is calling the callback.
783+
call_depth: usize = 0,
784+
758785
// Arena whose lifetime is for a single getter/setter/function/etc.
759786
// Largely used to get strings out of V8, like a stack trace from
760787
// a TryCatch. The allocator will be owned by the Scope, but the
761788
// arena itself is owned by the Executor so that we can re-use it
762789
// from scope to scope.
763-
call_arena: ArenaAllocator,
790+
call_arena: Allocator,
791+
_call_arena_instance: std.heap.ArenaAllocator,
764792

765793
// Arena whose lifetime is for a single page load, aka a Scope. Where
766794
// the call_arena lives for a single function call, the scope_arena
767795
// lives for the lifetime of the entire page. The allocator will be
768796
// owned by the Scope, but the arena itself is owned by the Executor
769797
// so that we can re-use it from scope to scope.
770-
scope_arena: ArenaAllocator,
798+
scope_arena: Allocator,
799+
_scope_arena_instance: std.heap.ArenaAllocator,
771800

772801
// When we need to load a resource (i.e. an external script), we call
773802
// this function to get the source. This is always a reference to the
@@ -790,13 +819,14 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
790819

791820
// not public, must be destroyed via env.stopExecutor()
792821
fn deinit(self: *Executor) void {
793-
if (self.scope) |*s| {
794-
s.deinit();
822+
if (self.scope != null) {
823+
self.endScope();
795824
}
796825
self.context.exit();
797826
self.handle_scope.deinit();
798-
self.call_arena.deinit();
799-
self.scope_arena.deinit();
827+
828+
self._call_arena_instance.deinit();
829+
self._scope_arena_instance.deinit();
800830
}
801831

802832
// Executes the src
@@ -889,16 +919,15 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
889919
v8.HandleScope.init(&handle_scope, self.isolate);
890920
self.scope = Scope{
891921
.handle_scope = handle_scope,
892-
.arena = self.scope_arena.allocator(),
893-
.call_arena = self.scope_arena.allocator(),
922+
.arena = self.scope_arena,
894923
};
895924
_ = try self._mapZigInstanceToJs(self.context.getGlobal(), global);
896925
}
897926

898927
pub fn endScope(self: *Executor) void {
899928
self.scope.?.deinit();
900929
self.scope = null;
901-
_ = self.scope_arena.reset(.{ .retain_with_limit = 1024 * 64 });
930+
_ = self._scope_arena_instance.reset(.{ .retain_with_limit = SCOPE_ARENA_RETAIN });
902931
}
903932

904933
// Given an anytype, turns it into a v8.Object. The anytype could be:
@@ -1090,7 +1119,6 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
10901119
// in executor, rather than having one for each of these.
10911120
pub const Scope = struct {
10921121
arena: Allocator,
1093-
call_arena: Allocator,
10941122
handle_scope: v8.HandleScope,
10951123
callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .{},
10961124
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{},
@@ -1148,7 +1176,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
11481176

11491177
self.callWithThis(this, args) catch |err| {
11501178
if (try_catch.hasCaught()) {
1151-
const allocator = self.executor.scope.?.call_arena;
1179+
const allocator = self.executor.call_arena;
11521180
result.stack = try_catch.stack(allocator) catch null;
11531181
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
11541182
} else {
@@ -1185,7 +1213,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
11851213
fn printFunc(self: Callback) !void {
11861214
const executor = self.executor;
11871215
const value = self.func.castToFunction().toValue();
1188-
const src = try valueToString(executor.call_arena.allocator(), value, executor.isolate, executor.context);
1216+
const src = try valueToString(executor.call_arena, value, executor.isolate, executor.context);
11891217
std.debug.print("{s}\n", .{src});
11901218
}
11911219
};
@@ -1209,7 +1237,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
12091237
pub fn setIndex(self: JsObject, index: usize, value: anytype) !void {
12101238
const key = switch (index) {
12111239
inline 0...1000 => |i| std.fmt.comptimePrint("{d}", .{i}),
1212-
else => try std.fmt.allocPrint(self.executor.scope_arena.allocator(), "{d}", .{index}),
1240+
else => try std.fmt.allocPrint(self.executor.scope_arena, "{d}", .{index}),
12131241
};
12141242
return self.set(key, value);
12151243
}
@@ -1500,7 +1528,7 @@ fn Caller(comptime E: type) type {
15001528
context: v8.Context,
15011529
isolate: v8.Isolate,
15021530
executor: *E.Executor,
1503-
call_allocator: Allocator,
1531+
call_arena: Allocator,
15041532

15051533
const Self = @This();
15061534

@@ -1512,17 +1540,34 @@ fn Caller(comptime E: type) type {
15121540
const context = isolate.getCurrentContext();
15131541
const executor: *E.Executor = @ptrFromInt(context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
15141542

1543+
executor.call_depth += 1;
15151544
return .{
15161545
.isolate = isolate,
15171546
.context = context,
15181547
.executor = executor,
1519-
.call_allocator = executor.scope.?.call_arena,
1548+
.call_arena = executor.call_arena,
15201549
};
15211550
}
15221551

15231552
fn deinit(self: *Self) void {
1524-
_ = self;
1525-
// _ = self.executor.call_arena.reset(.{ .retain_with_limit = 4096 });
1553+
const executor = self.executor;
1554+
const call_depth = executor.call_depth - 1;
1555+
executor.call_depth = call_depth;
1556+
1557+
// Because of callbacks, calls can be nested. Because of this, we
1558+
// can't clear the call_arena after _every_ call. Imagine we have
1559+
// arr.forEach((i) => { console.log(i); }
1560+
//
1561+
// First we call forEach. Inside of our forEach call,
1562+
// we call console.log. If we reset the call_arena after this call,
1563+
// it'll reset it for the `forEach` call after, which might still
1564+
// need the data.
1565+
//
1566+
// Therefore, we keep a call_depth, and only reset the call_arena
1567+
// when a top-level (call_depth == 0) function ends.
1568+
if (call_depth == 0) {
1569+
_ = executor._call_arena_instance.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
1570+
}
15261571
}
15271572

15281573
fn constructor(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void {
@@ -1690,7 +1735,7 @@ fn Caller(comptime E: type) type {
16901735
}
16911736

16921737
fn nameToString(self: *Self, name: v8.Name) ![]const u8 {
1693-
return valueToString(self.call_allocator, .{ .handle = name.handle }, self.isolate, self.context);
1738+
return valueToString(self.call_arena, .{ .handle = name.handle }, self.isolate, self.context);
16941739
}
16951740

16961741
fn assertSelfReceiver(comptime named_function: anytype) void {
@@ -1739,7 +1784,7 @@ fn Caller(comptime E: type) type {
17391784

17401785
const Exception = comptime getCustomException(named_function.S) orelse break :blk null;
17411786
if (function_error_set == Exception or isErrorSetException(Exception, err)) {
1742-
const custom_exception = Exception.init(self.call_allocator, err, named_function.js_name) catch |init_err| {
1787+
const custom_exception = Exception.init(self.call_arena, err, named_function.js_name) catch |init_err| {
17431788
switch (init_err) {
17441789
// if a custom exceptions' init wants to return a
17451790
// different error, we need to think about how to
@@ -1872,7 +1917,7 @@ fn Caller(comptime E: type) type {
18721917
if (js_parameter_count == 0) {
18731918
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
18741919
} else {
1875-
const arr = try self.call_allocator.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
1920+
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
18761921
for (arr, last_js_parameter..) |*a, i| {
18771922
const js_value = info.getArg(@as(u32, @intCast(i)));
18781923
a.* = try self.jsValueToZig(named_function, slice_type, js_value);
@@ -1943,10 +1988,10 @@ fn Caller(comptime E: type) type {
19431988
if (ptr.child == u8) {
19441989
if (ptr.sentinel()) |s| {
19451990
if (comptime s == 0) {
1946-
return valueToStringZ(self.call_allocator, js_value, self.isolate, self.context);
1991+
return valueToStringZ(self.call_arena, js_value, self.isolate, self.context);
19471992
}
19481993
} else {
1949-
return valueToString(self.call_allocator, js_value, self.isolate, self.context);
1994+
return valueToString(self.call_arena, js_value, self.isolate, self.context);
19501995
}
19511996
}
19521997

@@ -1972,7 +2017,7 @@ fn Caller(comptime E: type) type {
19722017

19732018
// Newer version of V8 appear to have an optimized way
19742019
// to do this (V8::Array has an iterate method on it)
1975-
const arr = try self.call_allocator.alloc(ptr.child, js_arr.length());
2020+
const arr = try self.call_arena.alloc(ptr.child, js_arr.length());
19762021
for (arr, 0..) |*a, i| {
19772022
a.* = try self.jsValueToZig(named_function, ptr.child, try js_obj.getAtIndex(context, @intCast(i)));
19782023
}

0 commit comments

Comments
 (0)