-
-
Notifications
You must be signed in to change notification settings - Fork 852
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
Introduce basic Memory<T> API-s #607
Conversation
…s.MemoryManager<T> API
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.
Nice work! I had a good read of the linked rules and I cannot see anything that violates those rules.
Naming is good and I'm glad to see GetSpan()
, I much prefer a definitive method of a property, less sleight-of-hand.
DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.Y, 0, nameof(rectangle)); | ||
DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, destinationBuffer.Width, nameof(rectangle)); | ||
DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, destinationBuffer.Height, nameof(rectangle)); | ||
ImageSharp.DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.X, 0, nameof(rectangle)); |
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.
We'll need to clean up and consolidate these Guard
classes sometime. I don't think there's anything in the ImageSharp ones that couldn't be moved to Core
. I'm actually tempted to make them public.
@@ -5,8 +5,8 @@ | |||
using System.Diagnostics; |
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.
When do we get to ditch this class? I don't think it's used much now.
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.
Image.SwapPixelsBuffers()
and ImageFrame.SwapPixelsBuffers()
won't work when the image is backed by an externally owned buffer thus those methods/processors that relay on that api need to know they won't work and throw a not supported exception. (this will effect all apis that resize the images backing buffer, resize, crop etc) this is why my original proposal for IBuffer has the method bool SwitchBuffer(IBuffer<T> buffer);
so that it would handle that aspect internally an allow this sort of API.
@tocsoft good finding thanks! |
Codecov Report
@@ Coverage Diff @@
## master #607 +/- ##
==========================================
+ Coverage 88.46% 88.56% +0.09%
==========================================
Files 872 879 +7
Lines 36822 37039 +217
Branches 2620 2631 +11
==========================================
+ Hits 32574 32802 +228
- Misses 3450 3455 +5
+ Partials 798 782 -16
Continue to review full report at Codecov.
|
@tocsoft after thinking a lot, it seems still most reasonable to manage the ownership move/swap logic in
For me it seems we are good with these rules, added a plenty of tests to ensure they work. |
where TPixel : struct, IPixel<TPixel> | ||
=> ref DangerousGetPinnableReferenceToPixelBuffer((IPixelSource<TPixel>)source); | ||
{ | ||
return source.PixelBuffer.Buffer.Memory; |
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.
we shouldn't expose the buffer as a Memory as we then can't swap the buffer and return the memory to the Array pool other wise someone could have a reference to a piece of memory that has been returned to someone else or was owned by an external process and it could leak dangerous things
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 image itself should be treated as Memory of sorts but can never share the hand over directly indefinitely.
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.
@tocsoft the same could be true for Span<T>
:
Span<int> testSpan = default;
using (IBuffer<T> buffer = allocator.Allocate<int>(10))
{
// span is assigned, we are on the same method stack in the using block:
testSpan = buffer.GetSpan();
}
using (IBuffer<T> somethingElse= allocator.Allocate<int>(10))
{
somethingElse.GetSpan()[5] = 666;
}
// we are out of the intended scope here:
int naiveUsersValue = testSpan[5];
Despite the dangers I think both image.GetPixelMemory()
and image.GetPixelRowMemory(y)
are useful methods for advanced users, because they allow heaping pixel rows, so users can use them in async API-s etc.
What we can do is:
- Document these pitfals
- Introduce a new level of indirection: Decouple
IBuffer<T>
fromMemoryManager<T>
, so we can transparently swap the buffer behind theMemory<T>
instance.
Implementing point 2. would take many hours, delaying the open-up of memory API-s further. We can actually do it later, as an additional increment.
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.
We have no one actively after Memory<T>
when Span<T>
as having a reference to the image is right next to it.
I vote we remove it from the API until we can expose it safely or at least its requested. Until then its an API we have to support which very easily (unlike span which is much harder and can only leak within a single stack) can leak the contents of memory at any random point in the future of the whole application.
I think its too much risk with near to zero benefits that can be handled by keeping a reference to 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.
@tocsoft you can't do the following with span:
async Task DoOperationOnPixelsAsync(Span<Rgba32> pixels)
{
await .....;
await ........;
}
One of our users (on gitter) walked into this pitfall resulting in a runtime failure, and he blamed ImageSharp for it. (This is fine, we can't expect everyone to understand stack-only types.) We decided to hide the Span<T>
API-s because of this story. There was a quite valid reasoning, that we should expose only the Memory<T>
variant.
I say we should expose either both the Span and the Memory API-s or neither of them. If we can't agree, I say let's internalize everything for now, turning this PR into a refactor one.
@JimBobSquarePants your thoughts?
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.
Actually ... you don't even need to have a goal of implementing something in a separate async operation for let's say perf. If you are implementing an ASP.NET controller, being in an async context most of the time, it's very easy to walk into a pitfall of this kind.
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.
Span<T>
is now hidden away in the advanced namespace so we are much less likely to get people confused about using Span with async etc.- you won't be doing pixel level operations over spans in a controller in MVC.. they will be advised to create a Processor and do there stuff there (MVC sorted)
- if you changes
Span<T>
inDoOperationOnPixelsAsync
withImage<T>
you can now work async and only have a manor (aggressively in lines called over to get theSpan<T>
as required)
If you are using the API's that expose Span<T>
we are deeming the user knowledgeable enough about the framework to handle/understand how async and Spans don't mix well.
If we don't expose Span<T>
then users can't write new IImageProcessor
s they can do that without Memory<T>
as that's exactly what we are doing internally.
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.
@mellinoe sorry for pulling you in, but I think your opinion might be very useful here!
@iamcarbon - same for you :)
TLDR:
- Exposing only
Span<TPixel>
API-s is dangerous, because it may lead to heap captures for users who don't follow (or don't know about) coding patterns we suggest. - Exposing
Memory<TPixel>
API-s is dangerous, because the memory may point to an "outdated" location after certain processors being executed. (Unless we spend many extra hours of coding.)
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.
@tocsoft if no one disagrees, I'm being tempted to accept your reasoning.
Exposing Memory<T>
could be an incremental addition later.
@tocsoft made Gonna merge this, if you are fine with the current status. |
I can't see anything I'm not happy with now. 👍 |
Introduce basic Memory<T> API-s
Prerequisites
Description
This PR introduces some memory API-s specified in #565. I think I managed to understand and follow the ownership management rules of
Memory<T>
. beta5 was originally intended to be a hotfix release, but I think it's worth to add these API-s.Features
AdvancedImageExtensions
:GetPixelMemory()
/GetPixelSpan()
for bothImage<T>
andImageFrame<T>
GetPixelRowMemory()
/GetPixelRowSpan()
for bothImage<T>
andImageFrame<T>
Image.WrapMemory(Memory<T>)
for wrapping an existing external memory area (eg. contents of aSystem.Drawing.BitmapData
)System.Drawing
)Internal Refactors
System.Buffers.MemoryManager<T>
(formerly known asOwnedMemory<T>
) byIBuffer<T>
implementationsShapeRegion
to allow droppingAllocateFake
Breaking Changes
MemoryManager
toMemoryAllocator
to avoid name conflicts withSystem.Buffers.MemoryManager<T>
SixLabors.Memory
namespace instead ofSixLabors.ImageSharp.Memory
. In order to concentrate the breaking changes into a single release, I decided to do the namespace move in this PR.DangerousGetPinnableReferenceToPixelBuffer()
is marked obsolete.MemoryMarshal.GetReference(source.GetPixelSpan())
should be used instead