@@ -27,6 +27,9 @@ const ArenaAllocator = std.heap.ArenaAllocator;
2727
2828const 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.
3134pub 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.
@@ -755,19 +779,25 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
755779 // a context, we can always get the Executor back.
756780 context : v8.Context ,
757781
782+ // Because calls can be nested (i.e.a function calling a callback),
783+ // we can only reset the call_arena when call_depth == 0. If we were
784+ // to reset it within a callback, it would invalidate the data of
785+ // the call which is calling the callback.
786+ call_depth : usize = 0 ,
787+
758788 // Arena whose lifetime is for a single getter/setter/function/etc.
759789 // Largely used to get strings out of V8, like a stack trace from
760790 // a TryCatch. The allocator will be owned by the Scope, but the
761791 // arena itself is owned by the Executor so that we can re-use it
762792 // from scope to scope.
763- call_arena : ArenaAllocator ,
793+ call_arena : Allocator ,
764794
765795 // Arena whose lifetime is for a single page load, aka a Scope. Where
766796 // the call_arena lives for a single function call, the scope_arena
767797 // lives for the lifetime of the entire page. The allocator will be
768798 // owned by the Scope, but the arena itself is owned by the Executor
769799 // so that we can re-use it from scope to scope.
770- scope_arena : ArenaAllocator ,
800+ scope_arena : Allocator ,
771801
772802 // When we need to load a resource (i.e. an external script), we call
773803 // this function to get the source. This is always a reference to the
@@ -790,13 +820,11 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
790820
791821 // not public, must be destroyed via env.stopExecutor()
792822 fn deinit (self : * Executor ) void {
793- if (self .scope ) | * s | {
794- s . deinit ();
823+ if (self .scope != null ) {
824+ self . endScope ();
795825 }
796826 self .context .exit ();
797827 self .handle_scope .deinit ();
798- self .call_arena .deinit ();
799- self .scope_arena .deinit ();
800828 }
801829
802830 // Executes the src
@@ -889,16 +917,16 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
889917 v8 .HandleScope .init (& handle_scope , self .isolate );
890918 self .scope = Scope {
891919 .handle_scope = handle_scope ,
892- .arena = self .scope_arena .allocator (),
893- .call_arena = self .scope_arena .allocator (),
920+ .arena = self .scope_arena ,
894921 };
895922 _ = try self ._mapZigInstanceToJs (self .context .getGlobal (), global );
896923 }
897924
898925 pub fn endScope (self : * Executor ) void {
899926 self .scope .? .deinit ();
900927 self .scope = null ;
901- _ = self .scope_arena .reset (.{ .retain_with_limit = 1024 * 64 });
928+ const scope_arena : * std.heap.ArenaAllocator = @ptrCast (@alignCast (self .scope_arena .ptr ));
929+ _ = scope_arena .reset (.{ .retain_with_limit = SCOPE_ARENA_RETAIN });
902930 }
903931
904932 // Given an anytype, turns it into a v8.Object. The anytype could be:
@@ -1090,7 +1118,6 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
10901118 // in executor, rather than having one for each of these.
10911119 pub const Scope = struct {
10921120 arena : Allocator ,
1093- call_arena : Allocator ,
10941121 handle_scope : v8.HandleScope ,
10951122 callbacks : std .ArrayListUnmanaged (v8 .Persistent (v8 .Function )) = .{},
10961123 identity_map : std .AutoHashMapUnmanaged (usize , PersistentObject ) = .{},
@@ -1148,7 +1175,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
11481175
11491176 self .callWithThis (this , args ) catch | err | {
11501177 if (try_catch .hasCaught ()) {
1151- const allocator = self .executor .scope .? . call_arena ;
1178+ const allocator = self .executor .call_arena ;
11521179 result .stack = try_catch .stack (allocator ) catch null ;
11531180 result .exception = (try_catch .exception (allocator ) catch @errorName (err )) orelse @errorName (err );
11541181 } else {
@@ -1185,7 +1212,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
11851212 fn printFunc (self : Callback ) ! void {
11861213 const executor = self .executor ;
11871214 const value = self .func .castToFunction ().toValue ();
1188- const src = try valueToString (executor .call_arena . allocator () , value , executor .isolate , executor .context );
1215+ const src = try valueToString (executor .call_arena , value , executor .isolate , executor .context );
11891216 std .debug .print ("{s}\n " , .{src });
11901217 }
11911218 };
@@ -1209,7 +1236,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
12091236 pub fn setIndex (self : JsObject , index : usize , value : anytype ) ! void {
12101237 const key = switch (index ) {
12111238 inline 0... 1000 = > | i | std .fmt .comptimePrint ("{d}" , .{i }),
1212- else = > try std .fmt .allocPrint (self .executor .scope_arena . allocator () , "{d}" , .{index }),
1239+ else = > try std .fmt .allocPrint (self .executor .scope_arena , "{d}" , .{index }),
12131240 };
12141241 return self .set (key , value );
12151242 }
@@ -1491,7 +1518,7 @@ fn Caller(comptime E: type) type {
14911518 context : v8.Context ,
14921519 isolate : v8.Isolate ,
14931520 executor : * E.Executor ,
1494- call_allocator : Allocator ,
1521+ call_arena : Allocator ,
14951522
14961523 const Self = @This ();
14971524
@@ -1503,17 +1530,34 @@ fn Caller(comptime E: type) type {
15031530 const context = isolate .getCurrentContext ();
15041531 const executor : * E.Executor = @ptrFromInt (context .getEmbedderData (1 ).castTo (v8 .BigInt ).getUint64 ());
15051532
1533+ executor .call_depth += 1 ;
15061534 return .{
15071535 .isolate = isolate ,
15081536 .context = context ,
15091537 .executor = executor ,
1510- .call_allocator = executor . scope .? .call_arena ,
1538+ .call_arena = executor .call_arena ,
15111539 };
15121540 }
15131541
15141542 fn deinit (self : * Self ) void {
1515- _ = self ;
1516- // _ = self.executor.call_arena.reset(.{ .retain_with_limit = 4096 });
1543+ const call_depth = self .executor .call_depth - 1 ;
1544+ self .executor .call_depth = call_depth ;
1545+
1546+ // Because of callbacks, calls can be nested. Because of this, we
1547+ // can't clear the call_arena after _every_ call. Imagine we have
1548+ // arr.forEach((i) => { console.log(i); }
1549+ //
1550+ // First we call forEach. Inside of our forEach call,
1551+ // we call console.log. If we reset the call_arena after this call,
1552+ // it'll reset it for the `forEach` call after, which might still
1553+ // need the data.
1554+ //
1555+ // Therefore, we keep a call_depth, and only reset the call_arena
1556+ // when a top-level (call_depth == 0) function ends.
1557+ if (call_depth == 0 ) {
1558+ const call_arena : * std.heap.ArenaAllocator = @ptrCast (@alignCast (self .call_arena .ptr ));
1559+ _ = call_arena .reset (.{ .retain_with_limit = CALL_ARENA_RETAIN });
1560+ }
15171561 }
15181562
15191563 fn constructor (self : * Self , comptime named_function : anytype , info : v8.FunctionCallbackInfo ) ! void {
@@ -1681,7 +1725,7 @@ fn Caller(comptime E: type) type {
16811725 }
16821726
16831727 fn nameToString (self : * Self , name : v8.Name ) ! []const u8 {
1684- return valueToString (self .call_allocator , .{ .handle = name .handle }, self .isolate , self .context );
1728+ return valueToString (self .call_arena , .{ .handle = name .handle }, self .isolate , self .context );
16851729 }
16861730
16871731 fn assertSelfReceiver (comptime named_function : anytype ) void {
@@ -1730,7 +1774,7 @@ fn Caller(comptime E: type) type {
17301774
17311775 const Exception = comptime getCustomException (named_function .S ) orelse break :blk null ;
17321776 if (function_error_set == Exception or isErrorSetException (Exception , err )) {
1733- const custom_exception = Exception .init (self .call_allocator , err , named_function .js_name ) catch | init_err | {
1777+ const custom_exception = Exception .init (self .call_arena , err , named_function .js_name ) catch | init_err | {
17341778 switch (init_err ) {
17351779 // if a custom exceptions' init wants to return a
17361780 // different error, we need to think about how to
@@ -1863,7 +1907,7 @@ fn Caller(comptime E: type) type {
18631907 if (js_parameter_count == 0 ) {
18641908 @field (args , tupleFieldName (params_to_map .len + offset - 1 )) = &.{};
18651909 } else {
1866- const arr = try self .call_allocator .alloc (last_parameter_type_info .pointer .child , js_parameter_count - params_to_map .len + 1 );
1910+ const arr = try self .call_arena .alloc (last_parameter_type_info .pointer .child , js_parameter_count - params_to_map .len + 1 );
18671911 for (arr , last_js_parameter .. ) | * a , i | {
18681912 const js_value = info .getArg (@as (u32 , @intCast (i )));
18691913 a .* = try self .jsValueToZig (named_function , slice_type , js_value );
@@ -1934,10 +1978,10 @@ fn Caller(comptime E: type) type {
19341978 if (ptr .child == u8 ) {
19351979 if (ptr .sentinel ()) | s | {
19361980 if (comptime s == 0 ) {
1937- return valueToStringZ (self .call_allocator , js_value , self .isolate , self .context );
1981+ return valueToStringZ (self .call_arena , js_value , self .isolate , self .context );
19381982 }
19391983 } else {
1940- return valueToString (self .call_allocator , js_value , self .isolate , self .context );
1984+ return valueToString (self .call_arena , js_value , self .isolate , self .context );
19411985 }
19421986 }
19431987
@@ -1963,7 +2007,7 @@ fn Caller(comptime E: type) type {
19632007
19642008 // Newer version of V8 appear to have an optimized way
19652009 // to do this (V8::Array has an iterate method on it)
1966- const arr = try self .call_allocator .alloc (ptr .child , js_arr .length ());
2010+ const arr = try self .call_arena .alloc (ptr .child , js_arr .length ());
19672011 for (arr , 0.. ) | * a , i | {
19682012 a .* = try self .jsValueToZig (named_function , ptr .child , try js_obj .getAtIndex (context , @intCast (i )));
19692013 }
0 commit comments