-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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 WriteableBitmap misuse in several places, avoid UnmanagedBlod disposal from finalizer #14181
Conversation
You can test this PR using the following package version. |
I would argue Avalonia doesn't properly implement WriteableBitmap to begin with ;) Also note that the original ColorPicker code is copied from WinUI and then a C#/.NET port from a few years ago that uses the original WriteableBitmap. So it's using a WriteableBitmap because that is what original code did and what you need to do in other XAML frameworks. Avalonia is just... different. I'll comment more specifically on the changes. In general though, I don't have any major objection to the change itself. You know more about this than me. However, I do think Avalonia should stop using unsafe code like this (at the control level) and use/implement WriteableBitmaps like everyone else.
I never liked ArrayList to begin with and only wrote it to avoid 1 extra copy. If we have a PooledList that can do this already that's great. I did not know about this Avalonia type. |
@@ -1187,7 +1171,7 @@ private async void CreateBitmapsAndColorMap() | |||
} | |||
}); | |||
|
|||
Dispatcher.UIThread.Post(() => | |||
await Dispatcher.UIThread.InvokeAsync(() => |
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 switch from Post to InvokeAsync? The original code didn't have any await and was done completely async and the bitmaps came in a bit later after the control loads.
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 method is a scope for pooled arrays. Without awaiting for Dispatcher call, pooled arrays are going to be disposed way too early.
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.
Hm, I need to test this then. As long as we aren't blocking loading everything else and displaying the control awaiting the bitmaps to load it's fine. But bitmaps must be completely async -- especially on WASM they take a long time to load.
_valueBitmap = _maxBitmap; | ||
break; | ||
case ColorSpectrumComponents.ValueSaturation: | ||
case ColorSpectrumComponents.SaturationValue: | ||
_hueRedBitmap?.Dispose(); |
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.
Hm... I don't have anything against using a type other than ArrayList. However, Dispose() implies more native resource usage and shouldn't really be needed in a managed environment. I don't like the dispose calls everywhere and these things are easy to forget about in other code paths...
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 should, really. We can leave bitmap to float in the memory, as they will be garbage collected anyway. But from our experience we know that skia objects finalizers are pretty slow, especially when there are many of them. Since we can dispose old value with not much issues - it should be done.
? pixelDataSize : 0; | ||
|
||
var newHsvValues = new List<Hsv>(pixelCount); | ||
using var bgraMinPixelData = new PooledList<byte>(pixelDataSize, ClearMode.Never); |
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.
Again, I'm not a fan of using a type for simple array data that requires disposing.
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.
Array pool data would require disposing either way, as array needs to be returned to the pool at some point.
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.
Well, I guess PooledList is overkill for this scenario. We only need an array with an .Add method for usability with existing code. Then we can re-use the same array reference as long as the bitmap size doesn't change. Just keep overwriting the data. No disposal needed.
I just don't like to see Dispose() much in high-level managed code... so it's just an observation. Then again, I also don't like the old ArrayList class either. Hopefully this all goes away in the future with composition brushes.
As discussed with @kekekeks, potential problem with previous attempts might be caused by the fact that previous WriteableBitmap instance was still used as a value for Image.Source property. It shouldn't raise any exceptions, if Image.Source value was replaced before disposing old bitmap. Even better would be to use shaders and render spectrum on the GPU directly, but we don't have that yet. Using skia directly would greatly limit the control. |
Yes, I had a later thought too. Upstream uses a custom composition brush for this on newer versions of Windows. We should do the same eventually. So I can accept that what we have now is a temporary solution solving a few things including performance. But it should be replaced in a few years when we have better options. |
ArrayList<byte> bgraPixelData = await ColorPickerHelpers.CreateComponentBitmapAsync( | ||
// siteToCapacity = true, because CreateComponentBitmapAsync sets bytes via indexer over pre-allocated buffer. | ||
using var bgraPixelData = new PooledList<byte>(pixelWidth * pixelHeight * 4, ClearMode.Never, true); | ||
await ColorPickerHelpers.CreateComponentBitmapAsync( |
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.
BTW, are we canceling previously queued operations when UpdateBackground is called again? Is there even a queue or do we have async operations executed on the thread pool in random order?
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, why is it event a list in the first place, could have been just some buffer rented from array pool
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.
BTW, are we canceling previously queued operations when UpdateBackground is called again?
No
Is there even a queue
No
do we have async operations executed on the thread pool in random order?
Theoretically, yes. In practice, no. The background bitmaps for both component sliders and the color spectrum DO NOT matter for the function of the color picker itself. Even if these bitmaps are completely wrong or missing the control will still work without an issue. They are visual-only and therefore it isn't considered important enough to worry about this. In practice I have never seen any issues with bitmaps rendering out of order and being noticeably different to the end user.
This is also why I originally had code calculating them completely async so the control can load and function while the bitmaps load (which can take a while on some platforms).
could have been just some buffer rented from array pool
The code follows the ideas of upstream WinUI which uses shared_ptr<vector<::byte>>
usage. I certainly wasn't aware you had a special PooledList at the time and didn't think to use a pooled array either. That said, a lot of the code relies on using .Add()-type ideas where we don't need to track an index in the buffer. That means an array can't be used directly without modifying some code I really didn't/don't want to modify.
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.
Task.Run possibly can be removed, as there is none in WinRT code. Especially when we reduced all the allocations.
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.
Task.Run possibly can be removed, as there is none in WinRT code. Especially when we reduced all the allocations.
I'm fine with having this as a separate discussion along with: #14181 (comment). I just want to keep bitmap generation async. Maybe the solution for that is also composition brushes though running on another thread.
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.
LGTM in general, probably need to investigate Task.Run usage separately
You can test this PR using the following package version. |
…isposal from finalizer (#14181) * Fix TrayIconImpl not disposing WriteableBitmap when it should * Replace WriteableBitmap with Bitmap in ColorPicker * Replace ArrayList with PooledList to avoid extra allocations
What does the pull request do?
Fixed issues
Technically #9092, but that issue explains an actual bug somewhere in WriteableBitmap code, which is a different issue to solve.
Double fixes #9051
cc @robloo