Skip to content

Optimize memory usage by leveraging CommunityToolkit.HighPerformance for buffer pooling #1381

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Magick.NET.Core/Magick.NET.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="7.0.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
35 changes: 35 additions & 0 deletions src/Magick.NET/Extensions/GravityExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright Dirk Lemstra https://github.com/dlemstra/Magick.NET.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;

namespace ImageMagick.Extensions
{
internal static class GravityExtensions
{
private static IImmutableDictionary<Gravity, ReadOnlyMemory<string>> GravityToEdge { get; } =
new Dictionary<Gravity, ReadOnlyMemory<string>>
{
[Gravity.North] = new[] { "north" },
[Gravity.Northeast] = new[] { "north", "east" },
[Gravity.East] = new[] { "east" },
[Gravity.Southeast] = new[] { "south", "east" },
[Gravity.South] = new[] { "south" },
[Gravity.Southwest] = new[] { "south", "west" },
[Gravity.West] = new[] { "west" },
[Gravity.Northwest] = new[] { "north", "west" },
}.ToImmutableDictionary();

/// <summary>
/// Converts the provided value into edge strings.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <returns>The edge strings composing the given value.</returns>
public static ReadOnlyMemory<string> ToEdge(this Gravity value)
=> GravityToEdge.TryGetValue(value, out var edge) ?
edge :
ReadOnlyMemory<string>.Empty;
}
}
3 changes: 2 additions & 1 deletion src/Magick.NET/Magick.NET.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="7.0.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
</ItemGroup>

<PropertyGroup Condition="'$(Platform)' == 'x86'">
Expand All @@ -56,7 +57,7 @@

<PropertyGroup Condition="'$(Platform)' == 'x64'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
</PropertyGroup>

<PropertyGroup Condition="'$(Platform)' == 'arm64'">
<PlatformTarget>ARM64</PlatformTarget>
Expand Down
107 changes: 52 additions & 55 deletions src/Magick.NET/MagickImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.HighPerformance;
using CommunityToolkit.HighPerformance.Buffers;
using ImageMagick.Extensions;

#if Q8
using QuantumType = System.Byte;
Expand Down Expand Up @@ -6534,9 +6537,8 @@ public string ToBase64(IWriteDefines defines)
/// <returns>A <see cref="byte"/> array.</returns>
public byte[] ToByteArray()
{
using var stream = new MemoryStream();
Write(stream);
return stream.ToArray();
using var bufferWriter = ToArrayPoolBufferWriter();
return bufferWriter.WrittenSpan.ToArray();
}

/// <summary>
Expand Down Expand Up @@ -6690,60 +6692,39 @@ public void Trim()
/// </summary>
/// <param name="edges">The edges that need to be trimmed.</param>
/// <exception cref="MagickException">Thrown when an error is raised by ImageMagick.</exception>
public void Trim(params Gravity[] edges)
public void Trim(ReadOnlySpan<Gravity> edges)
{
static IEnumerable<string> GravityToEdge(Gravity[] edges)
var artifact = string.Empty;

if (!edges.IsEmpty)
{
using var bufferWriter = new ArrayPoolBufferWriter<string>(initialCapacity: edges.Length);

foreach (var edge in edges)
{
switch (edge)
{
case Gravity.North:
yield return "north";
break;
case Gravity.Northeast:
yield return "north";
yield return "east";
break;
case Gravity.Northwest:
yield return "north";
yield return "west";
break;
case Gravity.East:
yield return "east";
break;
case Gravity.West:
yield return "west";
break;
case Gravity.South:
yield return "south";
break;
case Gravity.Southeast:
yield return "south";
yield return "east";
break;
case Gravity.Southwest:
yield return "south";
yield return "west";
break;
}
}
}
var innerEdges = edge.ToEdge();
var innerEdgesCount = innerEdges.Length;

var artifact = new List<string>();
foreach (var edge in GravityToEdge(edges))
{
if (!artifact.Contains(edge))
{
artifact.Add(edge);
var buffer = bufferWriter.GetSpan(innerEdgesCount);
innerEdges.Span.CopyTo(buffer);
bufferWriter.Advance(innerEdgesCount);
}

artifact = string.Join(",", bufferWriter.DangerousGetArray());
}

using var temporaryDefines = new TemporaryDefines(this);
temporaryDefines.SetArtifact("trim:edges", string.Join(",", artifact.ToArray()));
temporaryDefines.SetArtifact("trim:edges", artifact);
Trim();
}

/// <summary>
/// Trim the specified edges that are the background color from the image.
/// </summary>
/// <param name="edges">The edges that need to be trimmed.</param>
/// <exception cref="MagickException">Thrown when an error is raised by ImageMagick.</exception>
public void Trim(params Gravity[] edges) => Trim(edges.AsSpan());

/// <summary>
/// Trim edges that are the background color from the image. The property <see cref="BoundingBox"/> can be used to the
/// coordinates of the area that will be extracted.
Expand Down Expand Up @@ -7064,8 +7045,14 @@ public Task WriteAsync(FileInfo file, CancellationToken cancellationToken)
Throw.IfNull(nameof(file), file);

var format = EnumHelper.ParseMagickFormatFromExtension(file);
var bytes = format != MagickFormat.Unknown ? ToByteArray(format) : ToByteArray();
return FileHelper.WriteAllBytesAsync(file.FullName, bytes, cancellationToken);

if (format != MagickFormat.Unknown)
{
return WriteAsync(file, format, cancellationToken);
}

var bufferWriter = ToArrayPoolBufferWriter();
return FileHelper.WriteAllBytesAsync(file.FullName, bufferWriter.WrittenMemory, cancellationToken); ;
}

/// <summary>
Expand Down Expand Up @@ -7114,8 +7101,9 @@ public Task WriteAsync(FileInfo file, MagickFormat format, CancellationToken can
{
Throw.IfNull(nameof(file), file);

var bytes = ToByteArray(format);
return FileHelper.WriteAllBytesAsync(file.FullName, bytes, cancellationToken);
using var tempFormat = new TemporaryMagickFormat(this, format);
using var bufferWriter = ToArrayPoolBufferWriter();
return FileHelper.WriteAllBytesAsync(file.FullName, bufferWriter.WrittenMemory, cancellationToken);
}

/// <summary>
Expand All @@ -7134,12 +7122,12 @@ public Task WriteAsync(Stream stream)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <exception cref="MagickException">Thrown when an error is raised by ImageMagick.</exception>
public Task WriteAsync(Stream stream, CancellationToken cancellationToken)
public async Task WriteAsync(Stream stream, CancellationToken cancellationToken)
{
Throw.IfNull(nameof(stream), stream);
Throw.IfNull(nameof(stream), cancellationToken);

var bytes = ToByteArray();
return stream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
var bufferWriter = ToArrayPoolBufferWriter();
await stream.WriteAsync(bufferWriter.WrittenMemory, cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand Down Expand Up @@ -7261,8 +7249,9 @@ public Task WriteAsync(string fileName, MagickFormat format, CancellationToken c
var filePath = FileHelper.CheckForBaseDirectory(fileName);
Throw.IfNullOrEmpty(nameof(fileName), filePath);

var bytes = ToByteArray(format);
return FileHelper.WriteAllBytesAsync(filePath, bytes, cancellationToken);
using var tempFormat = new TemporaryMagickFormat(this, format);
var bufferWriter = ToArrayPoolBufferWriter();
return FileHelper.WriteAllBytesAsync(filePath, bufferWriter.WrittenMemory, cancellationToken);
}

internal static IMagickImage<QuantumType>? Clone(IMagickImage<QuantumType>? image)
Expand Down Expand Up @@ -7338,6 +7327,14 @@ internal int ChannelOffset(PixelChannel pixelChannel)
internal void SetNext(IMagickImage? image)
=> _nativeInstance.SetNext(GetInstance(image));

private ArrayPoolBufferWriter<byte> ToArrayPoolBufferWriter()
{
var bufferWriter = new ArrayPoolBufferWriter<byte>();
using var stream = bufferWriter.AsStream();
Write(stream);
return bufferWriter;
}

private static int GetExpectedByteLength(IPixelReadSettings<QuantumType> settings)
{
var length = GetExpectedLength(settings);
Expand Down
11 changes: 5 additions & 6 deletions src/Shared/FileHelper.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright Dirk Lemstra https://github.com/dlemstra/Magick.NET.
// Licensed under the Apache License, Version 2.0.

using CommunityToolkit.HighPerformance;
using System;
using System.IO;
using System.Threading;
Expand Down Expand Up @@ -43,14 +44,12 @@ public static async Task<byte[]> ReadAllBytesAsync(string fileName, Cancellation
#endif
}

internal static async Task WriteAllBytesAsync(string fileName, byte[] bytes, CancellationToken cancellationToken)
internal static Task WriteAllBytesAsync(string fileName, byte[] bytes, CancellationToken cancellationToken) => WriteAllBytesAsync(fileName, bytes.AsMemory(), cancellationToken);

internal static async Task WriteAllBytesAsync(string fileName, ReadOnlyMemory<byte> bytes, CancellationToken cancellationToken)
{
#if NETSTANDARD2_1
await File.WriteAllBytesAsync(fileName, bytes, cancellationToken).ConfigureAwait(false);
#else
using var fileStream = File.Open(fileName, FileMode.Create, FileAccess.Write);
await fileStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
#endif
await fileStream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
}
}
}