feat(servo): isolate profiles with hardware sidecars

This commit is contained in:
2026-07-09 20:27:22 -04:00
parent 78dc86b18e
commit c28ec2bee8
92 changed files with 10306 additions and 1485 deletions
+151 -81
View File
@@ -2,129 +2,199 @@
## Decision
ELY is a Servo-based browser. The page renderer lives in the application process and follows Servo's embedder model:
ELY runs one Servo process for each browser profile. Servo's configuration and site-data manager
are process-scoped, so the process boundary is the storage partition boundary for cookies, HTTP
cache, local storage, and other profile data.
```text
┌──────────────────────────── ELY App Process ────────────────────────────┐
│ │
│ GPUI chrome │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ Sidebar Toolbar Tabs Settings │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ Servo content host │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ Servo + WebView + RenderingContext │ │
│ │ notify_new_frame_ready -> repaint -> paint -> present │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
ELY App Process
GPUI chrome
WebSurfaceStore
Profile Runtime Broker
Profile A worker ── stdio IPC ──> Servo sidecar A ──> profiles/A/servo
Profile B worker ── stdio IPC ──> Servo sidecar B ──> profiles/B/servo
Private worker ── stdio IPC ──> Servo sidecar P ──> transient directory
```
The app process owns WebView lifecycle, navigation, input, permissions, frame readiness, and rendering context presentation.
Each sidecar binds to the first `ProfileId` it receives and rejects requests for another profile.
The parent broker also keys workers by `ProfileId + ProfileDataMode` and discards responses whose
scope no longer matches the tab session.
## Current Code Boundary
```text
WebSurfaceStore
-> GPUI NativeSurface
-> LiveRuntimeWorker
-> WebSurfaceRuntime
-> one LiveRuntimeWorker per profile scope
-> ServoLiveClient
-> ely_servo_sidecar live
-> SoftwareServoHost
-> servo::Servo + servo::WebView + servo::WindowRenderingContext
-> servo::Servo + servo::WebView
macOS
-> HardwareOffscreenContext (CGL + surfman)
-> IOSurface + Mach descriptor
-> CVPixelBuffer
-> GPUI surface element
Linux / Windows
-> SoftwareRenderingContext
-> RGBA frame
-> GPUI RenderImage
```
`ServoLiveClient` is now an in-process adapter over `ely_servo_host::SoftwareServoHost`. It shares one Servo runtime across profile scopes, creates multiple WebViews inside that runtime, and routes scroll, hover, click, keyboard, resize, zoom, navigation, and permissions through the `ServoHost` API.
The app process owns browser state, profile routing, input coalescing, frame cadence, and GPUI
presentation. A sidecar owns one Servo runtime, its WebViews, and the profile's persistent storage.
The line-delimited JSON protocol carries commands and frame metadata. Software frames append an
exact `width * height * 4` RGBA payload. macOS hardware frames set `rgba_byte_count` to zero and
select an imported IOSurface through `surface_handle` and `current_surface_id`. The Mach channel
transfers the IOSurface send right; the JSON port number is diagnostic metadata.
Normal page display now enters GPUI through `native_surface(...)`. GPUI creates a platform child surface for the content bounds, passes that raw handle into Servo's `WindowRenderingContext`, and presents with `paint_without_readback_with_completion`. The RGBA readback path remains inside `ely_servo_host` for low-level tests.
The protocol supports:
Current platform child surfaces:
- `handshake`: verify protocol version `2` before accepting browser commands.
- `ensure`: create or update a WebView, navigation, viewport, zoom, permissions, and input.
- `poll`: advance Servo and return pending frame or metadata state.
- `close`: destroy one tab's WebView.
- `shutdown`: acknowledge graceful process shutdown so Servo flushes profile storage.
- macOS: child `NSView` with an AppKit raw window handle.
- Windows: child `HWND` with a Win32 raw window handle.
- Linux/X11: child XCB window with XCB display/window handles.
- Linux/Wayland: child `wl_surface` attached as a `wl_subsurface` with Wayland display/surface handles.
`ensure` and `poll` carry `ready_surface_ids` and `pending_surface_ids`. The app distinguishes
completed imports from handles queued behind bounded importer backpressure. Cache entries retain
the IOSurface reference and keep only a weak reference to an active `CVPixelBuffer` backing. The
sidecar preserves pending publications, republishes a surface missing from both sets, and replays
the cached surface report once after an asynchronous import completes.
The release app locates an adjacent `ely_servo_sidecar` binary. `scripts/run_dev.sh` builds the
locked sidecar before starting the app and points the runtime at that binary. The macOS bundle
script places release executables in `Contents/MacOS`, installs `AppIcon.icns`, and verifies its
ad-hoc code signature. The native distribution script places adjacent release executables in one
directory for macOS, Linux, and Windows packaging inputs.
## Profile Data Contract
Persistent scopes use:
```text
<application-data-root>/profiles/<profile-id>/servo
```
The default Standard Profile stores its generated identity at
`<application-data-root>/profiles/default/profile-id`; later launches restore that identity before
constructing `BrowserCore`, so the persistent Servo path remains stable.
Transient scopes receive a `0700` directory under the user-specific ELY runtime directory on Linux
and the user-specific ELY cache directory on macOS and Windows. The root rejects symbolic links. A
root lock serializes creation and stale cleanup; each live directory holds an exclusive lease so
concurrent ELY instances preserve one another's data. Closing the final tab retires the profile
worker in the background, waits for sidecar shutdown, and then removes the directory. A new Private
scope starts immediately in a fresh random directory while earlier cleanup completes. Startup
removes unlocked crash remnants. A sidecar receives its directory at process launch and keeps it
for its process lifetime.
The integration gates prove these invariants with real Servo networking:
1. Cookie and local-storage values survive a graceful restart with the same directory.
2. A fresh directory starts with empty cookie and local-storage state.
3. Two simultaneous app-level profiles keep cookies, local storage, and HTTP cache isolated.
4. Reopening a tab in each profile observes only that profile's state and cached response.
5. Closing the final Private tab and reopening the same profile starts with empty site data.
## Upstream Servo Route
ELY pins Servo upstream commit `bc469fd5c17137373458508f88c3907cd1fcb69a` (workspace version
`0.4.0`) in `Cargo.toml` and `Cargo.lock`. That revision includes the July 2026 security fixes that
landed after the `v0.3.0` regular release. Servo's LTS line uses a separate six-month cadence.
ELY pins Servo upstream commit `bc469fd5c17137373458508f88c3907cd1fcb69a`, workspace version
`0.4.0`, in `Cargo.toml` and `Cargo.lock`.
Servo's own shell route is the model for the final ELY rendering path:
Servo's rendering lifecycle remains authoritative inside each sidecar:
```text
Window event
command or poll
-> Servo spin_event_loop
-> WebViewDelegate::notify_new_frame_ready
-> window request_redraw
-> WebView::paint
-> RenderingContext::present
-> RenderingContext frame
-> sidecar response
```
Relevant upstream evidence from Servo `bc469fd5c17137373458508f88c3907cd1fcb69a`:
Relevant upstream evidence at the pinned revision:
- `ports/servoshell/window.rs` creates `WebViewBuilder::new(state.servo(), platform_window.rendering_context())`.
- `ports/servoshell/window.rs` repaints with `webview.paint()` and `rendering_context().present()`.
- `ports/servoshell/running_app_state.rs` handles `notify_new_frame_ready` by marking the owning window for repaint.
- `components/paint/paint.rs` owns one WebRender painter per `RenderingContext` and keeps paint coordination inside Servo's rendering pipeline.
- `ports/servoshell/window.rs` builds WebViews from a Servo instance and rendering context.
- `ports/servoshell/window.rs` repaints with `webview.paint()` and presents the context.
- `ports/servoshell/running_app_state.rs` maps `notify_new_frame_ready` to repaint scheduling.
- `components/paint/paint.rs` owns one WebRender painter per rendering context.
## Target Boundaries
## Ownership
```text
crates/ely_browser_core
Owns browser domain state: tabs, spaces, profiles, search, settings.
Browser domain state: tabs, spaces, profiles, search, settings.
crates/ely_app/src/shell
Owns GPUI chrome and command surfaces.
GPUI chrome, profile broker, frame cadence, input, and presentation.
crates/ely_app/src/servo_embed
Owns in-process Servo runtime, platform content view attachment,
WebView lifecycle, repaint dispatch, and web input routing.
crates/ely_app/src/services
Sidecar discovery, process lifecycle, and wire client.
crates/ely_servo_host
Owns the current in-process compatibility adapter while the native
platform rendering surface lands.
Servo host API, rendering contexts, sidecar protocol, and process entry point.
```
Each production source file in the final embedding path stays below 500 lines. Large responsibilities split by ownership:
Each production source file in the embedding path stays below 500 lines. Responsibilities remain
split by lifecycle, protocol, session state, frame output, and browser-shell orchestration.
- `runtime.rs`: `Servo`, wake handling, webview registry.
- `platform_view.rs`: platform content surface attachment.
- `delegate.rs`: Servo `WebViewDelegate` implementation.
- `input.rs`: GPUI event to Servo input conversion.
- `paint.rs`: repaint and present coordination.
- `metadata.rs`: title, URL, favicon, load-state propagation.
## Rendering Transports
## Migration Slices
### macOS hardware transport
1. App process owns the Servo runtime and WebView registry.
2. App process routes web input and navigation directly into Servo.
3. GPUI exposes one `NativeSurfaceHandle` element contract for platform child surfaces.
4. macOS creates a child `NSView` and hands the AppKit raw handle to Servo.
5. Windows creates a child `HWND` and hands the Win32 raw handle to Servo.
6. Linux/X11 creates a child XCB window and hands the XCB raw handle to Servo.
7. Linux/Wayland creates a child `wl_surface`/subsurface and hands the Wayland raw handle to Servo.
8. GPUI web page display uses native content surface presentation for every platform target.
```text
Servo WebView::paint
-> CGL HardwareOffscreenContext
-> surfman swap chain
-> IOSurface
-> IOSurfaceCreateMachPort
-> mach_msg port descriptor
-> IOSurfaceLookupFromMachPort
-> CVPixelBuffer
-> GPUI surface element
-> Metal BGRA texture
```
The parent creates a unique bootstrap Mach service before spawning the sidecar. The sidecar looks
up that service and moves each new IOSurface send right in a complex Mach message. The app verifies
the system IOSurface ID and dimensions, wraps the imported surface in a `CVPixelBuffer`, and
releases the received Mach right. The cache retains up to 16 inactive IOSurfaces and temporarily
retains additional surfaces whose GPU backing is still active.
Each hardware frame carries `current_surface_id`. The first frame for an IOSurface also carries its
dimensions and system surface ID in `surface_handle`. GPUI presents the selected `CVPixelBuffer`
through its surface element and macOS Metal BGRA pipeline. This path keeps pixels on the GPU side of
the profile process boundary.
CoreVideo raises the IOSurface use count while a `CVPixelBuffer` backing is alive. The frame and
GPUI `SurfaceLease` share that backing, and the Metal command-buffer completion handler releases
the final GPU lease. The host retains presented surfman surfaces and returns an acknowledged,
non-current surface to the swap chain only after `IOSurfaceIsInUse` becomes false. A sidecar waits
for the app's import acknowledgement before painting the next hardware frame.
The macOS parent selects hardware rendering by default. Setting
`ELY_SERVO_RENDERING_CONTEXT=software` selects the RGBA path for software-specific diagnostics.
### Linux and Windows software transport
Linux and Windows use Servo's `SoftwareRenderingContext`. The sidecar paints into RGBA8 memory,
writes the JSON frame header, then writes the bounded raw payload on stdout. The app validates the
dimensions and byte count before allocating and creates a GPUI `RenderImage`.
## Acceptance Gates
- `cargo run` opens a normal web page with Servo inside the ELY app process.
- Scrolling a live web page uses Servo input events and Servo repaint callbacks.
- Page display presents through a Servo rendering context.
- macOS, Windows, Linux/X11, and Linux/Wayland each have a platform child surface implementation under the same `NativeSurfaceHandle` contract.
- Address/search, tabs, spaces, profiles, settings, permissions, and sync compile through existing domain APIs.
- Every new source file stays below 500 lines.
- User-facing chrome stays clean.
## First Platform Surface Target
```text
GPUI Window
-> GPUI NativeSurface element
-> platform child surface for web content bounds
-> Servo WindowRenderingContext
-> Servo WebView
```
This slice gives Servo a native surface in the ELY app process. The compatibility adapter remains a narrow bridge for tests and non-normal display probes.
- Workspace format, check, lint, and unit tests pass.
- Servo host check and lint pass with `servo-engine,hardware-render` enabled.
- The macOS hardware-context test presents a real IOSurface and exports a live Mach send right.
- The hardware sidecar test transfers that right across processes and resolves it with
`IOSurfaceLookupFromMachPort`.
- The app hardware test imports a real BGRA `CVPixelBuffer` and verifies its GPUI lease releases the
IOSurface use count.
- Sidecar persistence integration passes against a loopback HTTP server.
- PRD live-site tests pass through the GPUI web-surface adapter.
- Profile Cookie, local-storage, and HTTP-cache isolation passes through two live sidecars.
- The real-window render probe captures visible page content.
- The macOS application bundle contains both executables in `Contents/MacOS`.
- Native distributions place both executables at the distribution root.
- Every production source file stays below 500 lines.