mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-09 12:51:45 -04:00
serve: Improve cdp/bidi server
Significant rework of the CDP/BiDi server. There are two main changes: 1 - poll replaced with EPoll/Kqueue (1) 2 - make http serving a first class citizen The change from poll -> epoll/kqueue isn't performance driven, it's just about tighter code. Both epoll and kqueue let you associate arbitrary data with a socket, so we don't need to keep arrays in sync in order to associate a socket with a CDP by index. They both provide some event/notification mechanism, which is cleaner than the pipe required by poll. The poll -> epoll/kqueue change could almost have been mechanical. Making HTTP a first class citizen is the more significant of the two changes In `main`, a new connection always spawns a thread and, until does its own little read loop until the connection is upgraded. This is not efficient, it uses up a connection slot, and it's inconsistent with the final WebSocket connection which _is_ polled off the main loop. Using up a slot means that keepalive isn't possible, else HTTP connections would quickly use up all available slots/threads. This commit parses and serves HTTP requests on the main thread (safe because none of the processing is blocking). The approach is better streamlined for HTTP requests which never upgrade (/metrics, WebDriver) without causing any performance overhead for those that do. It simplifies some things (e.g. an "http" socket or a "websocket" socket is monitored and read in a similar manner (on the main loop)). It makes other things more complicated; the flow is no longer accept -> spawn -> upgrade -> websocket loop. It's loop -> accept -> loop -> process -> (http | ws). This is built ontop of the BiDi branch because (a) WebDriver is what needs better HTTP support and (b) some of the more mechanical changes already exist in that branch (e.g. src/cdp/, src/server.zig -> src/server/*) (1) kqueue landing in 2 commits from now on this branch.
This commit is contained in:
11 files changed
+2155
-1686
No files matched your search
+50
-39
@@ -19,63 +19,64 @@
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const WS = @import("../network/WS.zig");
|
||||
const Inbox = @import("../Inbox.zig");
|
||||
|
||||
const CDP = @import("cdp/CDP.zig");
|
||||
const Server = @import("Server.zig");
|
||||
const BiDi = @import("bidi/BiDi.zig");
|
||||
const Connection = @import("Connection.zig");
|
||||
const Browser = @import("../browser/Browser.zig");
|
||||
const Session = @import("../browser/Session.zig");
|
||||
|
||||
const WS = @import("WS.zig");
|
||||
const Link = @import("Link.zig");
|
||||
|
||||
const CDP = @import("cdp/CDP.zig");
|
||||
const BiDi = @import("bidi/BiDi.zig");
|
||||
|
||||
const log = lp.log;
|
||||
|
||||
// Parts of the driver are owned by the server run loop, parts are owned by
|
||||
// Parts of the driver are owned by the server loop, parts are owned by
|
||||
// the worker thread. The run loop reads messages and pushes to the inbox,
|
||||
// the worker mostly just writes to the socket.
|
||||
//
|
||||
// What every protocol has - a connection, a browser, a link to the network
|
||||
// thread - lives here rather than behind `impl`, so the shared paths are plain
|
||||
// field access. Only what genuinely differs switches on `impl`.
|
||||
const Driver = @This();
|
||||
|
||||
pub const Impl = union(enum) {
|
||||
// Doubles as the metrics label
|
||||
pub const Protocol = enum { cdp, bidi };
|
||||
|
||||
pub const Impl = union(Protocol) {
|
||||
cdp: *CDP,
|
||||
bidi: *BiDi,
|
||||
};
|
||||
|
||||
impl: Impl,
|
||||
conn: *Connection,
|
||||
|
||||
// every implementation has this
|
||||
conn: *Link,
|
||||
browser: *Browser,
|
||||
link: *Server.Link,
|
||||
|
||||
// The protocol's log scope, so shared code still logs as .cdp / .bidi.
|
||||
scope: log.Scope,
|
||||
|
||||
// Called from CDP.init / BiDi.init, where conn, link and browser are all
|
||||
// still undefined: we only take their addresses, which the impl's own
|
||||
// allocation already fixed.
|
||||
// Called from CDP.init / BiDi.init, where conn and browser are both still
|
||||
// undefined: we only take their addresses, which the impl's own allocation
|
||||
// already fixed.
|
||||
pub fn init(impl: Impl) Driver {
|
||||
return switch (impl) {
|
||||
// The tag names line up with the log scopes of the same name.
|
||||
inline else => |d, tag| .{
|
||||
.impl = impl,
|
||||
.conn = &d.conn,
|
||||
.link = &d.link,
|
||||
.browser = &d.browser,
|
||||
.scope = @field(log.Scope, @tagName(tag)),
|
||||
.scope = @field(log.Scope, @tagName(tag)), // The tag names line up with the log scopes of the same name.
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Server run loop. Received data, driver returns false to signal it should
|
||||
// disconnect.
|
||||
pub fn onData(self: *const Driver, data: []const u8) anyerror!bool {
|
||||
return self.conn.feed(data);
|
||||
// server loop. The socket is readable, drain up to budget bytes
|
||||
pub fn onReadable(self: *const Driver, budget: usize) anyerror!bool {
|
||||
const read = try self.conn.readAvailable(budget);
|
||||
if (read.pushed) {
|
||||
self.wakeup();
|
||||
}
|
||||
return read.keep;
|
||||
}
|
||||
|
||||
// Server run loop. Called when it drops the link unsolicited (peer EOF, ...)
|
||||
// server loop. Called when it drops the link unsolicited (peer EOF, ...)
|
||||
pub fn onLinkDisconnect(self: *const Driver, err: ?anyerror) void {
|
||||
const arena = self.browser.arena_pool.acquire(.tiny, "driver disconnect") catch |e| switch (e) {
|
||||
error.OutOfMemory => @panic("OOM"),
|
||||
@@ -84,6 +85,23 @@ pub fn onLinkDisconnect(self: *const Driver, err: ?anyerror) void {
|
||||
// when tick() discovers the terminatePending flag is set.
|
||||
self.browser.http_client.inbox.push(arena, .{ .disconnect = err });
|
||||
self.browser.env.requestTerminate();
|
||||
self.wakeup();
|
||||
}
|
||||
|
||||
// server loop. We used to send a nice WS close frame here but (a) it isn't strictly
|
||||
// required and (b) we'd have to protect against an interleaved write from
|
||||
// the worker thread.
|
||||
pub fn shutdown(self: *const Driver) void {
|
||||
self.browser.env.terminate();
|
||||
self.conn.shutdown();
|
||||
}
|
||||
|
||||
// a server-processed call (onReadable, onLinkDisconnect) wants to signal the
|
||||
// worker that there's data in its inbox waiting to be processed.
|
||||
fn wakeup(self: *const Driver) void {
|
||||
self.browser.http_client.handles.wakeup() catch |err| {
|
||||
log.err(self.scope, "wakeup", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
// Worker thread. We're processing messages from the inbox.
|
||||
@@ -136,12 +154,16 @@ pub fn run(self: *const Driver) void {
|
||||
// One iteration of the worker loop. Returns false to disconnect.
|
||||
fn tick(self: *const Driver) !bool {
|
||||
if (self.browser.env.terminatePending()) {
|
||||
// Maybe something bad happened (e.g. watchdog) or maybe the client
|
||||
// just disconnected. Check the inbox to see if there's a disconnect
|
||||
// message and, if so, it'll handle it directly.
|
||||
// Our own requestTerminate from onLinkDisconnect: the peer is gone or
|
||||
// sent garbage. Report it with its own close code, nothing to warn
|
||||
// about. Pops close/disconnect only: nothing else may be dispatched
|
||||
// in a shutting-down state.
|
||||
self.browser.http_client.drainTerminal() catch |err| switch (err) {
|
||||
error.ClientDisconnected => return false,
|
||||
};
|
||||
|
||||
// Anything else means someone decided this browser must die (e.g.
|
||||
// shutdown, or the heap limit was reached).
|
||||
log.warn(self.scope, "closing connection", .{ .reason = "pending terminate" });
|
||||
// The worker thread is the sole writer of this socket, so sending
|
||||
// the close frame here can't interleave with another write.
|
||||
@@ -195,14 +217,3 @@ fn pageWait(self: *const Driver) ?PageWait {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// signal handler thread
|
||||
pub fn shutdown(self: *const Driver) void {
|
||||
if (self.conn.state == .live) {
|
||||
self.browser.env.terminate();
|
||||
// We use to send a nice WS close frame here but (a) it isn't
|
||||
// strictly required and (b) we'd have to protect against an interleaved
|
||||
// write from the worker thread.
|
||||
}
|
||||
self.conn.shutdown();
|
||||
}
|
||||
Reference in new issue
Block a user