Skip to content

Conversation

@tocsoft
Copy link
Member

@tocsoft tocsoft commented May 6, 2020

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

This adds the API surface to support async saving and loading from streams.

This is really a bit of a cheat and doesn't actually process the stream asynchronously just helps prevent people, when loading from aspnetcore setting where syncIO is disabled, having to manually do the memory copy we keep tellign them to do.. we just do it for you if we believe it will be needed and allows us (in the future) to start actually implement the codecs asynchronously as and when we want.

This API addition does raise the whether we need/want to introduce sync XXXWithFormat() apis to match the async ones and depricate/hide the overloads with out the params to keep the sync and async APIs in sync.

Also should the FormattedImage structs be made IDisposable and pass the dispose along to the Image so you can do

using(var i = async Image.ImageWithFormat(stream)){
     i.Image.DoStuff();
}

vs

var i = async Image.ImageWithFormat(stream) 
using(var img = i.Image){
     img.DoStuff();
}

or do we use some other way of representing those sorts of APIs?

@codecov
Copy link

codecov bot commented May 6, 2020

Codecov Report

Merging #1196 into master will decrease coverage by 0.34%.
The diff coverage is 49.69%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1196      +/-   ##
==========================================
- Coverage   82.74%   82.40%   -0.35%     
==========================================
  Files         695      696       +1     
  Lines       30252    30553     +301     
  Branches     3429     3482      +53     
==========================================
+ Hits        25033    25176     +143     
- Misses       4523     4658     +135     
- Partials      696      719      +23     
Flag Coverage Δ
#unittests 82.40% <49.69%> (-0.35%) ⬇️
Impacted Files Coverage Δ
src/ImageSharp/Formats/Gif/GifDecoder.cs 50.00% <0.00%> (-42.31%) ⬇️
src/ImageSharp/Formats/Gif/GifDecoderCore.cs 79.20% <0.00%> (-4.70%) ⬇️
src/ImageSharp/Formats/Jpeg/JpegDecoder.cs 48.14% <0.00%> (-44.71%) ⬇️
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs 89.68% <0.00%> (-3.20%) ⬇️
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs 94.61% <ø> (ø)
src/ImageSharp/Formats/Png/PngDecoder.cs 47.61% <0.00%> (-43.30%) ⬇️
src/ImageSharp/Formats/Png/PngDecoderCore.cs 88.09% <0.00%> (-2.52%) ⬇️
src/ImageSharp/Formats/Tga/TgaDecoder.cs 45.45% <0.00%> (-45.46%) ⬇️
src/ImageSharp/Formats/Tga/TgaDecoderCore.cs 87.57% <0.00%> (-3.70%) ⬇️
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs 93.53% <50.00%> (-0.91%) ⬇️
... and 18 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 16c417f...73fed79. Read the comment docs.

}
else
{
// cheat for now do async copy of the stream into memory stream and use the sync version
Copy link
Member

@JimBobSquarePants JimBobSquarePants May 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use a pool backed memory stream that allocates small chunks in .Web. Not sure how performant reading would be though.

https://github.com/SixLabors/ImageSharp.Web/blob/master/src/ImageSharp.Web/ChunkedMemoryStream.cs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is maybe RecyclableMemoryStream an option? Its meant to be a drop in replacement for MemoryStream

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to have a look at a read-only version of this less overhead and should support all out target frameworks.

Copy link
Member

@antonfirsov antonfirsov May 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RecyclableMemoryStream seems more sophisticated at first glance.

Whichever we choose, I think it would make sense to integrate our adapted solution with MemoryAllocator.

@JimBobSquarePants
Copy link
Member

@tocsoft I'll review this over the weekend.

@antonfirsov
Copy link
Member

I will also try my best to have a look, but I need a couple of days, hope to make it before Saturday.

JimBobSquarePants and others added 14 commits May 17, 2020 11:19
Co-authored-by: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Co-authored-by: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Co-authored-by: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Co-authored-by: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Co-authored-by: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Co-authored-by: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Co-authored-by: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Co-authored-by: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Co-authored-by: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Co-authored-by: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Co-authored-by: Brian Popow <38701097+brianpopow@users.noreply.github.com>
@JimBobSquarePants
Copy link
Member

Making FormattedImage etc implement IDisposable would certainly make the code less awkward to consume, I'm not sure about the naming of those types though as they suggest to me that they affect the layout of the image in memory. I'm, thinking something like FormatImagePair

Copy link
Member

@antonfirsov antonfirsov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for joining the party late. Mostly comments / discussion starters, the only real concern is the one about the FormattedImage type.

/// <exception cref="UnknownImageFormatException">Image format not recognised.</exception>
/// <exception cref="InvalidImageContentException">Image contains invalid content.</exception>
/// <returns>A <see cref="Task{FormattedImage}"/> representing the asynchronous operation.</returns>
public static Task<FormattedImage> LoadWithFormatAsync(Stream stream)
Copy link
Member

@antonfirsov antonfirsov May 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering whether it would be better to have this family of methods under FormattedImage and FormattedImageInfo. Cleaner naming, but would be worse for discoverability.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structurally cleaner yeah, but I think we'll end up confusing people if we split out the async API. I'd rather avoid the questions.

/// <summary>
/// Struct to curry <see cref="Image"/> and <see cref="IImageFormat"/> for return from async overloads.
/// </summary>
public readonly struct FormattedImage : IEquatable<FormattedImage>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the benefit for defining this type as a struct instead of a class? (Performance doesn't matter here.)

Is it future proof in it's current form? We may want to extend it to contain more more stuff.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not happy with the name just now... Needs some thought

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been putting some thought to this; I really don't want it to be an extensible object.

My reasoning is that we are merely replicating the synchronous API here.

I'm still not happy with the name Formatted suggests the image data is encoded. Do we even need our own types? We could use a named Tuple return type?

public static Task<(Image image, IImageFormat Format)> LoadWithFormatAsync (...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this also means we can keep the existing synchronous out IImageFormat APIs. It becomes more obvious with a tuple we are working around the syntatic limitations of the language.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Named Tuples require a minimum language versions though (I'm assuming @tocsoft that's why you went the explicit struct route). Do we want to enforce that?

Dammit this is tricky.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest I totally didn't think about named tuples having a min version.

The reason I made an explicit type was because I was working on thought that you shouldn't return Tuples from public APIs, but on this sort of return site (working around language constructs) in hind sight I'm now less bothered in this instance. Even for older language versions just the types of the 2 properties are unique enough to distinguish what the items are.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I think I’m gonna push onwards with them then. The language constraints are low enough and they just feel right as an alternative to the sync API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's not an extensibility point, anonymous tuple looks better.

}
else
{
using (var ms = new MemoryStream())
Copy link
Member

@antonfirsov antonfirsov May 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future we may consider using our own variant of RecyclableMemoryStream to reduce allocation pressure.

Update: I joined the existing discussion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't even need one tbh. We know the length of the input stream, tightly control the lifecycle of the MemoryStream. We could simply rend an array from the shared pool and pass to the MemoryStream constructor.

{
if (stream.CanSeek)
{
return this.Decode<TPixel>(stream);
Copy link
Member

@antonfirsov antonfirsov May 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether this execution path is Stephen Toub proof, however I can live with it 😄

I have mixed feelings: the strict ASP.NET rules prohibiting sync code are unnecessary for CPU heavy imaging code path, but using async over sync is also far from being ideal. The picture is somewhat better if I view it as an implementation detail of decoders to be improved in a future by implementing actual async decoder path.

Comment on lines +35 to +48
Guard.NotNull(stream, nameof(stream));

var decoder = new BmpDecoderCore(configuration, this);

try
{
return await decoder.DecodeAsync<TPixel>(stream).ConfigureAwait(false);
}
catch (InvalidMemoryOperationException ex)
{
Size dims = decoder.Dimensions;

throw new InvalidImageContentException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can DRY away these code duplications with the help of an IImageDecoderCore interface if we want. (Same for encoders.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I like this. Can come later though since everything is internal (unless someone is desperately keen to do it).

@JimBobSquarePants
Copy link
Member

I need to pick this up as it's likely beneficial to #1205

@JimBobSquarePants JimBobSquarePants added this to the 1.0.0 milestone Jun 9, 2020
@JimBobSquarePants JimBobSquarePants merged commit b29bb74 into master Jun 9, 2020
@JimBobSquarePants JimBobSquarePants deleted the sw/fake-async-codecs branch June 9, 2020 23:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants