-
Notifications
You must be signed in to change notification settings - Fork 8.4k
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
Fix RenderThread's notify mechanism #4698
Conversation
I wonder if the CI failed because it's bugged or if it's because my code is bad. It failed on a test that uses the |
Reran failed x86 leg. That test has been a pain in our side. It may or may not be your fault. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I've read this code about 8 times, and I think it makes sense to me now. I think we'll have enough runway before the next release to make sure that this doesn't break anything mysteriously. Thanks for debugging this!
@msftbot make sure @miniksa signs off on this |
Hello @zadjii-msft! Because you've given me some instructions on how to help merge this pull request, I'll be modifying my merge approach. Here's how I understand your requirements for merging this pull request:
If this doesn't seem right to you, you can tell me to cancel these instructions and use the auto-merge policy that has been configured for this repository. Try telling me "forget everything I just told you". |
Last commit is a little perf improvement: (Sorry for removing |
@msftbot make sure @DHowett-MSFT signs off |
} | ||
|
||
// <-- | ||
// If `NotifyPaint` is called at this point, then it _will_ set |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent commenting. Thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me, but I like having @DHowett-MSFT as a double check on certain things and this is one of them.
Hello @DHowett-MSFT! Because this pull request has the p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (
|
(It looked good to me; GH didn't show that @zadjii-msft had signed off, but I would have been the third.) |
🎉 Once again, thanks for the contribution! This pull request was included in a set of conhost changes that was just |
Summary of the Pull Request
Fix a bug where the
Renderer::PaintFrame
method:RenderThread::NotifyThread
call but needs to be called because there the terminal was updated (theoretical bug)References
The bug was introduced by #3511.
PR Checklist
Detailed Description of the Pull Request / Additional comments
Before
First bug
In the original code,
_fNextFrameRequested
is set totrue
in render thread becausestd::atomic_flag::test_and_set
is called.This is wrong because it means that the render thread will render the terminal again even if there is no change after the last render.
I think the the goal was to load the boolean value for
_fNextFrameRequested
to check whether the thread should sleep or not.The problem is that there is no method on
std::atomic_flag
to load its boolean value. I guess what happened was that the "solution" that was found was to usestd::atomic_flag::test_and_set
, followed bystd::atomic_flag::clear
if the value wasfalse
originally (ifstd::atomic_flag::test_and_set
returnedfalse
) to restore the original value. I guess that this was believed to be equivalent to just a simple load, without doing any change to the value because it restores it at the end.But it's not: this is dangerous because if the value is changed to
true
between the call tostd::atomic_flag::test_and_set
and the call tostd::atomic_flag::clear
, then the value ends up beingfalse
at the end which is wrong because we don't want to change it! And if that value ends up beingfalse
, it means that we miss a render because we will wait on_hEvent
during the next iteration on the render thread.Well actually, here, this not even a problem because when that code is ran,
_fPainting
isfalse
which means that the other thread that modifies the_fNextFrameRequested
value throughRenderThread::NotifyPaint
will not actually modify_fNextFrameRequested
but rather callSetEvent
(see the method's body).But wait! There is a problem there too!
std::atomic_flag::test_and_set
is called for_fPainting
which sets its value totrue
. It was probably unintended. So actually, the next call toRenderThread::NotifyPaint
will end up modifying_fNextFrameRequested
which means that the data race I was talking about might happen!Second bug
Let's go back a little bit in my explanation. I was talking about the fact that:
The problem is that the reverse was done in the implementation:
std::atomic_flag::clear
is called if the value wastrue
originally!So at this point, if the value of
_fNextFrameRequested
wasfalse
, thenstd::atomic_flag::test_and_set
sets its is set totrue
and returnsfalse
. So for the next iteration,_fNextFrameRequested
istrue
and the render thread will re-render but that was not needed.After
I used
std::atomic<bool>
instead ofstd::atomic_flag
for_fNextFrameRequested
and the other atomic field because it has aload
and astore
method so we can actually load the value without changing it.I also replaced
_fPainting
by_fWaiting
, which is basically the opposite of_fPainting
but staystrue
for a little shorter than_fPainting
would stayfalse
. Indeed, I think that it makes more sense to directly wrap/scope just the call toWaitForSingleObject
by setting my atomic variable totrue
just before and tofalse
just after because:_fWaiting
is (that is, to callSetEvent
fromRenderThread::NotifyPaint
if it'strue
).true
for a little shorter which means less calls toSetEvent
.Warning
I don't really understand std::memory_orders.
So I used the default one (
std::memory_order_seq_cst
) which is the safest.I believe that if no read or write are reordered in the two threads (
RenderThread::NotifyPaint
andRenderThread::_ThreadProc
), then the code I wrote will behave correctly.I think that
std::memory_order_seq_cst
enforces that so it should be fine, but I'm not sure.Validation Steps Performed
I tried to reproduce the second bug that I described in the first section of this PR.
I put a breakpoint on
RenderThread::NotifyPaint
and onRenderer::PaintFrame
. Initially they are disabled. Then I ran the terminal in Release mode, waited a bit for the prompt to display and the cursor to start blinking. Then I enabled the breakpoints.Before
Each
RenderThread::NotifyPaint
is followed by 2Renderer::PaintFrame
calls. ❌After
Each
RenderThread::NotifyPaint
is followed by 1Renderer::PaintFrame
call. ✔️