[backend/drive] Fix ImageSharp memory leak, improve image processing memory footprint, don't generate thumbnails for animated images

This commit is contained in:
Laura Hausmann 2024-04-30 17:17:43 +02:00
parent dd062c6752
commit d56eda8464
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
2 changed files with 53 additions and 25 deletions

View file

@ -7,6 +7,7 @@ using Iceshrimp.Backend.Core.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using SixLabors.ImageSharp.Memory;
using WebPush; using WebPush;
namespace Iceshrimp.Backend.Core.Extensions; namespace Iceshrimp.Backend.Core.Extensions;
@ -193,6 +194,9 @@ public static class WebApplicationExtensions
app.Logger.LogInformation("Warming up meta cache..."); app.Logger.LogInformation("Warming up meta cache...");
await meta.WarmupCache(); await meta.WarmupCache();
SixLabors.ImageSharp.Configuration.Default.MemoryAllocator =
MemoryAllocator.Create(new MemoryAllocatorOptions { AllocationLimitMegabytes = 20 });
app.Logger.LogInformation("Initializing application, please wait..."); app.Logger.LogInformation("Initializing application, please wait...");
return instanceConfig; return instanceConfig;

View file

@ -11,6 +11,7 @@ using Iceshrimp.Backend.Core.Queues;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
@ -111,7 +112,7 @@ public class DriveService(
public async Task<DriveFile> StoreFile(Stream data, User user, DriveFileCreationRequest request) public async Task<DriveFile> StoreFile(Stream data, User user, DriveFileCreationRequest request)
{ {
var buf = new BufferedStream(data); await using var buf = new BufferedStream(data);
var digest = await DigestHelpers.Sha256DigestAsync(buf); var digest = await DigestHelpers.Sha256DigestAsync(buf);
logger.LogDebug("Storing file {digest} for user {userId}", digest, user.Id); logger.LogDebug("Storing file {digest} for user {userId}", digest, user.Id);
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Sha256 == digest); var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Sha256 == digest);
@ -148,50 +149,73 @@ public class DriveService(
DriveFile.FileProperties? properties = null; DriveFile.FileProperties? properties = null;
if (request.MimeType.StartsWith("image/") || request.MimeType == "image") var isImage = request.MimeType.StartsWith("image/") || request.MimeType == "image";
// skip images larger than 10MB
var isReasonableSize = buf.Length < 10 * 1024 * 1024;
if (isImage && isReasonableSize)
{ {
try try
{ {
var image = await Image.LoadAsync<Rgba32>(buf); var ident = await Image.IdentifyAsync(buf);
var isAnimated = ident.FrameMetadataCollection.Count != 0;
properties = new DriveFile.FileProperties { Width = ident.Size.Width, Height = ident.Size.Height };
// Correct mime type
if (request.MimeType == "image" && ident.Metadata.DecodedImageFormat?.DefaultMimeType != null)
request.MimeType = ident.Metadata.DecodedImageFormat.DefaultMimeType;
buf.Seek(0, SeekOrigin.Begin);
var limit = user.Host == null && !isAnimated
? 2048
: shouldStore
? 1024
: 200;
var width = Math.Min(ident.Width, limit);
var height = Math.Min(ident.Height, limit);
var size = new Size(width, height);
var options = new DecoderOptions { MaxFrames = 1, TargetSize = size };
using var image = await Image.LoadAsync<Rgba32>(options, buf);
image.Mutate(x => x.AutoOrient()); image.Mutate(x => x.AutoOrient());
// Calculate blurhash using a x200px image for improved performance // Calculate blurhash using a x200px image for improved performance
var blurhashImage = image.Clone(); using var blurhashImage = image.Clone();
blurhashImage.Mutate(p => p.Resize(image.Width > image.Height ? new Size(200, 0) : new Size(0, 200))); var blurOpts = new ResizeOptions { Size = new Size(200, 200), Mode = ResizeMode.Max };
blurhashImage.Mutate(p => p.Resize(blurOpts));
blurhash = Blurhasher.Encode(blurhashImage, 7, 7); blurhash = Blurhasher.Encode(blurhashImage, 7, 7);
// Correct mime type
if (request.MimeType == "image" && image.Metadata.DecodedImageFormat?.DefaultMimeType != null)
request.MimeType = image.Metadata.DecodedImageFormat.DefaultMimeType;
properties = new DriveFile.FileProperties { Width = image.Size.Width, Height = image.Size.Height };
if (shouldStore) if (shouldStore)
{ {
// Generate thumbnail // Generate thumbnail
var thumbnailImage = image.Clone(); using var thumbnailImage = image.Clone();
thumbnailImage.Metadata.ExifProfile = null; thumbnailImage.Metadata.ExifProfile = null;
thumbnailImage.Metadata.XmpProfile = null; thumbnailImage.Metadata.XmpProfile = null;
if (Math.Max(image.Size.Width, image.Size.Height) > 1000) if (Math.Max(image.Size.Width, image.Size.Height) > 1000)
thumbnailImage.Mutate(p => p.Resize(image.Width > image.Height {
? new Size(1000, 0) var thumbOpts = new ResizeOptions { Size = new Size(1000, 1000), Mode = ResizeMode.Max };
: new Size(0, 1000))); thumbnailImage.Mutate(p => p.Resize(thumbOpts));
}
thumbnail = new MemoryStream(); thumbnail = new MemoryStream();
var thumbEncoder = new WebpEncoder { Quality = 75, FileFormat = WebpFileFormatType.Lossy }; var thumbEncoder = new WebpEncoder { Quality = 75, FileFormat = WebpFileFormatType.Lossy };
await thumbnailImage.SaveAsWebpAsync(thumbnail, thumbEncoder); await thumbnailImage.SaveAsWebpAsync(thumbnail, thumbEncoder);
thumbnail.Seek(0, SeekOrigin.Begin); thumbnail.Seek(0, SeekOrigin.Begin);
// Generate webpublic for local users // Generate webpublic for local users, if image is not animated
if (user.Host == null) if (user.Host == null && !isAnimated)
{ {
var webpublicImage = image.Clone(); using var webpublicImage = image.Clone();
webpublicImage.Metadata.ExifProfile = null; webpublicImage.Metadata.ExifProfile = null;
webpublicImage.Metadata.XmpProfile = null; webpublicImage.Metadata.XmpProfile = null;
if (Math.Max(image.Size.Width, image.Size.Height) > 2048) if (Math.Max(image.Size.Width, image.Size.Height) > 2048)
webpublicImage.Mutate(p => p.Resize(image.Width > image.Height {
? new Size(2048, 0) var webpOpts = new ResizeOptions { Size = new Size(2048, 2048), Mode = ResizeMode.Max };
: new Size(0, 2048))); webpublicImage.Mutate(p => p.Resize(webpOpts));
}
var encoder = new WebpEncoder var encoder = new WebpEncoder
{ {