-
Notifications
You must be signed in to change notification settings - Fork 690
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
Avoid the blocking wait operation in RestoreOperationLogger #1588
Conversation
@sharwell, |
💭 The CI infrastructure seems to be down, as it hasn't picked up this PR for 15 minutes now. |
@sharwell Update: |
This explains why I had 149 random branches to delete when I forked the repository. 😄 |
Thanks for the PR @sharwell! I made an attempt at avoiding blocking the UI using a pumping logger on another thread and a blocking collection to hold message, similar to the other option you mention in the issue. This change looks much simpler which I like. Concerns
Last time this was investigated perf was improved by using different APIs for logging to VS, and by filtering out the messages earlier on log level. From your stats it looks like this change is needed to avoid blocking. |
// Run on the UI thread | ||
Do((_, progress) => | ||
var ignored = DoAsync((_, progress) => |
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.
nit: _ =
instead ignored
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 is a candidate feature for a future version of C# (dotnet/csharplang#111), but currently isn't available.
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.
It is for the return value.
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.
There is already a parameter named _
on the same line 😉
@@ -145,10 +149,20 @@ public override void Log(ILogMessage logMessage) | |||
// Avoid moving to the UI thread unless there is work to do | |||
if (reportProgress || showAsOutputMessage) | |||
{ | |||
// Make sure the message is queued in order of calls to LogAsync, but don't wait for the UI thread | |||
// to actually show it. | |||
_loggedMessages.Enqueue(Tuple.Create(reportProgress, showAsOutputMessage, logMessage)); |
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.
Why not use the tuple syntax?
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.
The tuple syntax would be very much preferable, but I wasn't sure if this project was using it yet.
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.
➡️ Currently System.ValueTuple
is not available for use in this project.
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.
Create a private struct with the two fields?
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.
@benaadams I predict that in the near future someone will finally add System.ValueTuple
to this project and go through the code searching for Tuple.Create
. The currently implementation works well with such a transition. It's also part of the reason why I created the field's comment in the form I did.
} | ||
} | ||
|
||
Log(logMessage); | ||
return Task.FromResult(0); |
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.
Avoid allocating the task every time by returning a cached one.
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 is an extremely common pattern in the current code base
- It didn't show up as a dominant contributor in recent profiling
➡️ Based on this, I decided to treat this as a code cleanup issue which can wait for a future commit dedicated to this effort.
The dequeue occurs on the UI thread, so printing is guaranteed to occur in the same order as the messages end up in the queue.
It is unlikely this needs throttling.
In my original work on this issue, I investigated transitioning to We're gaining a very real benefit by ensuring the progress of the background operation is not dependent on the responsiveness of the UI thread. As mentioned above, it's unlikely this will be a problem (the user can only observe the case if the UI responds, and the delay is only pronounced if the UI does not respond). If we do encounter cases that meet the described clearing scenario (for me the window is never automatically cleared), we can simply move the clear command into the queue to ensure it executes at an appropriate time. |
I don't understand why queue is needed if every call creates "abandoned" task. Alternatively the same effect would be achieved by simply replacing |
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.
Blocking for now to clarify the design.
I wasn't sure that we could depend on this line getting executed prior to
|
@AArnott Is there a better way to facilitate |
@sharwell My biggest concern here is scheduling abundance of ignored tasks as it may potentially cause reliability issues. For instance, consider a scenario when a user wants to shut the VS instance down while it's still printing hundreds of messages. I suggest keeping all of these tasks in a |
@alpaix Yes, it's certainly possible to do this without blocking any thread. Except in the shutdown case, where the whole point is to be able to block the UI thread until the async work has been completed (or canceled) so that you don't tear down the AppDomain while code is still running in it. |
@sharwell The There are things you can do to set priority for your UI thread request, based on VS-proprietary priority levels or Dispatcher priorities. |
Closing, this was merged with #1638 |
The approach used here is the more important of a two-part true solution. The natural solution would be using
ILogger.LogAsync
instead of the various synchronous calls currently in place. However, I realized that while such a solution would address the thread pool starvation problems, it still leaves NuGet susceptible to not making progress whenever the UI thread is blocked. The change here instead decouples the UI thread logging operations from the ongoing background work, but takes care to ensure that when the messages finally do appear, they appear in the same order they otherwise would have.Since the code makes no effort to ensure that the operations started by 2 actually execute in the same order as the messages in 1, all information necessary for printing the message on the UI thread must be held in the queue.
Fixes NuGet/Home#5663