Expose v8 CpuProfiler + add fast properties for some window properties

First, this exposes the v8 Profiler. Right now it's just a commented-out block
in `fetch` and meant for internal debugging.
Depends on: https://github.com/lightpanda-io/zig-v8-fork/pull/105

Use postAttach on Window to attach "static" properties. This comes from
profiling (lightpanda.io) and seeing window.get_self called tens of thousands
of times.
This commit is contained in:
Karl Seguin
2025-10-10 19:48:50 +08:00
parent c502bd901e
commit c381e4153d
6 changed files with 64 additions and 40 deletions

View File

@@ -5,8 +5,8 @@
.fingerprint = 0xda130f3af836cea0, .fingerprint = 0xda130f3af836cea0,
.dependencies = .{ .dependencies = .{
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/5ce9ade6c6d7f7ab9ab4582a6b1b36fdc8e246e2.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/305bb3706716d32d59b2ffa674731556caa1002b.tar.gz",
.hash = "v8-0.0.0-xddH6yTGAwA__kfiDozsVXL2_jnycrtDUfR8kIADQFUz", .hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
}, },
//.v8 = .{ .path = "../zig-v8-fork" } //.v8 = .{ .path = "../zig-v8-fork" }
}, },

View File

@@ -136,14 +136,6 @@ pub const Window = struct {
self.onload_callback = null; self.onload_callback = null;
} }
pub fn get_window(self: *Window) *Window {
return self;
}
pub fn get_navigator(self: *Window) *Navigator {
return &self.navigator;
}
pub fn get_location(self: *Window) *Location { pub fn get_location(self: *Window) *Location {
return &self.location; return &self.location;
} }
@@ -152,22 +144,6 @@ pub const Window = struct {
return page.navigateFromWebAPI(url, .{ .reason = .script }); return page.navigateFromWebAPI(url, .{ .reason = .script });
} }
pub fn get_console(self: *Window) *Console {
return &self.console;
}
pub fn get_crypto(self: *Window) *Crypto {
return &self.crypto;
}
pub fn get_self(self: *Window) *Window {
return self;
}
pub fn get_parent(self: *Window) *Window {
return self;
}
// frames return the window itself, but accessing it via a pseudo // frames return the window itself, but accessing it via a pseudo
// array returns the Window object corresponding to the given frame or // array returns the Window object corresponding to the given frame or
// iframe. // iframe.
@@ -205,10 +181,6 @@ pub const Window = struct {
return frames.get_length(); return frames.get_length();
} }
pub fn get_top(self: *Window) *Window {
return self;
}
pub fn get_document(self: *Window) ?*parser.DocumentHTML { pub fn get_document(self: *Window) ?*parser.DocumentHTML {
return self.document; return self.document;
} }
@@ -243,14 +215,6 @@ pub const Window = struct {
return &self.storage_shelf.?.bucket.session; return &self.storage_shelf.?.bucket.session;
} }
pub fn get_performance(self: *Window) *Performance {
return &self.performance;
}
pub fn get_screen(self: *Window) *Screen {
return &self.screen;
}
pub fn get_CSS(self: *Window) *Css { pub fn get_CSS(self: *Window) *Css {
return &self.css; return &self.css;
} }
@@ -463,6 +427,18 @@ pub const Window = struct {
// and thus the target has already been set to the document. // and thus the target has already been set to the document.
return self.base.redispatchEvent(evt); return self.base.redispatchEvent(evt);
} }
pub fn postAttach(self: *Window, js_this: js.This) !void {
try js_this.set("top", self, .{});
try js_this.set("self", self, .{});
try js_this.set("parent", self, .{});
try js_this.set("window", self, .{});
try js_this.set("crypto", &self.crypto, .{});
try js_this.set("screen", &self.screen, .{});
try js_this.set("console", &self.console, .{});
try js_this.set("navigator", &self.navigator, .{});
try js_this.set("performance", &self.performance, .{});
}
}; };
const TimerCallback = struct { const TimerCallback = struct {

View File

@@ -29,6 +29,8 @@ isolate: v8.Isolate,
v8_context: v8.Context, v8_context: v8.Context,
handle_scope: ?v8.HandleScope, handle_scope: ?v8.HandleScope,
cpu_profiler: ?v8.CpuProfiler = null,
// references Env.templates // references Env.templates
templates: []v8.FunctionTemplate, templates: []v8.FunctionTemplate,
@@ -1819,6 +1821,11 @@ fn zigJsonToJs(isolate: v8.Isolate, v8_context: v8.Context, value: std.json.Valu
} }
} }
pub fn getGlobalThis(self: *Context) js.This {
const js_global = self.v8_context.getGlobal();
return .{ .obj = .{ .js_obj = js_global, .context = self } };
}
// == Misc == // == Misc ==
// An interface for types that want to have their jsDeinit function to be // An interface for types that want to have their jsDeinit function to be
// called when the call context ends // called when the call context ends
@@ -1847,3 +1854,26 @@ const DestructorCallback = struct {
self.destructorFn(self.ptr); self.destructorFn(self.ptr);
} }
}; };
// == Profiler ==
pub fn startCpuProfiler(self: *Context) void {
if (builtin.mode != .Debug) {
// Still testing this out, don't have it properly exposed, so add this
// guard for the time being to prevent any accidental/weird prod issues.
@compileError("CPU Profiling is only available in debug builds");
}
std.debug.assert(self.cpu_profiler == null);
v8.CpuProfiler.useDetailedSourcePositionsForProfiling(self.isolate);
const cpu_profiler = v8.CpuProfiler.init(self.isolate);
const title = self.isolate.initStringUtf8("v8_cpu_profile");
cpu_profiler.startProfiling(title);
self.cpu_profiler = cpu_profiler;
}
pub fn stopCpuProfiler(self: *Context) ![]const u8 {
const title = self.isolate.initStringUtf8("v8_cpu_profile");
const profile = self.cpu_profiler.?.stopProfiling(title) orelse unreachable;
const serialized = profile.serialize(self.isolate).?;
return self.jsStringToZig(serialized, .{});
}

View File

@@ -27,7 +27,7 @@ pub fn setIndex(self: Object, index: u32, value: anytype, opts: SetOpts) !void {
return self.set(key, value, opts); return self.set(key, value, opts);
} }
pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) !void { pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{ FailedToSet, OutOfMemory }!void {
const context = self.context; const context = self.context;
const js_key = v8.String.initUtf8(context.isolate, key); const js_key = v8.String.initUtf8(context.isolate, key);

View File

@@ -849,7 +849,7 @@ pub const Page = struct {
_ = self.session.browser.transfer_arena.reset(.{ .retain_with_limit = 4 * 1024 }); _ = self.session.browser.transfer_arena.reset(.{ .retain_with_limit = 4 * 1024 });
} }
// extracted because this sis called from tests to set things up. // extracted because this is called from tests to set things up.
pub fn setDocument(self: *Page, html_doc: *parser.DocumentHTML) !void { pub fn setDocument(self: *Page, html_doc: *parser.DocumentHTML) !void {
const doc = parser.documentHTMLToDocument(html_doc); const doc = parser.documentHTMLToDocument(html_doc);
try parser.documentSetDocumentURI(doc, self.url.raw); try parser.documentSetDocumentURI(doc, self.url.raw);

View File

@@ -165,6 +165,24 @@ fn run(alloc: Allocator) !void {
// page // page
const page = try session.createPage(); const page = try session.createPage();
// // Comment this out to get a profile of the JS code in v8/profile.json.
// // You can open this in Chrome's profiler.
// // I've seen it generate invalid JSON, but I'm not sure why. It only
// // happens rarely, and I manually fix the file.
// page.js.startCpuProfiler();
// defer {
// if (page.js.stopCpuProfiler()) |profile| {
// std.fs.cwd().writeFile(.{
// .sub_path = "v8/profile.json",
// .data = profile,
// }) catch |err| {
// log.err(.app, "profile write error", .{ .err = err });
// };
// } else |err| {
// log.err(.app, "profile error", .{ .err = err });
// }
// }
_ = page.navigate(url, .{}) catch |err| switch (err) { _ = page.navigate(url, .{}) catch |err| switch (err) {
error.UnsupportedUriScheme, error.UriMissingHost => { error.UnsupportedUriScheme, error.UriMissingHost => {
log.fatal(.app, "invalid fetch URL", .{ .err = err, .url = url }); log.fatal(.app, "invalid fetch URL", .{ .err = err, .url = url });