Skip to content

Shared dbus.SessionBus + blocking MPRIS calls from tick thread spin the GTK main loop at 100% CPU #32

Description

@km1tch

Summary

With MediaPlugin installed and any Media action on the active page, StreamController's GTK main thread spins at ~100% CPU for the lifetime of the process (one full core, from launch until exit). The deck keeps working, so the burn is easy to miss unless you look at top. On Wayland the constant main-loop churn can also degrade compositor responsiveness (e.g. dropped frames in video playback while StreamController runs).

Root cause

StreamController core sets the GLib main loop as the default D-Bus main loop at startup (main.pyDBusGMainLoop(set_as_default=True)).

MediaController.__init__ then uses the shared session bus singleton:

self.session_bus = dbus.SessionBus()

Because of the default main loop, this connection's watch sources are attached to the GTK main context. But all of MediaController's MPRIS calls (list_names, Properties.Get, play/pause/next, album art, ...) are blocking calls made from the tick_actions thread, not from the main loop.

This is the classic dbus-python threads pathology: the blocking thread and the main loop share one connection. The connection's GSource keeps reporting "messages pending dispatch" while the tick thread owns the connection, so the main loop wakes, dispatches nothing, and immediately re-polls — forever.

Evidence

py-spy dump shows the main thread permanently "active" inside the GLib main loop with no Python callback ever on the stack:

Thread (active): "MainThread"
    run (gi/overrides/Gio.py:138)
    ...
    main (main.py:574)

A 10-second native profile (py-spy record --native) of the main thread resolves to:

  • ~43% in g_wakeup_signal eventfd write()s inside g_main_context_check/g_main_context_wait
  • ~33% in ppoll
  • remainder in g_main_context_dispatch bookkeeping — i.e. pure wakeup ping-pong, no real work

The MPRIS calls visible on the tick thread during the same profile: update_players (MediaController.py:26), get_matching_ifaces (MediaController.py:47/62), dbus/connection.py call_blocking.

Process stats from an affected machine: 60 hours of CPU accumulated over 70 hours of uptime, ~55 of those hours on the main thread alone; every other thread near idle.

Environment

  • StreamController source install (recent main), GNOME Wayland
  • Python 3.14, dbus-python 1.4.0, GLib 2.88
  • Reproduces from launch; independent of which player is running (tested with Firefox and MPD via MPRIS)

Fix

Give MediaController a private connection with no main-loop integration:

self.session_bus = dbus.SessionBus(private=True, mainloop=dbus.mainloop.NULL_MAIN_LOOP)

Blocking calls then poll their own socket (thread-safe in libdbus) and never touch the GLib context. The plugin registers no D-Bus signal receivers, so nothing here needs main-loop integration. Verified on a live deployment: main-thread CPU drops from a sustained 99.9% to idle and all MPRIS functionality keeps working. PR incoming.

Possibly related: some reports in StreamController/StreamController#608 (high idle CPU) may include this on top of the scroll-label issue, since this bug ships with one of the most popular plugins.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions