Compare commits

...

7 Commits

23 changed files with 776 additions and 500 deletions
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -7,9 +7,9 @@
<li>1.5</li>
<li>1.6</li>
</supportedVersions>
<author>bradson</author>
<author>bradson, localavaster, Lionel Colaso, Riggs0</author>
<modIconPath IgnoreIfNoMatchingField="True">PerformanceFish/ModIcon</modIconPath>
<url>https://github.com/bbradson/performance-fish</url>
<url>hhttps://git.hudsonriggs.systems/HRiggs/Performance-Fish</url>
<loadBefore>
<li>Dubwise.DubsPerformanceAnalyzer</li>
<li>Dubwise.DubsPerformanceAnalyzerButchered</li>
@@ -46,7 +46,7 @@
<packageId>bs.fishery</packageId>
<displayName>Fishery</displayName>
<steamWorkshopUrl>n/a</steamWorkshopUrl>
<downloadUrl>https://github.com/bbradson/Fishery</downloadUrl>
<downloadUrl>https://git.hudsonriggs.systems/HRiggs/Fishery</downloadUrl>
</li>
</modDependencies>
<incompatibleWith>
+1
View File
@@ -0,0 +1 @@
3811986740
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
-1
View File
@@ -1,4 +1,3 @@
NOTE: SWAP TO THE rimworld-1.6 BRANCH BEFORE DOWNLOADING OTHERWISE IT WILL NOT WORK FOR 1.6!
# Performance Fish
![](About/Preview.png?raw=true)
+7 -1
View File
@@ -3,6 +3,7 @@
<PropertyGroup>
<OutDir>..\..\1.6\Assemblies\</OutDir>
<Copyright>Copyright (c) 2026 Bradson</Copyright>
<PolySharpUseEmbeddedAttributeForGeneratedTypes>false</PolySharpUseEmbeddedAttributeForGeneratedTypes>
<FisheryRepoRoot Condition="Exists('$(MSBuildThisFileDirectory)..\..\..\Fishery\1.6\Assemblies\1Fishery.dll')">$(MSBuildThisFileDirectory)..\..\..\Fishery</FisheryRepoRoot>
<FisheryRepoRoot Condition="'$(FisheryRepoRoot)' == '' and Exists('$(MSBuildThisFileDirectory)..\..\..\Fishery-clean\1.6\Assemblies\1Fishery.dll')">$(MSBuildThisFileDirectory)..\..\..\Fishery-clean</FisheryRepoRoot>
</PropertyGroup>
@@ -13,8 +14,13 @@
<DefineConstants>V1_5;V1_6</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Krafs.Rimworld.Ref" Version="1.6.*" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net48" Version="1.0.3" PrivateAssets="all" />
<PackageReference Include="Krafs.Rimworld.Ref" Version="1.6.4871" />
<PackageReference Include="Lib.Harmony" Version="2.3.3" ExcludeAssets="runtime" />
<PackageReference Update="Zetrith.Prepatcher" Version="1.2.0" />
<PackageReference Update="PolySharp" Version="1.16.0" />
<PackageReference Update="Krafs.Publicizer" Version="2.3.2" />
<PackageReference Update="JetBrains.Annotations" Version="2023.3.0" />
<Reference Include="1Fishery">
<HintPath>$(FisheryRepoRoot)\1.6\Assemblies\1Fishery.dll</HintPath>
<Private>False</Private>
@@ -0,0 +1,39 @@
// Copyright (c) 2026
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
#if V1_6
using Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish;
public sealed class AbilityTrackerTickOptimization : ClassWithFishPrepatches
{
public sealed class AbilitiesTickPatch : FishPrepatch
{
public override string? Description { get; }
= "Reads the pawn ability list once per ability tick instead of twice, while still observing list changes between abilities.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(Pawn_AbilityTracker), nameof(Pawn_AbilityTracker.AbilitiesTick));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(AbilitiesTick);
public static void AbilitiesTick(Pawn_AbilityTracker instance)
{
for (var i = 0;; i++)
{
var abilities = instance.AllAbilitiesForReading;
if (i >= abilities.Count)
return;
abilities[i].AbilityTick();
}
}
}
}
#endif
+23 -17
View File
@@ -59,27 +59,33 @@ public static class Utility
return;
_gcInProgress = true;
if (_cachesToProcessForGC is null || _currentGCProgress >= _cachesToProcessForGC.Length)
try
{
_cachesToProcessForGC = All.ToArray();
_gcRate = GenDate.TicksPerTwelfth / _cachesToProcessForGC.CountNonEmpty();
_currentGCProgress = 0;
_destroyedThingCounter = 0;
}
if (_cachesToProcessForGC is null || _currentGCProgress >= _cachesToProcessForGC.Length)
{
_cachesToProcessForGC = All.ToArray();
var nonEmptyCount = _cachesToProcessForGC.CountNonEmpty();
_gcRate = nonEmptyCount > 0 ? GenDate.TicksPerTwelfth / nonEmptyCount : 1000;
_currentGCProgress = 0;
_destroyedThingCounter = 0;
if (nonEmptyCount == 0)
return;
}
while (_cachesToProcessForGC[_currentGCProgress].Count == 0
&& _cachesToProcessForGC.Length - _currentGCProgress > 1)
while (_currentGCProgress < _cachesToProcessForGC.Length
&& _cachesToProcessForGC[_currentGCProgress].Count == 0)
{
_currentGCProgress++;
}
if (_currentGCProgress < _cachesToProcessForGC.Length)
_cachesToProcessForGC[_currentGCProgress++].Clear();
}
finally
{
_currentGCProgress++;
_lastGCTick = TickHelper.TicksGame;
_gcInProgress = false;
}
if (_currentGCProgress < _cachesToProcessForGC.Length)
_cachesToProcessForGC[_currentGCProgress++].Clear();
_lastGCTick = TickHelper.TicksGame;
_gcInProgress = false;
}
}
+136 -29
View File
@@ -11,6 +11,15 @@ namespace PerformanceFish;
public sealed class CompTickOptimization : ClassWithFishPrepatches
{
#if V1_6
public struct ThingTickCache
{
public List<ThingComp>? Source;
public int Version;
public bool AllTrivial;
}
#endif
private static readonly MethodInfo _thingCompTickMethod
= AccessTools.DeclaredMethod(typeof(ThingComp), nameof(ThingComp.CompTick));
@@ -39,23 +48,19 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
private static readonly bool _hediffCompPostTickIntervalBaseIsTrivial
= IsTrivialVoidMethod(_hediffCompPostTickIntervalMethod);
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTick
= new(static () => _thingCompTickBaseIsTrivial && !HasHarmonyPatches(_thingCompTickMethod));
private struct BasePatchState
{
public int CheckedTick;
public bool HasChecked;
public bool CanSkip;
}
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTickInterval
= new(static () => _thingCompTickIntervalBaseIsTrivial && !HasHarmonyPatches(_thingCompTickIntervalMethod));
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTickRare
= new(static () => _thingCompTickRareBaseIsTrivial && !HasHarmonyPatches(_thingCompTickRareMethod));
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTickLong
= new(static () => _thingCompTickLongBaseIsTrivial && !HasHarmonyPatches(_thingCompTickLongMethod));
private static readonly Lazy<bool> _canSkipInheritedBaseHediffCompPostTick
= new(static () => _hediffCompPostTickBaseIsTrivial && !HasHarmonyPatches(_hediffCompPostTickMethod));
private static readonly Lazy<bool> _canSkipInheritedBaseHediffCompPostTickInterval = new(
static () => _hediffCompPostTickIntervalBaseIsTrivial && !HasHarmonyPatches(_hediffCompPostTickIntervalMethod));
private static BasePatchState _thingCompTickState;
private static BasePatchState _thingCompTickIntervalState;
private static BasePatchState _thingCompTickRareState;
private static BasePatchState _thingCompTickLongState;
private static BasePatchState _hediffCompPostTickState;
private static BasePatchState _hediffCompPostTickIntervalState;
private static readonly FishTable<Type, bool> _thingCompTickTypes = new()
{
@@ -89,10 +94,19 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
public sealed class ThingTickPatch : FishPrepatch
{
public override bool DefaultState => false;
public override bool DefaultState =>
#if V1_6
true;
#else
false;
#endif
public override string? Description { get; }
#if V1_6
= "Skips the component tick loop when every component inherits the unpatched, empty base CompTick method.";
#else
= "Skips inherited empty ThingComp.CompTick calls when the base method is still unpatched and trivial.";
#endif
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ThingWithComps), "Tick");
@@ -102,16 +116,61 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
public static void ReplacementBody(ThingWithComps instance)
{
#if V1_6
var comps = instance.comps;
if (comps is null)
return;
#else
var comps = instance.AllComps;
#endif
var count = comps.Count;
#if V1_6
// Small lists cost less to dispatch normally than to validate a cache.
if (count >= 4 && CanSkipInheritedBaseThingCompTick())
{
ref var cache = ref instance.ThingTickCache();
if (!ReferenceEquals(cache.Source, comps) || cache.Version != comps._version)
UpdateThingTickCache(comps, ref cache);
if (cache.AllTrivial)
return;
}
#else
var canSkipInheritedBase = CanSkipInheritedBaseThingCompTick();
#endif
for (var i = 0; i < count; i++)
{
#if V1_6
instance.comps![i].CompTick();
#else
var comp = comps[i];
if (!canSkipInheritedBase || !_thingCompTickTypes.GetOrAdd(comp.GetType()))
comp.CompTick();
#endif
}
}
#if V1_6
[MethodImpl(MethodImplOptions.NoInlining)]
private static void UpdateThingTickCache(List<ThingComp> comps, ref ThingTickCache cache)
{
var allTrivial = true;
for (var i = 0; i < comps.Count; i++)
{
var comp = comps[i];
if (comp is null || !_thingCompTickTypes.GetOrAdd(comp.GetType()))
{
allTrivial = false;
break;
}
}
cache.Source = comps;
cache.Version = comps._version;
cache.AllTrivial = allTrivial;
}
#endif
}
public sealed class ThingTickIntervalPatch : FishPrepatch
@@ -129,12 +188,22 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
public static void ReplacementBody(ThingWithComps instance, int delta)
{
#if V1_6
var comps = instance.comps;
if (comps is null)
return;
#else
var comps = instance.AllComps;
#endif
var count = comps.Count;
var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickInterval();
for (var i = 0; i < count; i++)
{
#if V1_6
var comp = instance.comps![i];
#else
var comp = comps[i];
#endif
if (!canSkipInheritedBase || !_thingCompTickIntervalTypes.GetOrAdd(comp.GetType()))
comp.CompTickInterval(delta);
}
@@ -156,12 +225,22 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
public static void ReplacementBody(ThingWithComps instance)
{
#if V1_6
var comps = instance.comps;
if (comps is null)
return;
#else
var comps = instance.AllComps;
#endif
var count = comps.Count;
var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickRare();
for (var i = 0; i < count; i++)
{
#if V1_6
var comp = instance.comps![i];
#else
var comp = comps[i];
#endif
if (!canSkipInheritedBase || !_thingCompTickRareTypes.GetOrAdd(comp.GetType()))
comp.CompTickRare();
}
@@ -183,12 +262,22 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
public static void ReplacementBody(ThingWithComps instance)
{
#if V1_6
var comps = instance.comps;
if (comps is null)
return;
#else
var comps = instance.AllComps;
#endif
var count = comps.Count;
var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickLong();
for (var i = 0; i < count; i++)
{
#if V1_6
var comp = instance.comps![i];
#else
var comp = comps[i];
#endif
if (!canSkipInheritedBase || !_thingCompTickLongTypes.GetOrAdd(comp.GetType()))
comp.CompTickLong();
}
@@ -214,13 +303,12 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
if (comps == null)
return;
var count = comps.Count;
var severityAdjustment = 0f;
var canSkipInheritedBase = CanSkipInheritedBaseHediffCompPostTick();
for (var i = 0; i < count; i++)
for (var i = 0; i < instance.comps!.Count; i++)
{
var comp = comps[i];
var comp = instance.comps[i];
if (!canSkipInheritedBase || !_hediffCompPostTickTypes.GetOrAdd(comp.GetType()))
comp.CompPostTick(ref severityAdjustment);
}
@@ -249,13 +337,12 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
if (comps == null)
return;
var count = comps.Count;
var severityAdjustment = 0f;
var canSkipInheritedBase = CanSkipInheritedBaseHediffCompPostTickInterval();
for (var i = 0; i < count; i++)
for (var i = 0; i < instance.comps!.Count; i++)
{
var comp = comps[i];
var comp = instance.comps[i];
if (!canSkipInheritedBase || !_hediffCompPostTickIntervalTypes.GetOrAdd(comp.GetType()))
comp.CompPostTickInterval(ref severityAdjustment, delta);
}
@@ -267,27 +354,47 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanSkipInheritedBaseThingCompTick()
=> _canSkipInheritedBaseThingCompTick.Value;
=> CanSkipBaseMethod(_thingCompTickMethod, _thingCompTickBaseIsTrivial, ref _thingCompTickState);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanSkipInheritedBaseThingCompTickInterval()
=> _canSkipInheritedBaseThingCompTickInterval.Value;
=> CanSkipBaseMethod(_thingCompTickIntervalMethod, _thingCompTickIntervalBaseIsTrivial,
ref _thingCompTickIntervalState);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanSkipInheritedBaseThingCompTickRare()
=> _canSkipInheritedBaseThingCompTickRare.Value;
=> CanSkipBaseMethod(_thingCompTickRareMethod, _thingCompTickRareBaseIsTrivial, ref _thingCompTickRareState);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanSkipInheritedBaseThingCompTickLong()
=> _canSkipInheritedBaseThingCompTickLong.Value;
=> CanSkipBaseMethod(_thingCompTickLongMethod, _thingCompTickLongBaseIsTrivial, ref _thingCompTickLongState);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanSkipInheritedBaseHediffCompPostTick()
=> _canSkipInheritedBaseHediffCompPostTick.Value;
=> CanSkipBaseMethod(_hediffCompPostTickMethod, _hediffCompPostTickBaseIsTrivial,
ref _hediffCompPostTickState);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanSkipInheritedBaseHediffCompPostTickInterval()
=> _canSkipInheritedBaseHediffCompPostTickInterval.Value;
=> CanSkipBaseMethod(_hediffCompPostTickIntervalMethod, _hediffCompPostTickIntervalBaseIsTrivial,
ref _hediffCompPostTickIntervalState);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanSkipBaseMethod(MethodBase method, bool isTrivial, ref BasePatchState state)
{
if (!isTrivial)
return false;
var tick = Find.TickManager?.TicksGame ?? int.MinValue;
if (!state.HasChecked || state.CheckedTick != tick)
{
state.CanSkip = !HasHarmonyPatches(method);
state.CheckedTick = tick;
state.HasChecked = true;
}
return state.CanSkip;
}
private static bool InheritsThingCompTick(Type type)
=> AccessTools.Method(type, nameof(ThingComp.CompTick))?.DeclaringType == typeof(ThingComp);
+111 -1
View File
@@ -72,13 +72,89 @@ public sealed class GasGridOptimization : ClassWithFishPrepatches
}
}
#if V1_6
public abstract class PackedGasPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override bool Enabled
{
get => base.Enabled && Get<TickPatch>().Enabled;
set => base.Enabled = value;
}
protected static uint PackedDensityAt(GasGrid grid, int index)
{
var grids = grid.ParallelGasGrids();
return ((uint)grids[3].DensityAt(index) << 24)
| ((uint)grids[2].DensityAt(index) << 16)
| ((uint)grids[1].DensityAt(index) << 8)
| grids[0].DensityAt(index);
}
}
public sealed class GetDirectIndexPatch : PackedGasPatch
{
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.GetDirect), [typeof(int)]);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static uint ReplacementBody(GasGrid __instance, int index)
=> PackedDensityAt(__instance, index);
}
public sealed class GetDirectCellPatch : PackedGasPatch
{
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.GetDirect), [typeof(IntVec3)]);
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static uint ReplacementBody(GasGrid __instance, IntVec3 c)
=> PackedDensityAt(__instance, c.CellToIndex(__instance.map));
}
public sealed class SetPackedDirectPatch : PackedGasPatch
{
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.SetDirect),
[typeof(int), typeof(uint)]);
public static void Postfix(GasGrid __instance, int index)
{
var density = __instance.gasDensity[index];
SetDirectPatch.ReplacementBody(__instance, index, (byte)density, (byte)(density >> 8),
(byte)(density >> 16), (byte)(density >> 24));
}
}
public sealed class ClearCellUnsafePatch : PackedGasPatch
{
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.ClearCellUnsafe));
public static void Postfix(GasGrid __instance, IntVec3 cell)
{
var index = cell.CellToIndex(__instance.map);
var grids = __instance.ParallelGasGrids();
for (var i = 0; i < grids.Length; i++)
grids[i].SetDirect(index, 0);
}
}
#endif
public sealed class TickPatch : FishPrepatch
{
public override List<Type> LinkedPatches { get; } =
[
typeof(SetDirectPatch),
#if V1_4
typeof(CalculateGasEffectsPatch),
typeof(ColorAtPatch),
#if V1_6
typeof(GetDirectIndexPatch), typeof(GetDirectCellPatch),
typeof(SetPackedDirectPatch), typeof(ClearCellUnsafePatch),
#endif
typeof(AddGasPatch), typeof(AnyGasAtPatch),
typeof(DensityAtPatch), typeof(Debug_ClearAllPatch), typeof(Debug_FillAllPatch),
@@ -174,6 +250,35 @@ public sealed class GasGridOptimization : ClassWithFishPrepatches
#endif
}
public sealed class CalculateGasEffectsPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override bool Enabled
{
get => base.Enabled && Get<TickPatch>().Enabled;
set => base.Enabled = value;
}
public override MethodBase TargetMethodBase { get; }
= AccessTools.PropertyGetter(typeof(GasGrid), nameof(GasGrid.CalculateGasEffects));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static bool ReplacementBody(GasGrid __instance)
{
// Vanilla uses this to decide whether to equalize gas through buildings.
var gasGrids = __instance.ParallelGasGrids();
for (var i = 0; i < gasGrids.Length; i++)
{
if (gasGrids[i].CalculateGasEffects)
return true;
}
return false;
}
}
#if !V1_4
public class ParallelJob : IJobParallelFor
{
@@ -186,6 +291,11 @@ public sealed class GasGridOptimization : ClassWithFishPrepatches
public sealed class ColorAtPatch : FishPrepatch
{
public override bool ShowSettings => false;
public override bool Enabled
{
get => base.Enabled && Get<TickPatch>().Enabled;
set => base.Enabled = value;
}
public override MethodBase TargetMethodBase { get; }
#if V1_4
@@ -61,7 +61,15 @@ public sealed class GeneTrackerOptimization : ClassWithFishPrepatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Dirty(Pawn_GeneTracker instance, List<Gene> genesToTick)
#if V1_6
{
ref var versions = ref instance.GeneListVersions();
return !versions.Initialized || versions.Xenogenes != instance.xenogenes._version
|| versions.Endogenes != instance.endogenes._version;
}
#else
=> GetListVersion(instance) != genesToTick._version;
#endif
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Update(Pawn_GeneTracker instance, List<Gene> genesToTick)
@@ -71,12 +79,21 @@ public sealed class GeneTrackerOptimization : ClassWithFishPrepatches
AddTickableGenes(genesToTick, instance.xenogenes);
AddTickableGenes(genesToTick, instance.endogenes);
#if V1_6
ref var versions = ref instance.GeneListVersions();
versions.Xenogenes = instance.xenogenes._version;
versions.Endogenes = instance.endogenes._version;
versions.Initialized = true;
#else
genesToTick._version = GetListVersion(instance);
#endif
}
#if !V1_6
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetListVersion(Pawn_GeneTracker instance)
=> (instance.xenogenes._version << 16) | (instance.endogenes._version & 0b1111_1111_1111_1111);
#endif
private static void AddTickableGenes(List<Gene> genesToTick, List<Gene> genes)
{
@@ -115,6 +132,15 @@ public sealed class GeneTrackerOptimization : ClassWithFishPrepatches
}
}
#if V1_6
public struct GeneListVersionState
{
public int Xenogenes;
public int Endogenes;
public bool Initialized;
}
#endif
public sealed class GeneTrackerTickIntervalPatch : FishPrepatch
{
// i am iffy about this
+36 -9
View File
@@ -53,11 +53,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : ThingComp
{
private List<ThingComp>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<ThingComp> comps) => comps._version != _listVersion;
public bool IsDirty(List<ThingComp> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(ThingWithComps thing, T? comp)
@@ -65,6 +67,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (thing.thingIDNumber < 0)
return;
_listReference = thing.comps;
_listVersion = thing.comps._version;
Comp = comp;
}
@@ -113,11 +116,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : CompProperties
{
private List<CompProperties>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<CompProperties> comps) => comps._version != _listVersion;
public bool IsDirty(List<CompProperties> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(ThingDef thingDef, T? comp)
@@ -125,6 +130,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (thingDef.shortHash == default)
return;
_listReference = thingDef.comps;
_listVersion = thingDef.comps._version;
Comp = comp;
}
@@ -174,11 +180,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue()
{
private List<CompProperties>? _listReference;
private int _listVersion = -2;
public bool HasComp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<CompProperties> comps) => comps._version != _listVersion;
public bool IsDirty(List<CompProperties> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(ThingDef thingDef, bool hasComp)
@@ -186,6 +194,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (thingDef.shortHash == default)
return;
_listReference = thingDef.comps;
_listVersion = thingDef.comps._version;
HasComp = hasComp;
}
@@ -236,11 +245,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : ThingComp
{
private List<CompProperties>? _listReference;
private int _listVersion = -2;
public bool HasComp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<CompProperties> comps) => comps._version != _listVersion;
public bool IsDirty(List<CompProperties> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(ThingDef thingDef, bool hasComp)
@@ -248,6 +259,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (thingDef.shortHash == default)
return;
_listReference = thingDef.comps;
_listVersion = thingDef.comps._version;
HasComp = hasComp;
}
@@ -296,11 +308,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : HediffComp
{
private List<HediffComp>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<HediffComp> comps) => comps._version != _listVersion;
public bool IsDirty(List<HediffComp> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(HediffWithComps hediff, T? comp)
@@ -308,6 +322,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (hediff.loadID < 0)
return;
_listReference = hediff.comps;
_listVersion = hediff.comps._version;
Comp = comp;
}
@@ -355,11 +370,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : HediffCompProperties
{
private List<HediffCompProperties>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<HediffCompProperties> comps) => comps._version != _listVersion;
public bool IsDirty(List<HediffCompProperties> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(HediffDef hediffDef, T? comp)
@@ -367,6 +384,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (hediffDef.shortHash == 0)
return;
_listReference = hediffDef.comps;
_listVersion = hediffDef.comps._version;
Comp = comp;
}
@@ -414,11 +432,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : AbilityComp
{
private List<AbilityComp>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<AbilityComp> comps) => comps._version != _listVersion;
public bool IsDirty(List<AbilityComp> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(Ability ability, T? comp)
@@ -426,6 +446,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (ability.Id < 0)
return;
_listReference = ability.comps;
_listVersion = ability.comps._version;
Comp = comp;
}
@@ -468,11 +489,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : WorldObjectComp
{
private List<WorldObjectComp>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<WorldObjectComp> comps) => comps._version != _listVersion;
public bool IsDirty(List<WorldObjectComp> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(WorldObject worldObject, T? comp)
@@ -480,6 +503,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (worldObject.ID < 0)
return;
_listReference = worldObject.comps;
_listVersion = worldObject.comps._version;
Comp = comp;
}
@@ -522,11 +546,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : MapComponent
{
private List<MapComponent>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<MapComponent> comps) => comps._version != _listVersion;
public bool IsDirty(List<MapComponent> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(Map map, T? comp)
@@ -534,6 +560,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (map.uniqueID < 0)
return;
_listReference = map.components;
_listVersion = map.components._version;
Comp = comp;
}
@@ -0,0 +1,150 @@
// Copyright (c) 2026 Bradson
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
using System.Collections;
using System.Runtime.CompilerServices;
namespace PerformanceFish.Listers;
// ListerThings contains object instances. An index map keeps removals constant-time without
// using FishTable's collision-chain relocation, which can fail during removal.
internal sealed class IndexedReferenceList<T> : IList<T>, IList, IReadOnlyList<T> where T : Thing
{
private readonly List<T> _items = [];
private readonly Dictionary<T, int> _indices = new(ReferenceComparer.Instance);
public T this[int index]
{
get => _items[index];
set
{
if (value is null)
throw new ArgumentNullException(nameof(value));
var previous = _items[index];
if (ReferenceEquals(previous, value))
return;
if (_indices.ContainsKey(value))
throw new InvalidOperationException("Thing is already in the type cache.");
_indices.Remove(previous);
_items[index] = value;
_indices.Add(value, index);
}
}
object? IList.this[int index]
{
get => this[index];
set => this[index] = RequireThing(value);
}
public int Count => _items.Count;
public bool IsReadOnly => false;
public bool IsFixedSize => false;
public bool IsSynchronized => false;
public object SyncRoot => this;
public void Add(T item)
{
if (item is null)
throw new ArgumentNullException(nameof(item));
if (_indices.ContainsKey(item))
return;
_indices.Add(item, _items.Count);
_items.Add(item);
}
int IList.Add(object? value)
{
var item = RequireThing(value);
if (_indices.TryGetValue(item, out var index))
return index;
Add(item);
return _items.Count - 1;
}
public bool Remove(T item)
{
if (item is null || !_indices.TryGetValue(item, out var index))
return false;
RemoveAt(index);
return true;
}
void IList.Remove(object? value)
{
if (value is T item)
Remove(item);
}
public void RemoveAt(int index)
{
var item = _items[index];
var lastIndex = _items.Count - 1;
var lastItem = _items[lastIndex];
_indices.Remove(item);
if (index != lastIndex)
{
_items[index] = lastItem;
_indices[lastItem] = index;
}
_items.RemoveAt(lastIndex);
}
public void Insert(int index, T item)
{
if (item is null)
throw new ArgumentNullException(nameof(item));
if ((uint)index > (uint)_items.Count)
throw new ArgumentOutOfRangeException(nameof(index));
if (_indices.ContainsKey(item))
return;
if (index == _items.Count)
{
Add(item);
return;
}
var displaced = _items[index];
_items.Add(displaced);
_items[index] = item;
_indices[displaced] = _items.Count - 1;
_indices.Add(item, index);
}
void IList.Insert(int index, object? value) => Insert(index, RequireThing(value));
public bool Contains(T item) => item is not null && _indices.ContainsKey(item);
bool IList.Contains(object? value) => value is T item && Contains(item);
public int IndexOf(T item) => item is not null && _indices.TryGetValue(item, out var index) ? index : -1;
int IList.IndexOf(object? value) => value is T item ? IndexOf(item) : -1;
public void Clear()
{
_indices.Clear();
_items.Clear();
}
public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
void ICollection.CopyTo(Array array, int index) => ((ICollection)_items).CopyTo(array, index);
public List<T>.Enumerator GetEnumerator() => _items.GetEnumerator();
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private static T RequireThing(object? value)
=> value is T item ? item : throw new ArgumentException("Expected a thing of the cached type.", nameof(value));
private sealed class ReferenceComparer : IEqualityComparer<T>
{
public static readonly ReferenceComparer Instance = new();
public bool Equals(T? x, T? y) => ReferenceEquals(x, y);
public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj);
}
}
+11 -1
View File
@@ -33,6 +33,12 @@ public sealed class ThingsPrepatches : ClassWithFishPrepatches
lock (__instance.LockObject())
{
// Re-registering the same thing must not duplicate the def and group lists.
if (__instance.IndexMapByDef().ContainsKey(t.GetKey())
&& __instance.listsByDef.TryGetValue(t.def, out var existing)
&& existing.Contains(t))
return;
AddToDefList(__instance, t);
if (t is IHaulSource haulSource)
_haulSources(__instance).Add(haulSource);
@@ -572,8 +578,12 @@ public sealed class Things : ClassWithFishPatches
};
private static readonly MethodInfo _thingsListInitializerDefinition
= methodof(InitializeThingsList<Thing>).GetGenericMethodDefinition();
= methodof(InitializeReferenceThingsList<Thing>).GetGenericMethodDefinition();
private static IndexedReferenceList<T> InitializeReferenceThingsList<T>() where T : Thing => new();
// Keep the public factory for mods that call it directly; the ListerThings type cache uses the
// reference-indexed implementation above.
public static unsafe IndexedFishSet<T> InitializeThingsList<T>() where T : Thing
#pragma warning disable CS8622
=> new(static () => new(0,
@@ -16,6 +16,8 @@ public sealed class NeedsTrackerCaching : ClassWithFishPrepatches
{
public sealed class TryGetNeedPatch : FishPrepatch
{
public override bool DefaultState => true;
public override string? Description { get; }
= "Caches generic need lookups behind the needs list version.";
@@ -30,6 +32,8 @@ public sealed class NeedsTrackerCaching : ClassWithFishPrepatches
public sealed class TryGetNeedOutPatch : FishPrepatch
{
public override bool DefaultState => true;
public override string? Description { get; }
= "Caches generic need lookups behind the needs list version.";
@@ -55,18 +59,18 @@ public sealed class NeedsTrackerCaching : ClassWithFishPrepatches
return cache.Found;
}
return UpdateCache(instance, typeof(T), out need, ref cache);
return UpdateCache(instance, out need, ref cache);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool UpdateCache<T>(Pawn_NeedsTracker instance, Type needType, out T? need, ref CacheValue cache)
private static bool UpdateCache<T>(Pawn_NeedsTracker instance, out T? need, ref CacheValue cache)
where T : Need
{
var needs = instance.needs;
for (var i = 0; i < needs.Count; i++)
{
var candidate = needs[i];
if (candidate.GetType() != needType)
if (candidate.GetType() != typeof(T))
continue;
need = (T)candidate;
+3 -6
View File
@@ -39,7 +39,7 @@ public static class OnAssembliesLoaded
PerformanceFishMod.AllPatchClasses = PerformanceFishMod.InitializeAllPatchClasses<IHasFishPatch>();
PerformanceFishMod.AllPrepatchClasses ??= PerformanceFishMod.InitializeAllPatchClasses<ClassWithFishPrepatches>();
TryInitialize(AnalyzerFixes.Patch, true);
TryInitialize(AnalyzerFixes.Patch);
_ = FishSettings.Instance;
@@ -109,14 +109,11 @@ public static class OnAssembliesLoaded
}
}
private static async void TryInitialize(Action action, bool async = false)
private static void TryInitialize(Action action)
{
try
{
if (async)
await Task.Run(action).ConfigureAwait(false);
else
action();
action();
}
catch (Exception ex)
{
+10 -5
View File
@@ -90,6 +90,7 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
public List<MethodInfo> BenchmarkHarmonyMethodInfos { get; } = [];
private bool Patched { get; set; }
private bool ShouldBePatched { get; set; }
private readonly HashSet<MethodBase> _patchedTargets = [];
public virtual string? Name => null;
@@ -190,15 +191,16 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
protected virtual void ApplyPatch()
{
Patched = true;
foreach (var method in TargetMethodInfos)
var targets = TargetMethodInfos.ToArray();
foreach (var method in targets)
{
if (_patchedTargets.Contains(method))
continue;
try
{
if (IsBenchmarking)
{
Patched = false;
if (AccessTools.EnumeratorMoveNext(method) is { } moveNext)
{
BenchmarkHarmonyMethodInfos.Add(PerformanceFishMod.Harmony!.Patch(moveNext,
@@ -229,6 +231,7 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
transpiler: TryMakeHarmonyMethod(TranspilerMethodInfo, TranspilerMethodPriority),
finalizer: TryMakeHarmonyMethod(FinalizerMethodInfo, FinalizerMethodPriority)));
}
_patchedTargets.Add(method);
}
}
@@ -240,6 +243,7 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
FinalizerMethodInfo.FullDescription()}:\n{e}");
}
}
Patched = !IsBenchmarking && targets.All(_patchedTargets.Contains);
}
private static HarmonyMethod? TryMakeHarmonyMethod(MethodInfo? method, int priority = -1, string[]? before = null,
@@ -249,7 +253,7 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
public virtual void TryUnpatch()
{
ShouldBePatched = false;
if ( /*!Enabled &&*/ Patched)
if (Patched || _patchedTargets.Count != 0)
ApplyUnpatching();
}
@@ -269,6 +273,7 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
});
}
}
_patchedTargets.Clear();
}
private static void TryUnpatchMethod(MethodBase targetMethod, MethodInfo? patchMethod)
@@ -3,7 +3,6 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
@@ -15,16 +14,14 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
{
public sealed class WorldObjectsHolderTickPatch : FishPrepatch
{
private static FishTable<Type, bool> _skippableCompTypes = null!;
private static FishTable<Type, bool> _skippableWorldObjectTypes = null!;
private static readonly List<IThingHolder> _tmpThingHolders = [];
public override bool DefaultState => true;
private static List<WorldObject>? _cachedWorldObjectsReference;
private static List<WorldObject>? _cachedSnapshotReference;
private static int _cachedSnapshotVersion = -2;
public override string? Description { get; }
= "The world objects holder is responsible for ticking every world object. This includes settlements, "
+ "caravans, outposts and any other object placed in the world, instead of maps. Normally it ticks "
+ "everything equally, including many static objects that cannot possibly affect anything from a tick. "
+ "This patch improves the world objects holder to determine objects that need ticking in advance, cache "
+ "the list of them, and only tick those, skipping all the others.";
= "Reuses the world-object tick snapshot until the source list changes. Every object still receives DoTick.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(WorldObjectsHolder), nameof(WorldObjectsHolder.WorldObjectsHolderTick));
@@ -38,7 +35,7 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
UpdateCache(instance);
var worldObjects = WorldObjectsHolder.tmpWorldObjects;
for (var i = worldObjects.Count; i-- > 0;)
for (var i = 0; i < worldObjects.Count; i++)
#if V1_6
worldObjects[i].DoTick();
#else
@@ -48,178 +45,40 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
public static void UpdateCache(WorldObjectsHolder instance)
{
var staticWorldObjects = WorldObjectsHolder.tmpWorldObjects;
staticWorldObjects.Clear();
var instanceWorldObjects = instance.worldObjects;
for (var i = instanceWorldObjects.Count; i-- > 0;)
{
var worldObject = instanceWorldObjects[i];
if (worldObject is not MapParent { HasMap: true }
&& CanSkipWorldObjectTick(worldObject)
&& (worldObject is not Settlement settlement || settlement.trader?.stock is null)
&& CanSkipCompTick(worldObject))
{
continue;
}
staticWorldObjects.Add(worldObject);
}
var snapshot = WorldObjectsHolder.tmpWorldObjects;
snapshot.Clear();
snapshot.AddRange(instanceWorldObjects);
CachedWorldObjectsVersion = instanceWorldObjects._version;
CachedMapsVersion = Current.gameInt.maps._version;
}
private static bool CanSkipWorldObjectTick(WorldObject worldObject)
{
return IsSkippableWorldObjectType(worldObject.GetType()) && !HasTickableContents(worldObject);
}
private static bool HasTickableContents(WorldObject worldObject)
{
if (worldObject is not IThingHolder holder)
return false;
lock (_tmpThingHolders)
{
_tmpThingHolders.Clear();
_tmpThingHolders.Add(holder);
holder.GetChildHolders(_tmpThingHolders);
for (var i = 0; i < _tmpThingHolders.Count; i++)
{
var thingHolder = _tmpThingHolders[i];
if (thingHolder is IThingHolderTickable tickable && !tickable.ShouldTickContents)
continue;
var directlyHeldThings = thingHolder.GetDirectlyHeldThings();
if (directlyHeldThings?.Owner is not null and not Map and not Caravan)
{
_tmpThingHolders.Clear();
return true;
}
}
_tmpThingHolders.Clear();
return false;
}
}
private static bool CanSkipCompTick(WorldObject worldObject)
{
var comps = worldObject.comps;
if (comps.Count == 0)
return true;
for (var i = comps.Count; i-- > 0;)
{
var comp = comps[i];
if (comp is EnterCooldownComp { Active: true })
return false;
if (!IsSkippableCompType(comp.GetType()))
return false;
}
return true;
_cachedWorldObjectsReference = instanceWorldObjects;
_cachedSnapshotReference = snapshot;
_cachedSnapshotVersion = snapshot._version;
}
public static int
CachedWorldObjectsVersion = -2,
CachedMapsVersion = -2;
private static Type?[]
_whitelistedTickingCompTypes =
[
typeof(WorldObjectComp)
],
_whitelistedWorldObjectTypes =
[
typeof(WorldObject),
typeof(MapParent),
typeof(Settlement),
ModCompatibility.Types.RealRuins.POIWorldObject
];
public static void AddCompToWhiteList(Type compType)
{
_whitelistedTickingCompTypes = _whitelistedTickingCompTypes.Add(compType);
ResetSkippableCompTypes();
}
public static void AddWorldObjectToWhiteList(Type worldObjectType)
{
_whitelistedWorldObjectTypes = _whitelistedWorldObjectTypes.Add(worldObjectType);
ResetSkippableWorldObjectTypes();
}
private static void ResetSkippableCompTypes()
{
_skippableCompTypes = new()
{
ValueInitializer = static type => ComputeIsSkippableCompType(type)
};
}
private static void ResetSkippableWorldObjectTypes()
{
_skippableWorldObjectTypes = new()
{
ValueInitializer = static type => ComputeIsSkippableWorldObjectType(type)
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsSkippableCompType(Type type) => _skippableCompTypes.GetOrAdd(type);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsSkippableWorldObjectType(Type type) => _skippableWorldObjectTypes.GetOrAdd(type);
private static bool ComputeIsSkippableCompType(Type type)
{
if (AccessTools.Method(type, nameof(WorldObjectComp.CompTick))?.DeclaringType is not { } declaringType
|| !_whitelistedTickingCompTypes.Contains(declaringType))
{
return false;
}
#if V1_6
return AccessTools.Method(type, nameof(WorldObjectComp.CompTickInterval), [typeof(int)])?.DeclaringType
is { } intervalDeclaringType
&& _whitelistedTickingCompTypes.Contains(intervalDeclaringType);
#else
return true;
#endif
}
private static bool ComputeIsSkippableWorldObjectType(Type type)
{
if (AccessTools.Method(type, "Tick")?.DeclaringType is not { } declaringType
|| !_whitelistedWorldObjectTypes.Contains(declaringType))
{
return false;
}
#if V1_6
return AccessTools.Method(type, "TickInterval", [typeof(int)])?.DeclaringType is { } intervalDeclaringType
&& _whitelistedWorldObjectTypes.Contains(intervalDeclaringType);
#else
return true;
#endif
}
// Retained for mods that called the old whitelist API. The snapshot no longer skips any ticks.
public static void AddCompToWhiteList(Type compType) => SetDirty();
public static void AddWorldObjectToWhiteList(Type worldObjectType) => SetDirty();
public static bool CacheDirty(WorldObjectsHolder instance)
=> CachedWorldObjectsVersion != instance.worldObjects._version
|| CachedMapsVersion != Current.gameInt.maps._version;
{
var source = instance.worldObjects;
var snapshot = WorldObjectsHolder.tmpWorldObjects;
return !ReferenceEquals(_cachedWorldObjectsReference, source)
|| CachedWorldObjectsVersion != source._version
|| !ReferenceEquals(_cachedSnapshotReference, snapshot)
|| _cachedSnapshotVersion != snapshot._version
|| snapshot.Count != source.Count;
}
public static void SetDirty() => CachedWorldObjectsVersion = CachedMapsVersion = -2;
static WorldObjectsHolderTickPatch()
{
ResetSkippableCompTypes();
ResetSkippableWorldObjectTypes();
Cache.Utility.Cleared += SetDirty;
}
}
@@ -35,6 +35,16 @@ public static class PrepatcherFields
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static extern List<Gene> GenesToTick(this Pawn_GeneTracker geneTracker);
#if V1_6
[PrepatcherField]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static extern ref GeneTrackerOptimization.GeneListVersionState GeneListVersions(this Pawn_GeneTracker geneTracker);
[PrepatcherField]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static extern ref CompTickOptimization.ThingTickCache ThingTickCache(this ThingWithComps thing);
#endif
[PrepatcherField]
[ValueInitializer(nameof(CreateWorldPawnsCache))]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -15,6 +15,8 @@ public sealed class PawnRenderingOptimizations : ClassWithFishPrepatches
public sealed class IsHiddenFromPlayerPatch : FishPrepatch
{
public override bool DefaultState => true;
// not 100% on this even benefitting, bionics, scars, etc likely worsen performance here because of the extra check
// probably better to just track if a hediff adds invisibility
public override string? Description { get; }
@@ -53,6 +55,8 @@ public sealed class PawnRenderingOptimizations : ClassWithFishPrepatches
public sealed class PawnRenderTreeComputeMatrixPatch : FishPrepatch
{
public override bool DefaultState => true;
public override string? Description { get; }
= "Avoids Matrix4x4 inverse work when undoing a pivot translation in PawnRenderTree.ComputeMatrix.";
@@ -85,6 +89,8 @@ public sealed class PawnRenderingOptimizations : ClassWithFishPrepatches
public sealed class DamageFlasherCurColorPatch : FishPrepatch
{
public override bool DefaultState => true;
private static readonly AccessTools.FieldRef<DamageFlasher, int> _lastDamageTick
= AccessTools.FieldRefAccess<DamageFlasher, int>("lastDamageTick");
@@ -110,6 +116,8 @@ public sealed class PawnRenderingOptimizations : ClassWithFishPrepatches
public sealed class DamageFlasherGetDamagedMatPatch : FishPrepatch
{
public override bool DefaultState => true;
private static readonly AccessTools.FieldRef<DamageFlasher, int> _lastDamageTick
= AccessTools.FieldRefAccess<DamageFlasher, int>("lastDamageTick");
@@ -1,52 +0,0 @@
// Copyright (c) 2023 bradson
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
namespace PerformanceFish.Rendering;
public sealed class TextureAtlasPatches : ClassWithFishPatches
{
public sealed class StaticTextureAtlas_ApplyTextureCompression : FishPatch
{
public override bool DefaultState => false;
public override string? Description { get; }
= "Routes texture atlas compression through the GPU path when compute shaders are available, "
+ "regardless of the VRAM threshold check in UnityData.ComputeShadersSupported. Prevents native "
+ "crashes in debug Mono builds.";
public override MethodBase TargetMethodInfo
=> AccessTools.DeclaredMethod(typeof(StaticTextureAtlas), "ApplyTextureCompression");
public static bool Prefix(StaticTextureAtlas __instance, bool noGpuCompressionSupport)
{
if (!noGpuCompressionSupport || !SystemInfo.supportsComputeShaders)
return true;
if (__instance.ColorTexture != null)
{
var name = __instance.ColorTexture.name;
var compressed = StaticTextureAtlas.FastCompressDXT(__instance.ColorTexture, deleteOriginal: true);
compressed.name = name;
_colorTextureField.SetValue(__instance, compressed);
}
if (__instance.MaskTexture != null)
{
var name = __instance.MaskTexture.name;
var compressed = StaticTextureAtlas.FastCompressDXT(__instance.MaskTexture, deleteOriginal: true);
compressed.name = name;
_maskTextureField.SetValue(__instance, compressed);
}
return false;
}
private static readonly FieldInfo _colorTextureField
= AccessTools.DeclaredField(typeof(StaticTextureAtlas), "colorTexture");
private static readonly FieldInfo _maskTextureField
= AccessTools.DeclaredField(typeof(StaticTextureAtlas), "maskTexture");
}
}
+161 -203
View File
@@ -522,10 +522,13 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
public sealed class GetCustomAttributes : FishPatch
{
private static readonly Type RuntimeTypeType = typeof(object).GetType();
public override bool DefaultState => true;
public override string? Description { get; }
= "Caches attributes for reflection lookups. Decent load time improvement.";
= "Caches empty attribute lookups for fixed metadata. Nonempty results are recreated so attribute "
+ "constructors and mutable attribute instances retain normal reflection behavior.";
public override Delegate? TargetMethodGroup { get; }
= (Func<ICustomAttributeProvider, Type, bool, object[]>)global::System.MonoCustomAttrs
@@ -535,6 +538,14 @@ public sealed class ReflectionCaching : ClassWithFishPatches
public static bool Prefix(ICustomAttributeProvider obj, Type attributeType, bool inherit,
out object[]? __result, out CustomAttributeState __state)
{
if (obj is null || attributeType is null || !HasFixedAttributeMetadata(obj)
|| attributeType.GetType() != RuntimeTypeType || attributeType.Assembly.IsDynamic)
{
__state = default;
__result = null;
return true;
}
var key = new CustomAttributeCache(obj, attributeType.TypeHandle, inherit);
__state = new() { Key = key };
@@ -561,10 +572,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
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;
@@ -574,7 +582,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
var centralLookup = TryUseCache(CustomAttributeCache.GetDirectly, ref key, out var centralResult);
if (centralLookup == CustomAttributeCacheLookup.Hit)
{
result = CopyCachedAttributes(centralResult!);
result = centralResult;
CustomAttributeCache.Get[key] = new()
{
Attributes = centralResult,
@@ -598,7 +606,10 @@ public sealed class ReflectionCaching : ClassWithFishPatches
if (!__state.ShouldStore || __result is null)
return;
StoreCompleted(ref __state.Key, __result);
if (__result.Length == 0)
StoreCompleted(ref __state.Key, __result);
else
ClearInProgress(ref __state.Key);
}
[MethodImpl(MethodImplOptions.NoInlining)]
@@ -656,7 +667,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
var value = new CustomAttributeCacheValue
{
Attributes = CopyCachedAttributes(result),
Attributes = result,
Status = CustomAttributeCacheStatus.Completed
};
@@ -691,9 +702,16 @@ 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 bool HasFixedAttributeMetadata(ICustomAttributeProvider provider)
=> provider.GetType().Assembly == typeof(object).Assembly && (provider switch
{
MemberInfo member => !member.Module.Assembly.IsDynamic,
ParameterInfo parameter => parameter.Member is MemberInfo member
&& !member.Module.Assembly.IsDynamic,
Assembly assembly => !assembly.IsDynamic,
Module module => !module.Assembly.IsDynamic,
_ => false
});
private static object _lock = new();
}
@@ -999,12 +1017,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches
public sealed class FieldInfoPatches
{
private static readonly Type? RuntimeFieldInfoType
= AccessTools.TypeByName("System.Reflection.RuntimeFieldInfo")
?? AccessTools.TypeByName("System.Reflection.MonoField");
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";
= "Uses cached getters for ordinary fields. Generic, dynamic and invalid calls use Mono reflection.";
//public GetValue_Patch()
//{
@@ -1024,9 +1046,12 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Prefix(FieldInfo __instance, object? obj, ref object? __result)
{
ValidateGetValueCall(__instance, obj);
if (__instance.GetType() != RuntimeFieldInfoType)
return true;
ref var cache = ref FieldGetters.GetOrAddReference(__instance.FieldHandle);
if (!CanUseFieldFastPath(__instance, obj, ref cache.FastPath))
return true;
if (cache.Dirty
&& !FieldGetters.UpdateAsyncCache<FieldInfoGetterCache, FieldInfo, FieldGetter>(ref cache,
@@ -1038,38 +1063,14 @@ 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 bool DefaultState => true;
public override string Description { get; }
= "Optimizes the SetValue method by invoking it through specialized cached delegates";
= "Uses cached setters only for ordinary fields and already-compatible values. Other calls use Mono reflection.";
public override MethodBase? TargetMethodInfo { get; }
= AccessTools.DeclaredMethod(typeof(FieldInfo), nameof(FieldInfo.SetValue),
@@ -1078,7 +1079,13 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Prefix(FieldInfo __instance, object? obj, object? value)
{
if (__instance.GetType() != RuntimeFieldInfoType)
return true;
ref var cache = ref FieldSetters.GetOrAddReference(__instance.FieldHandle);
if (!CanUseFieldFastPath(__instance, obj, ref cache.FastPath, forSetter: true)
|| !CanSetFieldExactly(ref cache.FastPath, value))
return true;
if (cache.Dirty && !TryUpdate(__instance, ref cache))
return true;
@@ -1090,29 +1097,67 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool TryUpdate(FieldInfo __instance, ref FieldInfoSetterCache 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 "
+ $"setting a value will always fail.\n{new StackTrace(2, true)}");
return false;
}
}
private static Task<FieldSetter> _fieldSetterFailureTask = new(static () => null!); // sentinel only
public struct FieldFastPath
{
public bool Checked;
public bool Unsupported;
public bool IsStatic;
public bool IsValueType;
public Type? DeclaringType;
public Type? FieldType;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanUseFieldFastPath(FieldInfo info, object? obj, ref FieldFastPath state,
bool forSetter = false)
{
if (!state.Checked)
InitializeFieldFastPath(info, ref state, forSetter);
if (state.Unsupported)
return false;
if (state.IsStatic)
return true;
return obj is not null && obj is not MarshalByRefObject
&& (obj.GetType() == state.DeclaringType || state.DeclaringType!.IsInstanceOfType(obj));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanSetFieldExactly(ref FieldFastPath state, object? value)
{
if (value is null)
return !state.IsValueType;
var fieldType = state.FieldType!;
var valueType = value.GetType();
return valueType == fieldType || !state.IsValueType && fieldType.IsAssignableFrom(valueType);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void InitializeFieldFastPath(FieldInfo info, ref FieldFastPath state, bool forSetter)
{
var declaringType = info.DeclaringType;
var fieldType = info.FieldType;
state.DeclaringType = declaringType;
state.FieldType = fieldType;
state.IsStatic = info.IsStatic;
state.IsValueType = fieldType.IsValueType;
// Constructed generic fields can share a RuntimeFieldHandle but need different delegates.
state.Unsupported = declaringType is null || declaringType.IsGenericType
|| declaringType.ContainsGenericParameters
|| typeof(MarshalByRefObject).IsAssignableFrom(declaringType)
|| info.Module.Assembly.IsDynamic || info.GetRequiredCustomModifiers().Length != 0
|| info.IsLiteral || forSetter && info.IsInitOnly || fieldType.IsPointer
|| fieldType.IsByRef || fieldType.ContainsGenericParameters;
state.Checked = true;
}
public static FieldGetter? MakeGetterDelegate(FieldInfo info)
{
@@ -1197,6 +1242,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
public FieldGetter? Result { get; set; }
public Task<FieldGetter>? Task { get; set; }
public FieldFastPath FastPath;
public bool Dirty => Result == null;
@@ -1208,6 +1254,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
public FieldSetter? Result { get; set; }
public Task<FieldSetter>? Task { get; set; }
public FieldFastPath FastPath;
public bool Dirty => Result == null;
@@ -1223,13 +1270,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches
public sealed class MethodBasePatches
{
private static readonly Type? RuntimeMethodInfoType
= AccessTools.TypeByName("System.Reflection.RuntimeMethodInfo")
?? AccessTools.TypeByName("System.Reflection.MonoMethod");
public sealed class Invoke_Patch : FishPatch
{
public override bool DefaultState => false; // TODO: fix
public override bool DefaultState => true;
public override string Description { get; }
= "Optimizes the Invoke method by compiling specialized cached delegates. Disabled by default due to "
+ "issues with various mods, like RIMMSqol and Performance Optimizer";
= "Uses cached delegates for static, parameterless methods with fixed metadata. Other calls use Mono reflection.";
public override MethodBase? TargetMethodInfo { get; }
= AccessTools.DeclaredMethod(typeof(MethodBase), nameof(MethodBase.Invoke),
@@ -1238,22 +1288,21 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Prefix(MethodBase __instance, object? obj, object[]? parameters, ref object? __result)
{
ref var cache = ref MethodInvokers.GetOrAddReference(__instance.MethodHandle);
if (__instance is not MethodInfo methodInfo
|| methodInfo.GetType() != RuntimeMethodInfoType || parameters is { Length: > 0 })
return true;
// if (cache.Dirty) // TODO: remove
// {
// if (OnStartup.State.Initialized)
// {
// Log.Message($"Called Invoke for {__instance.FullDescription()} with {obj.ToStringSafe()}, {
// parameters?.Length.ToStringSafe() ?? "null"} parameters");
// }
//
// if (!MethodInvokers.UpdateAsyncCache<MethodInvokerCache, MethodBase, MethodInvoker>(ref cache,
// __instance.MethodHandle, __instance))
// {
// return true;
// }
// }
ref var cache = ref MethodInvokers.GetOrAddReference(__instance.MethodHandle);
if (cache.Unsupported)
return true;
if (!cache.EligibilityChecked)
{
cache.EligibilityChecked = true;
cache.Unsupported = !CanCacheInvoke(methodInfo);
if (cache.Unsupported)
return true;
}
if (cache.Dirty
&& !MethodInvokers.UpdateAsyncCache<MethodInvokerCache, MethodBase, MethodInvoker>(ref cache,
@@ -1262,58 +1311,31 @@ public sealed class ReflectionCaching : ClassWithFishPatches
return true;
}
__result = cache.Result!(obj, parameters);
try
{
__result = cache.Result!(obj, parameters);
}
catch (Exception ex)
{
throw new TargetInvocationException(ex);
}
return false;
}
}
// Similar to HarmonyLib.MethodInvoker, but correctly handles a couple of edge cases Harmony would throw on.
[SuppressMessage("Globalization", "CA1310")]
// Restrict the delegate to a method whose reflection behavior can be reproduced cheaply.
public static MethodInvoker? MakeInvokeDelegate(MethodBase methodBase)
{
if (methodBase.DeclaringType is null && methodBase.Name.StartsWith("PadMethod"))
return null; // padding methods generated by harmony for linux
if (methodBase is not MethodInfo methodInfo || !CanCacheInvoke(methodInfo))
return null;
try
{
var methodInfo = methodBase as MethodInfo;
if (methodInfo is { ReturnType.IsByRef: true })
return null; // how to box a ref return?
var constructorInfo = methodBase as ConstructorInfo;
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();
if (methodInfo != null && !methodBase.IsStatic)
EmitInstanceArgument(il, methodBase);
for (var i = 0; i < parameters.Length; i++)
{
il.Emit(FishTranspiler.Argument(1));
il.Emit(FishTranspiler.Constant(i));
var parameterType = parameters[i].ParameterType;
if (parameterType.IsByRef)
EmitInvokeByRefParameterLoad(il, parameterType);
else
EmitInvokeParameterLoad(il, parameterType);
}
if (methodInfo != null)
EmitInvokeMethodCall(methodBase, il, methodInfo);
else if (constructorInfo != null)
il.Emit(FishTranspiler.New(constructorInfo));
EmitInvokeMethodCall(methodInfo, il);
il.Emit(FishTranspiler.Return);
@@ -1328,109 +1350,45 @@ public sealed class ReflectionCaching : ClassWithFishPatches
}
}
private static bool CanCacheInvoke(MethodBase methodBase, MethodInfo? methodInfo,
ConstructorInfo? constructorInfo, ParameterInfo[] parameters)
private static bool CanCacheInvoke(MethodInfo methodInfo)
{
if (methodBase.ContainsGenericParameters
|| (methodBase.CallingConvention & CallingConventions.VarArgs) != 0)
try
{
var declaringType = methodInfo.DeclaringType;
var returnType = methodInfo.ReturnType;
// Constructed generic methods and types need more than a RuntimeMethodHandle as a key.
return methodInfo.IsStatic && !methodInfo.IsGenericMethod
&& declaringType is not null && !declaringType.IsGenericType
&& !declaringType.ContainsGenericParameters
&& !methodInfo.Module.Assembly.IsDynamic
&& methodInfo.GetParameters().Length == 0
&& !methodInfo.ContainsGenericParameters
&& (methodInfo.CallingConvention & CallingConventions.VarArgs) == 0
&& !returnType.IsByRef && !returnType.IsPointer
&& !returnType.ContainsGenericParameters
&& methodInfo.GetMethodBody() is not null;
}
catch (Exception)
{
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)
private static void EmitInvokeMethodCall(MethodInfo methodInfo, ILGenerator il)
{
// 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));
if (!methodInfo.ReturnType.IsValueType)
return;
il.Emit(methodInfo.ReturnType == typeof(void)
? FishTranspiler.Null
: FishTranspiler.Box(methodInfo.ReturnType));
}
private static void EmitInvokeParameterLoad(ILGenerator il, Type parameterType)
{
il.Emit(FishTranspiler.LoadElement<object>());
il.Emit(parameterType.IsValueType
? FishTranspiler.Call(typeof(ReflectionCaching),
parameterType.IsNullable() ? nameof(UnboxNullableSafely) : nameof(UnboxSafely),
[typeof(object)],
[
parameterType.IsNullable()
? Nullable.GetUnderlyingType(parameterType)
: parameterType
])
: FishTranspiler.Call(typeof(ReflectionCaching), nameof(CastOrConvert),
generics: [parameterType]));
}
private static void EmitInvokeByRefParameterLoad(ILGenerator il, Type parameterType)
{
parameterType = parameterType.GetElementType()!;
il.Emit(FishTranspiler.LoadElementAddress<object>());
if (parameterType.IsNullable())
{
il.Emit(FishTranspiler.Call(typeof(ReflectionCaching), nameof(VerifyNullableBoxObject),
generics: [Nullable.GetUnderlyingType(parameterType)]));
il.Emit(FishTranspiler.UnboxAddress(parameterType));
}
else
{
il.Emit(parameterType.IsValueType
? FishTranspiler.Call(typeof(ReflectionCaching), nameof(UnboxRefSafely),
generics:
[
/*parameterType.IsNullable()
? Nullable.GetUnderlyingType(parameterType)
:*/ parameterType
])
: FishTranspiler.Call(typeof(ReflectionCaching), nameof(CastRefSafely), generics: [parameterType]));
}
il.Emit(FishTranspiler.Call(methodInfo));
if (methodInfo.ReturnType == typeof(void))
il.Emit(FishTranspiler.Null);
else if (methodInfo.ReturnType.IsValueType)
il.Emit(FishTranspiler.Box(methodInfo.ReturnType));
}
public record struct MethodInvokerCache : IAsyncCacheable<RuntimeMethodHandle, MethodBase, MethodInvoker>
{
public MethodInvoker? Result { get; set; }
public Task<MethodInvoker>? Task { get; set; }
public bool EligibilityChecked;
public bool Unsupported;
public bool Dirty => Result == null;
public ValueTask<MethodInvoker?> MakeResultAsync(RuntimeMethodHandle key, MethodBase method)