git.lucas.co / cce-browser
web browser (Servo)
git clone https://git.lucas.co/cce-browser.git

commit64f54796b0865dea4e0bb5e82ba5cea79bd1d9ed
parentb3e3e8abea
authorLucas Galante <[email protected]>
date2026-09-01 10:22
perf: cce-browser-open, a slim forwarder for the click-to-tab path

Forwarding a link through the full binary costs ~20ms of dynamic-library
loading (libWPEWebKit and friends) before main() runs. The desktop entry
now execs cce-browser-open: ~500KB, links only libc, forwards in ~4ms,
and execs the real browser when no instance answers. It duplicates the
tiny client protocol on purpose — importing instance.rs or cce_ui would
drag the link flags back in.

Co-Authored-By: Claude Fable 5 <[email protected]>

 CLAUDE.md           | 14 ++++++++---
 Cargo.toml          |  9 +++++++
 cce-browser.desktop |  2 +-
 src/bin/open.rs     | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 90 insertions(+), 5 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 0ca02e3..ffec663 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -13,12 +13,13 @@ truth, there is no push remote). Read the workspace-level
 `../cce-compositor/WORKSPACE.md` first: workspace layout, the `cce-ui` toolkit, config
 conventions, and the multi-repo rules all live there.
 
-Six files, ~2.8k lines:
+Seven files, ~2.9k lines:
 
 | file | what it owns |
 | --- | --- |
 | `src/main.rs` | `BrowserApp` — the `cce-ui` `Application`: chrome layout, hit-testing, the URL line editor, key/pointer routing |
 | `src/instance.rs` | single-instance forwarding: a later launch hands its argument to the running instance's socket and exits |
+| `src/bin/open.rs` | `cce-browser-open`, the desktop entry's `Exec` target: a ~500KB forwarder linking only libc (~4ms vs ~22ms through the full binary), exec'ing `cce-browser` when no instance answers |
 | `src/webview.rs` | `ServoHost` — Servo boot, the delegate, one `WebView` per tab, the frame pipeline |
 | `src/pages.rs` | the `cce:` protocol handler and its History / Bookmarks stores |
 | `src/downloads.rs` | the chrome-side download pipeline (Servo has none) |
@@ -57,12 +58,17 @@ trait, the input-event constructors, and `Preferences`/`Opts` to be where it bro
 
 ## Single instance
 
-An external open (`xdg-open` → the desktop entry's `cce-browser %u`) spawns a
-fresh process per link. `src/instance.rs` turns that into a tab: `main()` tries
+An external open (the desktop entry's `%u`) spawns a fresh process per link.
+`src/instance.rs` turns that into a tab: `main()` tries
 `/tmp/cce-browser-<WAYLAND_DISPLAY>.sock` (the standard `cce_ui::ipc`
 convention; display keying isolates shadow sessions) before any engine or
 Wayland work, forwards `open <arg>` / `new-tab` and exits on success, or binds
-the socket and becomes the instance. The listener thread pushes
+the socket and becomes the instance. The desktop entry's `Exec` is
+**`cce-browser-open`** (`src/bin/open.rs`), a forwarder that links only libc —
+the full binary spends ~20ms loading libWPEWebKit before `main()` runs, the
+slim bin forwards in ~4ms — and execs `cce-browser` when nothing answers. It
+deliberately duplicates the tiny client protocol rather than import anything;
+keep it, `instance.rs`, and the socket-path convention in agreement. The listener thread pushes
 `Message::OpenExternal` into calloop; `update()` parses the relayed argument
 with `parse_startup_arg` — it *is* a launch argument, so the URL bar's
 domain-guess parsing stays wrong for it — and a forwarded relative file path is
diff --git a/Cargo.toml b/Cargo.toml
index cdf799a..060e5dd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,6 +39,15 @@ rustix = { version = "0.38", features = ["event"], optional = true }
 bindgen = "0.72"
 pkg-config = "0.3"
 
+# The desktop entry's Exec target: a forwarder that links (almost) nothing,
+# so the click-to-tab path skips the ~20ms of library loading the full
+# binary pays before main(). Explicit [[bin]] only for the name — src/bin/
+# autodiscovery would call it `open`. ccebuild installs it automatically
+# via cargo metadata; no Makefile involvement.
+[[bin]]
+name = "cce-browser-open"
+path = "src/bin/open.rs"
+
 # The WPE examples need the generated bindings, so they only build with
 # the feature — otherwise `cargo test` would try them on a default build.
 
diff --git a/cce-browser.desktop b/cce-browser.desktop
index 0532434..6b07de2 100644
--- a/cce-browser.desktop
+++ b/cce-browser.desktop
@@ -3,7 +3,7 @@ Type=Application
 Name=Browser
 GenericName=Web Browser
 Comment=Browse the web
-Exec=cce-browser %u
+Exec=cce-browser-open %u
 Icon=cce-browser
 Terminal=false
 Categories=Network;WebBrowser;
diff --git a/src/bin/open.rs b/src/bin/open.rs
new file mode 100644
index 0000000..6180f8e
--- /dev/null
+++ b/src/bin/open.rs
@@ -0,0 +1,70 @@
+//! `cce-browser-open` — the launch-latency half of single-instance.
+//!
+//! The desktop entry's `Exec` points here, not at `cce-browser`: forwarding a
+//! link through the full browser binary costs ~20ms of dynamic-library loading
+//! (libWPEWebKit and friends) before `main()` runs, all on the click-to-tab
+//! path. This bin exists to link nothing, forward in a few ms, and only
+//! `exec` the real browser when no instance answers.
+//!
+//! It therefore deliberately duplicates the client half of the socket
+//! protocol instead of importing it: `src/instance.rs` (same crate) is the
+//! server side and the fallback client, `cce_ui::ipc::socket_path` is the
+//! path convention. All three must agree on `/tmp/cce-browser-<display>.sock`
+//! and the `open <arg>` / `new-tab` lines. The protocol is small on purpose;
+//! change it in both files or not at all.
+
+use std::io::{BufRead, BufReader, Write};
+use std::os::unix::net::UnixStream;
+use std::os::unix::process::CommandExt;
+
+fn socket_path() -> String {
+    // Mirrors cce_ui::ipc::socket_path("cce-browser") — not imported, so this
+    // bin stays free of cce-ui's native link flags.
+    match std::env::var("WAYLAND_DISPLAY") {
+        Ok(d) if !d.is_empty() => format!("/tmp/cce-browser-{d}.sock"),
+        _ => "/tmp/cce-browser.sock".to_string(),
+    }
+}
+
+/// One forwarding attempt; false on any failure. Mirrors
+/// `instance::try_forward`, ack wait included — exiting on write alone races
+/// the instance actually reading the line.
+fn try_forward(arg: Option<&str>) -> bool {
+    let Ok(mut stream) = UnixStream::connect(socket_path()) else {
+        return false;
+    };
+    let command = match arg {
+        Some(a) => {
+            // A relative file path is resolved against *this* process's cwd —
+            // the instance's differs, so it must travel absolute.
+            let p = std::path::Path::new(a);
+            let abs = if p.exists() {
+                std::fs::canonicalize(p)
+                    .ok()
+                    .and_then(|c| c.to_str().map(String::from))
+            } else {
+                None
+            };
+            format!("open {}\n", abs.as_deref().unwrap_or(a))
+        }
+        None => "new-tab\n".to_string(),
+    };
+    if stream.write_all(command.as_bytes()).is_err() {
+        return false;
+    }
+    let mut reply = String::new();
+    BufReader::new(stream).read_line(&mut reply).is_ok()
+}
+
+fn main() {
+    let args: Vec<String> = std::env::args().skip(1).collect();
+    if try_forward(args.first().map(String::as_str)) {
+        return;
+    }
+    // No instance answered: become one. PATH resolution matches the desktop
+    // entry convention (~/.local/bin first); the browser runs its own
+    // forward_or_claim, which settles any launch race from here on.
+    let err = std::process::Command::new("cce-browser").args(&args).exec();
+    eprintln!("cce-browser-open: could not exec cce-browser: {err}");
+    std::process::exit(1);
+}