@@ -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.
@@ -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