mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-04-01 09:56:43 +00:00
Enable v8 snapshots
There are two layers here. The first is that, on startup, a v8 SnapshotCreator is created, and a snapshot-specific isolate/context is setup with our browser environment. This contains most of what was in Env.init and a good chunk of what was in ExecutionWorld.createContext. From this, we create a v8.StartupData which is used for the creation of all subsequent contexts. The snapshot sits at the application level, above the Env - it's re-used for all envs/isolates, so this gives a nice performance boost for both 1 connection opening multiple pages or multiple connections opening 1 page. The second layer is that the Snapshot data can be embedded into the binary, so that it doesn't have to be created on startup, but rather created at build-time. This improves the startup time (though, I'm not really sure how to measure that accurately...). The first layer is the big win (and just works as-is without any build / usage changes). with snapshot total runs 1000 total duration (ms) 7527 avg run duration (ms) 7 min run duration (ms) 5 max run duration (ms) 41 without snapshot total runs 1000 total duration (ms) 9350 avg run duration (ms) 9 min run duration (ms) 8 max run duration (ms) 42 To embed a snapshot into the binary, we first need to create the snapshot file: zig build -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin And then build using the new snapshot_path argument: zig build -Dsnapshot_path=../../snapshot.bin -Doptimize=ReleaseFast The paths are weird, I know...since it's embedded, it needs to be inside the project path, hence we put it in src/snapshot.bin. And since it's embedded relative to the embedder (src/browser/js/Snapshot.zig) the path has to be relative to that, hence ../../snapshot.bin. I'm open to suggestions on improving this.
This commit is contained in:
@@ -26,14 +26,13 @@ const bridge = @import("bridge.zig");
|
||||
const Caller = @import("Caller.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const Platform = @import("Platform.zig");
|
||||
const Snapshot = @import("Snapshot.zig");
|
||||
const Inspector = @import("Inspector.zig");
|
||||
const ExecutionWorld = @import("ExecutionWorld.zig");
|
||||
const NamedFunction = Caller.NamedFunction;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const JsApis = bridge.JsApis;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
||||
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
||||
@@ -53,28 +52,22 @@ isolate: v8.Isolate,
|
||||
// just kept around because we need to free it on deinit
|
||||
isolate_params: *v8.CreateParams,
|
||||
|
||||
// Given a type, we can lookup its index in JS_API_LOOKUP and then have
|
||||
// access to its TunctionTemplate (the thing we need to create an instance
|
||||
// of it)
|
||||
// I.e.:
|
||||
// const index = @field(JS_API_LOOKUP, @typeName(type_name))
|
||||
// const template = templates[index];
|
||||
templates: [JsApis.len]v8.FunctionTemplate,
|
||||
|
||||
context_id: usize,
|
||||
|
||||
const Opts = struct {};
|
||||
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
|
||||
templates: []v8.FunctionTemplate,
|
||||
|
||||
pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
|
||||
// var params = v8.initCreateParams();
|
||||
pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env {
|
||||
var params = try allocator.create(v8.CreateParams);
|
||||
errdefer allocator.destroy(params);
|
||||
|
||||
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
|
||||
params.snapshot_blob = @ptrCast(&snapshot.startup_data);
|
||||
|
||||
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
|
||||
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
|
||||
|
||||
params.external_references = &snapshot.external_references;
|
||||
|
||||
var isolate = v8.Isolate.init(params);
|
||||
errdefer isolate.deinit();
|
||||
|
||||
@@ -88,42 +81,35 @@ pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
|
||||
|
||||
isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback);
|
||||
|
||||
var temp_scope: v8.HandleScope = undefined;
|
||||
v8.HandleScope.init(&temp_scope, isolate);
|
||||
defer temp_scope.deinit();
|
||||
// // Allocate templates array dynamically to avoid comptime dependency on JsApis.len
|
||||
const templates = try allocator.alloc(v8.FunctionTemplate, JsApis.len);
|
||||
errdefer allocator.free(templates);
|
||||
|
||||
const env = try allocator.create(Env);
|
||||
errdefer allocator.destroy(env);
|
||||
{
|
||||
var temp_scope: v8.HandleScope = undefined;
|
||||
v8.HandleScope.init(&temp_scope, isolate);
|
||||
defer temp_scope.deinit();
|
||||
const context = v8.Context.init(isolate, null, null);
|
||||
|
||||
env.* = .{
|
||||
.context_id = 0,
|
||||
.platform = platform,
|
||||
.isolate = isolate,
|
||||
.templates = undefined,
|
||||
.allocator = allocator,
|
||||
.isolate_params = params,
|
||||
};
|
||||
context.enter();
|
||||
defer context.exit();
|
||||
|
||||
// Populate our templates lookup. generateClass creates the
|
||||
// v8.FunctionTemplate, which we store in our env.templates.
|
||||
// The ordering doesn't matter. What matters is that, given a type
|
||||
// we can get its index via: @field(types.LOOKUP, type_name)
|
||||
const templates = &env.templates;
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
@setEvalBranchQuota(10_000);
|
||||
JsApi.Meta.class_id = i;
|
||||
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, generateClass(JsApi, isolate)).castToFunctionTemplate();
|
||||
}
|
||||
|
||||
// Above, we've created all our our FunctionTemplates. Now that we
|
||||
// have them all, we can hook up the prototypes.
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
||||
templates[i].inherit(templates[proto_index]);
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
JsApi.Meta.class_id = i;
|
||||
const data = context.getDataFromSnapshotOnce(snapshot.data_start + i);
|
||||
const function = v8.FunctionTemplate{ .handle = @ptrCast(data) };
|
||||
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, function).castToFunctionTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
return env;
|
||||
return .{
|
||||
.context_id = 0,
|
||||
.isolate = isolate,
|
||||
.platform = platform,
|
||||
.allocator = allocator,
|
||||
.templates = templates,
|
||||
.isolate_params = params,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Env) void {
|
||||
@@ -131,7 +117,7 @@ pub fn deinit(self: *Env) void {
|
||||
self.isolate.deinit();
|
||||
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
|
||||
self.allocator.destroy(self.isolate_params);
|
||||
self.allocator.destroy(self);
|
||||
self.allocator.free(self.templates);
|
||||
}
|
||||
|
||||
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
|
||||
@@ -149,7 +135,6 @@ pub fn pumpMessageLoop(self: *const Env) bool {
|
||||
pub fn runIdleTasks(self: *const Env) void {
|
||||
return self.platform.inner.runIdleTasks(self.isolate, 1);
|
||||
}
|
||||
|
||||
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
|
||||
return .{
|
||||
.env = self,
|
||||
@@ -207,148 +192,3 @@ fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
|
||||
.note = "This should be updated to call window.unhandledrejection",
|
||||
});
|
||||
}
|
||||
|
||||
// Give it a Zig struct, get back a v8.FunctionTemplate.
|
||||
// The FunctionTemplate is a bit like a struct container - it's where
|
||||
// we'll attach functions/getters/setters and where we'll "inherit" a
|
||||
// prototype type (if there is any)
|
||||
fn generateClass(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate {
|
||||
const template = generateConstructor(JsApi, isolate);
|
||||
attachClass(JsApi, isolate, template);
|
||||
return template;
|
||||
}
|
||||
|
||||
// Normally this is called from generateClass. Where generateClass creates
|
||||
// the constructor (hence, the FunctionTemplate), attachClass adds all
|
||||
// of its functions, getters, setters, ...
|
||||
// But it's extracted from generateClass because we also have 1 global
|
||||
// object (i.e. the Window), which gets attached not only to the Window
|
||||
// constructor/FunctionTemplate as normal, but also through the default
|
||||
// FunctionTemplate of the isolate (in createContext)
|
||||
pub fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
|
||||
const template_proto = template.getPrototypeTemplate();
|
||||
|
||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||
inline for (declarations) |d| {
|
||||
const name: [:0]const u8 = d.name;
|
||||
const value = @field(JsApi, name);
|
||||
const definition = @TypeOf(value);
|
||||
|
||||
switch (definition) {
|
||||
bridge.Accessor => {
|
||||
const js_name = v8.String.initUtf8(isolate, name).toName();
|
||||
const getter_callback = v8.FunctionTemplate.initCallback(isolate, value.getter);
|
||||
if (value.setter == null) {
|
||||
if (value.static) {
|
||||
template.setAccessorGetter(js_name, getter_callback);
|
||||
} else {
|
||||
template_proto.setAccessorGetter(js_name, getter_callback);
|
||||
}
|
||||
} else {
|
||||
std.debug.assert(value.static == false);
|
||||
const setter_callback = v8.FunctionTemplate.initCallback(isolate, value.setter);
|
||||
template_proto.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
|
||||
}
|
||||
},
|
||||
bridge.Function => {
|
||||
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
|
||||
const js_name: v8.Name = v8.String.initUtf8(isolate, name).toName();
|
||||
if (value.static) {
|
||||
template.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||
} else {
|
||||
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||
}
|
||||
},
|
||||
bridge.Indexed => {
|
||||
const configuration = v8.IndexedPropertyHandlerConfiguration{
|
||||
.getter = value.getter,
|
||||
};
|
||||
template_proto.setIndexedProperty(configuration, null);
|
||||
},
|
||||
bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{
|
||||
.getter = value.getter,
|
||||
.setter = value.setter,
|
||||
.deleter = value.deleter,
|
||||
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
|
||||
}, null),
|
||||
bridge.Iterator => {
|
||||
// Same as a function, but with a specific name
|
||||
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
|
||||
const js_name = if (value.async)
|
||||
v8.Symbol.getAsyncIterator(isolate).toName()
|
||||
else
|
||||
v8.Symbol.getIterator(isolate).toName();
|
||||
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||
},
|
||||
bridge.Property => {
|
||||
const js_value = switch (value) {
|
||||
.int => |v| js.simpleZigValueToJs(isolate, v, true, false),
|
||||
};
|
||||
|
||||
const js_name = v8.String.initUtf8(isolate, name).toName();
|
||||
// apply it both to the type itself
|
||||
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
||||
|
||||
// and to instances of the type
|
||||
template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
||||
},
|
||||
bridge.Constructor => {}, // already handled in generateClasss
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (@hasDecl(JsApi.Meta, "htmldda")) {
|
||||
const instance_template = template.getInstanceTemplate();
|
||||
instance_template.markAsUndetectable();
|
||||
instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func);
|
||||
}
|
||||
}
|
||||
|
||||
// Even if a struct doesn't have a `constructor` function, we still
|
||||
// `generateConstructor`, because this is how we create our
|
||||
// FunctionTemplate. Such classes exist, but they can't be instantiated
|
||||
// via `new ClassName()` - but they could, for example, be created in
|
||||
// Zig and returned from a function call, which is why we need the
|
||||
// FunctionTemplate.
|
||||
fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate {
|
||||
const callback = blk: {
|
||||
if (@hasDecl(JsApi, "constructor")) {
|
||||
break :blk JsApi.constructor.func;
|
||||
}
|
||||
|
||||
break :blk struct {
|
||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const iso = caller.isolate;
|
||||
log.warn(.js, "Illegal constructor call", .{ .name = @typeName(JsApi) });
|
||||
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
|
||||
info.getReturnValue().set(js_exception);
|
||||
return;
|
||||
}
|
||||
}.wrap;
|
||||
};
|
||||
|
||||
const template = v8.FunctionTemplate.initCallback(isolate, callback);
|
||||
if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
||||
template.getInstanceTemplate().setInternalFieldCount(1);
|
||||
}
|
||||
const class_name = v8.String.initUtf8(isolate, if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi));
|
||||
template.setClassName(class_name);
|
||||
return template;
|
||||
}
|
||||
|
||||
pub fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
|
||||
@setEvalBranchQuota(2000);
|
||||
comptime {
|
||||
const T = JsApi.bridge.type;
|
||||
if (!@hasField(T, "_proto")) {
|
||||
return null;
|
||||
}
|
||||
const Ptr = std.meta.fieldInfo(T, ._proto).type;
|
||||
const F = @typeInfo(Ptr).pointer.child;
|
||||
return bridge.JsApiLookup.getId(F.JsApi);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user