From d42f000f8ff15b659a0535f9dec8454f6b2008ff Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Tue, 17 Mar 2026 06:35:00 -0400 Subject: [PATCH] spike: multi-window and RDP frame transport research results Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/spikes/multi-window-results.md | 129 ++++++++++++++++ docs/spikes/rdp-frame-transport-results.md | 171 +++++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 docs/spikes/multi-window-results.md create mode 100644 docs/spikes/rdp-frame-transport-results.md diff --git a/docs/spikes/multi-window-results.md b/docs/spikes/multi-window-results.md new file mode 100644 index 0000000..083a504 --- /dev/null +++ b/docs/spikes/multi-window-results.md @@ -0,0 +1,129 @@ +# Spike: Multi-Window Support in Wails v3 + +**Status:** Research-based (not yet validated on Windows) +**Date:** 2026-03-17 +**Target platform:** Windows (developing on macOS) +**Wails version:** v3.0.0-alpha.74 + +--- + +## Context + +Wraith needs to support detached sessions — users should be able to pop out +an SSH or RDP session into its own window while the main connection manager +remains open. This spike evaluates three approaches, ranked by preference. + +--- + +## Plan A: Wails v3 Native Multi-Window + +**Status: LIKELY WORKS** based on API documentation. + +### How it works + +- `app.Window.NewWithOptions()` creates a new OS-level window at runtime. +- Each window can load a different URL or frontend route (e.g., + `/session/rdp/3` in one window, `/` in the main window). +- All windows share the same Go backend services — no IPC or inter-process + marshalling required. Bindings registered on the application are callable + from any window. +- Window lifecycle events (`OnClose`, `OnFocus`, etc.) are available for + cleanup. + +### Example (pseudocode) + +```go +win, err := app.Window.NewWithOptions(application.WindowOptions{ + Title: "RDP — server-01", + Width: 1280, + Height: 720, + URL: "/session/rdp/3", +}) +``` + +### Risks + +| Risk | Severity | Mitigation | +|------|----------|------------| +| Alpha API — method signatures may change before v3 stable | Medium | Pin to a known-good alpha tag; wrap calls behind an internal interface so migration is a single-file change. | +| Platform-specific quirks on Windows (DPI, focus, taskbar grouping) | Low | Test on Windows during Phase 2. Wails uses webview2 on Windows which is mature. | +| Window count limits or resource leaks | Low | Cap concurrent detached windows (e.g., 8). Ensure `OnClose` releases resources. | + +--- + +## Plan B: Floating Panels (CSS-based) + +**Status: FALLBACK** — no external dependency, purely frontend. + +### How it works + +- Detached sessions render as draggable, resizable `position: fixed` panels + within the main Wails window. +- Each panel contains its own Vue component instance (terminal emulator or + RDP canvas). +- Panels can be minimised, maximised within the viewport, or snapped to + edges. + +### Pros + +- Zero dependency on Wails multi-window API. +- Works on any platform without additional testing. +- Simpler state management — everything lives in one window context. + +### Cons + +- Sessions share the same viewport — limited screen real estate. +- Cannot span multiple monitors. +- Feels less native than real OS windows. + +### Implementation cost + +Small. Requires a `` wrapper component with drag/resize +handlers. Libraries like `vue3-draggable-resizable` exist but a lightweight +custom implementation (~150 LOC) is preferable to avoid dependency churn. + +--- + +## Plan C: Browser Mode + +**Status: EMERGENCY** — last resort if both Plan A and Plan B are inadequate. + +### How it works + +- Wails v3 supports a server mode where the frontend is served over HTTP on + `localhost`. +- Detached sessions open in the user's default browser via + `open(url, '_blank')` or `runtime.BrowserOpenURL()`. +- The browser tab communicates with Go services through the same HTTP + endpoint. + +### Pros + +- Guaranteed to work — it is just a web page. +- Users can arrange tabs freely across monitors. + +### Cons + +- Breaks the desktop-app experience. +- Browser tabs lack access to Wails runtime bindings; all communication must + go through HTTP/WebSocket, requiring a parallel transport layer. +- Security surface increases — localhost HTTP server is accessible to other + local processes. + +--- + +## Recommendation + +**Start with Plan A.** The Wails v3 `NewWithOptions` API is documented and +consistent with how other multi-window desktop frameworks (Electron, +Tauri v2) work. The alpha stability risk is mitigated by wrapping calls +behind an internal interface. + +If Plan A fails during Windows validation, **Plan B requires only frontend +CSS changes** — no backend work is wasted. Plan C is reserved for scenarios +where neither A nor B is viable. + +## Next Step + +Validate Plan A on Windows during Phase 2 when SSH sessions exist and there +is a real payload to render in a second window. diff --git a/docs/spikes/rdp-frame-transport-results.md b/docs/spikes/rdp-frame-transport-results.md new file mode 100644 index 0000000..febf9e8 --- /dev/null +++ b/docs/spikes/rdp-frame-transport-results.md @@ -0,0 +1,171 @@ +# Spike: RDP Frame Transport Mechanisms + +**Status:** Research-based (not yet benchmarked) +**Date:** 2026-03-17 +**Target platform:** Windows (developing on macOS) +**Wails version:** v3.0.0-alpha.74 + +--- + +## Context + +When Wraith connects to a remote desktop via FreeRDP, the Go backend +receives raw bitmap frames that must be delivered to the frontend for +rendering on an HTML ``. This spike evaluates three transport +approaches, estimating throughput for a 1920x1080 session at 30 fps. + +--- + +## Approach 1: Local HTTP Endpoint + +### How it works + +1. Go spins up a local HTTP server on a random high port + (`net.Listen("tcp", "127.0.0.1:0")`). +2. Each frame is JPEG-encoded and served at a predictable URL + (e.g., `http://127.0.0.1:{port}/frame?session=3`). +3. The frontend fetches frames via `fetch()`, `` tag, or + `ReadableStream` for chunked delivery. + +### Throughput estimate + +| Metric | Value | +|--------|-------| +| 1080p RGBA raw | ~8 MB/frame | +| 1080p JPEG (quality 80) | ~100-200 KB/frame | +| At 30 fps (JPEG) | ~3-6 MB/s | +| Loopback bandwidth | >1 GB/s | + +Loopback HTTP can handle this with headroom to spare. + +### Pros + +- No base64 overhead — binary JPEG bytes transfer directly. +- Standard HTTP semantics; easy to debug with browser DevTools. +- Can use `Transfer-Encoding: chunked` or Server-Sent Events for + push-based delivery. +- Can serve multiple sessions on the same server with different paths. + +### Cons + +- Requires an extra listening port on localhost. +- Potential firewall or endpoint-security issues on locked-down Windows + enterprise machines. +- Slightly more complex setup (port allocation, CORS headers for Wails + webview origin). + +--- + +## Approach 2: Wails Bindings (Base64) + +### How it works + +1. Go encodes each frame as a JPEG, then base64-encodes the result. +2. A Wails-bound method (`SessionService.GetFrame(sessionID)`) returns the + base64 string. +3. The frontend decodes the string, creates an `ImageBitmap` or sets it as a + data URI, and draws it on a ``. + +### Throughput estimate + +| Metric | Value | +|--------|-------| +| 1080p JPEG (quality 80) | ~100-200 KB/frame | +| Base64 of JPEG (+33%) | ~133-270 KB/frame | +| At 30 fps | ~4-8 MB/s of string data | +| Wails IPC overhead | Negligible for this payload size | + +This is feasible. Modern JavaScript engines handle base64 decoding at +several hundred MB/s. + +### Pros + +- No extra ports — everything flows through the existing Wails IPC channel. +- Works out of the box with Wails bindings; no additional infrastructure. +- No firewall concerns. + +### Cons + +- 33% base64 size overhead on every frame. +- CPU cost of `base64.StdEncoding.EncodeToString()` in Go and `atob()` in + JS on every frame (though both are fast). +- Polling-based unless combined with Wails events to signal frame + availability. +- May bottleneck at very high resolutions (4K) or high FPS (60+). + +--- + +## Approach 3: Wails Events (Streaming) + +### How it works + +1. Go emits each frame as a Wails event: + `app.EmitEvent("frame:3", base64JpegString)`. +2. The frontend subscribes: `wails.Events.On("frame:3", handler)`. +3. The handler decodes and renders on canvas. + +### Throughput estimate + +Same as Approach 2 — the payload is identical (base64 JPEG). The difference +is delivery mechanism (push vs. pull). + +### Pros + +- Push-based — the frontend receives frames as soon as they are available + with no polling delay. +- Natural Wails pattern; aligns with how other real-time data (connection + status, notifications) already flows. + +### Cons + +- Same 33% base64 overhead as Approach 2. +- Wails event bus may not be optimised for high-frequency, large-payload + events. This is unvalidated. +- Harder to apply backpressure — if the frontend cannot keep up, events + queue without flow control. + +--- + +## Throughput Summary + +| Approach | Payload/frame | 30 fps throughput | Extra infra | +|----------|--------------|-------------------|-------------| +| 1 — Local HTTP | ~150 KB (binary JPEG) | ~4.5 MB/s | Localhost port | +| 2 — Wails bindings | ~200 KB (base64 JPEG) | ~6 MB/s | None | +| 3 — Wails events | ~200 KB (base64 JPEG) | ~6 MB/s | None | + +All three approaches are within comfortable limits for 1080p at 30 fps. +The differentiator is operational simplicity, not raw throughput. + +--- + +## Recommendation + +**Start with Approach 2 (base64 JPEG via Wails bindings).** + +Rationale: + +1. JPEG compression brings 1080p frames down to ~200 KB, making the 33% + base64 overhead manageable (~6 MB/s at 30 fps). +2. No extra ports or firewall concerns — important for enterprise Windows + environments where Wraith will be deployed. +3. Simple implementation: one Go method, one frontend call per frame. +4. If polling latency is a problem, upgrade to Approach 3 (events) with + minimal code change — the payload encoding is identical. + +**If benchmarking reveals issues** (dropped frames, high CPU from +encoding), fall back to Approach 1 (local HTTP) which eliminates base64 +overhead entirely. The migration path is straightforward: replace the +`fetch(dataUri)` call with `fetch(httpUrl)`. + +--- + +## Next Step + +Benchmark during Phase 3 when FreeRDP integration is in progress and real +frame data is available. Key metrics to capture: + +- End-to-end frame latency (Go encode to canvas paint) +- CPU utilisation on both Go and browser sides +- Frame drop rate at 30 fps and 60 fps +- Memory pressure from base64 string allocation/GC