From 01e83b45b58842dba58d5772d61d8d0120f6456c Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 16 Feb 2026 19:16:39 +0800 Subject: [PATCH] Add internal field caching (for window.document and window.console) This expands the caching capabilities which were first added in https://github.com/lightpanda-io/browser/pull/1552 Internal field caching requires up-front memory, but is faster. It is currently enabled for window.document and window.console - two very frequently accessed values. Implementations must correctly provide an internal field index, with consideration for index 0 which may or may not be reserved for the type (it depends on the type). comptime checks run to make sure this is correct, but it would probably be nice to at least let them be declared in any order. This commit also removes the special handling for loading the window. This used to rely on the window not having any internal fields, but it now has them for caching so it can't be detected that way. Instead, the window is loaded like any other object. (But now we have to special case the initial window TAO creation to make it behave like any other Zig instance). --- src/browser/js/Caller.zig | 36 ++++++++++++++++++++----- src/browser/js/Env.zig | 19 ++++++++++++- src/browser/js/Snapshot.zig | 47 ++++++++++++++++++++++++++++++--- src/browser/js/TaggedOpaque.zig | 27 ------------------- src/browser/js/bridge.zig | 4 +++ src/browser/webapi/Window.zig | 5 ++-- 6 files changed, 98 insertions(+), 40 deletions(-) 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, .{});