diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index 88b31e48..7cd8ec35 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -471,13 +471,14 @@ pub const Function = struct { // We support two ways to cache a value directly into a v8::Object. The // difference between the two is like the difference between a Map // and a Struct. - // 1 - Using the object's private state with a v8::Private key. Think of - // this as a HashMap. It takes no memory if the cache isn't used - // but has overhead when used. - // 2 - (TODO) Using the object's internal fields. Think of this as + // 1 - Using the object's internal fields. Think of this as // adding a field to the struct. It's fast, but the space is reserved // upfront for _every_ instance, whether we use it or not. // + // 2 - Using the object's private state with a v8::Private key. Think of + // this as a HashMap. It takes no memory if the cache isn't used + // but has overhead when used. + // // Consider `window.document`, (1) we have relatively few Window objects, // (2) They all have a document and (3) The document is accessed _a lot_. // An internal field makes sense. @@ -485,9 +486,9 @@ pub const Function = struct { // Consider `node.childNodes`, (1) we can have 20K+ node objects, (2) // 95% of nodes will never have their .childNodes access by JavaScript. // Private map lookup makes sense. - const Caching = union(enum) { + pub const Caching = union(enum) { + internal: u8, private: []const u8, - // TODO internal_field: u8, }; }; @@ -567,6 +568,24 @@ pub const Function = struct { const return_value = info.getReturnValue(); switch (cache) { + .internal => |idx| { + if (v8.v8__Object__GetInternalField(js_this, idx)) |cached| { + // means we can't cache undefined, since we can't tell the + // difference between "it isn't in the cache" and "it's + // in the cache with a valud of undefined" + if (!v8.v8__Value__IsUndefined(cached)) { + return_value.set(cached); + return true; + } + } + + // store this so that we can quickly save the result into the cache + cache_state.* = .{ + .js_this = js_this, + .v8_context = v8_context, + .mode = .{ .internal = idx }, + }; + }, .private => |private_symbol| { const global_handle = &@field(ctx.env.private_symbols, private_symbol).handle; const private_key: *const v8.Private = v8.v8__Global__Get(global_handle, ctx.isolate.handle).?; @@ -599,11 +618,14 @@ pub const Function = struct { js_this: *const v8.Object, v8_context: *const v8.Context, mode: union(enum) { + internal: u8, private: *const v8.Private, }, pub fn save(self: *const CacheState, comptime cache: Opts.Caching, js_value: js.Value) void { - if (comptime cache == .private) { + if (comptime cache == .internal) { + v8.v8__Object__SetInternalField(self.js_this, self.mode.internal, js_value.handle); + } else { var out: v8.MaybeBool = undefined; v8.v8__Object__SetPrivate(self.js_this, self.v8_context, self.mode.private, js_value.handle, &out); } diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index e6386e12..8b64ad1f 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -226,14 +226,31 @@ pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context { // Get the global template that was created once per isolate const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?)); + v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime Snapshot.countInternalFields(Window.JsApi)); const v8_context = v8.v8__Context__New(isolate.handle, global_template, null).?; // Create the v8::Context and wrap it in a v8::Global var context_global: v8.Global = undefined; v8.v8__Global__New(isolate.handle, v8_context, &context_global); - // our window wrapped in a v8::Global + // get the global object for the context, this maps to our Window const global_obj = v8.v8__Context__Global(v8_context).?; + { + // Store our TAO inside the internal field of the global object. This + // maps the v8::Object -> Zig instance. Almost all objects have this, and + // it gets setup automatically as objects are created, but the Window + // object already exists in v8 (it's the global) so we manually create + // the mapping here. + const tao = try context_arena.create(@import("TaggedOpaque.zig")); + tao.* = .{ + .value = @ptrCast(page.window), + .prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr, + .prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len), + .subtype = .node, // this probably isn't right, but it's what we've been doing all along + }; + v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao); + } + // our window wrapped in a v8::Global var global_global: v8.Global = undefined; v8.v8__Global__New(isolate.handle, global_obj, &global_global); diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig index 6610fb7c..21946536 100644 --- a/src/browser/js/Snapshot.zig +++ b/src/browser/js/Snapshot.zig @@ -433,9 +433,12 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT }; const template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?); - if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) { - const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template); - v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, 1); + { + const internal_field_count = comptime countInternalFields(JsApi); + if (internal_field_count > 0) { + const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template); + v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count); + } } const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi); const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len)); @@ -443,6 +446,44 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT return template; } +pub fn countInternalFields(comptime JsApi: type) u8 { + var last_used_id = 0; + var cache_count: u8 = 0; + + inline for (@typeInfo(JsApi).@"struct".decls) |d| { + const name: [:0]const u8 = d.name; + const value = @field(JsApi, name); + const definition = @TypeOf(value); + + switch (definition) { + inline bridge.Accessor, bridge.Function => { + const cache = value.cache orelse continue; + if (cache != .internal) { + continue; + } + // We assert that they are declared in-order. This isn't necessary + // but I don't want to do anything fancy to look for gaps or + // duplicates. + const internal_id = cache.internal; + if (internal_id != last_used_id + 1) { + @compileError(@typeName(JsApi) ++ "." ++ name ++ " has a non-monotonic cache index"); + } + last_used_id = internal_id; + cache_count += 1; // this is just last_used, but it's more explicit this way + }, + else => {}, + } + } + + if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) { + return cache_count; + } + + // we need cache_count internal fields, + 1 for the TAO pointer (the v8 -> Zig) + // mapping) itself. + return cache_count + 1; +} + // Attaches JsApi members to the prototype template (normal case) fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void { const target = v8.v8__FunctionTemplate__PrototypeTemplate(template); diff --git a/src/browser/js/TaggedOpaque.zig b/src/browser/js/TaggedOpaque.zig index d62e735b..3a8250ce 100644 --- a/src/browser/js/TaggedOpaque.zig +++ b/src/browser/js/TaggedOpaque.zig @@ -95,33 +95,6 @@ pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R { } const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle); - // Special case for Window: the global object doesn't have internal fields - // Window instance is stored in context.page.window instead - if (internal_field_count == 0) { - // Normally, this would be an error. All JsObject that map to a Zig type - // are either `empty_with_no_proto` (handled above) or have an - // interalFieldCount. The only exception to that is the Window... - const isolate = v8.v8__Object__GetIsolate(js_obj_handle).?; - const context = js.Context.fromIsolate(.{ .handle = isolate }); - - const Window = @import("../webapi/Window.zig"); - if (T == Window) { - return context.page.window; - } - - // ... Or the window's prototype. - // We could make this all comptime-fancy, but it's easier to hard-code - // the EventTarget - - const EventTarget = @import("../webapi/EventTarget.zig"); - if (T == EventTarget) { - return context.page.window._proto; - } - - // Type not found in Window's prototype chain - return error.InvalidArgument; - } - // if it isn't an empty struct, then the v8.Object should have an // InternalFieldCount > 0, since our toa pointer should be embedded // at index 0 of the internal field count. diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 623d898e..02c31fc2 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -160,10 +160,12 @@ pub const Constructor = struct { pub const Function = struct { static: bool, arity: usize, + cache: ?Caller.Function.Opts.Caching = null, func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void, fn init(comptime T: type, comptime func: anytype, comptime opts: Caller.Function.Opts) Function { return .{ + .cache = opts.cache, .static = opts.static, .arity = getArity(@TypeOf(func)), .func = struct { @@ -193,11 +195,13 @@ pub const Function = struct { pub const Accessor = struct { static: bool = false, + cache: ?Caller.Function.Opts.Caching = null, getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null, setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null, fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Caller.Function.Opts) Accessor { var accessor = Accessor{ + .cache = opts.cache, .static = opts.static, }; diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index c70ce671..6e184b21 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -713,18 +713,19 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; + pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = .{ .internal = 1 } }); + pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = .{ .internal = 2 } }); + pub const top = bridge.accessor(Window.getWindow, null, .{}); pub const self = bridge.accessor(Window.getWindow, null, .{}); pub const window = bridge.accessor(Window.getWindow, null, .{}); pub const parent = bridge.accessor(Window.getWindow, null, .{}); - pub const console = bridge.accessor(Window.getConsole, null, .{}); pub const navigator = bridge.accessor(Window.getNavigator, null, .{}); pub const screen = bridge.accessor(Window.getScreen, null, .{}); pub const visualViewport = bridge.accessor(Window.getVisualViewport, null, .{}); pub const performance = bridge.accessor(Window.getPerformance, null, .{}); pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{}); pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{}); - pub const document = bridge.accessor(Window.getDocument, null, .{}); pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{}); pub const history = bridge.accessor(Window.getHistory, null, .{}); pub const navigation = bridge.accessor(Window.getNavigation, null, .{});