add comp tick, needs tracker and pawn rendering optimizations, cache more dates, fix reflection patches, default to disabled for texture atlas patch

This commit is contained in:
localavaster
2026-04-18 15:18:26 -06:00
parent 1acf3ce319
commit 7d96ecdb1d
10 changed files with 984 additions and 80 deletions
@@ -10,7 +10,6 @@ using System.Security;
using System.Threading.Tasks;
using PerformanceFish.Cache;
using PerformanceFish.ModCompatibility;
#if false
using ConstructorInfoCache
= PerformanceFish.Cache.ByReference<System.RuntimeTypeHandle, System.Reflection.BindingFlags,
PerformanceFish.System.ReflectionCaching.RecordArray<System.Type>,
@@ -57,13 +56,11 @@ using ActivatorCache
using CustomAttributeCache
= PerformanceFish.Cache.ByReferenceClassic<System.Reflection.ICustomAttributeProvider, System.RuntimeTypeHandle, bool,
PerformanceFish.System.ReflectionCaching.MonoCustomAttrs.CustomAttributeCacheValue>;
#endif
namespace PerformanceFish.System;
public sealed class ReflectionCaching : ClassWithFishPatches
{
#if false
static ReflectionCaching() // necessary to prevent recursion between patches and function pointer creation
{
try
@@ -89,7 +86,6 @@ public sealed class ReflectionCaching : ClassWithFishPatches
ex}");
}
}
#endif
public record struct StateAndFlags
{
@@ -97,7 +93,6 @@ public sealed class ReflectionCaching : ClassWithFishPatches
public BindingFlags Flags;
}
#if false
public sealed class TypePatches
{
public sealed class GetField_Patch : FishPatch
@@ -387,6 +382,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches
public sealed class GetFullName : FishPatch
{
public override bool DefaultState => true;
public override string? Description { get; }
= "Normally averages at over 1000ns per call. Caching makes this about 30 times faster, including "
+ "Harmony overhead. Type.Name is similarly slow too, but unfortunately can't be patched as it's an "
@@ -397,54 +394,83 @@ public sealed class ReflectionCaching : ClassWithFishPatches
= AccessTools.DeclaredPropertyGetter(typeof(RuntimeType), nameof(RuntimeType.FullName));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Prefix(RuntimeType __instance, ref string? __result, out bool __state)
public static bool Prefix(RuntimeType __instance, ref string? __result, out FullNameState __state)
{
ref var cache = ref TypeFullNameCache.GetOrAddReference(__instance.TypeHandle);
__state = default;
if (_fullNameRecursionDepth != 0)
return true;
if (!cache.Cached && !TryGetFromCentralCache(__instance, ref cache))
return __state = true;
__state.ReleaseGuard = true;
_fullNameRecursionDepth++;
var cache = TypeFullNameCache.GetOrAddReference(__instance.TypeHandle);
var fetchedFromCentralCache = false;
if (!cache.Cached)
{
fetchedFromCentralCache = TryGetFromCentralCache(__instance, out cache);
if (!fetchedFromCentralCache)
{
__state.ShouldStore = true;
return true;
}
}
if (fetchedFromCentralCache)
TypeFullNameCache.GetExistingReference(__instance.TypeHandle) = cache;
__result = cache.Name;
return __state = false;
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool TryGetFromCentralCache(RuntimeType instance,
ref TypeFullNameCacheValue cache)
out TypeFullNameCacheValue cache)
{
lock (_lock)
{
ref var centralCache
= ref TypeFullNameCache.GetDirectly.GetOrAddReference(instance.TypeHandle);
cache.Name = centralCache.Name;
cache = TypeFullNameCache.GetDirectly.GetOrAddReference(instance.TypeHandle);
if (!cache.Cached)
return false;
}
return cache.Cached;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Postfix(RuntimeType __instance, string? __result, bool __state)
public static void Postfix(RuntimeType __instance, string? __result, FullNameState __state)
{
if (!__state)
if (!__state.ShouldStore)
return;
UpdateCache(__instance, __result);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Exception? Finalizer(Exception? __exception, ref FullNameState __state)
{
if (__state.ReleaseGuard)
_fullNameRecursionDepth--;
return __exception;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void UpdateCache(RuntimeType __instance, string? __result)
{
ref var cache = ref TypeFullNameCache.GetExistingReference(__instance.TypeHandle);
cache.Name = __result;
var cache = new TypeFullNameCacheValue
{
Cached = true,
Name = __result
};
TypeFullNameCache.GetExistingReference(__instance.TypeHandle) = cache;
lock (_lock)
{
ref var centralCache
= ref TypeFullNameCache.GetDirectly.GetReference(__instance.TypeHandle);
centralCache.Name = __result;
}
TypeFullNameCache.GetDirectly.GetOrAddReference(__instance.TypeHandle) = cache;
}
[ThreadStatic]
private static int _fullNameRecursionDepth;
private static object _lock = new();
}
@@ -479,24 +505,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches
public PropertyInfo? Info;
}
public record struct TypeFullNameCacheValue()
public record struct FullNameState
{
private object? _value = typeof(void);
public bool ReleaseGuard;
public bool ShouldStore;
}
public string? Name
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Unsafe.As<string?>(_value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => _value = value;
}
public bool Cached
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _value is not MemberInfo;
}
public record struct TypeFullNameCacheValue
{
public bool Cached;
public string? Name;
}
}
@@ -504,6 +522,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
public sealed class GetCustomAttributes : FishPatch
{
public override bool DefaultState => true;
public override string? Description { get; }
= "Caches attributes for reflection lookups. Decent load time improvement.";
@@ -539,16 +559,22 @@ public sealed class ReflectionCaching : ClassWithFishPatches
private static CustomAttributeCacheLookup TryGetCachedAttributes(ref CustomAttributeCache key,
out object[]? result)
{
var localLookup = TryUseCache(CustomAttributeCache.Get, ref key, out result);
if (localLookup == CustomAttributeCacheLookup.Hit)
{
result = CopyCachedAttributes(result!);
return CustomAttributeCacheLookup.Hit;
}
if (localLookup == CustomAttributeCacheLookup.InProgress)
return localLookup;
lock (_lock)
{
var localLookup = TryUseCache(CustomAttributeCache.Get, ref key, out result);
if (localLookup != CustomAttributeCacheLookup.Miss)
return localLookup;
var centralLookup = TryUseCache(CustomAttributeCache.GetDirectly, ref key, out var centralResult);
if (centralLookup == CustomAttributeCacheLookup.Hit)
{
result = centralResult;
result = CopyCachedAttributes(centralResult!);
CustomAttributeCache.Get[key] = new()
{
Attributes = centralResult,
@@ -567,19 +593,19 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Postfix(ICustomAttributeProvider obj, object[]? __result,
in CustomAttributeState __state)
ref CustomAttributeState __state)
{
if (!__state.ShouldStore || __result is null)
return;
StoreCompleted(ref Unsafe.AsRef(in __state.Key), __result);
StoreCompleted(ref __state.Key, __result);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static Exception? Finalizer(Exception? __exception, in CustomAttributeState __state)
public static Exception? Finalizer(Exception? __exception, ref CustomAttributeState __state)
{
if (__state.ShouldStore && __exception != null)
ClearInProgress(ref Unsafe.AsRef(in __state.Key));
ClearInProgress(ref __state.Key);
return __exception;
}
@@ -630,7 +656,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
var value = new CustomAttributeCacheValue
{
Attributes = result,
Attributes = CopyCachedAttributes(result),
Status = CustomAttributeCacheStatus.Completed
};
@@ -665,6 +691,10 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object[] CopyCachedAttributes(object[] attributes)
=> attributes.Length != 0 ? (object[])attributes.Clone() : Array.Empty<object>();
private static object _lock = new();
}
@@ -707,8 +737,12 @@ public sealed class ReflectionCaching : ClassWithFishPatches
Guard.IsNotNull(array);
Guard.IsLessThanOrEqualTo(array.Length, byte.MaxValue);
Array = array;
var length = array.Length;
_hashCode = HashCode.Combine(length, length > 0 && Array[0] is { } item ? item.GetHashCode() : 0);
var hashCode = array.Length;
for (var i = 0; i < array.Length; i++)
hashCode = global::System.HashCode.Combine(hashCode, array[i]);
_hashCode = hashCode;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -717,13 +751,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches
if (Array is null)
return other.Array is null;
if (_hashCode != other._hashCode)
return false;
if (other.Array is null || Array.Length != other.Array.Length)
return false;
var length = Array.Length;
for (var i = 0; i < length; i++)
{
if (!Array[i].Equals(other.Array![i]))
if (!EqualityComparer<T>.Default.Equals(Array[i], other.Array[i]))
return false;
}
@@ -738,6 +775,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
public sealed class CreateInstance_Type : FishPatch
{
public override bool DefaultState => true;
public override List<string> IncompatibleModIDs { get; } = [PackageIDs.MULTIPLAYER];
public override string? Description { get; }
@@ -749,21 +788,94 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Prefix(Type? type, ref object __result)
{
if (type is null)
if (type is not RuntimeType runtimeType)
return true;
ref var cache = ref ActivatorCache.GetOrAddReference(type.TypeHandle);
ref var cache = ref ActivatorCache.GetOrAddReference(runtimeType.TypeHandle);
if (ReferenceEquals(cache.Result, _unsupportedActivatorSentinel))
return true;
if (cache.Result is not null)
return TryInvoke(ref __result, cache.Result);
if (!CanUseFastPath(runtimeType))
return MarkUnsupported(runtimeType, ref cache);
if (cache.Dirty
&& !ActivatorCache.UpdateAsyncCache<ActivatorCacheValue, Type, Func<object>>(ref cache,
type.TypeHandle, type))
&& !ActivatorCache.UpdateAsyncCache<ActivatorCacheValue, RuntimeType, Func<object>>(ref cache,
runtimeType.TypeHandle, runtimeType))
{
return true;
}
__result = cache.Result!();
return false;
try
{
__result = cache.Result!();
return false;
}
catch (OverflowException)
{
throw;
}
catch (Exception ex)
{
throw new TargetInvocationException(ex);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool MarkUnsupported(RuntimeType type, ref ActivatorCacheValue cache)
{
cache.Result = _unsupportedActivatorSentinel;
lock (ActivatorCache.SyncLock)
ActivatorCache.GetDirectly.GetOrAddReference(type.TypeHandle).Result = _unsupportedActivatorSentinel;
return true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool TryInvoke(ref object __result, Func<object> activator)
{
try
{
__result = activator();
return false;
}
catch (OverflowException)
{
throw;
}
catch (Exception ex)
{
throw new TargetInvocationException(ex);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanUseFastPath(RuntimeType type)
{
if (type.HasElementType
|| type.ContainsGenericParameters
|| type.IsInterface
|| type.IsAbstract
|| type.IsCOMObject
|| type == typeof(void)
|| type == typeof(TypedReference)
|| type == typeof(ArgIterator)
|| type == typeof(RuntimeArgumentHandle))
{
return false;
}
return type.IsValueType
|| type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null)
is not null;
}
private static readonly Func<object> _unsupportedActivatorSentinel
= static () => throw new InvalidOperationException("Unsupported activator sentinel");
}
// public sealed class CreateInstance_Type_Args : FishPatch
@@ -772,14 +884,34 @@ public sealed class ReflectionCaching : ClassWithFishPatches
// = (Func<Type, object[], object>)Activator.CreateInstance;
// }
public record struct ActivatorCacheValue : IAsyncCacheable<RuntimeTypeHandle, Type, Func<object>>
public record struct ActivatorCacheValue : IAsyncCacheable<RuntimeTypeHandle, RuntimeType, Func<object>>
{
public Func<object>? Result { get; set; }
public Task<Func<object>>? Task { get; set; }
public bool Dirty => Result == null;
public ValueTask<Func<object>?> MakeResultAsync(RuntimeTypeHandle key, Type type)
=> new(Reflection.CreateConstructorDelegate<Func<object>>(type));
public ValueTask<Func<object>?> MakeResultAsync(RuntimeTypeHandle key, RuntimeType type)
=> new(MakeActivatorDelegate(type));
[MethodImpl(MethodImplOptions.NoInlining)]
private static Func<object>? MakeActivatorDelegate(RuntimeType type)
{
if (type.IsValueType)
return Reflection.CreateConstructorDelegate<Func<object>>(type);
var constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null,
Type.EmptyTypes, null);
if (constructor is null)
return null;
var dynamicMethod = new DynamicMethod($"Activator_{type.Name}", typeof(object), Type.EmptyTypes,
type, true);
var il = dynamicMethod.GetILGenerator();
il.Emit(FishTranspiler.New(constructor));
il.Emit(FishTranspiler.Return);
return (Func<object>)dynamicMethod.CreateDelegate(typeof(Func<object>));
}
}
// public record struct ActivatorWithArgumentsCacheValue : IAsyncCacheable<ActivatorWithArgumentsCache, Type,
@@ -869,6 +1001,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
public sealed class GetValue_Patch : FishPatch
{
public override bool DefaultState => true;
public override string Description { get; }
= "Optimizes the GetValue method by invoking it through specialized cached delegates";
@@ -890,6 +1024,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Prefix(FieldInfo __instance, object? obj, ref object? __result)
{
ValidateGetValueCall(__instance, obj);
ref var cache = ref FieldGetters.GetOrAddReference(__instance.FieldHandle);
if (cache.Dirty
@@ -902,10 +1038,36 @@ public sealed class ReflectionCaching : ClassWithFishPatches
__result = cache.Result!(obj);
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ValidateGetValueCall(FieldInfo info, object? obj)
{
if (!info.IsStatic)
{
if (obj is null)
throw new TargetException("Non-static field requires a target");
var declaringType = info.DeclaringType!;
if (!declaringType.IsAssignableFrom(obj.GetType()))
{
throw new ArgumentException(
$"Field {info.Name} defined on type {declaringType} is not a field on the target object which is of type {obj.GetType()}.",
nameof(obj));
}
}
if (!info.IsLiteral && info.DeclaringType!.ContainsGenericParameters)
{
throw new InvalidOperationException(
"Late bound operations cannot be performed on fields with types for which Type.ContainsGenericParameters is true.");
}
}
}
public sealed class SetValue_Patch : FishPatch
{
public override bool DefaultState => false;
public override string Description { get; }
= "Optimizes the SetValue method by invoking it through specialized cached delegates";
@@ -927,14 +1089,20 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool TryUpdate(FieldInfo __instance, ref FieldInfoSetterCache cache)
=> cache.Task != _fieldSetterFailureTask && __instance.IsConst()
? ErrorForInvalidCallAttempt(__instance, ref cache)
: FieldSetters.UpdateAsyncCache<FieldInfoSetterCache, FieldInfo, FieldSetter>(ref cache,
{
if (__instance.IsConst())
return ErrorForInvalidCallAttempt(__instance, ref cache);
return FieldSetters.UpdateAsyncCache<FieldInfoSetterCache, FieldInfo, FieldSetter>(ref cache,
__instance.FieldHandle, __instance);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool ErrorForInvalidCallAttempt(FieldInfo info, ref FieldInfoSetterCache cache)
{
if (cache.Task == _fieldSetterFailureTask)
return false;
cache.Task = _fieldSetterFailureTask;
Log.Error($"FieldInfo.SetValue has been called on '{
info.FullDescription()}'. This is a const that gets copied at compile time. An attempt at "
@@ -944,7 +1112,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
}
}
private static Task<FieldSetter> _fieldSetterFailureTask = new(static () => null!);
private static Task<FieldSetter> _fieldSetterFailureTask = new(static () => null!); // sentinel only
public static FieldGetter? MakeGetterDelegate(FieldInfo info)
{
@@ -1116,14 +1284,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches
if (methodInfo is null && constructorInfo is null)
return null;
var parameters = methodBase.GetParameters();
if (!CanCacheInvoke(methodBase, methodInfo, constructorInfo, parameters))
return null;
var dm = new DynamicMethod($"MethodInvoker_{methodBase.Name}", typeof(object),
[typeof(object), typeof(object[])],
GetOwnerType(methodBase), true);
var il = dm.GetILGenerator();
var parameters = methodBase.GetParameters();
if (methodInfo != null && !methodBase.IsStatic)
EmitInstanceArgument(il, methodBase);
@@ -1158,6 +1328,49 @@ public sealed class ReflectionCaching : ClassWithFishPatches
}
}
private static bool CanCacheInvoke(MethodBase methodBase, MethodInfo? methodInfo,
ConstructorInfo? constructorInfo, ParameterInfo[] parameters)
{
if (methodBase.ContainsGenericParameters
|| (methodBase.CallingConvention & CallingConventions.VarArgs) != 0)
{
return false;
}
if (methodInfo != null && !IsSupportedInvokeType(methodInfo.ReturnType))
return false;
if (constructorInfo?.DeclaringType is { } declaringType
&& !IsSupportedInvokeType(declaringType))
{
return false;
}
for (var i = 0; i < parameters.Length; i++)
{
if (!IsSupportedInvokeParameter(parameters[i].ParameterType))
return false;
}
return true;
}
private static bool IsSupportedInvokeParameter(Type parameterType)
{
// object[] stores nullable value arguments as boxed T or null, not boxed Nullable<T>, so the
// current byref fast path cannot safely hand out a Nullable<T>&.
if (parameterType.IsByRef && parameterType.GetElementType()!.IsNullable())
return false;
if (parameterType.IsByRef)
parameterType = parameterType.GetElementType()!;
return IsSupportedInvokeType(parameterType);
}
private static bool IsSupportedInvokeType(Type type)
=> !type.IsPointer && !type.ContainsGenericParameters;
private static void EmitInvokeMethodCall(MethodBase methodBase, ILGenerator il, MethodInfo methodInfo)
{
il.Emit(FishTranspiler.Call(methodBase));
@@ -1252,7 +1465,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[SecuritySafeCritical]
public static ref TTo CastRefSafely<TTo>(ref object? from)
public static ref TTo CastRefSafely<TTo>(ref object? from) where TTo : class?
=> ref from is TTo or null
? ref Unsafe.As<object?, TTo>(ref from)
: ref ThrowInvalidCastException<TTo>();
@@ -1264,7 +1477,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
? ref Unsafe.Unbox<TTo>(from)
: ref Unsafe.Unbox<TTo>(from
= from is null
? default
? default(TTo)
: FisheryLib.Convert.Type<object, TTo>(from));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -1314,5 +1527,4 @@ public sealed class ReflectionCaching : ClassWithFishPatches
// populated with dynamic methods by MethodBasePatches.MakeInvokeDelegate, MakeGetterDelegate and
// MakeSetterDelegate
}
#endif
}