Go + Wails v3 + Vue 3 + SQLite + FreeRDP3 (purego) 183 tests, 76 source files, 9,910 lines of code Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5.3 KiB
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
- Go spins up a local HTTP server on a random high port
(
net.Listen("tcp", "127.0.0.1:0")). - Each frame is JPEG-encoded and served at a predictable URL
(e.g.,
http://127.0.0.1:{port}/frame?session=3). - The frontend fetches frames via
fetch(),<img>tag, orReadableStreamfor 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: chunkedor 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
- Go encodes each frame as a JPEG, then base64-encodes the result.
- A Wails-bound method (
SessionService.GetFrame(sessionID)) returns the base64 string. - The frontend decodes the string, creates an
ImageBitmapor 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 andatob()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
- Go emits each frame as a Wails event:
app.EmitEvent("frame:3", base64JpegString). - The frontend subscribes:
wails.Events.On("frame:3", handler). - 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:
- JPEG compression brings 1080p frames down to ~200 KB, making the 33% base64 overhead manageable (~6 MB/s at 30 fps).
- No extra ports or firewall concerns — important for enterprise Windows environments where Wraith will be deployed.
- Simple implementation: one Go method, one frontend call per frame.
- 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