feat(servo): isolate profiles with hardware sidecars
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user