Skip to content

Replace hotplug polling with a registry watcher thread - #64

Merged
psi4j merged 1 commit into
psi4j:mainfrom
Magniquick:hotplug-registry-watcher
Sep 15, 2026
Merged

psi4j merged 1 commit into
psi4j:mainfrom
Magniquick:hotplug-registry-watcher

Conversation

@Magniquick

Copy link
Copy Markdown
Contributor

Written with an LLM. Tested on real hardware, not just compiled.

Problem

The main loop chops every idle sleep into 10ms chunks only so poll_hotplug()
runs. Signals never needed it, since recv_timeout returns as soon as one
arrives. Each poll is a full Wayland roundtrip, so it wakes the compositor too.

Idle at night on my machine, the main thread averaged about 190 voluntary
context switches per second with nothing happening.

Fix

A watcher thread holds its own Wayland connection, binds only wl_registry,
and sends HotplugCheck when a wl_output global appears or disappears. The
main loop consumes that inside its sleep, so it does not re-evaluate or re-log
the schedule, and otherwise sleeps its whole interval in one wait.

Main-thread idle wakeups after: 0/s over 20s. The watcher is also 0/s, since it
blocks in blocking_dispatch. Process total goes from about 290/s to about
100/s; the remainder is the IPC server thread's 10ms recv_timeout in
src/state/ipc/server.rs, which this PR does not touch.

The watcher must not share the backend's connection. poll_hotplug() calls
EventQueue::roundtrip(), which blocks until its wl_display.sync callback
arrives, and a second reader can consume that callback and strand the roundtrip
forever. I hit this: the process hung and ignored SIGTERM.

HotplugMode replaces the implicit assumption that every backend polls. The
hyprsunset backend never overrode poll_hotplug, so it was waking once per
chunk to call an empty function. It now sleeps uninterrupted.

The loop still calls poll_hotplug() once per iteration before sleeping, which
covers what the watcher cannot: an output arriving while the backend is still
starting, and any HotplugCheck dropped while test mode or an interrupted
reload had the loop busy. If the watcher dies, its guard clears a liveness flag
and sends a final HotplugCheck, so the loop warns once and falls back to a 1s
poll instead of finishing a sleep that could run for hours unwatched.

Tested

Arch, Hyprland 0.56.2, Intel Xe.

  • DRM CTM read back from the kernel with modetest, identical to the unpatched
    binary at 3300K/90% gamma. Four reloads tracked exactly; stopping leaves
    identity.
  • Hyprland CTM backend, nested: hyprctl output create headless applies the CTM
    to the new output, remove drops it. Idle wakeups unchanged after both.
  • wlr-gamma backend, nested headless sway: swaymsg create_output and
    output ... unplug both detected.
  • Killing the compositor produces the fallback warning, and sunsetr keeps
    running.
  • Clean SIGTERM in about 500ms. 243 tests, fmt and clippy clean.

Happy to split the HotplugMode change out if you would rather take it
separately.

The main loop chopped every idle sleep into 10ms chunks so that
poll_hotplug() could run. Signals never needed that, since recv_timeout
returns as soon as one arrives; it existed only to notice new outputs.
Each poll is a full Wayland roundtrip, so it woke the compositor too.
Measured on one machine, idle at night with nothing happening, the main
thread averaged about 190 voluntary context switches per second.

Add a watcher thread that holds its own Wayland connection, binds only
wl_registry, tracks which global names are wl_outputs, and sends
HotplugCheck when one appears or disappears. The main loop consumes that
inside its sleep, so it neither re-evaluates nor re-logs the schedule and
the check consumes no sleep budget. Otherwise it sleeps its whole
interval in one wait. Main-thread idle wakeups afterwards: 0 per second
over 20 seconds, and the watcher is also 0 because it blocks in
blocking_dispatch. Process total goes from about 290 to about 100 per
second; the remainder is the IPC server thread's 10ms recv_timeout in
src/state/ipc/server.rs, which this change does not touch.

The watcher must not share the backend's connection. poll_hotplug()
implementations call EventQueue::roundtrip(), which blocks reading the
socket until its wl_display.sync callback arrives, and a second reader on
the same connection can consume that callback and strand the roundtrip
forever, hanging the poll and the shutdown behind it.

HotplugMode replaces the implicit assumption that every backend needs
polling. The hyprsunset backend never overrode poll_hotplug, so it was
waking once per chunk to call an empty function; it now sleeps
uninterrupted.

The loop still calls poll_hotplug() once per iteration before sleeping,
which covers what the watcher cannot: an output that arrives while the
backend is still starting, and any HotplugCheck dropped while test mode
or an interrupted reload had the loop occupied. If the watcher dies, from
a compositor exit or a protocol error, its guard clears a liveness flag
and sends a final HotplugCheck, so the loop warns once and falls back to
a 1s poll instead of finishing a sleep that could run for hours with
nothing watching.

Tested on Hyprland and on wlr-gamma-control. The DRM CTM read back from
the kernel is identical to the unpatched binary. Output add and remove
are detected on both backends, with idle wakeups unchanged afterwards.
Killing the compositor produces the fallback warning and sunsetr keeps
running. Reloads, clean SIGTERM, 243 tests, fmt and clippy all pass.
Copilot AI lite review requested due to automatic review settings September 7, 2026 19:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It introduces new cross-thread Wayland/event-loop behavior and alters main-loop sleep semantics in ways that are difficult to fully validate automatically.

Pull request overview

This PR removes frequent hotplug polling from the core sleep loop by introducing a dedicated Wayland registry watcher thread that notifies the main loop when wl_output globals appear/disappear, allowing uninterrupted sleeps while still reacting to output changes.

Changes:

  • Added a HotplugMode backend capability to declare how output hotplug should be detected (NotNeeded vs WaylandRegistry).
  • Introduced a Wayland-registry-only watcher thread that sends a SignalMessage::HotplugCheck to wake the main loop on output add/remove.
  • Updated signal draining and test-mode handling to intentionally drop HotplugCheck to avoid re-emission/spin, with targeted unit tests.
File summaries
File Description
src/io/signals.rs Adds HotplugCheck and updates drain behavior + tests to drop it during reload draining.
src/core/tests.rs Adds unit tests for registry watcher state tracking and liveness guard behavior.
src/core/mod.rs Implements watcher thread + registry dispatch, integrates watcher-driven wakeups into the main loop sleep logic with fallback polling.
src/commands/test.rs Ensures HotplugCheck is ignored during test mode to prevent spinning; adds a unit test.
src/backend/wayland/mod.rs Declares Wayland backend uses HotplugMode::WaylandRegistry.
src/backend/mod.rs Introduces HotplugMode and a hotplug_mode() trait method for backends.
src/backend/hyprland/mod.rs Declares Hyprland backend uses HotplugMode::WaylandRegistry.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@psi4j

psi4j commented Sep 8, 2026

Copy link
Copy Markdown
Owner

I've got a whole lot going on right now with University and work, but I'll take a look at these as soon as I can allocate some free time to this, hopefully some time later this week.

@Magniquick

Copy link
Copy Markdown
Contributor Author

I've got a whole lot going on right now with University and work, but I'll take a look at these as soon as I can allocate some free time to this, hopefully some time later this week.

same here :/ (hence the llms used). uni work is always a pita
best of luck :D

@psi4j
psi4j merged commit ab4e085 into psi4j:main Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants