Skip to content

Commit

Permalink
Merge pull request #2596 from almostchristian/develop
Browse files Browse the repository at this point in the history
Improve performance of EnumUtility generic methods
  • Loading branch information
mmsmits authored Sep 26, 2023
2 parents 4b11cf2 + 80c5fc3 commit f623ba5
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 7 deletions.
62 changes: 62 additions & 0 deletions src/Benchmarks/EnumUtilityBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using BenchmarkDotNet.Attributes;
using Hl7.Fhir.Model;
using Hl7.Fhir.Utility;
using System;

namespace Firely.Sdk.Benchmarks
{
[MemoryDiagnoser]
public class EnumUtilityBenchmarks
{
private static readonly SearchParamType StringSearchParam = SearchParamType.String;
private static readonly Enum StringSearchParamEnum = StringSearchParam;

[Benchmark]
public string EnumToString()
=> SearchParamType.String.ToString();

[Benchmark]
public string EnumGetName()
=> Enum.GetName(StringSearchParam);

[Benchmark]
public string EnumUtilityGetLiteral()
=> EnumUtility.GetLiteral(StringSearchParam);

[Benchmark]
public string EnumUtilityGetLiteralNonGeneric()
=> EnumUtility.GetLiteral(StringSearchParamEnum);

[Benchmark]
public SearchParamType EnumParse()
=> Enum.Parse<SearchParamType>("String");

[Benchmark]
public SearchParamType EnumParseIgnoreCase()
=> Enum.Parse<SearchParamType>("string", true);

[Benchmark]
public SearchParamType EnumUtilityParseLiteral()
=> EnumUtility.ParseLiteral<SearchParamType>("string").Value;

[Benchmark]
public Enum EnumUtilityParseLiteralNonGeneric()
=> EnumUtility.ParseLiteral("string", typeof(SearchParamType));

[Benchmark]
public SearchParamType EnumUtilityParseLiteralIgnoreCase()
=> EnumUtility.ParseLiteral<SearchParamType>("string", true).Value;

[Benchmark]
public Enum EnumUtilityParseLiteralIgnoreCaseNonGeneric()
=> EnumUtility.ParseLiteral("string", typeof(SearchParamType), true);

[Benchmark]
public string? EnumUtilityGetSystem()
=> EnumUtility.GetSystem(StringSearchParam);

[Benchmark]
public string EnumUtilityGetSystemNonGeneric()
=> EnumUtility.GetSystem(StringSearchParamEnum);
}
}
86 changes: 80 additions & 6 deletions src/Hl7.Fhir.Base/Utility/EnumUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,28 @@ public static class EnumUtility
/// </summary>
public static string GetLiteral(this Enum e) => getEnumMapping(e.GetType()).GetLiteral(e);

/// <summary>
/// Retrieves the literal value for the code represented by this enum <typeparamref name="T"/>, or the member name itself if there
/// is no literal value defined.
/// </summary>
public static string GetLiteral<T>(this T e) where T : struct, Enum => EnumMappingCache<T>.GetLiteral(e);

/// <summary>
/// Retrieves the literal value for the code represented by this nullable enum <typeparamref name="T"/>, or the member name itself if there
/// is no literal value defined, or null if the enum does not have a value.
/// </summary>
public static string? GetLiteral<T>(this T? e) where T : struct, Enum => e.HasValue ? e.Value.GetLiteral() : null;

/// <summary>
/// Retrieves the system canonical for the code represented by this enum value, or <c>null</c> if there is no system defined.
/// </summary>
public static string? GetSystem(this Enum e) => e.GetAttributeOnEnum<EnumLiteralAttribute>()?.System ?? e.GetType().GetCustomAttribute<FhirEnumerationAttribute>()?.DefaultCodeSystem;

/// <summary>
/// Retrieves the system canonical for the code represented by this enum <typeparamref name="T"/>, or <c>null</c> if there is no system defined.
/// </summary>
public static string? GetSystem<T>(this T e) where T : struct, Enum => EnumMappingCache<T>.GetSystem(e);

/// <summary>
/// Retrieves the description for this enum value or the enumeration value itself if there is no description defined.
/// </summary>
Expand All @@ -48,8 +65,8 @@ public static string GetDocumentation(this Enum e) =>
/// <summary>
/// Finds an enumeration value from enum <typeparamref name="T"/> where the literal is the same as <paramref name="rawValue"/>.
/// </summary>
public static T? ParseLiteral<T>(string? rawValue, bool ignoreCase = false) where T : struct
=> (T?)(object?)ParseLiteral(rawValue, typeof(T), ignoreCase);
public static T? ParseLiteral<T>(string? rawValue, bool ignoreCase = false) where T : struct, Enum
=> EnumMappingCache<T>.ParseLiteral(rawValue, ignoreCase);

/// <summary>
/// Gets the human readable name defined for the enumeration <paramref name="enumType"/>.
Expand All @@ -59,11 +76,68 @@ public static string GetDocumentation(this Enum e) =>
/// <summary>
/// Gets the human readable name defined for the enumeration <typeparamref name="T"/>.
/// </summary>
public static string GetName<T>() where T : struct => GetName(typeof(T));
public static string GetName<T>() where T : struct, Enum => EnumMappingCache<T>.Name;

private static EnumMapping getEnumMapping(Type enumType)
=> CACHE.GetOrAdd(enumType, t => EnumMapping.Create(t));

private static class EnumMappingCache<TEnum>
where TEnum : struct, Enum
{
static EnumMappingCache()
{
var t = typeof(TEnum);
var enumAttr = t.GetTypeInfo().GetCustomAttribute<FhirEnumerationAttribute>();
Name = enumAttr?.BindingName ?? t.Name;
DefaultCodeSystem = enumAttr?.DefaultCodeSystem;
foreach (var enumValue in ReflectionHelper.FindEnumFields(t))
{
var attr = ReflectionHelper.GetAttribute<EnumLiteralAttribute>(enumValue);
string literal = attr?.Literal ?? enumValue.Name;

var value = (TEnum)enumValue.GetValue(null)!;

_enumToLiteral.Add(value, literal);
_literalToEnum.Add(literal, value);
_caseInsensitiveLiteralToEnum.Add(literal, value);
if (attr?.System is string systemVal)
{
_enumToSystem.Add(value, systemVal);
}
}
}

public static string Name { get; }

public static string? DefaultCodeSystem { get; }

private static readonly Dictionary<string, TEnum> _literalToEnum = new();
private static readonly Dictionary<string, TEnum> _caseInsensitiveLiteralToEnum = new(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<TEnum, string> _enumToLiteral = new();
private static readonly Dictionary<TEnum, string> _enumToSystem = new();

public static string? GetSystem(TEnum value) =>
!_enumToSystem.TryGetValue(value, out string? result)
? DefaultCodeSystem
: result;

public static string GetLiteral(TEnum value) =>
!_enumToLiteral.TryGetValue(value, out string? result)
? throw new InvalidOperationException($"Should only pass enum values that are member of the given enum: {value} is not a member of {Name}.")
: result;

public static TEnum? ParseLiteral(string? literal, bool ignoreCase)
{
if (literal is null) return null;

var success = ignoreCase
? _caseInsensitiveLiteralToEnum.TryGetValue(literal, out TEnum result)
: _literalToEnum.TryGetValue(literal, out result);

return success ? result : null;
}
}

internal class EnumMapping
{
internal EnumMapping(string name, Type enumType)
Expand All @@ -79,7 +153,7 @@ internal EnumMapping(string name, Type enumType)
public Type EnumType { get; private set; }

private readonly Dictionary<string, Enum> _literalToEnum = new();
private readonly Dictionary<string, Enum> _lowercaseLiteralToEnum = new();
private readonly Dictionary<string, Enum> _caseInsensitiveLiteralToEnum = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<Enum, string> _enumToLiteral = new();

public string GetLiteral(Enum value) =>
Expand All @@ -92,7 +166,7 @@ public string GetLiteral(Enum value) =>
if (literal is null) return null;

var success = ignoreCase
? _lowercaseLiteralToEnum.TryGetValue(literal.ToLowerInvariant(), out Enum? result)
? _caseInsensitiveLiteralToEnum.TryGetValue(literal, out Enum? result)
: _literalToEnum.TryGetValue(literal, out result);

return success ? result : null;
Expand All @@ -113,7 +187,7 @@ public static EnumMapping Create(Type enumType)

result._enumToLiteral.Add(value, literal);
result._literalToEnum.Add(literal, value);
result._lowercaseLiteralToEnum.Add(literal.ToLowerInvariant(), value);
result._caseInsensitiveLiteralToEnum.Add(literal, value);
}

return result;
Expand Down
65 changes: 64 additions & 1 deletion src/Hl7.Fhir.Support.Tests/Utility/EnumMappingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using FluentAssertions;
using Hl7.Fhir.Utility;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Diagnostics;

namespace Hl7.Fhir.Support.Tests.Utils
Expand All @@ -35,6 +36,25 @@ public void TestCreation()
Assert.IsNull(TestEnum.Item3.GetSystem());
}

[TestMethod]
public void TestCreationNonGeneric()
{
Assert.AreEqual("Testee", EnumUtility.GetName(typeof(TestEnum)));
Assert.AreEqual(TestEnum.Item1, EnumUtility.ParseLiteral("Item1", typeof(TestEnum)));
Assert.IsNull(EnumUtility.ParseLiteral("Item2", typeof(TestEnum)));
Assert.AreEqual(TestEnum.Item2, EnumUtility.ParseLiteral("ItemTwo", typeof(TestEnum)));
Assert.IsNull(EnumUtility.ParseLiteral("iTeM1", typeof(TestEnum)));
Assert.AreEqual(TestEnum.Item1, EnumUtility.ParseLiteral("iTeM1", typeof(TestEnum), ignoreCase: true));

Assert.AreEqual("Item1", ((Enum)TestEnum.Item1).GetLiteral());
Assert.AreEqual("ItemTwo", ((Enum)TestEnum.Item2).GetLiteral());
Assert.AreEqual("This is item two", TestEnum.Item2.GetDocumentation());
Assert.AreEqual("http://example.org/test-system", ((Enum)TestEnum.Item2).GetSystem());
Assert.AreEqual("ItemThree", ((Enum)TestEnum.Item3).GetLiteral());
Assert.AreEqual("Item3", ((Enum)TestEnum.Item3).GetDocumentation());
Assert.IsNull(((Enum)TestEnum.Item3).GetSystem());
}

[FhirEnumeration("Testee")]
enum TestEnum
{
Expand Down Expand Up @@ -107,6 +127,21 @@ public void EnumParsingPerformance()
Assert.IsTrue(sw.ElapsedMilliseconds < 100);
}

[TestMethod]
public void EnumParsingPerformanceNonGeneric()
{
var sw = new Stopwatch();
sw.Start();

for (var i = 0; i < 10000; i++)
EnumUtility.ParseLiteral("male", typeof(TestAdministrativeGender));

sw.Stop();

Debug.WriteLine(sw.ElapsedMilliseconds);
Assert.IsTrue(sw.ElapsedMilliseconds < 100);
}

[TestMethod]
public void TestEnumMapping()
{
Expand All @@ -118,14 +153,42 @@ public void TestEnumMapping()
Assert.AreEqual("a", X.a.GetDocumentation()); // default documentation = name of item
}

[TestMethod]
public void TestEnumMappingNonGeneric()
{
Assert.AreEqual(TestAdministrativeGender.Male, EnumUtility.ParseLiteral("male", typeof(TestAdministrativeGender)));
Assert.IsNull(EnumUtility.ParseLiteral("maleX", typeof(TestAdministrativeGender)));
Assert.AreEqual(X.a, EnumUtility.ParseLiteral("a", typeof(X)));

Assert.AreEqual("Male", TestAdministrativeGender.Male.GetDocumentation());
Assert.AreEqual("a", X.a.GetDocumentation()); // default documentation = name of item
}

[TestMethod]
public void EnumLiteralPerformance()
{
var result = string.Empty;
var value = TestAdministrativeGender.Male;

var sw = Stopwatch.StartNew();
for (var i = 0; i < 50_000; i++)
result = value.GetLiteral();
sw.Stop();

Assert.AreEqual("male", result);
Debug.WriteLine(sw.ElapsedMilliseconds);
Assert.IsTrue(sw.ElapsedMilliseconds < 500);
}

[TestMethod]
public void EnumLiteralPerformanceNonGeneric()
{
var result = string.Empty;
Enum value = TestAdministrativeGender.Male;

var sw = Stopwatch.StartNew();
for (var i = 0; i < 50_000; i++)
result = TestAdministrativeGender.Male.GetLiteral();
result = value.GetLiteral();
sw.Stop();

Assert.AreEqual("male", result);
Expand Down

0 comments on commit f623ba5

Please sign in to comment.