-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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 possible freeze when downloading avatars #5868
Fix possible freeze when downloading avatars #5868
Conversation
// let's make sure this doesn't happen on the current thread (likely the UI thread) | ||
var readTask = Task.Run(() => webClient.OpenReadTaskAsync(imageUrl)); | ||
|
||
using (var imageStream = await readTask) | ||
{ | ||
return Image.FromStream(imageStream); |
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.
using (var imageStream = await readTask)
{
using (var img = System.Drawing.Image.FromStream(imageStream)
{
return (Image)img.Clone();
}
}
Remarks
You must keep the stream open for the lifetime of the Image.
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 unrelated to the fix, isn't it ?
I'm surprised by this piece of documentation, by the way : it is also stated for the overload Image.FromStream(Stream, bool)
but not for the "main" overload Image.FromStream(Stream, bool, bool)
, to which the others redirect. It's hard to tell for sure since the implementation delegates quickly to something native, but my guess is that the stream is actually read during the call, and the documentation is incorrect.
Also note that there is another place where Image.FromStream
is called with the same pattern, within a using
block.
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 creates a GDI+ handle. Since the docs say it and the code here. Since we don't know how GDI+ works and how cleanup works in GDI+ and they did mention it in the docs, I don't see a problem of using clone to make sure. The fact you were touching the code made me see the issue. Yes we will need to visit all image.fromstream calls.
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.
Also this is where cleanup occurs. So my using on the image does cleanup the image from the stream and we have an image that we don't have to worry about the stream closing mess.
Dispose() passes true into Dispose(bool).
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 why:
GDI+, and therefore the System.Drawing namespace, may defer the decoding of raw image bits until the bits are required by the image. Additionally, even after the image has been decoded, GDI+ may determine that it is more efficient to discard the memory for a large Bitmap and to re-decode later. Therefore, GDI+ must have access to the source bits for the image for the life of the Bitmap or the Image object.
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.
Doing more research. Looks like clone might not be enough. https://stackoverflow.com/questions/12709360/whats-the-difference-between-bitmap-clone-and-new-bitmapbitmap/13935966#13935966
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.
Anyway, assuming something has to be done (not 100% clear : this pattern exists as it is since at least 8d27233, 8 years ago, apparently without actual issue), it should be in a separate PR, shouldn't it ?
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.
Alright. We will get it from the horse's mouth. Then we can raise an issue to fix it based on their feedback.
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.
Anyway, assuming something has to be done (not 100% clear : this pattern exists as it is since at least 8d27233, 8 years ago, apparently without actual issue), it should be in a separate PR, shouldn't it ?
Yes, I agree, this PR is about blocking UI.
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 will unblock the UI thread, but it will also potentially lock an arbitrary number of thread pool threads. If the user loads a repo and scrolls through commits requiring a large number of uncached user avatars, the thread pool will be exhausted and the application will freeze.
A robust solution needs to limit the number of concurrent requests.
I'm not near a computer, but are we addressing the issue at the right place?
The code in question is async, perhaps the call site needs to be updated to
use the JTF to unblock the UI thread?
…On Mon, Dec 10, 2018, 8:08 PM Drew Noakes ***@***.***> wrote:
***@***.**** commented on this pull request.
This will unblock the UI thread, but it will also potentially lock an
arbitrary number of thread pool threads. If the user loads a repo and
scrolls through commits requiring a large number of uncached user avatars,
the thread pool will be exhausted and the application will freeze.
A robust solution needs to limit the number of concurrent requests.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#5868 (review)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AEMyXlFbA_K0sprWAoeSIEayXWuvZiZ3ks5u3iSggaJpZM4ZJmk2>
.
|
@RussKie : The problem is that the code is not really async : it blocks synchronously (before returning the Task), even though the calling site does use JTF (if my understanding is correct). @drewnoakes : Yes, this is possible, at least in theory. I'm not sure how likely it is in practice, though. OTOH, this is a workaround for something that is essentially a bug in .Net, I think keeping things as minimal as possible has some appeal. I'll try to see if I can address this, but I can't tell when. |
Agreed. BTW I would rather see
Depending upon the machine, you can hit thread pool exhaustion at 25 threads. We do a lot of background work at startup too. I would not brush this off as theoretical.
Adding such protection can be minimal. Use a The code would end up looking something like: private readonly SemaphoreSlim _downloadSemaphore = new SemaphoreSlim(initialCount: 10); And later: using (var webClient = new WebClient { Proxy = WebRequest.DefaultWebProxy })
{
webClient.Proxy.Credentials = CredentialCache.DefaultCredentials;
// WebClient.OpenReadTaskAsync can block before returning a task, so make sure we
// make such calls on the thread pool, and limit the number of concurrent requests.
// Get onto background thread
await TaskScheduler.Default;
// Limit the number of concurrent download requests
await _downloadSemaphore.WaitAsync();
try
{
using (var imageStream = await webClient.OpenReadTaskAsync(imageUrl))
{
return Image.FromStream(imageStream);
}
}
finally
{
_downloadSemaphore.Release();
}
} That's completely untested, but gets the idea across. |
@drewnoakes : That's indeed the kind of stuff ( |
No problem. I'll put a PR together. |
This method can block synchronously. This change moves calls to the thread pool, and limits the number of concurrent downloads to prevent thread pool exhaustion. Fixes gitextensions#5859. Follows and replaces gitextensions#5868.
@lanfeust69 please review #5878. Thanks. |
This method can block synchronously. This change moves calls to the thread pool, and limits the number of concurrent downloads to prevent thread pool exhaustion. Fixes gitextensions#5859. Follows and replaces gitextensions#5868.
Superseded by #5878 |
Fixes #5859