Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dedicated HID IO thread to prevent blocking by HIDAPIs slow processing of OutputReports #4585

Merged
merged 95 commits into from
Feb 20, 2022

Conversation

JoergAtGithub
Copy link
Member

@JoergAtGithub JoergAtGithub commented Dec 27, 2021

This PR fixes performance issues by slow execution of the hid_write command of HIDAPI. This can block the controller mapping thread for several milliseconds (in my case of Traktor Z2 on Windows it takes ~7ms for each OutputReport). This is a performance regression introduced in #2179. The thread safe usage is guaranteed here as well, because all HIDAPI IO happens serialized in the same thread.

Improvements of this PR:
-Ensure that the ring-buffer of InputReports is polled between hid_write of two OutputReports
-Move HID IO in a dedicated thread, this allows the JavaScript controller mapping to continue, while HIDAPI waits for completion of the data transfer of the OutputReport
-Skip sending of OutputReports, if the data didn't changed - as already implemented for polled InputReports
-Skip sending superseded OutputReports, if the mapping sends them faster than HIDAPI can send them
-Added timing information to --controllerDebug output, that mapping developers can understand the timing behavior
-It does no longer relies on timers, which behave unreliable for the needed short periods and depend on the different OSes.

@github-actions github-actions bot added the build label Dec 27, 2021
@JoergAtGithub JoergAtGithub force-pushed the hid_io_thread2 branch 4 times, most recently from 039e29b to 387bc55 Compare December 27, 2021 23:02
… block the controller thread for several milliseconds per OutputReport:

-Ensure that the ring-buffer of InputReports is polled between hid_write of two OutputReports
-Move HID IO in a dedicated thread, this allows the JavaScript controller mapping to continue, while HIDAPI waits for completion of the data transfer of the OutputReport
-Skip sending of OutputReports, if the data didn't changed - as already implemented for polled InputReports
-Added timing information to --controllerDebug output, that mapping developers can understand the timing behavior
@Be-ing
Copy link
Contributor

Be-ing commented Dec 28, 2021

Does this risk bringing back the deadlock issue solved in #2179?

@JoergAtGithub
Copy link
Member Author

Does this risk bringing back the deadlock issue solved in #2179?

No, the reason for the deadlock was that hid_read and hid_write were used in different threads, but both used the same non-thread-safe device datastructure of hidapi.
This is not the case here, all HID IO is implemented in the same thread.

src/controllers/hid/hidio.h Outdated Show resolved Hide resolved
src/controllers/hid/hidio.h Outdated Show resolved Hide resolved
src/controllers/hid/hidio.h Outdated Show resolved Hide resolved
src/controllers/hid/hidio.h Outdated Show resolved Hide resolved
src/controllers/hid/hidio.h Outdated Show resolved Hide resolved
src/controllers/hid/hidio.cpp Outdated Show resolved Hide resolved
src/controllers/hid/hidcontroller.cpp Outdated Show resolved Hide resolved
src/controllers/hid/hidio.cpp Outdated Show resolved Hide resolved
src/controllers/hid/hidio.h Outdated Show resolved Hide resolved
src/controllers/hid/hidio.h Outdated Show resolved Hide resolved
@uklotzde
Copy link
Contributor

Please note that I am not planning to decide when the PR is ready to be merged. I am done with C++ and consider myself unable to do this. Just wanted to point out my serious concerns.

Removed unnecessary parent QObject
Make the pointers to hid_device structure const, but the structure itself is not const. Added a comment to explain this.
Use default constructor for m_lastSentOutputReport
Deleted empty deconstructors
Use atomicStoreRelaxed(m_stop instead of m_stop =
@Be-ing
Copy link
Contributor

Be-ing commented Dec 28, 2021

Please separate distinct changes into separate commits whenever feasible. You don't need to go back and split up the last commit, but please keep that in mind going forward. If we want to squash them at the end, we can.

Use standard Event Loop for HidIoThread
Removed no longer needed implementation of IsPolling
@JoergAtGithub JoergAtGithub force-pushed the hid_io_thread2 branch 2 times, most recently from 22007fc to da8b9b7 Compare January 5, 2022 21:49
… the performance of the logging statements from milliseconds to some microseconds
@JoergAtGithub JoergAtGithub force-pushed the hid_io_thread2 branch 2 times, most recently from 8d65947 to f2fac26 Compare January 5, 2022 23:35
CMakeLists.txt Outdated Show resolved Hide resolved
@uklotzde
Copy link
Contributor

@ywwg Please update your review after you requested changes for issues that have long been resolved.

…tects the input report polling data

Improved the comment describing this mutex
Copy link
Contributor

@uklotzde uklotzde left a comment

Choose a reason for hiding this comment

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

Thank you, Jörg! Your commit messages helped a lot to keep track of the changes for the re-reviews.

I haven't tested the code in action yet, maybe later this week. But this should not block merging. It has already been confirmed that outsourcing the interaction with the HID API into a separate thread has improved the situation.

No promises that the code is correct. Nevertheless, LGTM

@uklotzde
Copy link
Contributor

We should probably stash the commits before merging!

@uklotzde uklotzde merged commit 924f7ed into mixxxdj:main Feb 20, 2022
@JoergAtGithub
Copy link
Member Author

Thank you all for your review feedback - without this, the code wouldn't have reached the maturity, that it has now!

Comment on lines +87 to +88
// This means there is always a one to one relationship to the state of control(s)/LED(s),
// and if the state is not changed, there's no need to execute the time consuming hid_write again.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is incorrect and has broken the output messages driving the motor in my Kontrol S4 Mk3 mapping. The motor stops turning if no output packets are sent. To keep a constant velocity, identical output packets must be sent repeatedly.

Copy link
Contributor

Choose a reason for hiding this comment

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

I can think of three ways forward:

  1. Remove this optimization
  2. Add an optional parameter to the send function to opt in to allowing duplicate packets.
  3. Add a separate function like sendAllowDuplicates for this

Copy link
Member Author

Choose a reason for hiding this comment

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

I will look how to solve this!

Copy link
Contributor

@uklotzde uklotzde Feb 23, 2022

Choose a reason for hiding this comment

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

The default should behavior should ignore duplicate reports. The timing and jitter of the reports is unpredictable and usually not suitable for implementing a real-time control cycle that requires exact cycle times.

I vote for adding an extra parameter to override the default behavior if needed, e.g. sendDuplicateReport

Copy link
Member Author

Choose a reason for hiding this comment

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

I would be fine with solution 2. too!

But I could also imagine fourth solution:
4. Add a function configureOutputReport(int ReportID, bool sendDuplicates = false) which needs to be called only once in the init section of the JS mapping code.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer option 2 to add an optional parameter to the existing send function rather than add more functions for this edge case.

Copy link
Contributor

@uklotzde uklotzde Feb 24, 2022

Choose a reason for hiding this comment

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

I recommend to avoid stateful code if possible. Let the script pass an additional parameter on each invocation. It is less error prone and provides more flexibility.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, than I will provide a PR for 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sync Controller polling with audio buffer (Latency)
7 participants