Skip to content

Commit

Permalink
fixing memory leak in CreateInstance due to hashkey issue
Browse files Browse the repository at this point in the history
  • Loading branch information
Marco Silipo committed Dec 18, 2023
1 parent 8c60c9f commit 7b4f287
Show file tree
Hide file tree
Showing 5 changed files with 469 additions and 1,858 deletions.
167 changes: 167 additions & 0 deletions X39.Util.Tests/CreateInstanceViaJsonSerializerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#if NET7_0_OR_GREATER
using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using NUnit.Framework;

namespace X39.Util.Tests;

public class CreateInstanceViaJsonSerializerTest
{
#region SerializerTest

public class PrimitivePair
{
public PrimitiveString String { get; set; }
public PrimitiveUShort UShort { get; set; }
}

public class StronglyTypedPrimitiveConverterFactory : JsonConverterFactory
{
#pragma warning disable CS0618
private interface ICreator
{
public object Value { get; }
}

private class Creator<TType, TPrimitive> : ICreator
where TType : IStronglyTypedPrimitive<TType, TPrimitive>, new()
{
public object Value { get; }

public Creator(TPrimitive primitive)
{
Value = new TType {Value = primitive};
}
}

private class StronglyTypedPrimitiveConverter<TType, TPrimitive> : JsonConverter<TType>
where TType : IStronglyTypedPrimitive<TType, TPrimitive>, new()
{
public override TType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (!IsPrimitiveTokenType(reader.TokenType))
return JsonSerializer.Deserialize<TType>(ref reader);

if (typeof(TPrimitive).IsEquivalentTo(typeof(string)))
{
var value = reader.GetString() ?? string.Empty;

return (TType) typeof(Creator<,>)
.MakeGenericType(typeof(TType), typeof(string))
.CreateInstanceWith<ICreator>(value)
.Value;
}

if (typeof(TPrimitive).IsEquivalentTo(typeof(ushort)))
{
var value = reader.GetUInt16();

return (TType) typeof(Creator<,>)
.MakeGenericType(typeof(TType), typeof(ushort))
.CreateInstanceWith<ICreator>(value)
.Value;
}

throw new NotSupportedException($"The primitive {typeof(TPrimitive).FullName()} is not supported");
}

public override void Write(Utf8JsonWriter writer, TType value, JsonSerializerOptions options)
{
switch (value.Value)
{
case string stringValue:
writer.WriteStringValue(stringValue);
break;
case ushort ushortValue:
writer.WriteNumberValue(ushortValue);
break;
default:
throw new NotSupportedException(
$"The primitive {typeof(TPrimitive).FullName()} is not supported");
}
}

private static bool IsPrimitiveTokenType(JsonTokenType tokenType)
{
return tokenType is
JsonTokenType.String or
JsonTokenType.Number;
}
}

/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeof(IStronglyTypedPrimitive).IsAssignableFrom(typeToConvert);
}

/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var interfaceType = typeToConvert.GetInterfaces()
.First((q) => q.IsGenericType(typeof(IStronglyTypedPrimitive<,>)));
return typeof(StronglyTypedPrimitiveConverter<,>)
.MakeGenericType(interfaceType.GetGenericArguments())
.CreateInstanceWith<JsonConverter>();
}
#pragma warning restore CS0618
}

public interface IStronglyTypedPrimitive
{
}

#pragma warning disable CS0618
public interface IStronglyTypedPrimitive<TType, TPrimitive> : IStronglyTypedPrimitive
where TType : IStronglyTypedPrimitive<TType, TPrimitive>, new()
#pragma warning restore CS0618
{
public TPrimitive Value { get; init; }
}

public readonly struct PrimitiveString : IStronglyTypedPrimitive<PrimitiveString, string>
{
public PrimitiveString(string value)
{
Value = value;
}

public string Value { get; init; }
}

public readonly struct PrimitiveUShort : IStronglyTypedPrimitive<PrimitiveUShort, ushort>
{
public PrimitiveUShort(ushort value)
{
Value = value;
}

public ushort Value { get; init; }
}

#endregion

[Test]
public void Test()
{
const string json = "{\"String\":\"AB\",\"UShort\":123}";

for (var i = 0; i < 1000; i++)
{
JsonSerializer.Deserialize<PrimitivePair>(json, new JsonSerializerOptions
{
Converters =
{
new StronglyTypedPrimitiveConverterFactory()
}
});
}

Assert.AreEqual(4, TypeExtensionMethods.CreateInstanceCache.Count);
}
}


#endif
143 changes: 120 additions & 23 deletions X39.Util/TypeExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,122 @@ namespace X39.Util;
public static partial class TypeExtensionMethods
{
// ReSharper disable once IdentifierTypo
private static readonly RWLConcurrentDictionary<Type, Type> DeNulledTypeCache = new();
private static readonly RWLConcurrentDictionary<Type, string> FullNameCache = new();
private static readonly RWLConcurrentDictionary<Type, string> NameCache = new();
private static readonly RWLConcurrentDictionary<Type, Type> BaseTypeCache = new();
private static readonly RWLConcurrentDictionary<Type, bool> IsObsoleteCache = new();

#if NET5_0_OR_GREATER || NETSTANDARD2_0 || NETSTANDARD2_1 || NET47 || NET471 || NET472
private static readonly RWLConcurrentDictionary<(Type returnType, Type[] arguments), Delegate> CreateInstanceCache =
new();
internal static readonly RWLConcurrentDictionary<Type, Type> DeNulledTypeCache = new();
internal static readonly RWLConcurrentDictionary<Type, string> FullNameCache = new();
internal static readonly RWLConcurrentDictionary<Type, string> NameCache = new();
internal static readonly RWLConcurrentDictionary<Type, Type> BaseTypeCache = new();
internal static readonly RWLConcurrentDictionary<Type, bool> IsObsoleteCache = new();

internal struct InstanceCacheKey : IEquatable<InstanceCacheKey>
{
public bool Equals(InstanceCacheKey other)
{
return Type.IsEquivalentTo(other.Type)
&& Arguments.Length == other.Arguments.Length
&& Arguments.Zip(other.Arguments, (l, r) => l.IsEquivalentTo(r)).All((q) => q);
}

public override bool Equals(object? obj)
{
return obj is InstanceCacheKey other && Equals(other);
}

public override int GetHashCode()
{
#if NET5_0_OR_GREATER || NETSTANDARD2_1
switch (Arguments.Length)
{
case 0: return Type.GetHashCode();
case 1:
return HashCode.Combine(
Type.GetHashCode(),
Arguments[0].GetHashCode());
case 2:
return HashCode.Combine(
Type.GetHashCode(),
Arguments[0].GetHashCode(),
Arguments[1].GetHashCode());
case 3:
return HashCode.Combine(
Type.GetHashCode(),
Arguments[0].GetHashCode(),
Arguments[1].GetHashCode(),
Arguments[2].GetHashCode());
case 4:
return HashCode.Combine(
Type.GetHashCode(),
Arguments[0].GetHashCode(),
Arguments[1].GetHashCode(),
Arguments[2].GetHashCode(),
Arguments[3].GetHashCode());
case 5:
return HashCode.Combine(
Type.GetHashCode(),
Arguments[0].GetHashCode(),
Arguments[1].GetHashCode(),
Arguments[2].GetHashCode(),
Arguments[3].GetHashCode(),
Arguments[4].GetHashCode());
case 6:
return HashCode.Combine(
Type.GetHashCode(),
Arguments[0].GetHashCode(),
Arguments[1].GetHashCode(),
Arguments[2].GetHashCode(),
Arguments[3].GetHashCode(),
Arguments[4].GetHashCode(),
Arguments[5].GetHashCode());
case 7:
return HashCode.Combine(
Type.GetHashCode(),
Arguments[0].GetHashCode(),
Arguments[1].GetHashCode(),
Arguments[2].GetHashCode(),
Arguments[3].GetHashCode(),
Arguments[4].GetHashCode(),
Arguments[5].GetHashCode(),
Arguments[6].GetHashCode());
default:
var hashCode = new HashCode();
hashCode.Add(Type.GetHashCode());
foreach (var arg in Arguments)
{
hashCode.Add(arg.GetHashCode());
}

return hashCode.ToHashCode();
}
#else
private static readonly RWLConcurrentDictionary<Tuple<Type, Type[]>, Delegate> CreateInstanceCache =
new();
unchecked
{
return Arguments.Select((q) => q.GetHashCode()).Aggregate(
Type.GetHashCode(),
(l, r) => (int)(r + 0x9e3779b9 + (l << 6) + (l >> 2)));
}
#endif
}

public static bool operator ==(InstanceCacheKey left, InstanceCacheKey right)
{
return left.Equals(right);
}

public static bool operator !=(InstanceCacheKey left, InstanceCacheKey right)
{
return !left.Equals(right);
}

public InstanceCacheKey(Type type, Type[] arguments)
{
Type = type;
Arguments = arguments;
}

public Type Type;
public Type[] Arguments;
}

internal static readonly RWLConcurrentDictionary<InstanceCacheKey, Delegate> CreateInstanceCache = new();

/// <summary>
/// Clears all cached data for the methods provided by <see cref="TypeExtensionMethods"/>.
Expand All @@ -42,6 +145,7 @@ public static void ClearCache()
NameCache.Clear();
BaseTypeCache.Clear();
CreateInstanceCache.Clear();
ClearDynCache();
}

/// <summary>
Expand Down Expand Up @@ -183,11 +287,7 @@ public static Type GetDeNulledType(this Type type)
/// <returns>The newly created instance</returns>
public static object CreateInstance(this Type t)
{
#if NET5_0_OR_GREATER || NETSTANDARD2_0 || NETSTANDARD2_1 || NET47 || NET471 || NET472
var key = (t, Type.EmptyTypes);
#else
var key = Tuple.Create(t, Type.EmptyTypes);
#endif
var key = new InstanceCacheKey(t, Type.EmptyTypes);
if (CreateInstanceCache.TryGetValue(key, out var @delegate))
return @delegate.DynamicInvoke()!;
CreateInstanceCache[key] = @delegate = Expression.Lambda(Expression.New(t)).Compile();
Expand Down Expand Up @@ -252,13 +352,9 @@ public static object CreateInstanceWith(this Type t, Type[] types, object?[] arg
Debug.Assert(
types.Length == args.Length,
$"The count of args ({args.Length}) is not equal to the count of types ({types.Length})");
#if NET5_0_OR_GREATER || NETSTANDARD2_0 || NETSTANDARD2_1 || NET47 || NET471 || NET472
var key = (t, types);
#else
var key = Tuple.Create(t, types);
#endif
var key = new InstanceCacheKey(t, types);
if (CreateInstanceCache.TryGetValue(key, out var @delegate))
return @delegate.DynamicInvoke()!;
return @delegate.DynamicInvoke(args)!;
var constructor = t.GetConstructor(
bindingAttr: System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance |
Expand All @@ -277,6 +373,7 @@ public static object CreateInstanceWith(this Type t, Type[] types, object?[] arg
CreateInstanceCache[key] = @delegate = lambdaExpression.Compile();
return @delegate.DynamicInvoke(args)!;
}

/// <summary>
/// Creates a new instance of the <paramref name="t"/> with the given <paramref name="args"/>.
/// The method will use the given <paramref name="types"/> to determine which constructor to use.
Expand Down Expand Up @@ -384,7 +481,7 @@ public static bool IsObsolete(this Type type, bool useCache = true)
return flag;
var attribute = type.GetCustomAttribute<ObsoleteAttribute>();
var tmp = attribute is not null;
if (tmp is false && type.DeclaringType?.IsObsolete(useCache: useCache) is {} classObsolete)
if (tmp is false && type.DeclaringType?.IsObsolete(useCache: useCache) is { } classObsolete)
tmp = classObsolete;
if (useCache)
IsObsoleteCache[type] = tmp;
Expand Down
Loading

0 comments on commit 7b4f287

Please sign in to comment.