Skip to content

Commit b83b23a

Browse files
committed
Re-introduce call_arena
Because of callbacks, calls into Zig can be nested. Previously, the call_arena was reset after _every_ call. When calls are nested, this doesn't work - the nested call resets the arena, which the caller might still need. A `call_depth` integer was added to the Executor. Each call starts by incrementing the call_depth and, on deinit, decrements the call_depth. The call_arena is only reset when the call_depth == 0. This could result in lower memory use. Also promoted the call_arena and scope_arena to the Env. Practically speaking, nothing has changed, since they're still reset under the exact same conditions. However, when an executor ends and a new one is started, it can now reuse the retained_capacity of the previous arenas. This should result in fewer allocations.
1 parent 66ec087 commit b83b23a

File tree

2 files changed

+71
-27
lines changed

2 files changed

+71
-27
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: 70 additions & 26 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,14 @@ 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+
186+
// See the Executor's call_arena and scope_arena allocators. Having these
187+
// here allows the arena to be re-used by different executors.
188+
call_arena: ArenaAllocator,
189+
scope_arena: ArenaAllocator,
190+
180191
const Self = @This();
181192

182193
const State = S;
@@ -212,6 +223,8 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
212223
.gc_hints = opts.gc_hints,
213224
.global_scope = global_scope,
214225
.prototype_lookup = undefined,
226+
.call_arena = ArenaAllocator.init(allocator),
227+
.scope_arena = ArenaAllocator.init(allocator),
215228
.executor_pool = std.heap.MemoryPool(Executor).init(allocator),
216229
};
217230

@@ -247,6 +260,8 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
247260
}
248261

249262
pub fn deinit(self: *Self) void {
263+
self.call_arena.deinit();
264+
self.scope_arena.deinit();
250265
self.global_scope.deinit();
251266
self.isolate.exit();
252267
self.isolate.deinit();
@@ -260,6 +275,10 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
260275
}
261276

262277
pub fn startExecutor(self: *Self, comptime Global: type, state: State, module_loader: anytype) !*Executor {
278+
if (comptime builtin.mode == .Debug) {
279+
std.debug.assert(self.has_executor == false);
280+
self.has_executor = true;
281+
}
263282
const isolate = self.isolate;
264283
const templates = &self.templates;
265284

@@ -332,16 +351,14 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
332351
context.setEmbedderData(1, data);
333352
}
334353

335-
const allocator = self.allocator;
336-
337354
executor.* = .{
338355
.state = state,
339356
.context = context,
340357
.isolate = isolate,
341358
.templates = templates,
342359
.handle_scope = handle_scope,
343-
.call_arena = ArenaAllocator.init(allocator),
344-
.scope_arena = ArenaAllocator.init(allocator),
360+
.call_arena = self.call_arena.allocator(),
361+
.scope_arena = self.scope_arena.allocator(),
345362
.module_loader = .{
346363
.ptr = @ptrCast(module_loader),
347364
.func = @TypeOf(module_loader.*).fetchModuleSource,
@@ -375,6 +392,13 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
375392
if (self.gc_hints) {
376393
self.isolate.lowMemoryNotification();
377394
}
395+
396+
if (comptime builtin.mode == .Debug) {
397+
std.debug.assert(self.has_executor == true);
398+
self.has_executor = false;
399+
}
400+
_ = self.call_arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
401+
_ = self.scope_arena.reset(.{ .retain_with_limit = SCOPE_ARENA_RETAIN });
378402
}
379403

380404
// Give it a Zig struct, get back a v8.FunctionTemplate.
@@ -742,19 +766,25 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
742766
// a context, we can always get the Executor back.
743767
context: v8.Context,
744768

769+
// Because calls can be nested (i.e.a function calling a callback),
770+
// we can only reset the call_arena when call_depth == 0. If we were
771+
// to reset it within a callback, it would invalidate the data of
772+
// the call which is calling the callback.
773+
call_depth: usize = 0,
774+
745775
// Arena whose lifetime is for a single getter/setter/function/etc.
746776
// Largely used to get strings out of V8, like a stack trace from
747777
// a TryCatch. The allocator will be owned by the Scope, but the
748778
// arena itself is owned by the Executor so that we can re-use it
749779
// from scope to scope.
750-
call_arena: ArenaAllocator,
780+
call_arena: Allocator,
751781

752782
// Arena whose lifetime is for a single page load, aka a Scope. Where
753783
// the call_arena lives for a single function call, the scope_arena
754784
// lives for the lifetime of the entire page. The allocator will be
755785
// owned by the Scope, but the arena itself is owned by the Executor
756786
// so that we can re-use it from scope to scope.
757-
scope_arena: ArenaAllocator,
787+
scope_arena: Allocator,
758788

759789
// When we need to load a resource (i.e. an external script), we call
760790
// this function to get the source. This is always a reference to the
@@ -777,13 +807,11 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
777807

778808
// not public, must be destroyed via env.stopExecutor()
779809
fn deinit(self: *Executor) void {
780-
if (self.scope) |*s| {
781-
s.deinit();
810+
if (self.scope != null) {
811+
self.endScope();
782812
}
783813
self.context.exit();
784814
self.handle_scope.deinit();
785-
self.call_arena.deinit();
786-
self.scope_arena.deinit();
787815
}
788816

789817
// Executes the src
@@ -876,16 +904,16 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
876904
v8.HandleScope.init(&handle_scope, self.isolate);
877905
self.scope = Scope{
878906
.handle_scope = handle_scope,
879-
.arena = self.scope_arena.allocator(),
880-
.call_arena = self.scope_arena.allocator(),
907+
.arena = self.scope_arena,
881908
};
882909
_ = try self._mapZigInstanceToJs(self.context.getGlobal(), global);
883910
}
884911

885912
pub fn endScope(self: *Executor) void {
886913
self.scope.?.deinit();
887914
self.scope = null;
888-
_ = self.scope_arena.reset(.{ .retain_with_limit = 1024 * 64 });
915+
const scope_arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(self.scope_arena.ptr));
916+
_ = scope_arena.reset(.{ .retain_with_limit = SCOPE_ARENA_RETAIN });
889917
}
890918

891919
// Wrap a v8.Value, largely so that we can provide a convenient
@@ -1056,7 +1084,6 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
10561084
// in executor, rather than having one for each of these.
10571085
pub const Scope = struct {
10581086
arena: Allocator,
1059-
call_arena: Allocator,
10601087
handle_scope: v8.HandleScope,
10611088
callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .{},
10621089
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{},
@@ -1113,7 +1140,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
11131140

11141141
self.call(args) catch |err| {
11151142
if (try_catch.hasCaught()) {
1116-
const allocator = self.executor.scope.?.call_arena;
1143+
const allocator = self.executor.call_arena;
11171144
result.stack = try_catch.stack(allocator) catch null;
11181145
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
11191146
} else {
@@ -1144,7 +1171,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
11441171
fn printFunc(self: *const @This()) !void {
11451172
const executor = self.executor;
11461173
const value = self.func.castToFunction().toValue();
1147-
const src = try valueToString(executor.call_arena.allocator(), value, executor.isolate, executor.context);
1174+
const src = try valueToString(executor.call_arena, value, executor.isolate, executor.context);
11481175
std.debug.print("{s}\n", .{src});
11491176
}
11501177
};
@@ -1168,7 +1195,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
11681195
pub fn setIndex(self: JsObject, index: usize, value: anytype) !void {
11691196
const key = switch (index) {
11701197
inline 0...1000 => |i| std.fmt.comptimePrint("{d}", .{i}),
1171-
else => try std.fmt.allocPrint(self.executor.scope_arena.allocator(), "{d}", .{index}),
1198+
else => try std.fmt.allocPrint(self.executor.scope_arena, "{d}", .{index}),
11721199
};
11731200
return self.set(key, value);
11741201
}
@@ -1421,7 +1448,7 @@ fn Caller(comptime E: type) type {
14211448
context: v8.Context,
14221449
isolate: v8.Isolate,
14231450
executor: *E.Executor,
1424-
call_allocator: Allocator,
1451+
call_arena: Allocator,
14251452

14261453
const Self = @This();
14271454

@@ -1433,17 +1460,34 @@ fn Caller(comptime E: type) type {
14331460
const context = isolate.getCurrentContext();
14341461
const executor: *E.Executor = @ptrFromInt(context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
14351462

1463+
executor.call_depth += 1;
14361464
return .{
14371465
.isolate = isolate,
14381466
.context = context,
14391467
.executor = executor,
1440-
.call_allocator = executor.scope.?.call_arena,
1468+
.call_arena = executor.call_arena,
14411469
};
14421470
}
14431471

14441472
fn deinit(self: *Self) void {
1445-
_ = self;
1446-
// _ = self.executor.call_arena.reset(.{ .retain_with_limit = 4096 });
1473+
const call_depth = self.executor.call_depth - 1;
1474+
self.executor.call_depth = call_depth;
1475+
1476+
// Because of callbacks, calls can be nested. Because of this, we
1477+
// can't clear the call_arena after _every_ call. Imagine we have
1478+
// arr.forEach((i) => { console.log(i); }
1479+
//
1480+
// First we call forEach. Inside of our forEach call,
1481+
// we call console.log. If we reset the call_arena after this call,
1482+
// it'll reset it for the `forEach` call after, which might still
1483+
// need the data.
1484+
//
1485+
// Therefore, we keep a call_depth, and only reset the call_arena
1486+
// when a top-level (call_depth == 0) function ends.
1487+
if (call_depth == 0) {
1488+
const call_arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(self.call_arena.ptr));
1489+
_ = call_arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
1490+
}
14471491
}
14481492

14491493
fn constructor(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void {
@@ -1611,7 +1655,7 @@ fn Caller(comptime E: type) type {
16111655
}
16121656

16131657
fn nameToString(self: *Self, name: v8.Name) ![]const u8 {
1614-
return valueToString(self.call_allocator, .{ .handle = name.handle }, self.isolate, self.context);
1658+
return valueToString(self.call_arena, .{ .handle = name.handle }, self.isolate, self.context);
16151659
}
16161660

16171661
fn assertSelfReceiver(comptime named_function: anytype) void {
@@ -1660,7 +1704,7 @@ fn Caller(comptime E: type) type {
16601704

16611705
const Exception = comptime getCustomException(named_function.S) orelse break :blk null;
16621706
if (function_error_set == Exception or isErrorSetException(Exception, err)) {
1663-
const custom_exception = Exception.init(self.call_allocator, err, named_function.js_name) catch |init_err| {
1707+
const custom_exception = Exception.init(self.call_arena, err, named_function.js_name) catch |init_err| {
16641708
switch (init_err) {
16651709
// if a custom exceptions' init wants to return a
16661710
// different error, we need to think about how to
@@ -1793,7 +1837,7 @@ fn Caller(comptime E: type) type {
17931837
if (js_parameter_count == 0) {
17941838
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
17951839
} else {
1796-
const arr = try self.call_allocator.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
1840+
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
17971841
for (arr, last_js_parameter..) |*a, i| {
17981842
const js_value = info.getArg(@as(u32, @intCast(i)));
17991843
a.* = try self.jsValueToZig(named_function, slice_type, js_value);
@@ -1862,7 +1906,7 @@ fn Caller(comptime E: type) type {
18621906
},
18631907
.slice => {
18641908
if (ptr.child == u8) {
1865-
return valueToString(self.call_allocator, js_value, self.isolate, self.context);
1909+
return valueToString(self.call_arena, js_value, self.isolate, self.context);
18661910
}
18671911

18681912
// TODO: TypedArray
@@ -1887,7 +1931,7 @@ fn Caller(comptime E: type) type {
18871931

18881932
// Newer version of V8 appear to have an optimized way
18891933
// to do this (V8::Array has an iterate method on it)
1890-
const arr = try self.call_allocator.alloc(ptr.child, js_arr.length());
1934+
const arr = try self.call_arena.alloc(ptr.child, js_arr.length());
18911935
for (arr, 0..) |*a, i| {
18921936
a.* = try self.jsValueToZig(named_function, ptr.child, try js_obj.getAtIndex(context, @intCast(i)));
18931937
}

0 commit comments

Comments
 (0)