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