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:
parent
d57cd6cfbb
commit
d42f000f8f
129
docs/spikes/multi-window-results.md
Normal file
129
docs/spikes/multi-window-results.md
Normal 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.
|
||||||
171
docs/spikes/rdp-frame-transport-results.md
Normal file
171
docs/spikes/rdp-frame-transport-results.md
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user