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

REFAC(client, audion): Various refactorings in AudioOutput #6708

Merged
merged 9 commits into from
Feb 7, 2025

Conversation

Krzmbrzl
Copy link
Member

All changes are related to making the control flow more explicit and also more deterministic than it currently is with relying on Qt's signal/slot mechanism for handling buffer deletions. This is important as sometimes, a given buffer really needs to be removed now and not just sometime in the future.
The current code achieves this because in the specific context that the relevant functions are called, the connection that Qt will make are direct and therefore no asynchronous operations take place. But this is absolutely not obvious from the current code. It should, however, be with the refactored code.

Checks

If QMultiHash::value() was called at the exact same time that other
parts of the code mutate the map, this could lead to a data race.

By protecting the map access via the associated lock, concurrent
modification is prevented (assuming all mutating code obtain a write
lock as they should).
This is now encoded into the type system as it is illegal to dereference
a void *. Previously, calling functions just needed to know that the
passed pointer was never going to be dereferenced (in order to
determined whether or not the function call may happen while not holding
the qrwlOutputs lock.
@Krzmbrzl Krzmbrzl requested a review from Hartmnt January 18, 2025 18:42
@Krzmbrzl
Copy link
Member Author

@Hartmnt I just went through our discussion in #5926 again and I think the entire business of double-deletion was wrong because the removeBuffer function that does the deletion searches for the to-be-deleted buffer in the qmOutputs map. Only if it finds it in there, will it do the deletion and in that case, it will remove the associated map entry.
Therefore, no double deletion should be possible 🤔

Copy link
Member

@Hartmnt Hartmnt left a comment

Choose a reason for hiding this comment

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

@Krzmbrzl I tried to read through our old conversation, but I am not able to follow all the thinking from back then. I approve refactoring the AudioOutput logic anyway 😄

This then probably also addresses #6613 right?

What are your thoughts on back-porting this? I tend towards "too big and too prone to introduce new problems"

@Krzmbrzl
Copy link
Member Author

Krzmbrzl commented Jan 19, 2025

This then probably also addresses #6613 right?

Unfortunately, I don't think it does. These really are refactorings that leave the actual behavior (mostly) the same.

And given that the only fix in here is the potential data race on map access, which I would consider to ne very unlikely in the first place, I would tend to not backport this.

The AudioOutput::removeUser function used to initiate a signal being
emitted that leads to the deletion of the audio buffer related to the
given user and also to the removal of that user (and buffer) from the
qmOutputs map.

Since removeUser has (thus far) always been called from the main thread
(in which the AudioOutput QThread object lives), the connection has been
direct, meaning that once removeUser returns to the caller, the user has
indeed been removed from AudioOutput.
However, if removeUser was called from a different thread, the
connection would be queued, meaning that AudioOutput still holds a
reference (or rather pointer) to that user when removeUser returns. This
can easily lead to a memory corruption issue.

Therefore, this commit ensures that removeUser always makes a direct
function call to remove all references to that user so that the caller
can be sure that AudioOutput will have forgotten about that particular
user once that function returns.
Before, the actual wiping of elements was delegated to the removeBuffer
function by means of Qt's signal/slot mechanism. This required a rather
inefficient implementation inside wipe() (using an auxiliary list), lead
to constant locking and re-locking (since a write lock is acquired and
released for every buffer individually) and made it unclear whether
after AudioOutput::wipe() returns, the buffer has actually been wiped
(due to the way the Auto connection type for Qt signal/slots work).

This commit ensures that wipe no longer delegates the work, thereby
fixing all the abovementioned issues.
AudioOutput::addFrameToBuffer used to emit a signal for deleting the old
speech buffer. However, it implicitly relied on that connection being
direct (i.e. it relied on addFrameToBuffer to be called from the main
thread) as otherwise, there would be the potential for a memory leak
where the speech buffer is not processed by removeBuffer before the
addFrameToBuffer call has replaced the associated entry in qmOutputs. At
that point speech will be the last reference pointing to that object,
and as soon as speech goes out of scope, the memory for that object
would have leaked.

In order to make the control flow more clear, we now skip the signal
emission and instead call removeBuffer directly. This has been done in a
way that also prevents the need for an additional acquisition of a write
lock.
@Krzmbrzl Krzmbrzl force-pushed the refac-audio-processing branch from 1ba092d to 8fc7a93 Compare February 7, 2025 18:09
@Krzmbrzl Krzmbrzl merged commit a218014 into mumble-voip:master Feb 7, 2025
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants