Skip to content

libusb: Fix high CPU usage in callback thread#783

Merged
Youw merged 10 commits intoconnection-callbackfrom
copilot/optimize-hidapi-thread-wait
Apr 24, 2026
Merged

libusb: Fix high CPU usage in callback thread#783
Youw merged 10 commits intoconnection-callbackfrom
copilot/optimize-hidapi-thread-wait

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 30, 2026

Callback_thread used hidapi_thread_cond_timedwait with an absolute
timespec already in the past, causing a tight spin loop. Replaced
with cond_wait + predicate pattern so the thread sleeps until an
event or a shutdown signal arrives.
Multithreading fixes folded in along the way:

  • Queue mutex moved from libusb_thread to callback_thread (its
    natural condvar partner), with signal-under-mutex to avoid a
    lost-wakeup race at shutdown.
  • process_hotplug_event() now runs under hid_hotplug_context.mutex
    to match hid_hotplug_register_callback()'s HID_API_HOTPLUG_ENUMERATE
    traversal of hid_hotplug_context.devs — fixes a use-after-free
    where a concurrent disconnect could free a list node mid-traversal.
  • devs teardown moved to hid_internal_hotplug_cleanup() (after
    hidapi_thread_join) so every access to devs happens under
    hid_hotplug_context.mutex.
  • hid_hotplug_register_callback() now frees devs on the
    libusb_hotplug_register_callback() error path, which previously
    leaked.

Closes: #782

Assisted-by: Copilot:claude-sonnet-4.6
Assisted-by: Copilot:claude-opus-4.6
Assisted-by: Claude:claude-opus-4.7

Copilot AI and others added 2 commits March 30, 2026 09:45
…attern

Replace broken hidapi_thread_cond_timedwait (which used a fixed absolute
timestamp in the past, causing a spin loop) with hidapi_thread_cond_wait
using the standard condition variable predicate pattern.

- callback_thread: Use cond_wait with proper predicate loop for zero CPU
  overhead when idle and immediate response on events/shutdown
- hid_libusb_hotplug_callback: Protect queue and signal under
  callback_thread.mutex to prevent lost wakeups
- hotplug_thread: Lock mutex before signaling for clean shutdown

Agent-Logs-Url: https://github.com/libusb/hidapi/sessions/a47a730b-fbb6-4042-9afb-00e5aa94817d

Co-authored-by: Youw <5939659+Youw@users.noreply.github.com>
Comment thread .gitignore
Copilot AI changed the title [WIP] Optimize hidapi thread wait to reduce CPU usage Fix callback_thread busy-loop by using proper condition variable pattern Mar 30, 2026
Copilot finished work on behalf of Youw March 30, 2026 09:49
Copilot AI requested a review from Youw March 30, 2026 09:49
Comment thread libusb/hid.c
@Youw
Copy link
Copy Markdown
Member

Youw commented Mar 30, 2026

@copilot We used to handle all evens in a queue while holding the lock on a mutex, and now we lock it only when manipulating the queue, but not when handling the event. Is there a benefit in that approach?

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 30, 2026

@copilot We used to handle all evens in a queue while holding the lock on a mutex, and now we lock it only when manipulating the queue, but not when handling the event. Is there a be...

Yes — releasing the lock during event processing avoids two problems:

  1. Blocking the producer: hid_libusb_hotplug_callback (which runs on hotplug_thread) must acquire the same mutex to enqueue events. If we held the lock during the (potentially long) callback invocation, the producer would stall for that entire duration. Releasing it lets new events be enqueued concurrently while an older one is being dispatched.

  2. Deadlock prevention: If user callback code calls back into HIDAPI (e.g., hid_hotplug_deregister_callback), that call will attempt to acquire callback_thread.mutex. Holding it during processing would deadlock.

The tradeoff is that hid_hotplug_context.devs (the known-device list maintained inside process_hotplug_event) is modified without the lock, but that is safe because only callback_thread reads or writes devs — events are still processed serially within the single consumer thread.

@Youw
Copy link
Copy Markdown
Member

Youw commented Mar 30, 2026

@copilot I also see that devs is accessed from hid_hotplug_register_callback, even though it is done before the thread is created, maybe move that initialisation inside of the thread (to match cleanup of that variable at the end of the thread).
And would be great to explicitly mark that variable that it should only be accessed from that thread.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 30, 2026

@copilot I also see that devs is accessed from hid_hotplug_register_callback, even though it is done before the thread is created, maybe move that initialisation inside of the th...

Done in 3f4721e. devs is now initialized at the start of callback_thread (paired with its cleanup at the end), and its struct comment marks it as "Only accessed from callback_thread." The HID_API_HOTPLUG_ENUMERATE case in hid_hotplug_register_callback now enumerates devices locally (freed after use) so it never touches the thread-owned devs.

Copilot AI changed the title Fix callback_thread busy-loop by using proper condition variable pattern Fix callback_thread busy-loop and queue synchronization Mar 30, 2026
Copilot finished work on behalf of Youw March 30, 2026 10:53
Copilot stopped work on behalf of Youw due to an error March 30, 2026 10:54
…ess_hotplug_event

In process_hotplug_event(), the devs list was modified without holding
hid_hotplug_context.mutex. Meanwhile, hid_hotplug_register_callback()
iterates devs under that mutex during HID_API_HOTPLUG_ENUMERATE. A device
disconnect processed by callback_thread could free a devs entry while
the ENUMERATE loop was still traversing it, causing a use-after-free.

Fix by wrapping process_hotplug_event() with hid_hotplug_context.mutex.
The mutex is recursive, so hid_internal_invoke_callbacks() (which also
acquires it) works correctly. This aligns with how the linux backend
handles the same scenario.

Agent-Logs-Url: https://github.com/libusb/hidapi/sessions/cb1dc652-1f96-457d-8319-55e4a6f6e6f3

Co-authored-by: Youw <5939659+Youw@users.noreply.github.com>
Copilot AI changed the title Fix callback_thread busy-loop and queue synchronization libusb: protect devs list with mutex in process_hotplug_event Mar 30, 2026
Copilot finished work on behalf of Youw March 30, 2026 11:25
@Youw
Copy link
Copy Markdown
Member

Youw commented Mar 30, 2026

@CalcProgrammer1 could you check this change in you environment regarding high CPU usage? it should give even better result.

@Youw Youw changed the title libusb: protect devs list with mutex in process_hotplug_event libusb: Fix high CPU usage in callback thread Mar 30, 2026
@mcuee mcuee added the libusb Related to libusb backend label Mar 31, 2026
@Youw Youw marked this pull request as ready for review March 31, 2026 10:48
Copilot AI changed the title libusb: Fix high CPU usage in callback thread libusb: fix use-after-free race in hotplug devs list during ENUMERATE Mar 31, 2026
Copilot finished work on behalf of Youw March 31, 2026 11:05
@Youw Youw changed the title libusb: fix use-after-free race in hotplug devs list during ENUMERATE libusb: Fix high CPU usage in callback thread Mar 31, 2026
The struct comment on hid_hotplug_context.devs claimed it was protected
by hid_hotplug_context.mutex, but the final cleanup in callback_thread
ran under only callback_thread.mutex — inconsistent with all other
accesses (process_hotplug_event, hid_hotplug_register_callback) that
hold hid_hotplug_context.mutex when reading or writing devs.

Move the free to hid_internal_hotplug_cleanup(), after
hidapi_thread_join(libusb_thread) returns, so every access to devs —
including the teardown free — happens while holding the context mutex.

Also fix a leak on the hid_hotplug_register_callback error path: when
libusb_hotplug_register_callback fails, no hotplug thread is created,
so hid_internal_hotplug_cleanup() can't be used to unwind the devs list
assigned by the preceding hid_enumerate() call. Free it inline on that
path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Youw
Copy link
Copy Markdown
Member

Youw commented Apr 24, 2026

I think this PR has more than enought changes already. Will merge it like this and going to review/update more of this using Claude - it already spotted potential races and UB in current implementation.

@Youw Youw merged commit 1889e9f into connection-callback Apr 24, 2026
21 checks passed
@Youw Youw deleted the copilot/optimize-hidapi-thread-wait branch April 24, 2026 10:59
@mcuee mcuee added the hotplug Related to hotplug label Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hotplug Related to hotplug libusb Related to libusb backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants