Decompiled source of MemoryJanitor v1.3.0

plugins/MemoryJanitor.dll

Decompiled 5 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Profiling;
using UnityEngine.SceneManagement;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("MemoryJanitor")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.3.0.0")]
[assembly: AssemblyInformationalVersion("1.3.0")]
[assembly: AssemblyProduct("MemoryJanitor")]
[assembly: AssemblyTitle("MemoryJanitor")]
[assembly: AssemblyVersion("1.3.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace MemoryJanitor
{
	[BepInPlugin("lcmemfix.MemoryJanitor", "MemoryJanitor", "1.3.0")]
	public class Plugin : BaseUnityPlugin
	{
		public const string Guid = "lcmemfix.MemoryJanitor";

		internal static ManualLogSource Log;

		internal static Runner Host;

		internal static ConfigEntry<bool> UnloadAudio;

		internal static ConfigEntry<float> MinClipSeconds;

		internal static ConfigEntry<bool> IncludePreloaded;

		internal static ConfigEntry<float> SweepSeconds;

		internal static ConfigEntry<float> IdleMinutes;

		internal static ConfigEntry<bool> UnloadUnusedAssets;

		internal static ConfigEntry<float> DelaySeconds;

		internal static ConfigEntry<bool> Reports;

		internal static ConfigEntry<int> MipmapLimit;

		private void Awake()
		{
			//IL_013d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0147: Expected O, but got Unknown
			//IL_0156: Unknown result type (might be due to invalid IL or missing references)
			//IL_015b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0161: Expected O, but got Unknown
			//IL_0161: Unknown result type (might be due to invalid IL or missing references)
			//IL_0178: Unknown result type (might be due to invalid IL or missing references)
			Log = ((BaseUnityPlugin)this).Logger;
			UnloadAudio = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio", "UnloadIdleDecompressedAudio", true, "When the ship is back in orbit, free the decompressed sample data of Decompress-On-Load clips that are not playing. Unity reloads a clip the next time it is played.");
			MinClipSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "MinClipSeconds", 3f, "Only unload clips at least this long. Short clips cost little and reloading them often is wasteful.");
			IncludePreloaded = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio", "IncludePreloadedClips", false, "Also unload clips marked 'Preload Audio Data'. Frees more, but those clips may stutter once when first replayed.");
			SweepSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "SweepSeconds", 30f, "Also free idle Decompress-On-Load clips every this many seconds, not just in orbit. 0 = orbit only. Only the cheap audio pass runs this often; the full unused-asset unload freezes the game for a moment, so it stays in orbit.");
			IdleMinutes = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "IdleMinutes", 2f, "In the periodic sweep, only free clips that haven't been heard playing for this long, so sounds in use don't reload.");
			UnloadUnusedAssets = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "UnloadUnusedAssets", true, "Run Resources.UnloadUnusedAssets and a GC after the audio pass, to drop anything left behind by the last moon.");
			DelaySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "DelaySeconds", 4f, "Seconds to wait after reaching orbit before cleaning, so the moon scene has finished unloading.");
			Reports = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "MemoryReports", false, "Write a memory breakdown to the log at the main menu, after each orbit cleanup and 40 s after each landing.");
			MipmapLimit = ((BaseUnityPlugin)this).Config.Bind<int>("Textures", "MipmapLimit", 1, new ConfigDescription("Skip this many of the largest mip levels on every mipmapped texture. Lethal Company renders the world at 620x364, so 1 (half resolution, about 75% less texture memory) looks the same; 2 saves more. 0 = off.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3), Array.Empty<object>()));
			ApplyMipmapLimit();
			GameObject val = new GameObject("MemoryJanitorRunner");
			Object.DontDestroyOnLoad((Object)val);
			((Object)val).hideFlags = (HideFlags)61;
			Host = val.AddComponent<Runner>();
			new Harmony("lcmemfix.MemoryJanitor").PatchAll(typeof(Patches));
			Log.LogInfo((object)"MemoryJanitor loaded");
		}

		internal static void ApplyMipmapLimit()
		{
			if (MipmapLimit.Value > 0 && QualitySettings.globalTextureMipmapLimit != MipmapLimit.Value)
			{
				QualitySettings.globalTextureMipmapLimit = MipmapLimit.Value;
				Log.LogInfo((object)$"Texture mipmap limit set to {MipmapLimit.Value}");
			}
		}
	}
	internal class Runner : MonoBehaviour
	{
		private static readonly HashSet<string> NotMoons = new HashSet<string> { "SampleSceneRelay", "InitScene", "InitSceneLaunchOptions", "InitSceneLANMode", "MainMenu", "ColdOpen1", "CompanyBuilding" };

		private bool menuReported;

		private float nextSweep;

		private readonly Dictionary<int, float> lastHeard = new Dictionary<int, float>();

		private void Update()
		{
			float value = Plugin.SweepSeconds.Value;
			if (!(value <= 0f) && Plugin.UnloadAudio.Value && !(Time.realtimeSinceStartup < nextSweep))
			{
				nextSweep = Time.realtimeSinceStartup + value;
				Sweep();
			}
		}

		private void Sweep()
		{
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			//IL_009e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a4: Invalid comparison between Unknown and I4
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			float num = Plugin.IdleMinutes.Value * 60f;
			AudioSource[] array = Object.FindObjectsOfType<AudioSource>();
			foreach (AudioSource val in array)
			{
				if ((Object)(object)val.clip != (Object)null && val.isPlaying)
				{
					lastHeard[((Object)val.clip).GetInstanceID()] = realtimeSinceStartup;
				}
			}
			int num2 = 0;
			long num3 = 0L;
			AudioClip[] array2 = Resources.FindObjectsOfTypeAll<AudioClip>();
			foreach (AudioClip val2 in array2)
			{
				if ((Object)(object)val2 == (Object)null || (int)val2.loadType != 0 || (int)val2.loadState != 2 || val2.length < Plugin.MinClipSeconds.Value || (val2.preloadAudioData && !Plugin.IncludePreloaded.Value))
				{
					continue;
				}
				int instanceID = ((Object)val2).GetInstanceID();
				if (!lastHeard.TryGetValue(instanceID, out var value))
				{
					lastHeard[instanceID] = realtimeSinceStartup;
				}
				else if (!(realtimeSinceStartup - value < num))
				{
					long num4 = (long)val2.samples * (long)val2.channels * 2;
					if (val2.UnloadAudioData())
					{
						num2++;
						num3 += num4;
						lastHeard.Remove(instanceID);
					}
				}
			}
			if (num2 > 0)
			{
				Plugin.Log.LogInfo((object)$"Sweep: freed {num2} idle audio clips (~{num3 / 1048576} MB)");
			}
		}

		private void OnEnable()
		{
			SceneManager.sceneLoaded += OnSceneLoaded;
		}

		private void OnDisable()
		{
			SceneManager.sceneLoaded -= OnSceneLoaded;
		}

		private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
		{
			//IL_0060: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Invalid comparison between Unknown and I4
			Plugin.ApplyMipmapLimit();
			if (!Plugin.Reports.Value)
			{
				return;
			}
			if (((Scene)(ref scene)).name == "MainMenu")
			{
				if (!menuReported)
				{
					menuReported = true;
					((MonoBehaviour)this).StartCoroutine(ReportAfter(20f, "main menu"));
				}
			}
			else if (!NotMoons.Contains(((Scene)(ref scene)).name) && (int)mode == 1)
			{
				((MonoBehaviour)this).StartCoroutine(ReportAfter(40f, "landed on " + ((Scene)(ref scene)).name));
			}
		}

		private IEnumerator ReportAfter(float seconds, string label)
		{
			yield return (object)new WaitForSecondsRealtime(seconds);
			Report.Write(label);
		}

		internal IEnumerator CleanAfterRound()
		{
			yield return (object)new WaitForSeconds(Plugin.DelaySeconds.Value);
			long before = Report.PrivateBytes();
			int clips = 0;
			long freed = 0L;
			if (Plugin.UnloadAudio.Value)
			{
				HashSet<AudioClip> hashSet = new HashSet<AudioClip>();
				AudioSource[] array = Resources.FindObjectsOfTypeAll<AudioSource>();
				foreach (AudioSource val in array)
				{
					if ((Object)(object)val != (Object)null && (Object)(object)val.clip != (Object)null && val.isPlaying)
					{
						hashSet.Add(val.clip);
					}
				}
				AudioClip[] array2 = Resources.FindObjectsOfTypeAll<AudioClip>();
				foreach (AudioClip val2 in array2)
				{
					if (!((Object)(object)val2 == (Object)null) && !hashSet.Contains(val2) && (int)val2.loadType == 0 && (int)val2.loadState == 2 && !(val2.length < Plugin.MinClipSeconds.Value) && (!val2.preloadAudioData || Plugin.IncludePreloaded.Value))
					{
						long num = (long)val2.samples * (long)val2.channels * 2;
						if (val2.UnloadAudioData())
						{
							clips++;
							freed += num;
						}
					}
				}
			}
			if (Plugin.UnloadUnusedAssets.Value)
			{
				yield return Resources.UnloadUnusedAssets();
				GC.Collect();
			}
			long num2 = Report.PrivateBytes();
			Plugin.Log.LogInfo((object)($"Orbit cleanup: unloaded {clips} idle audio clips (~{freed / 1048576} MB of samples). " + $"Private bytes {before / 1048576} MB -> {num2 / 1048576} MB"));
			if (Plugin.Reports.Value)
			{
				Report.Write("orbit");
			}
		}
	}
	internal static class Report
	{
		private struct ProcessMemoryCounters
		{
			public uint cb;

			public uint PageFaultCount;

			public ulong PeakWorkingSetSize;

			public ulong WorkingSetSize;

			public ulong QuotaPeakPagedPoolUsage;

			public ulong QuotaPagedPoolUsage;

			public ulong QuotaPeakNonPagedPoolUsage;

			public ulong QuotaNonPagedPoolUsage;

			public ulong PagefileUsage;

			public ulong PeakPagefileUsage;

			public ulong PrivateUsage;
		}

		internal const long MB = 1048576L;

		[DllImport("kernel32.dll")]
		private static extern IntPtr GetCurrentProcess();

		[DllImport("kernel32.dll", EntryPoint = "K32GetProcessMemoryInfo")]
		private static extern bool GetProcessMemoryInfo(IntPtr process, out ProcessMemoryCounters counters, uint size);

		internal static long PrivateBytes()
		{
			try
			{
				ProcessMemoryCounters counters = new ProcessMemoryCounters
				{
					cb = (uint)Marshal.SizeOf<ProcessMemoryCounters>()
				};
				return (long)(GetProcessMemoryInfo(GetCurrentProcess(), out counters, counters.cb) ? counters.PrivateUsage : 0);
			}
			catch
			{
				return 0L;
			}
		}

		internal static void Write(string label)
		{
			Plugin.Log.LogInfo((object)($"[Report:{label}] private={PrivateBytes() / 1048576} MB, unity allocated={Profiler.GetTotalAllocatedMemoryLong() / 1048576} MB, " + $"reserved={Profiler.GetTotalReservedMemoryLong() / 1048576} MB, mono={Profiler.GetMonoUsedSizeLong() / 1048576} MB, " + $"gfx={Profiler.GetAllocatedMemoryForGraphicsDriver() / 1048576} MB, mip limit={QualitySettings.globalTextureMipmapLimit}"));
			int num = 0;
			foreach (AssetBundle allLoadedAssetBundle in AssetBundle.GetAllLoadedAssetBundles())
			{
				_ = allLoadedAssetBundle;
				num++;
			}
			Plugin.Log.LogInfo((object)$"[Report:{label}] loaded asset bundles: {num}");
			Type<Texture2D>(label, (Func<Texture2D, string>)((Texture2D t) => ((Texture)t).width + "x" + ((Texture)t).height + " " + ((object)t.format/*cast due to .constrained prefix*/).ToString() + (((Texture)t).isReadable ? " readable" : "")), (Func<Texture2D, long>)TextureBytes);
			Type<Cubemap>(label, (Func<Cubemap, string>)((Cubemap t) => ((Texture)t).width + " " + ((object)t.format/*cast due to .constrained prefix*/).ToString()), (Func<Cubemap, long>)TextureBytes);
			Type<Texture3D>(label, (Func<Texture3D, string>)((Texture3D t) => ((Texture)t).width + "x" + ((Texture)t).height + "x" + t.depth), (Func<Texture3D, long>)null);
			Type<Texture2DArray>(label, (Func<Texture2DArray, string>)((Texture2DArray t) => ((Texture)t).width + "x" + ((Texture)t).height + "x" + t.depth), (Func<Texture2DArray, long>)null);
			Type<RenderTexture>(label, (Func<RenderTexture, string>)((RenderTexture t) => ((Texture)t).width + "x" + ((Texture)t).height + " " + ((object)t.graphicsFormat/*cast due to .constrained prefix*/).ToString()), (Func<RenderTexture, long>)null);
			Type<AudioClip>(label, (Func<AudioClip, string>)((AudioClip c) => $"{c.length:F0}s {c.loadType} {c.loadState}"), (Func<AudioClip, long>)AudioBytes);
			Type<Mesh>(label, (Func<Mesh, string>)((Mesh m) => m.vertexCount + " verts" + (m.isReadable ? " readable" : "")), (Func<Mesh, long>)null);
			Type<Shader>(label, (Func<Shader, string>)((Shader s) => ""), (Func<Shader, long>)null);
			Type<Material>(label, (Func<Material, string>)((Material m) => ""), (Func<Material, long>)null);
			Type<AnimationClip>(label, (Func<AnimationClip, string>)((AnimationClip a) => ""), (Func<AnimationClip, long>)null);
			Type<Font>(label, (Func<Font, string>)((Font f) => ""), (Func<Font, long>)null);
			Type<GameObject>(label, (Func<GameObject, string>)((GameObject g) => ""), (Func<GameObject, long>)null);
		}

		private static long TextureBytes(Texture t)
		{
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			long num = 0L;
			int width = t.width;
			int height = t.height;
			int num2 = Mathf.Max(1, t.mipmapCount);
			int num3 = ((!(t is Cubemap)) ? 1 : 6);
			for (int i = 0; i < num2; i++)
			{
				num += GraphicsFormatUtility.ComputeMipmapSize(Mathf.Max(1, width >> i), Mathf.Max(1, height >> i), t.graphicsFormat);
			}
			return num * num3;
		}

		private static long AudioBytes(AudioClip c)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Invalid comparison between Unknown and I4
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			if ((int)c.loadState != 2)
			{
				return 0L;
			}
			if ((int)c.loadType != 0)
			{
				return 0L;
			}
			return (long)c.samples * (long)c.channels * 2;
		}

		private static void Type<T>(string label, Func<T, string> describe, Func<T, long> estimate) where T : Object
		{
			T[] array = Resources.FindObjectsOfTypeAll<T>();
			long num = 0L;
			List<KeyValuePair<long, T>> list = new List<KeyValuePair<long, T>>(array.Length);
			T[] array2 = array;
			foreach (T val in array2)
			{
				long num2 = Profiler.GetRuntimeMemorySizeLong((Object)(object)val);
				if (num2 == 0L && estimate != null)
				{
					num2 = estimate(val);
				}
				num += num2;
				list.Add(new KeyValuePair<long, T>(num2, val));
			}
			list.Sort((KeyValuePair<long, T> a, KeyValuePair<long, T> b) => b.Key.CompareTo(a.Key));
			Plugin.Log.LogInfo((object)$"[Report:{label}] {typeof(T).Name}: {array.Length} objects, {num / 1048576} MB");
			for (int num3 = 0; num3 < list.Count && num3 < 10 && list[num3].Key > 0; num3++)
			{
				Plugin.Log.LogInfo((object)$"[Report:{label}]    {(double)list[num3].Key / 1048576.0:F1} MB  {((Object)list[num3].Value).name}  {describe(list[num3].Value)}");
			}
		}
	}
	internal static class Patches
	{
		[HarmonyPostfix]
		[HarmonyPatch(typeof(StartOfRound), "SetShipReadyToLand")]
		private static void AfterReachingOrbit()
		{
			((MonoBehaviour)Plugin.Host).StartCoroutine(Plugin.Host.CleanAfterRound());
		}
	}
}