diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-05-31 18:32:23 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-05-31 18:32:23 -0400 |
commit | 9992915f565578949cad8d9bb8ceb360e0db5c85 (patch) | |
tree | 1ff34ba24733fcdf44f52fb8ee10b2a956f1a708 | |
parent | 9ef3f7edb1589a52794c7da7075996d4a02de6e7 (diff) | |
download | SMAPI-9992915f565578949cad8d9bb8ceb360e0db5c85.tar.gz SMAPI-9992915f565578949cad8d9bb8ceb360e0db5c85.tar.bz2 SMAPI-9992915f565578949cad8d9bb8ceb360e0db5c85.zip |
replace MemoryCache with custom cache
This was causing significant frame stutters for some players since the migration to .NET 5 in Stardew Valley 1.5.5.
-rw-r--r-- | build/common.targets | 3 | ||||
-rwxr-xr-x | build/unix/prepare-install-package.sh | 4 | ||||
-rw-r--r-- | build/windows/prepare-install-package.ps1 | 4 | ||||
-rw-r--r-- | docs/release-notes.md | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/CacheEntry.cs | 30 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/Reflector.cs | 110 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs | 57 | ||||
-rw-r--r-- | src/SMAPI/SMAPI.csproj | 1 |
9 files changed, 119 insertions, 94 deletions
diff --git a/build/common.targets b/build/common.targets index 2e37e729..b2441af8 100644 --- a/build/common.targets +++ b/build/common.targets @@ -69,10 +69,7 @@ <Copy SourceFiles="$(TargetDir)\MonoMod.Common.dll" DestinationFolder="$(GamePath)\smapi-internal" /> <!-- .NET dependencies --> - <Copy SourceFiles="$(TargetDir)\System.Configuration.ConfigurationManager.dll" DestinationFolder="$(GamePath)\smapi-internal" /> <Copy SourceFiles="$(TargetDir)\System.Management.dll" DestinationFolder="$(GamePath)\smapi-internal" Condition="$(OS) == 'Windows_NT'" /> - <Copy SourceFiles="$(TargetDir)\System.Runtime.Caching.dll" DestinationFolder="$(GamePath)\smapi-internal" /> - <Copy SourceFiles="$(TargetDir)\System.Security.Permissions.dll" DestinationFolder="$(GamePath)\smapi-internal" /> </Target> <Target Name="CopyDefaultMods" Condition="'$(MSBuildProjectName)' == 'SMAPI.Mods.ConsoleCommands' OR '$(MSBuildProjectName)' == 'SMAPI.Mods.ErrorHandler' OR '$(MSBuildProjectName)' == 'SMAPI.Mods.SaveBackup'"> diff --git a/build/unix/prepare-install-package.sh b/build/unix/prepare-install-package.sh index 9b195f37..01c3a0ec 100755 --- a/build/unix/prepare-install-package.sh +++ b/build/unix/prepare-install-package.sh @@ -142,15 +142,11 @@ for folder in ${folders[@]}; do cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json" if [ $folder == "linux" ] || [ $folder == "macOS" ]; then cp "$installAssets/unix-launcher.sh" "$bundlePath" - cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal" else cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config" fi # copy .NET dependencies - cp "$smapiBin/System.Configuration.ConfigurationManager.dll" "$bundlePath/smapi-internal" - cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal" - cp "$smapiBin/System.Security.Permissions.dll" "$bundlePath/smapi-internal" if [ $folder == "windows" ]; then cp "$smapiBin/System.Management.dll" "$bundlePath/smapi-internal" fi diff --git a/build/windows/prepare-install-package.ps1 b/build/windows/prepare-install-package.ps1 index 5e116019..6731486b 100644 --- a/build/windows/prepare-install-package.ps1 +++ b/build/windows/prepare-install-package.ps1 @@ -162,16 +162,12 @@ foreach ($folder in $folders) { cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json" if ($folder -eq "linux" -or $folder -eq "macOS") { cp "$installAssets/unix-launcher.sh" "$bundlePath" - cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal" } else { cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config" } # copy .NET dependencies - cp "$smapiBin/System.Configuration.ConfigurationManager.dll" "$bundlePath/smapi-internal" - cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal" - cp "$smapiBin/System.Security.Permissions.dll" "$bundlePath/smapi-internal" if ($folder -eq "windows") { cp "$smapiBin/System.Management.dll" "$bundlePath/smapi-internal" } diff --git a/docs/release-notes.md b/docs/release-notes.md index cdc0f548..31f6edc2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ # Release notes ## Upcoming release +* For players: + * Optimized reflection cache to reduce frame skips for some players. * For mod authors: * Removed `runtimeconfig.json` setting which impacted hot reload support. diff --git a/src/SMAPI/Framework/Reflection/CacheEntry.cs b/src/SMAPI/Framework/Reflection/CacheEntry.cs deleted file mode 100644 index 27f48a1f..00000000 --- a/src/SMAPI/Framework/Reflection/CacheEntry.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// <summary>A cached member reflection result.</summary> - internal readonly struct CacheEntry - { - /********* - ** Accessors - *********/ - /// <summary>Whether the lookup found a valid match.</summary> - [MemberNotNullWhen(true, nameof(CacheEntry.MemberInfo))] - public bool IsValid => this.MemberInfo != null; - - /// <summary>The reflection data for this member (or <c>null</c> if invalid).</summary> - public MemberInfo? MemberInfo { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="memberInfo">The reflection data for this member (or <c>null</c> if invalid).</param> - public CacheEntry(MemberInfo? memberInfo) - { - this.MemberInfo = memberInfo; - } - } -} diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs index 79575c26..502a8519 100644 --- a/src/SMAPI/Framework/Reflection/Reflector.cs +++ b/src/SMAPI/Framework/Reflection/Reflector.cs @@ -1,6 +1,6 @@ using System; using System.Reflection; -using System.Runtime.Caching; +using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.Reflection { @@ -12,10 +12,7 @@ namespace StardewModdingAPI.Framework.Reflection ** Fields *********/ /// <summary>The cached fields and methods found via reflection.</summary> - private readonly MemoryCache Cache = new(typeof(Reflector).FullName!); - - /// <summary>The sliding cache expiration time.</summary> - private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5); + private readonly IntervalMemoryCache<string, MemberInfo?> Cache = new(); /********* @@ -136,6 +133,15 @@ namespace StardewModdingAPI.Framework.Reflection return method!; } + /**** + ** Management + ****/ + /// <summary>Start a new cache interval, clearing stale reflection lookups.</summary> + public void NewCacheInterval() + { + this.Cache.StartNewInterval(); + } + /********* ** Private methods @@ -149,20 +155,23 @@ namespace StardewModdingAPI.Framework.Reflection private IReflectedField<TValue>? GetFieldFromHierarchy<TValue>(Type type, object? obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - FieldInfo? field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () => - { - for (Type? curType = type; curType != null; curType = curType.BaseType) + FieldInfo? field = this.GetCached( + 'f', type, name, isStatic, + fetch: () => { - FieldInfo? fieldInfo = curType.GetField(name, bindingFlags); - if (fieldInfo != null) + for (Type? curType = type; curType != null; curType = curType.BaseType) { - type = curType; - return fieldInfo; + FieldInfo? fieldInfo = curType.GetField(name, bindingFlags); + if (fieldInfo != null) + { + type = curType; + return fieldInfo; + } } - } - return null; - }); + return null; + } + ); return field != null ? new ReflectedField<TValue>(type, obj, field, isStatic) @@ -178,20 +187,23 @@ namespace StardewModdingAPI.Framework.Reflection private IReflectedProperty<TValue>? GetPropertyFromHierarchy<TValue>(Type type, object? obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - PropertyInfo? property = this.GetCached<PropertyInfo>($"property::{isStatic}::{type.FullName}::{name}", () => - { - for (Type? curType = type; curType != null; curType = curType.BaseType) + PropertyInfo? property = this.GetCached( + 'p', type, name, isStatic, + fetch: () => { - PropertyInfo? propertyInfo = curType.GetProperty(name, bindingFlags); - if (propertyInfo != null) + for (Type? curType = type; curType != null; curType = curType.BaseType) { - type = curType; - return propertyInfo; + PropertyInfo? propertyInfo = curType.GetProperty(name, bindingFlags); + if (propertyInfo != null) + { + type = curType; + return propertyInfo; + } } - } - return null; - }); + return null; + } + ); return property != null ? new ReflectedProperty<TValue>(type, obj, property, isStatic) @@ -206,47 +218,41 @@ namespace StardewModdingAPI.Framework.Reflection private IReflectedMethod? GetMethodFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - MethodInfo? method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => - { - for (Type? curType = type; curType != null; curType = curType.BaseType) + MethodInfo? method = this.GetCached( + 'm', type, name, isStatic, + fetch: () => { - MethodInfo? methodInfo = curType.GetMethod(name, bindingFlags); - if (methodInfo != null) + for (Type? curType = type; curType != null; curType = curType.BaseType) { - type = curType; - return methodInfo; + MethodInfo? methodInfo = curType.GetMethod(name, bindingFlags); + if (methodInfo != null) + { + type = curType; + return methodInfo; + } } - } - return null; - }); + return null; + } + ); return method != null - ? new ReflectedMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + ? new ReflectedMethod(type, obj, method, isStatic: isStatic) : null; } /// <summary>Get a method or field through the cache.</summary> /// <typeparam name="TMemberInfo">The expected <see cref="MemberInfo"/> type.</typeparam> - /// <param name="key">The cache key.</param> + /// <param name="memberType">A letter representing the member type (like 'm' for method).</param> + /// <param name="type">The type whose members are being reflected.</param> + /// <param name="memberName">The member name.</param> + /// <param name="isStatic">Whether the member is static.</param> /// <param name="fetch">Fetches a new value to cache.</param> - private TMemberInfo? GetCached<TMemberInfo>(string key, Func<TMemberInfo?> fetch) + private TMemberInfo? GetCached<TMemberInfo>(char memberType, Type type, string memberName, bool isStatic, Func<TMemberInfo?> fetch) where TMemberInfo : MemberInfo { - // get from cache - if (this.Cache.Contains(key)) - { - CacheEntry entry = (CacheEntry)this.Cache[key]; - return entry.IsValid - ? (TMemberInfo)entry.MemberInfo - : default; - } - - // fetch & cache new value - TMemberInfo? result = fetch(); - CacheEntry cacheEntry = new(result); - this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry }); - return result; + string key = $"{memberType}{(isStatic ? 's' : 'i')}{type.FullName}:{memberName}"; + return (TMemberInfo?)this.Cache.GetOrSet(key, fetch); } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 67f78400..c453562f 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1164,6 +1164,8 @@ namespace StardewModdingAPI.Framework protected void OnNewDayAfterFade() { this.EventManager.DayEnding.RaiseEmpty(); + + this.Reflection.NewCacheInterval(); } /// <summary>A callback invoked after an asset is fully loaded through a content manager.</summary> diff --git a/src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs b/src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs new file mode 100644 index 00000000..d2b69f51 --- /dev/null +++ b/src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Utilities +{ + /// <summary>A memory cache with sliding expiry based on custom intervals, with no background processing.</summary> + /// <typeparam name="TKey">The cache key type.</typeparam> + /// <typeparam name="TValue">The cache value type.</typeparam> + /// <remarks>This is optimized for small caches that are reset relatively rarely. Each cache entry is marked as hot (accessed since the interval started) or stale. + /// When a new interval is started, stale entries are cleared and hot entries become stale.</remarks> + internal class IntervalMemoryCache<TKey, TValue> + where TKey : notnull + { + /********* + ** Fields + *********/ + /// <summary>The cached values that were accessed during the current interval.</summary> + private Dictionary<TKey, TValue> HotCache = new(); + + /// <summary>The cached values that will expire on the next interval.</summary> + private Dictionary<TKey, TValue> StaleCache = new(); + + + /********* + ** Public methods + *********/ + /// <summary>Get a value from the cache, fetching it first if needed.</summary> + /// <param name="cacheKey">The unique key for the cached value.</param> + /// <param name="get">Get the latest data if it's not in the cache yet.</param> + public TValue GetOrSet(TKey cacheKey, Func<TValue> get) + { + // from hot cache + if (this.HotCache.TryGetValue(cacheKey, out TValue? value)) + return value; + + // from stale cache + if (this.StaleCache.TryGetValue(cacheKey, out value)) + { + this.HotCache[cacheKey] = value; + return value; + } + + // new value + value = get(); + this.HotCache[cacheKey] = value; + return value; + } + + /// <summary>Start a new cache interval, removing any stale entries.</summary> + public void StartNewInterval() + { + this.StaleCache.Clear(); + if (this.HotCache.Count is not 0) + (this.StaleCache, this.HotCache) = (this.HotCache, this.StaleCache); // swap hot cache to stale + } + } +} diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 27044679..95249bfd 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -28,7 +28,6 @@ <PackageReference Include="Pintail" Version="2.1.0" /> <PackageReference Include="Platonymous.TMXTile" Version="1.5.9" /> <PackageReference Include="System.Reflection.Emit" Version="4.7.0" /> - <PackageReference Include="System.Runtime.Caching" Version="5.0.0" /> </ItemGroup> <ItemGroup> |