mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 09:46:05 -04:00
Merge pull request #2897 from lightpanda-io/v8_max_memory
v8, mem: Add v8 configuration option + heap limit protection
This commit is contained in:
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.5.0'
|
||||
default: 'v0.5.1'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
|
||||
2
.github/actions/v8-snapshot/action.yml
vendored
2
.github/actions/v8-snapshot/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig-v8 release tag the prebuilt lib came from'
|
||||
required: false
|
||||
default: 'v0.5.0'
|
||||
default: 'v0.5.1'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM debian:stable-slim
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG V8=14.9.207.35
|
||||
ARG ZIG_V8=v0.5.0
|
||||
ARG ZIG_V8=v0.5.1
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.5.0.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH613pAgDFIxC9Xq1zCPVj2n-8Dxsm-TVMfSRVzq6Z",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.5.1.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH6_vtAgAdI8KeSyuBCgVOEFDN_0BfMekjdP6fkRqk",
|
||||
},
|
||||
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||
.brotli = .{
|
||||
|
||||
@@ -44,7 +44,7 @@ arena_pool: ArenaPool,
|
||||
app_dir_path: ?[]const u8,
|
||||
|
||||
pub fn init(allocator: Allocator, config: *const Config) !*App {
|
||||
const platform = try Platform.init();
|
||||
const platform = try Platform.init(config.v8Flags());
|
||||
errdefer platform.deinit();
|
||||
|
||||
const snapshot = try Snapshot.load();
|
||||
|
||||
@@ -114,6 +114,8 @@ const CommonOptions = .{
|
||||
.{ .name = "disable_subframes", .type = bool },
|
||||
.{ .name = "disable_workers", .type = bool },
|
||||
.{ .name = "enable_external_stylesheets", .type = bool },
|
||||
.{ .name = "v8_flags_unsafe", .type = ?[]const u8 },
|
||||
.{ .name = "v8_max_heap_mb", .type = ?u32 },
|
||||
};
|
||||
|
||||
fn dumpValidator(_: Allocator, args: *std.process.ArgIterator) !?DumpFormat {
|
||||
@@ -337,6 +339,20 @@ pub fn enableExternalStylesheets(self: *const Config) bool {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn v8Flags(self: *const Config) ?[]const u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| opts.v8_flags_unsafe,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn v8MaxHeapMb(self: *const Config) ?u32 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| opts.v8_max_heap_mb,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpProxy(self: *const Config) ?[:0]const u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| opts.http_proxy,
|
||||
|
||||
@@ -115,6 +115,7 @@ pub fn init(self: *Browser, app: *App, opts: InitOpts, cdp: ?*CDP) !void {
|
||||
.fc_identity_pool = .init(allocator),
|
||||
.selector_cache = .init(allocator),
|
||||
};
|
||||
self.env.protectHeapLimit();
|
||||
try self.http_client.init(allocator, &app.network, cdp);
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,14 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
||||
|
||||
params.external_references = &snapshot.external_references;
|
||||
|
||||
if (app.config.v8MaxHeapMb()) |mb| {
|
||||
v8.v8__ResourceConstraints__ConfigureDefaultsFromHeapSize(
|
||||
¶ms.constraints,
|
||||
0,
|
||||
@as(usize, mb) * 1024 * 1024,
|
||||
);
|
||||
}
|
||||
|
||||
var isolate = js.Isolate.init(params);
|
||||
errdefer isolate.deinit();
|
||||
const isolate_handle = isolate.handle;
|
||||
@@ -391,7 +399,7 @@ pub fn runMicrotasks(self: *Env) void {
|
||||
|
||||
const v8_isolate = self.isolate.handle;
|
||||
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(v8_isolate)) {
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(v8_isolate) or self.terminatePending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -409,7 +417,7 @@ pub fn runMicrotasks(self: *Env) void {
|
||||
}
|
||||
|
||||
pub fn runMacrotasks(self: *Env) !void {
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(self.isolate.handle)) {
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(self.isolate.handle) or self.terminatePending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -542,6 +550,34 @@ pub fn terminate(self: *Env) void {
|
||||
v8.v8__Isolate__TerminateExecution(self.isolate.handle);
|
||||
}
|
||||
|
||||
// We need a stable pointer for *Env, so can't be setup in init.
|
||||
pub fn protectHeapLimit(self: *Env) void {
|
||||
v8.v8__Isolate__AddNearHeapLimitCallback(self.isolate.handle, nearHeapLimit, self);
|
||||
}
|
||||
|
||||
// v8 is telling us it's about to run out of memory for this isolate. We'll
|
||||
// do two things:
|
||||
// 1 - Terminate the execution (attempting to prevent v8 from OOM'ing the process)
|
||||
// 2 - Tell v8 that it can use 8MB more memory, hopefully giving it enough memory
|
||||
// to properly shutdown
|
||||
//
|
||||
// The terminate must go through requestTerminate (RequestInterrupt), NOT a
|
||||
// direct TerminateExecution: we're mid-GC, which is an arbitrary point —
|
||||
// possibly inside a microtask run — and flagging termination there lets the
|
||||
// run complete with a result while the isolate is terminating, tripping
|
||||
// V8's DCHECK(maybe_result.is_null()) in MicrotaskQueue::RunMicrotasks. The
|
||||
// interrupt lands at a stack-guard check, where termination unwinds the way
|
||||
// V8 expects.
|
||||
fn nearHeapLimit(data: ?*anyopaque, current_limit: usize, initial_limit: usize) callconv(.c) usize {
|
||||
const self: *Env = @ptrCast(@alignCast(data.?));
|
||||
log.err(.app, "JS heap limit reached", .{
|
||||
.initial_limit = initial_limit,
|
||||
.current_limit = current_limit,
|
||||
});
|
||||
self.requestTerminate();
|
||||
return current_limit + 8 * 1024 * 1024;
|
||||
}
|
||||
|
||||
// Called from the network thread, caused v8 to eventually call terminateInterrupt
|
||||
pub fn requestTerminate(self: *Env) void {
|
||||
self.terminate_requested.store(true, .release);
|
||||
|
||||
@@ -22,7 +22,11 @@ const v8 = js.v8;
|
||||
const Platform = @This();
|
||||
handle: *v8.Platform,
|
||||
|
||||
pub fn init() !Platform {
|
||||
pub fn init(v8_flags: ?[]const u8) !Platform {
|
||||
if (v8_flags) |flags| {
|
||||
v8.v8__V8__SetFlagsFromString(flags.ptr, flags.len);
|
||||
}
|
||||
|
||||
if (v8.v8__V8__InitializeICU() == false) {
|
||||
return error.FailedToInitializeICU;
|
||||
}
|
||||
|
||||
@@ -239,6 +239,14 @@ pub fn sendJSON(self: *CDP, message: anytype) !void {
|
||||
}
|
||||
|
||||
pub fn tick(self: *CDP) !bool {
|
||||
// terminatePending means someone decided this browser must die (e.g. the
|
||||
// heap limit was reached). Nothing in the CDP path ever calls cancelTerminate
|
||||
// so the flag can't be a stale leftover here. Exit.
|
||||
if (self.browser.env.terminatePending()) {
|
||||
log.warn(.cdp, "closing connection", .{ .reason = "pending terminate" });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Liveness is enforced by TCP keepalive configured in
|
||||
// Server.configureSocket; the wakeup lets V8 run or terminate.
|
||||
const wait_ms: u32 = 1000; // 1s
|
||||
|
||||
10
src/help.zon
10
src/help.zon
@@ -338,6 +338,16 @@
|
||||
\\ still sends Sec-Ch-Ua. Incompatible with --user-agent-suffix.
|
||||
\\ --user-agent-suffix <STRING>
|
||||
\\ Suffix appended to the Lightpanda/X.Y User-Agent.
|
||||
\\ --v8-flags-unsafe <FLAGS>
|
||||
\\ Flags passed as-is to the V8 JavaScript engine, space-separated.
|
||||
\\ e.g. --v8-flags-unsafe "--expose-gc --stack-size 1000".
|
||||
\\ Unsupported escape hatch: V8 does not validate flags against the
|
||||
\\ prebuilt snapshot, so an incompatible flag can misbehave or
|
||||
\\ crash at any point.
|
||||
\\ --v8-max-heap-mb <INT>
|
||||
\\ Maximum V8 heap size in megabytes, per browser instance. Values
|
||||
\\ below ~16 are clamped by V8 and all behave the same.
|
||||
\\ Defaults to the V8 default (based on available memory).
|
||||
\\ --web-bot-auth-domain <DOMAIN>
|
||||
\\ Your domain, e.g. yourdomain.com.
|
||||
\\ --web-bot-auth-key-file <PATH>
|
||||
|
||||
@@ -22,7 +22,26 @@ const lp = @import("lightpanda");
|
||||
pub fn main() !void {
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
var platform = try lp.js.Platform.init();
|
||||
// usage: snapshot_creator [--v8-flags-unsafe "<flags>"] [outfile]
|
||||
// A snapshot only deserializes correctly under the flags it was created
|
||||
// with, so a runtime using --v8-flags-unsafe needs a snapshot built with
|
||||
// the same value.
|
||||
var v8_flags: ?[]const u8 = null;
|
||||
var out_path: ?[]const u8 = null;
|
||||
var args = try std.process.argsWithAllocator(allocator);
|
||||
_ = args.next(); // executable name
|
||||
while (args.next()) |arg| {
|
||||
if (std.mem.eql(u8, arg, "--v8-flags-unsafe")) {
|
||||
v8_flags = args.next() orelse {
|
||||
std.debug.print("--v8-flags-unsafe requires a value\n", .{});
|
||||
return error.MissingArgument;
|
||||
};
|
||||
} else {
|
||||
out_path = arg;
|
||||
}
|
||||
}
|
||||
|
||||
var platform = try lp.js.Platform.init(v8_flags);
|
||||
defer platform.deinit();
|
||||
|
||||
const snapshot = try lp.js.Snapshot.create();
|
||||
@@ -30,9 +49,7 @@ pub fn main() !void {
|
||||
|
||||
var is_stdout = true;
|
||||
var file = std.fs.File.stdout();
|
||||
var args = try std.process.argsWithAllocator(allocator);
|
||||
_ = args.next(); // executable name
|
||||
if (args.next()) |n| {
|
||||
if (out_path) |n| {
|
||||
is_stdout = false;
|
||||
file = try std.fs.cwd().createFile(n, .{});
|
||||
}
|
||||
|
||||
@@ -143,6 +143,7 @@ pub fn init(
|
||||
// + terminate/microtask carrier; the agent context is bare (no WebAPIs).
|
||||
self.env = lp.js.Env.init(app, .{}) catch return error.RuntimeInitFailed;
|
||||
errdefer self.env.deinit();
|
||||
self.env.protectHeapLimit();
|
||||
|
||||
try self.createContext();
|
||||
errdefer self.resetContext();
|
||||
|
||||
Reference in New Issue
Block a user