spike: multi-window and RDP frame transport research results

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-17 06:35:00 -04:00
parent d57cd6cfbb
commit d42f000f8f
2 changed files with 300 additions and 0 deletions

View File

@ -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 `<FloatingPanel>` 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.

View File

@ -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 `<canvas>`. 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()`, `<img>` 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 `<canvas>`.
### 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