Start to eagerly reset globals.

Currently, when you create a Global (Value, Object, Function, ...) it exists
until the context is destroyed.

This PR adds the ability to eagerly free them when they fall out of scope, which
is only possible because of the new finalizer hooks.

Previously, we had js.Value, js.Value.Global; js.Function, js.Function.Global,
etc. This PR introduces a .Temp variant: js.Value.Temp and js.Function.Temp.
This is purely a discriminatory type and it behaves (and IS) a Global. The
difference is that it can be released:

  page.js.release(self._on_ready_state_change.?)

Why a new type? There's no guarantee that a global (the existing .Global or the
new .Temp) will get released before the context ends. For this reason, we always
track them in order to free the on context deninit:

```zig
    for (self.global_functions.items) |*global| {
        v8.v8__Global__Reset(global);
    }
```

If a .Temp is eagerly released, we need to remove it from this list. The simple
solution would be to switch `global_functions` from an ArrayList to a HashMap.
But that adds overhead for values that we know we'll never be able to eagerly
release. For this reason, .Temp are stored in a hashmap (and can be released)
and .Globla are stored in an ArrayList (and cannot be released). It's a micro-
optimization...eagerly releasing doesn't have to O(N) scan the list, and we only
pay the memory overhead of the hashmap for values that have a change to be
eagerly freed.

Eager-freeing is now applied to both the callbacn and the values for window
timers (setTimeout, setInterval, RAF). And to the XHR ready_state_change
callback. (we'll do more as we go).
This commit is contained in:
Karl Seguin
2026-01-21 18:07:49 +08:00
parent b63d93e325
commit c63c85071a
6 changed files with 189 additions and 130 deletions

View File

@@ -293,61 +293,29 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
}
}
if (T == js.Function) {
// we're returning a callback
return .{ .local = self, .handle = @ptrCast(value.handle) };
}
// zig fmt: off
switch (T) {
js.Value => return value,
js.Exception => return .{ .local = self, .handle = isolate.throwException(value.handle) },
if (T == js.Function.Global) {
// Auto-convert Global to local for bridge
return .{ .local = self, .handle = @ptrCast(value.local(self).handle) };
}
inline
js.Function,
js.Object,
js.Promise,
js.String => return .{ .local = self, .handle = @ptrCast(value.handle) },
if (T == js.Object) {
// we're returning a v8.Object
return .{ .local = self, .handle = @ptrCast(value.handle) };
}
if (T == js.Object.Global) {
// Auto-convert Global to local for bridge
return .{ .local = self, .handle = @ptrCast(value.local(self).handle) };
}
if (T == js.Value.Global) {
// Auto-convert Global to local for bridge
return .{ .local = self, .handle = @ptrCast(value.local(self).handle) };
}
if (T == js.Promise.Global) {
// Auto-convert Global to local for bridge
return .{ .local = self, .handle = @ptrCast(value.local(self).handle) };
}
if (T == js.PromiseResolver.Global) {
// Auto-convert Global to local for bridge
return .{ .local = self, .handle = @ptrCast(value.local(self).handle) };
}
if (T == js.Module.Global) {
// Auto-convert Global to local for bridge
return .{ .local = self, .handle = @ptrCast(value.local(self).handle) };
}
if (T == js.Value) {
return value;
}
if (T == js.Promise) {
return .{ .local = self, .handle = @ptrCast(value.handle) };
}
if (T == js.Exception) {
return .{ .local = self, .handle = isolate.throwException(value.handle) };
}
if (T == js.String) {
return .{ .local = self, .handle = @ptrCast(value.handle) };
inline
js.Function.Global,
js.Function.Temp,
js.Value.Global,
js.Value.Temp,
js.Object.Global,
js.Promise.Global,
js.PromiseResolver.Global,
js.Module.Global => return .{ .local = self, .handle = @ptrCast(value.local(self).handle) },
else => {}
}
// zig fmt: on
if (@hasDecl(T, "runtimeGenericWrap")) {
const wrap = try value.runtimeGenericWrap(self.ctx.page);
@@ -596,17 +564,17 @@ pub fn jsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !T {
// probeJsValueToZig. Avoids having to duplicate this logic when probing.
fn jsValueToStruct(self: *const Local, comptime T: type, js_val: js.Value) !?T {
return switch (T) {
js.Function => {
js.Function, js.Function.Global, js.Function.Temp => {
if (!js_val.isFunction()) {
return null;
}
return .{ .local = self, .handle = @ptrCast(js_val.handle) };
},
js.Function.Global => {
if (!js_val.isFunction()) {
return null;
}
return try (js.Function{ .local = self, .handle = @ptrCast(js_val.handle) }).persist();
const js_func = js.Function{ .local = self, .handle = @ptrCast(js_val.handle) };
return switch (T) {
js.Function => js_func,
js.Function.Temp => try js_func.temp(),
js.Function.Global => try js_func.persist(),
else => unreachable,
};
},
// zig fmt: off
js.TypedArray(u8), js.TypedArray(u16), js.TypedArray(u32), js.TypedArray(u64),
@@ -620,6 +588,7 @@ fn jsValueToStruct(self: *const Local, comptime T: type, js_val: js.Value) !?T {
},
js.Value => js_val,
js.Value.Global => return try js_val.persist(),
js.Value.Temp => return try js_val.temp(),
js.Object => {
if (!js_val.isObject()) {
return null;