mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
At a high level, this does for Events what was recently done for XHR, Fetch and
Observers. Events are self-contained in their own arena from the ArenaPool and
are registered with v8 to be finalized.
But events are more complicated than those other types. For one, events have
a prototype chain. (XHR also does, but it's always the top-level object that's
created, whereas it's valid to create a base Event or something that inherits
from Event). But the _real_ complication is that Events, unlike previous types,
can be created from Zig or from V8.
This is something that Fetch had to deal with too, because the Response is only
given to V8 on success. So in Fetch, there's a period of time where Zig is
solely responsible for the Response, until it's passed to v8. But with events
it's a lot more subtle.
There are 3 possibilities:
1 - An Event is created from v8. This is the simplest, and it simply becomes a
a weak reference for us. When v8 is done with it, the finalizer is called.
2 - An Event is created in Zig (e.g. window.load) and dispatched to v8. Again
we can rely on the v8 finalizer.
3 - An event is created in Zig, but not dispatched to v8 (e.g. there are no
listeners), Zig has to release the event.
(It's worth pointing out that one thing that still keeps this relatively
straightforward is that we never hold on to Events past some pretty clear point)
Now, it would seem that #3 is the only issue we have to deal with, and maybe
we can do something like:
```
if (event_manager.hasListener("load", capture)) {
try event_manager.dispatch(event);
} else {
event.deinit();
}
```
In fact, in many cases, we could use this to optimize not even creating the
event:
```
if (event_manager.hasListener("load, capture)) {
const event = try createEvent("load", capture);
try event_manager.dispatch(event);
}
```
And that's an optimization worth considering, but it isn't good enough to
properly manage memory. Do you see the issue? There could be a listener (so we
think v8 owns it), but we might never give the value to v8. Any failure between
hasListener and actually handing the value to v8 would result in a leak.
To solve this, the bridge will now set a _v8_handover flag (if present) once it
has created the finalizer_callback entry. So dispatching code now becomes:
```
const event = try createEvent("load", capture);
defer if (!event._v8_handover) event.deinit(false);
try event_manager.dispatch(event);
```
The v8 finalizer callback was also improved. Previously, we just embedded the
pointer to the zig object. In the v8 callback, we could cast that back to T
and call deinit. But, because of possible timing issues between when (if) v8
calls the finalizer, and our own cleanup, the code would check in the context to
see if the ptr was still valid. Wait, what? We're using the ptr to get the
context to see if the ptr is valid?
We now store a pointer to the FinalizerCallback which contains the context.
So instead of something stupid like:
```
// note, if the identity_map doesn't contain the value, then value is likely
// invalid, and value.page will segfault
value.page.js.identity_map.contains(@intFromPtr(value))
```
We do:
```
if (fc.ctx.finalizer_callbacks.contains(@intFromPtr(fc.value)) {
// fc.value is safe to use
}
```
134 lines
4.5 KiB
Zig
134 lines
4.5 KiB
Zig
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
|
//
|
|
// Francis Bouvier <francis@lightpanda.io>
|
|
// Pierre Tachoire <pierre@lightpanda.io>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
const std = @import("std");
|
|
const lp = @import("lightpanda");
|
|
const builtin = @import("builtin");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const log = lp.log;
|
|
const App = lp.App;
|
|
const Config = lp.Config;
|
|
const SigHandler = @import("Sighandler.zig");
|
|
pub const panic = lp.crash_handler.panic;
|
|
|
|
pub fn main() !void {
|
|
// allocator
|
|
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
|
|
// - in Release mode we use the c allocator
|
|
var gpa_instance: std.heap.DebugAllocator(.{ .stack_trace_frames = 10 }) = .init;
|
|
const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator;
|
|
|
|
defer if (builtin.mode == .Debug) {
|
|
if (gpa_instance.detectLeaks()) std.posix.exit(1);
|
|
};
|
|
|
|
// arena for main-specific allocations
|
|
var main_arena_instance = std.heap.ArenaAllocator.init(gpa);
|
|
const main_arena = main_arena_instance.allocator();
|
|
defer main_arena_instance.deinit();
|
|
|
|
run(gpa, main_arena) catch |err| {
|
|
log.fatal(.app, "exit", .{ .err = err });
|
|
std.posix.exit(1);
|
|
};
|
|
}
|
|
|
|
fn run(allocator: Allocator, main_arena: Allocator) !void {
|
|
const args = try Config.parseArgs(main_arena);
|
|
defer args.deinit(main_arena);
|
|
|
|
switch (args.mode) {
|
|
.help => {
|
|
args.printUsageAndExit(args.mode.help);
|
|
return std.process.cleanExit();
|
|
},
|
|
.version => {
|
|
std.debug.print("{s}\n", .{lp.build_config.git_commit});
|
|
return std.process.cleanExit();
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
if (args.logLevel()) |ll| {
|
|
log.opts.level = ll;
|
|
}
|
|
if (args.logFormat()) |lf| {
|
|
log.opts.format = lf;
|
|
}
|
|
if (args.logFilterScopes()) |lfs| {
|
|
log.opts.filter_scopes = lfs;
|
|
}
|
|
|
|
// _app is global to handle graceful shutdown.
|
|
var app = try App.init(allocator, &args);
|
|
|
|
defer app.deinit();
|
|
app.telemetry.record(.{ .run = {} });
|
|
|
|
switch (args.mode) {
|
|
.serve => |opts| {
|
|
var sighandler = SigHandler{ .arena = main_arena };
|
|
try sighandler.install();
|
|
|
|
log.debug(.app, "startup", .{ .mode = "serve", .snapshot = app.snapshot.fromEmbedded() });
|
|
const address = std.net.Address.parseIp(opts.host, opts.port) catch |err| {
|
|
log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port });
|
|
return args.printUsageAndExit(false);
|
|
};
|
|
|
|
// _server is global to handle graceful shutdown.
|
|
var server = try lp.Server.init(app, address);
|
|
defer server.deinit();
|
|
|
|
try sighandler.on(lp.Server.stop, .{&server});
|
|
|
|
// max timeout of 1 week.
|
|
const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(u32, opts.timeout) * 1000;
|
|
server.run(address, timeout) catch |err| {
|
|
log.fatal(.app, "server run error", .{ .err = err });
|
|
return err;
|
|
};
|
|
},
|
|
.fetch => |opts| {
|
|
const url = opts.url;
|
|
log.debug(.app, "startup", .{ .mode = "fetch", .dump = opts.dump, .url = url, .snapshot = app.snapshot.fromEmbedded() });
|
|
|
|
var fetch_opts = lp.FetchOpts{
|
|
.wait_ms = 5000,
|
|
.dump = .{
|
|
.strip = opts.strip,
|
|
.with_base = opts.withbase,
|
|
},
|
|
};
|
|
|
|
var stdout = std.fs.File.stdout();
|
|
var writer = stdout.writer(&.{});
|
|
if (opts.dump) {
|
|
fetch_opts.writer = &writer.interface;
|
|
}
|
|
|
|
lp.fetch(app, url, fetch_opts) catch |err| {
|
|
log.fatal(.app, "fetch error", .{ .err = err, .url = url });
|
|
return err;
|
|
};
|
|
},
|
|
else => unreachable,
|
|
}
|
|
}
|