Repository navigation
GTK: audio bell leaks a GStreamer pipeline + GL-context threads per ring (idle CPU climbs over time) #13647
Description
Discussed in #12808
Originally posted by nikicat May 25, 2026
Issue Description
With bell-features = audio and an (optional) bell-audio-path set, every terminal bell leaks a GStreamer pipeline and its GL-context threads. They are never torn down, so over a long-running session CPU usage climbs steadily and thread count grows without bound.
After ~1.5 days of normal use (multiple long-lived TUI sessions that ring the bell on completion), a single Ghostty process had accumulated 3,482 threads and was consuming ~3 CPU cores while idle. Thread-name histogram from /proc/<pid>/task/*/comm:
1358 gldisplay-event
696 ghostty:gdrv
696 ghostt:traceq
679 gstglcontext
14 renderer / io / io-reader (normal)
That is ~696 GStreamer playbin/GL pipelines that were never freed — almost exactly one per bell rung. The leaked threads are parked in __futex_wait (0 nonvoluntary ctx switches), but the ~1358 GL display event loops still burn multiple cores.
Root cause (from source)
ringBell allocates a fresh MediaFile on every bell:
src/apprt/gtk/class/surface.zig:2489→media.fromFilename(path)→gtk.MediaFile.newForFilename(path)- On Linux the default GTK media backend is GStreamer, and
libgstgtk4wires the playback through a GL sink — so even an audio-only.ogaspins up agstglcontext+gldisplay-eventthread set per pipeline.
Cleanup relies solely on notify::ended → unref (src/apprt/gtk/media.zig:96-102). Critically, the error handler is connected only when required == true (src/apprt/gtk/media.zig:50-58):
if (required) {
_ = gobject.Object.signals.notify.connect(media_file, ..., .{ .detail = "error" });
}
// "ended" handler always connected; calls media_file.unref()So for an optional bell-audio-path (the common case), any MediaFile that errors or never reaches the ended state is never unref'd, and its pipeline + GL threads leak. The ~696 leaked sets ≈ the number of bells fits this. (A happy-path teardown leak in the GStreamer GL backend is also possible and would compound it — see "Open question".)
The 100 ms per-surface BEL rate-limit (#9810, present since 1.3.0) caps the acute DoS freeze but does nothing for this slow per-bell leak, since legitimate bells fire far below 10/s.
This is the same gtk_media_file_new_for_filename-per-bell path implicated in the crash in #11720 (missing playbin3), and any teardown/unref fix should be checked against the GTK ownership double-free discussed in #12306.
Expected Behavior
Ringing the audio bell N times leaves the thread/pipeline count bounded (ideally constant), and idle CPU returns to baseline after playback finishes.
Actual Behavior
Each audio bell leaks a GStreamer pipeline and its GL threads. Thread count and idle CPU grow without bound over the lifetime of the process.
Reproduction Steps
- Config:
bell-features = audio+ a validbell-audio-path. - Spam bells, spaced past the 100 ms rate-limit:
while true; do printf '\a'; sleep 0.2; done - Watch the thread count climb and not recover:
watch -n1 'ls /proc/$(pidof ghostty)/task | wc -l'
Ghostty Logs
No response
Ghostty Version
Ghostty 1.3.1-arch2
(Code path is unchanged on tip — the 79162279d "enabling reuse" refactor only relocated the helpers; the bell call site still allocates a new MediaFile per ring.)
OS Version Information
Arch Linux. GTK 4.22.4, libadwaita 1.9.0, GStreamer 1.28.3 (gst-plugins-base/good 1.28.3). Default (GStreamer) GTK media backend (GTK_MEDIA unset); libgstgtk4.so present.
(Linux only) Display Server
Wayland
(Linux only) Desktop Environment/Window Manager
No response
Minimal Ghostty Configuration
bell-features = audio,attention,title,border
bell-audio-path = /path/to/bell.ogaAdditional Relevant Configuration
Triggered in practice by long-lived TUI programs that ring the bell on each completion (e.g. AI coding agents), accumulating hundreds of bells over a multi-day session.
Suggested fixes
- Reuse a single
MediaFileper surface (seek(0)+play()on each bell) to bound pipelines to 1 per surface regardless of root cause. Note: the Audible ping not working as expected #8957 "plays only once" bug was reuse without seeking back to start, soseek(0)is the key detail. - And/or connect the error handler unconditionally so the optional-audio path also tears down.
- Longer term, consider a non-GStreamer bell (e.g. libcanberra / direct PipeWire) to avoid the GL pipeline entirely.
Open question for maintainers
Does the leak occur on the happy path (i.e. does the GStreamer GTK4/GL backend fail to join gstglcontext/gldisplay-event threads even when ended fires and unref runs), or only on the error/never-ended path? The fix scope depends on this. A minimal GTK4 harness that loops new_for_filename → play → await ended → unref while sampling g_type_get_instance_count(gtk_media_file_get_type()) (GOBJECT_DEBUG=instance-count) and /proc/self/task would disambiguate.
Activity
GTK: move audio bell processing to the application
GTK: move audio bell processing to the application (#13657)