Fast and correct resizing for DynamicImage - #2639
Conversation
…ndermagick; not wired up to anything yet
…necessary in-memory copy of the entire image
|
|
||
| # Other features | ||
| rayon = ["dep:rayon", "ravif?/threading"] # Enables multi-threading | ||
| rayon = ["dep:rayon", "ravif?/threading", "pic-scale-safe/rayon"] # Enables multi-threading |
There was a problem hiding this comment.
I want to share an important point about using Rayon for heavy workloads.
Rayon uses some sort of global scheduler see docs that shared between all tasks.
Consider the following pseudocode:
[array_of_many_images_about_80-100].par_iter()
.for_each(|image| {
do_resize_with_rayoun_enabled()
});Due to this rayon rules this code is vulnerable to DDoS attacks since it quickly exhaust RAM because rayon schedules all images at once where other ones are still not finished. I discovered this surprising behavior in libblur on my Mac M3, processing around 80–100 images is enough to knock the system out. That's a fairly small number. Image resizing is a bit less greedy, but it still shows the same characteristics. For some typical shared, or small server with 1-2GB RAM and 10GB of disk and 1-2 vCPU this amount I think the value is 10 times lower.
Even to my low security standards I had to remove rayon from pic-scale, libblur etc and make more classic solution (https://crates.io/crates/novtb), that's not counting it is actually 10% because it's planning heavy jobs better.
Even if we won't actually fix it here, I think it worth document that this API is subject to vulnerability to DDoS attacks with rayon enabled and require some additional attention to it.
There was a problem hiding this comment.
Thanks for letting me know! I've disabled rayon to sidestep this issue.
We can flip it on and update the documentation in the next semver-breaking release.
Separately, it is indeed very odd that rayon doesn't let us limit the amount of items concurrently in flight.
|
I thought it would end up being upstreamed rather than included :) What do you think about transferring pic-scale-safe to image-rs org? I’m not quite sure how it would work on crates.io though, since the naming rules may require the crate to remain under my namespace 🤔 |
|
That sounds good to me but I'd like to hear from the other maintainers first. I don't think I have the required Github permissions in any case. |
|
We received feedback that adding new default dependencies in patch releases complicates things for distros. Unfortunately, we can't make minor non-breaking changes until doing a 1.0 release. On a somewhat related note, I've really been wanting to fix the inconsistency between |
Yes, I think
Distros also take issue with us upgrading major versions of crates, so us bumping But I do believe it's time to start merging breaking changes, now that we know 0.25.8 doesn't need any urgent fixes. I'd love to make this into a breaking change and eliminate the extra copy, and also start merging things like #2636 |
…to the previous API
|
Now that API breaks are allowed, I've eliminated the mandatory copy. The API user can now choose to explicitly clone the image to hold on to an older version of it, or let it be overwritten. I'm not sure if resizing should operate in-place or if it should accept an owned |
|
Since there is an API break anyway, I'd suggest renaming the Perhaps Mitchell-Netravali filter could be added as well, since it's the most commonly used filter nowadays. |
|
If I'm understanding correctly, the reason for the API change is that My instinct is that in the future we may want to again change around how scratch buffers are handled. For instance, nearest sampling shouldn't need a scratch buffer and in other cases we might be able to use a scratch buffer that's much smaller than the input image. In which case, perhaps it would be better to keep the existing API? |
|
No, The original buffer is premultiplied by alpha channel in place, which is done by the code added in this PR. This is necessary to handle resizing images with alpha correctly, see #2422. Nearest neighbour is indeed a special case that does not need premultiplying by alpha. I think it can be exposed separately as Whether to keep the original buffer around and make a copy internally (but only for images with alpha), or whether to always consume the buffer and have the API user make a copy explicitly (wasteful for images without alpha), comes down to what we expect the common case to be. In Or I guess there's also a third option of making a more complicated API that surfaces these differences. |
|
Ah, using the original input to apply premultiplication makes sense. In that case I'm in favor of taking I also noticed that there's two |
|
I'm going to replace I was going to do that in a follow-up PR, but if you'd like me to include it here I'm open to that. |
|
That sounds good for a separate PR! |
|
So this one's good to go? I'd like to get this wrapped up before I move on to thumbnailing, etc. |
|
I'm going to go ahead and merge this, and then build other changes outlined in the PR description on top of this. |
The imageBitmapScale test in ./tests/unit/image_bitmap_test.ts started to fail after update image-rs/image#2639
The imageBitmapScale test in ./tests/unit/image_bitmap_test.ts started to fail after update image-rs/image#2639
The imageBitmapScale test in ./tests/unit/image_bitmap_test.ts started to fail after update image-rs/image#2639
|
You probably have the |
|
Nope! No rayon feature, during the benchmark only 1 core at a time is maxed. It truly is just that much better. Oh, no |
This PR migrates resizing DynamicImage from the built-in resizing impl that's hamstrung by the limitations of our generic APIs to the
pic-scale-safecrate by @awxkee. This code is ported from the one I had inwondermagickfor months now.Performance
This implementation is ~5x faster than the previous one when running single-threaded. It also has optional parallelism gated by the
rayonfeature that takes performance even further.I had to do an unnecessary copy to adhere to the existing API; we can eliminate it in the next semver-breaking release.
Fixes #2223
Correctness
Unlike the previous implementation, this one automatically premultiplies the pixels by alpha before resizing, but this is done only when necessary to avoid precision loss. Premultiplication is then reversed after resizing. This happens entirely transparently for the user. This API just automatically does the right thing for images with alpha.
Fixes #2422
Future work
Not part of this PR to make it easier to review:
pic-scale-safetoImageBufferwrapping concrete types such asRgb,Rgbaand even some types that aren't inDynamicImagelikeBrgorLumaF32thumbnailmethods to produce high-quality thumbnails by resizing down to 5x the using nearest-neighbor and then downscaling properly from there, likeimagemagickdoesGamma and CICP are not taken into account. But the only reasonable way to do that is convert to a 16-bit linear colorspace and do the resizing there, which is too expensive to do transparently.
It is theoretically possible to make
pic-scale-safecode entirely generic and fold it intoimagecodebase, but it will require making nontrivial and semver-breaking improvements to our generic APIs first, and I don't think they should block this work.