question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

MagickImage.Format/ExifProfile.GetValue() allocate 2KB/11KB of memory after each call

See original GitHub issue

Description

Hello Devs,

I’ve been profiling a website’s memory and detected an enormous amount of memory allocated by ImageMagick’s MagickImage.Format/ExifProfile.GetValue() methods - new 2KB/11KB added after each call. These members call EnumHelper.Parse() methods, which in its turn call Enum.GetValues()/Enum.GetNames methods. When invoked on MagickFormat enum, Enum.GetValues()/Enum.GetNames return arrays with 267 values which after method execution immediately become memory garbage and intensify GC pressure thus slowing down the website. The extensive memory allocation is also present when invoking EnumHelper.ConvertFlags() due to the same reason - Enum.GetValues().

Steps to Reproduce

The MagickImage.Format allocation can be reproduced on any image, while the ExifProfile.GetValue() allocation is reproducible when executing a benchmark below on any image with Exif data:

image

` using BenchmarkDotNet.Attributes; using ImageMagick;

namespace Benchmarks.ImageMagick { [MemoryDiagnoser] public class MagickImageBenchmark { private MagickImage _image;

	[GlobalSetup]
	public void GlobalSetup()
	{
		_image = new MagickImage(@"..\..\..\..\..\..\..\..\Data\Images\MagickImageBenchmark\station.jpg");
	}

	// result: mean 5.5 microseconds, allocated 2 KB
	[Benchmark]
	public MagickFormat BenchmarkImageMagickFormat() => _image.Format;

	// result: mean 466.6 microseconds, allocated 11 KB
	[Benchmark]
	public IExifValue? BenchmarkExifProfileGetValue() => _image.GetExifProfile().GetValue(ExifTag.Orientation);

	[GlobalCleanup]
	public void GlobalCleanup()
	{
		_image.Dispose();
	}
}

} `

System Configuration

  • Magick.NET version: Magick.NET-Q16-x64, 9.0.0
  • Environment: Windows 10

Solution

I’ve created a fix to the issue that you can use for free in Magick.NET as is, or modify it before applying. Here is the result of a benchmark that executes the original and fixed version of Parse(string, Enum), Parse(int, Enum), Parse(ushort, Enum), ConvertFlags(Enum) 10 times and measures the execution time and memory allocation.

image

As you can see the execution time and memory allocations decreased dramatically. The benchmark itself:

` using BenchmarkDotNet.Attributes; using ImageMagick; using ImageMagick.Formats;

namespace Benchmarks.ImageMagick { [MemoryDiagnoser] public class EnumHelperBenchmark { private static readonly string[] Formats = { “Jpg”, “Png”, “WebP”, “Tiff”, “Gif”, “Bmp”, “Svg”, “Avif”, “Psd”, “Pdf” }; private static readonly int[] FormatValues = { 111, 179, 250, 231, 78, 18, 225, 12, 193, 164 };

	private static readonly JpegProfileTypes[] ProfileTypes = { 
		JpegProfileTypes.App, JpegProfileTypes.App | JpegProfileTypes.Exif, JpegProfileTypes.App | JpegProfileTypes.Xmp,
		JpegProfileTypes.Icc, JpegProfileTypes.Icc | JpegProfileTypes.Xmp, JpegProfileTypes.Icc | JpegProfileTypes.Iptc,
		JpegProfileTypes.Xmp, JpegProfileTypes.Xmp | JpegProfileTypes.Exif, JpegProfileTypes.Xmp | JpegProfileTypes.Iptc,
		JpegProfileTypes.App | JpegProfileTypes.Xmp | JpegProfileTypes.Exif | JpegProfileTypes.Icc | JpegProfileTypes.Iptc
	};

	[Benchmark]
	public void BenchmarkOriginalStringEnumParse()
	{
		foreach (string format in Formats)
		{
			EnumHelper.Parse(format, MagickFormat.Unknown);
		}
	}

	[Benchmark]
	public void BenchmarkImprovedStringEnumParse()
	{
		foreach (string format in Formats)
		{
			EnumHelperImproved.Parse(format, MagickFormat.Unknown);
		}
	}

	[Benchmark]
	public void BenchmarkOriginalIntEnumParse()
	{
		foreach (int formatValue in FormatValues)
		{
			EnumHelper.Parse(formatValue, MagickFormat.Unknown);
		}
	}

	[Benchmark]
	public void BenchmarkImprovedIntEnumParse()
	{
		foreach (int formatValue in FormatValues)
		{
			EnumHelperImproved.Parse(formatValue, MagickFormat.Unknown);
		}
	}

	[Benchmark]
	public void BenchmarkOriginalUshortEnumParse()
	{
		foreach (int formatValue in FormatValues)
		{
			EnumHelper.Parse((ushort)formatValue, MagickFormat.Unknown);
		}
	}

	[Benchmark]
	public void BenchmarkImprovedUshortEnumParse()
	{
		foreach (int formatValue in FormatValues)
		{
			EnumHelperImproved.Parse((ushort)formatValue, MagickFormat.Unknown);
		}
	}

	[Benchmark]
	public void BenchmarkOriginalConvertFlags()
	{
		foreach (JpegProfileTypes profileType in ProfileTypes)
		{
			EnumHelper.ConvertFlags(profileType);
		}
	}

	[Benchmark]
	public void BenchmarkImprovedConvertFlags()
	{
		foreach (JpegProfileTypes profileType in ProfileTypes)
		{
			EnumHelperImproved.ConvertFlags(profileType);
		}
	}
}

} `

Solution details

ConvertFlags() :

  • Stores a copy of Enum.GetValues() in a thread-safe cache implemented as ConcurrentDictionary.
  • Removes redundant ToArray() call in “string.Join(”,“, flags.ToArray())” code.

Parse(string, Enum) :

  • Stores a copy of Enum.GetNames() in a thread-safe cache implemented as ConcurrentDictionary.
  • Applies a binary search while seeking for an Enum name in the cache. Note that a cache item is getting sorted before populating the cache in order to be eligible for the binary search.

Parse(int, Enum)/Parse(ushort, Enum) :

  • Stores a copy of Enum.GetValues() in a thread-safe cache implemented as ConcurrentDictionary.
  • Applies a binary search while seeking for an Enum value in the cache. Note that a cache item is getting sorted before populating the cache in order to be eligible for the binary search.

The fixed version of EnumHelper is here: EnumHelper.cs.txt

And thanks for creating this awesome library!

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:9 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
dlemstracommented, Feb 1, 2022

Those changes have been pushed yesterday and I will try to publish a new release later this week.

1reaction
dlemstracommented, Feb 4, 2022

Turns out that I can use ToString() without removing the spaces works with ImageMagick so this will also be included in the next release.

Read more comments on GitHub >

github_iconTop Results From Across the Web

No results found

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found