diff options
Diffstat (limited to 'src')
4 files changed, 107 insertions, 33 deletions
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs index 27dbeaba6a..77abb45f2a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs @@ -37,14 +37,16 @@ public interface IJellyfinDatabaseProvider void ConfigureConventions(ModelConfigurationBuilder configurationBuilder); /// <summary> - /// If supported this should run any periodic maintaince tasks. + /// If supported this should run any periodic maintaince tasks, reclaiming unused space and refreshing the query + /// planner statistics. Also used after migrations have modified the database. /// </summary> /// <param name="cancellationToken">The token to abort the operation.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> Task RunScheduledOptimisation(CancellationToken cancellationToken); /// <summary> - /// If supported this should perform any actions that are required on stopping the jellyfin server. + /// If supported this should perform any actions that are required on stopping the jellyfin server, including the + /// same maintenance as <see cref="RunScheduledOptimisation(CancellationToken)"/>. /// </summary> /// <param name="cancellationToken">The token that will be used to abort the operation.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index 8020fe1f93..f11cde7e48 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -103,17 +103,9 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider } /// <inheritdoc/> - public async Task RunScheduledOptimisation(CancellationToken cancellationToken) + public Task RunScheduledOptimisation(CancellationToken cancellationToken) { - var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - await using (context.ConfigureAwait(false)) - { - await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false); - await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false); - await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false); - await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false); - _logger.LogInformation("jellyfin.db optimized successfully!"); - } + return OptimizeAsync(cancellationToken); } /// <inheritdoc/> @@ -125,19 +117,37 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider /// <inheritdoc/> public async Task RunShutdownTask(CancellationToken cancellationToken) { + // Run before disposing the application + try + { + await OptimizeAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + // A missed optimization only costs performance, so never fail the shutdown over this. + _logger.LogError(ex, "Error while optimizing jellyfin.db"); + } + + SqliteConnection.ClearAllPools(); + } + + private async Task OptimizeAsync(CancellationToken cancellationToken) + { if (DbContextFactory is null) { return; } - // Run before disposing the application var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { - await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("PRAGMA analysis_limit=0", cancellationToken).ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("ANALYZE", cancellationToken).ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false); + _logger.LogInformation("jellyfin.db optimized successfully!"); } - - SqliteConnection.ClearAllPools(); } /// <inheritdoc/> diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 3e353db8de..4cdff055f4 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -1,8 +1,10 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using BlurHashSharp.SkiaSharp; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; @@ -22,22 +24,15 @@ namespace Jellyfin.Drawing.Skia; public class SkiaEncoder : IImageEncoder { private const string SvgFormat = "svg"; + + // The light sharpening kernel applied after resizing, see ResizeImage. + private const float SharpenCenterWeight = 1.4f; + private const float SharpenNeighborWeight = -0.1f; + private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; private readonly ILogger<SkiaEncoder> _logger; private readonly IApplicationPaths _appPaths; private static readonly SKTypeface?[] _typefaces = InitializeTypefaces(); - private static readonly SKImageFilter _imageFilter = SKImageFilter.CreateMatrixConvolution( - new SKSizeI(3, 3), - [ - 0, -.1f, 0, - -.1f, 1.4f, -.1f, - 0, -.1f, 0 - ], - 1f, - 0f, - new SKPointI(1, 1), - SKShaderTileMode.Clamp, - true); /// <summary> /// The default sampling options, equivalent to old high quality filter settings when upscaling. @@ -561,8 +556,8 @@ public class SkiaEncoder : IImageEncoder /// <returns>The resized image.</returns> internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false) { - using var surface = SKSurface.Create(targetInfo); - using var canvas = surface.Canvas; + using var target = new SKBitmap(targetInfo); + using var canvas = new SKCanvas(target); using var paint = new SKPaint(); paint.IsAntialias = isAntialias; paint.IsDither = isDither; @@ -574,7 +569,6 @@ public class SkiaEncoder : IImageEncoder ? DefaultSamplingOptions : UpscaleSamplingOptions; - paint.ImageFilter = _imageFilter; canvas.DrawBitmap( source, SKRect.Create(0, 0, source.Width, source.Height), @@ -582,7 +576,75 @@ public class SkiaEncoder : IImageEncoder samplingOptions, paint); - return surface.Snapshot(); + SharpenInPlace(target); + + return SKImage.FromBitmap(target); + } + + /// <summary> + /// Applies the light 3x3 sharpening kernel to the bitmap in place. + /// + /// This is equivalent to the SKImageFilter.CreateMatrixConvolution paint filter that + /// was previously part of the resize draw call. Since the SkiaSharp 3 update that + /// filter no longer has a fast CPU path and takes multiple seconds per image on the + /// software rasterizer, so the same kernel is applied directly instead. + /// </summary> + /// <param name="bitmap">The bitmap to sharpen. Must use a color type with four bytes per pixel; other color types are returned unchanged.</param> + internal static void SharpenInPlace(SKBitmap bitmap) + { + if (bitmap.BytesPerPixel != 4) + { + return; + } + + var width = bitmap.Width; + var height = bitmap.Height; + var stride = bitmap.RowBytes; + var pixels = bitmap.GetPixels(); + if (width == 0 || height == 0 || pixels == IntPtr.Zero) + { + return; + } + + var length = stride * height; + var source = ArrayPool<byte>.Shared.Rent(length); + var result = ArrayPool<byte>.Shared.Rent(length); + try + { + Marshal.Copy(pixels, source, 0, length); + + for (var y = 0; y < height; y++) + { + // The kernel clamps at the edges: out-of-bounds taps reuse the edge pixel. + var row = y * stride; + var up = y == 0 ? row : row - stride; + var down = y == height - 1 ? row : row + stride; + + for (var x = 0; x < width; x++) + { + var col = x * 4; + var left = x == 0 ? col : col - 4; + var right = x == width - 1 ? col : col + 4; + + for (var channel = 0; channel < 4; channel++) + { + var value = (SharpenCenterWeight * source[row + col + channel]) + + (SharpenNeighborWeight * (source[up + col + channel] + + source[down + col + channel] + + source[row + left + channel] + + source[row + right + channel])); + result[row + col + channel] = (byte)Math.Clamp((int)(value + 0.5f), 0, 255); + } + } + } + + Marshal.Copy(result, 0, pixels, length); + } + finally + { + ArrayPool<byte>.Shared.Return(source); + ArrayPool<byte>.Shared.Return(result); + } } /// <inheritdoc/> diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 5518d9b954..bdffecdc8a 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -15,7 +15,7 @@ <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Extensions</PackageId> - <VersionPrefix>12.0.0</VersionPrefix> + <VersionPrefix>13.0.0</VersionPrefix> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> |
