[backend/drive] Add ImageProcessorConcurrency configuration option, default it to 8 (was: unrestricted)

This commit is contained in:
Laura Hausmann 2024-08-08 05:35:50 +02:00
parent aaf3be209d
commit 1fba1ab119
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
4 changed files with 32 additions and 19 deletions

View file

@ -207,6 +207,8 @@ public sealed class Config
public int MaxResolutionMpx { get; init; } = 30; public int MaxResolutionMpx { get; init; } = 30;
public bool LocalOnly { get; init; } = false; public bool LocalOnly { get; init; } = false;
[Range(0, 128)] public int ImageProcessorConcurrency { get; init; } = 8;
public string MaxFileSize public string MaxFileSize
{ {
get => MaxFileSizeBytes.ToString(); get => MaxFileSizeBytes.ToString();

View file

@ -343,7 +343,7 @@ public class DriveService(
} }
private async Task<ImageVerTriple?> ProcessAndStoreFileVersion( private async Task<ImageVerTriple?> ProcessAndStoreFileVersion(
ImageVersion version, Func<Stream>? encode, string fileName ImageVersion version, Func<Task<Stream>>? encode, string fileName
) )
{ {
if (encode == null) return null; if (encode == null) return null;
@ -354,7 +354,7 @@ public class DriveService(
try try
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
stream = encode(); stream = await encode();
sw.Stop(); sw.Stop();
logger.LogDebug("Encoding {version} image took {ms} ms", logger.LogDebug("Encoding {version} image took {ms} ms",
version.Key.ToString().ToLowerInvariant(), sw.ElapsedMilliseconds); version.Key.ToString().ToLowerInvariant(), sw.ElapsedMilliseconds);
@ -426,11 +426,7 @@ public class DriveService(
private static string GenerateAccessKey(string prefix = "", string extension = "webp") private static string GenerateAccessKey(string prefix = "", string extension = "webp")
{ {
var guid = Guid.NewGuid().ToStringLower(); var guid = Guid.NewGuid().ToStringLower();
// @formatter:off return extension.Length > 0 ? $"{prefix}-{guid}.{extension}" : $"{prefix}-{guid}";
return prefix.Length > 0
? extension.Length > 0 ? $"{prefix}-{guid}.{extension}" : $"{prefix}-{guid}"
: extension.Length > 0 ? $"{guid}.{extension}" : guid;
// @formatter:on
} }
private static string CleanMimeType(string? mimeType) private static string CleanMimeType(string? mimeType)

View file

@ -2,6 +2,7 @@ using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Services.ImageProcessing; using Iceshrimp.Backend.Core.Services.ImageProcessing;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using static Iceshrimp.Backend.Core.Services.ImageProcessing.ImageVersion; using static Iceshrimp.Backend.Core.Services.ImageProcessing.ImageVersion;
@ -15,6 +16,8 @@ public class ImageProcessor
private readonly List<IImageProcessor> _imageProcessors; private readonly List<IImageProcessor> _imageProcessors;
private readonly ILogger<ImageProcessor> _logger; private readonly ILogger<ImageProcessor> _logger;
private readonly SemaphorePlus _semaphore;
private readonly int _concurrency;
public ImageProcessor( public ImageProcessor(
ILogger<ImageProcessor> logger, IOptionsMonitor<Config.StorageSection> config, ILogger<ImageProcessor> logger, IOptionsMonitor<Config.StorageSection> config,
@ -24,6 +27,8 @@ public class ImageProcessor
_logger = logger; _logger = logger;
_config = config; _config = config;
_imageProcessors = imageProcessors.OrderBy(p => p.Priority).ToList(); _imageProcessors = imageProcessors.OrderBy(p => p.Priority).ToList();
_concurrency = config.CurrentValue.MediaProcessing.ImageProcessorConcurrency;
_semaphore = new SemaphorePlus(Math.Max(_concurrency, 1));
// @formatter:off // @formatter:off
if (_imageProcessors.Count == 0) if (_imageProcessors.Count == 0)
@ -63,17 +68,31 @@ public class ImageProcessor
// @formatter:on // @formatter:on
var results = formats var results = formats
.ToDictionary<ImageVersion, ImageVersion, Func<Stream>?>(p => p, ProcessImageFormat) .ToDictionary<ImageVersion, ImageVersion, Func<Task<Stream>>?>(p => p, ProcessImageFormat)
.AsReadOnly(); .AsReadOnly();
return new ProcessedImage(ident) { RequestedFormats = results, Blurhash = blurhash }; return new ProcessedImage(ident) { RequestedFormats = results, Blurhash = blurhash };
Func<Stream>? ProcessImageFormat(ImageVersion p) Func<Task<Stream>>? ProcessImageFormat(ImageVersion p)
{ {
if (p.Format is ImageFormat.Keep) return () => new MemoryStream(buf); if (p.Format is ImageFormat.Keep) return () => Task.FromResult<Stream>(new MemoryStream(buf));
var proc = _imageProcessors.FirstOrDefault(i => i.CanEncode(p.Format)); var proc = _imageProcessors.FirstOrDefault(i => i.CanEncode(p.Format));
if (proc == null) return null; if (proc == null) return null;
return () => proc.Encode(buf, ident, p.Format); return async () =>
{
if (_concurrency is 0)
return proc.Encode(buf, ident, p.Format);
await _semaphore.WaitAsync();
try
{
return proc.Encode(buf, ident, p.Format);
}
finally
{
_semaphore.Release();
}
};
} }
} }
@ -117,7 +136,7 @@ public class ImageProcessor
{ {
public string? Blurhash; public string? Blurhash;
public required IReadOnlyDictionary<ImageVersion, Func<Stream>?> RequestedFormats; public required IReadOnlyDictionary<ImageVersion, Func<Task<Stream>>?> RequestedFormats;
public ProcessedImage(IImageInfo info) public ProcessedImage(IImageInfo info)
{ {
@ -129,9 +148,9 @@ public class ImageProcessor
public ProcessedImage(IImageInfo info, Stream original, DriveFileCreationRequest request) : this(info) public ProcessedImage(IImageInfo info, Stream original, DriveFileCreationRequest request) : this(info)
{ {
var format = new ImageFormat.Keep(Path.GetExtension(request.Filename), request.MimeType); var format = new ImageFormat.Keep(Path.GetExtension(request.Filename), request.MimeType);
RequestedFormats = new Dictionary<ImageVersion, Func<Stream>?> RequestedFormats = new Dictionary<ImageVersion, Func<Task<Stream>>?>
{ {
{ new ImageVersion(KeyEnum.Original, format), () => original } { new ImageVersion(KeyEnum.Original, format), () => Task.FromResult(original) }
}; };
} }
} }

View file

@ -161,10 +161,6 @@ MaxFileSize = 10M
;; Caution: metadata (e.g. location data) for locally originating images will *not* be stripped for files larger than this ;; Caution: metadata (e.g. location data) for locally originating images will *not* be stripped for files larger than this
MaxResolutionMpx = 30 MaxResolutionMpx = 30
;; Should you prefer to reject locally originating images that exceed MaxResolutionMpx, set this option to true.
;; Note that this does not apply to remote images, or to local images in a format not supported by the configured image processor.
FailIfImageExceedsMaxRes = false
;; Maxmimum concurrent image encode tasks to run. (0 = no limit) ;; Maxmimum concurrent image encode tasks to run. (0 = no limit)
ImageProcessorConcurrency = 8 ImageProcessorConcurrency = 8