Fix desktop-bridge GPU leak: ~28 MiB and ~14 nvidia fds per GStreamer pipeline - #2895
Merged
Merged
Conversation
…DA stream leak Spec-Ref: helix-specs@e6de6545e:002371_fix-desktop-bridge-gpu
Spec-Ref: helix-specs@e6de6545e:002371_fix-desktop-bridge-gpu
Spec-Ref: helix-specs@dcb27dbac:002371_fix-desktop-bridge-gpu
…er retry Spec-Ref: helix-specs@db31d7e1c:002371_fix-desktop-bridge-gpu
Spec-Ref: helix-specs@b6aeb10d5:002371_fix-desktop-bridge-gpu
…sktop-bridge-gpu Spec-Ref: helix-specs@3b41119cb:002371_fix-desktop-bridge-gpu
Spec-Ref: helix-specs@3b41119cb:002371_fix-desktop-bridge-gpu
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
One
desktop-bridgeaccumulated 9.3 GB of GPU memory and 1564 open/dev/nvidia0fds over 45 hours on a shared 16 GB RTX 2000 Ada, exhausted thecard, and broke a different user's desktop. This fixes the leak and the three
things that made it worse or harder to diagnose.
Measured on a live
desktop-bridge, driving real stream connect/disconnectcycles (each cycle creates and destroys a GStreamer pipeline), counting
/dev/nvidia0entries in/proc/<pid>/fd:+14 per cycle, unbounded → flat. The GStreamer leaks tracer goes from a
per-cycle set of survivors to zero live objects. Full write-up in
design/2026-07-29-desktop-bridge-gpu-leak.md.The leak was two independent bugs
1. Rust plugin — this was the entire GPU leak.
desktop/gst-pipewire-zerocopy/src/pipewiresrc/imp.rshadstd::mem::forget(ctx)on what its comment assumed was a rare race. It isn't:gst_cuda_ensure_element_context()runs a context query that GStreamer answers bycalling our own
set_context()synchronously on the same thread, so that branchis taken on every pipeline start.
ctxaliases aGstCudaContextit doesn'town (unreffing it really would double-free) but it does own the
GstCudaStreamcreated alongside it — and a stream holds a reference on its context, so every
pipeline stranded a whole CUDA context. New
CUDAContext::release_stream_only()drops the stream and forgets only the borrowed pointer.
2. Go side — leaked GStreamer objects plus a latent crash.
api/pkg/desktop/gst_pipeline.gorelied on GC finalizers for four transfer-fullreferences (
PullSample,Sample.GetBuffer,GetElementByName,GetPipelineBus). It also unreffed the pipeline while leaving its finalizerarmed —
gst_parse_launchreturns a floating ref that go-glib ref-sinks, sothat
Unreffreed the object and the next GC double-freed it. The new leak testreproduced that as
g_object_unref: assertion 'G_IS_OBJECT (object)' failed+SIGSEGV in
runtime.runFinalizers;start-desktop-bridge.sh's restart-loopcomment ("can crash ... segfault during WebSocket reconnection") was this.
The Go fix alone was deployed and measured first: object leaks gone, crash gone,
fd count unchanged. Only the tracer dump pointed at the plugin.
Changes
api/pkg/desktop/gst_pipeline.go— explicit, ordered release of every go-gstobject in
Stop();releaseGObject/releaseSample/releaseBufferhelpersthat disarm the finalizer before unreffing; bus flushed and released; per-frame
sample and buffer released in
onNewSample; probe element released indiagnoseGPUEncoderFailure.desktop/gst-pipewire-zerocopy,desktop/wayland-display-core— the CUDAstream leak.
scripts/build-zerocopy-plugin.sh— Rust 1.85.0 → 1.87.0 to matchDockerfile.ubuntu-helix. The script could not build the plugin at all.api/pkg/desktop/gpu_vendor.go(new) — GPU vendor from/dev/nvidia0and theDRM PCI vendor id, not from encoder plugin availability. An exhausted GPU used
to make
checkGstElement("nvh264enc")go false, reclassifying NVIDIA hardwareas AMD/Intel and taking a branch that delivers zero frames — while logging
GNOME + AMD/Intel detectedon an RTX 2000 Ada. NVIDIA without NVENC now failsloudly with a specific client-facing error; explicit
HELIX_ENCODERstill wins.api/pkg/desktop/gpu_guard.go(new) — 30 s self-check on this process's/dev/nvidia0fd count; warn at 200, refuse new pipelines at 400 (healthy is~52). Nothing detected the incident for 45 hours.
api/pkg/desktop/shared_video_source.go— circuit breaker latches after 10trips instead of permitting one leaking instantiation per cooldown forever (it
opened 209 times during the incident); cleared only by an explicit user retry.
frontend/.../websocket-stream.ts,DesktopStreamViewer.tsx— the retrybudget now resets only on
videoStarted, not on a socket that merely opened. A2 s "stabilized" timer and
resetRetryState()onconnectionCompletewerezeroing it every cycle, which is why the existing backoff and 10-attempt
give-up never fired across 2383 connections. Give-up is now terminal with a
real error plus Retry, and reconnection is suspended while the tab is hidden.
Tests
api/pkg/desktop/gst_pipeline_leak_test.go(new) — N × pipelinecreate/destroy, asserting
/dev/nvidia0fd count and GPU MiB are flat, in boththe "never started" and "started + frames" shapes. Skips cleanly without a GPU.
Note it does not reproduce the CUDA leak: a synthetic
videotestsrc ! cudaupload ! nvh264encpipeline is flat even on broken code —that one needs the real
pipewirezerocopysrcpath, so the live-cyclemeasurement above is its regression evidence. It does fail on unmodified code,
via the finalizer double-unref crash.
api/pkg/desktop/ws_stream_gpu_detect_test.go(new) — NVIDIA+NVENC,NVIDIA-without-NVENC, AMD, AMD-with-nvenc-element-present, Sway, macOS
virtio-gpu, and explicit-override.
Verified end-to-end
Deployed the fixed binary and plugin into a live desktop container and watched
the stream in the browser: 55–60 FPS, fd count flat across repeated
create/destroy cycles, no crashes. Not a counter in a unit test — actual frames.
🔗 Open in Helix
📋 Spec:
🚀 Built with Helix