- Fixed assembly unloading. However, requires 'plugin_forcerungc' to be run multiple times over ~30 seconds at the main menu.

This commit is contained in:
MapleWheels
2026-05-15 18:41:39 -04:00
parent ada9a09410
commit cfde6f3579
3 changed files with 123 additions and 98 deletions
@@ -332,17 +332,9 @@ namespace Barotrauma
void RunStateUnloaded_OnEnter(State<RunState> currentState) void RunStateUnloaded_OnEnter(State<RunState> currentState)
{ {
Logger.LogMessage("LuaCs unloaded state entered"); Logger.LogMessage("LuaCs unloaded state entered");
Logger.LogResults(PackageManagementService.StopRunningPackages());
if (PackageManagementService.IsAnyPackageRunning()) DisposeLuaCsConfig();
{ Logger.LogResults(PackageManagementService.UnloadAllPackages());
Logger.LogResults(PackageManagementService.StopRunningPackages());
}
if (PackageManagementService.IsAnyPackageLoaded())
{
DisposeLuaCsConfig();
Logger.LogResults(PackageManagementService.UnloadAllPackages());
}
EventService.Reset(); EventService.Reset();
ConfigService.Reset(); ConfigService.Reset();
@@ -362,11 +354,7 @@ namespace Barotrauma
void RunStateLoadedNoExec_OnEnter(State<RunState> currentState) void RunStateLoadedNoExec_OnEnter(State<RunState> currentState)
{ {
Logger.LogMessage("LuaCs no execution state entered"); Logger.LogMessage("LuaCs no execution state entered");
Logger.LogResults(PackageManagementService.StopRunningPackages());
if (PackageManagementService.IsAnyPackageRunning())
{
Logger.LogResults(PackageManagementService.StopRunningPackages());
}
if (!PackageManagementService.IsAnyPackageLoaded()) if (!PackageManagementService.IsAnyPackageLoaded())
{ {
@@ -256,6 +256,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
} }
} }
[MethodImpl(MethodImplOptions.NoInlining)]
public Result<Assembly> CompileScriptAssembly([NotNull] string assemblyName, public Result<Assembly> CompileScriptAssembly([NotNull] string assemblyName,
bool compileWithInternalAccess, bool compileWithInternalAccess,
ImmutableArray<SyntaxTree> syntaxTrees, ImmutableArray<SyntaxTree> syntaxTrees,
@@ -348,6 +349,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
} }
} }
[MethodImpl(MethodImplOptions.NoInlining)]
public FluentResults.Result<Assembly> LoadAssemblyFromFile(string assemblyFilePath, public FluentResults.Result<Assembly> LoadAssemblyFromFile(string assemblyFilePath,
ImmutableArray<string> additionalDependencyPaths) ImmutableArray<string> additionalDependencyPaths)
{ {
@@ -434,6 +436,8 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
} }
} }
[MethodImpl(MethodImplOptions.NoInlining)]
public FluentResults.Result<Assembly> GetAssemblyByName(string assemblyName) public FluentResults.Result<Assembly> GetAssemblyByName(string assemblyName)
{ {
if (IsDisposed) if (IsDisposed)
@@ -481,6 +485,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
} }
} }
[MethodImpl(MethodImplOptions.NoInlining)]
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies() public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies()
{ {
if (IsDisposed) if (IsDisposed)
@@ -501,6 +506,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
} }
} }
[MethodImpl(MethodImplOptions.NoInlining)]
public IEnumerable<Type> UnsafeGetTypesInAssemblies() public IEnumerable<Type> UnsafeGetTypesInAssemblies()
{ {
if (IsDisposed) if (IsDisposed)
@@ -529,6 +535,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
} }
} }
[MethodImpl(MethodImplOptions.NoInlining)]
public Result<Type> GetTypeInAssemblies(string typeName) public Result<Type> GetTypeInAssemblies(string typeName)
{ {
if (IsDisposed) if (IsDisposed)
@@ -557,14 +564,12 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
return; // we don't want to invoke events twice nor cause strong GC handles. return; // we don't want to invoke events twice nor cause strong GC handles.
IsDisposed = true; IsDisposed = true;
this.Unload(); this.Unload();
this.DisposeInternal(); GC.SuppressFinalize(this);
// we want to call base finalizers
//GC.SuppressFinalize(this);
} }
~AssemblyLoader() ~AssemblyLoader()
{ {
this.DisposeInternal(); this.Unload();
} }
private void OnUnload(AssemblyLoadContext context) private void OnUnload(AssemblyLoadContext context)
@@ -579,9 +584,8 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
Thread.Sleep(1000/Timing.FixedUpdateRate-1); Thread.Sleep(1000/Timing.FixedUpdateRate-1);
} }
var wf = new WeakReference<IAssemblyLoaderService>(this);
_loadedAssemblyData.Clear();
_onUnload?.Invoke(this); _onUnload?.Invoke(this);
this.DisposeInternal();
} }
private void DisposeInternal() private void DisposeInternal()
@@ -592,6 +596,9 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
base.Unloading -= OnUnload; base.Unloading -= OnUnload;
this._dependencyResolvers.Clear(); this._dependencyResolvers.Clear();
this._loadedAssemblyData.Clear(); this._loadedAssemblyData.Clear();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
GC.WaitForFullGCComplete(10);
} }
protected override Assembly Load(AssemblyName assemblyName) protected override Assembly Load(AssemblyName assemblyName)
@@ -660,6 +667,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
public readonly ImmutableArray<Type> Types; public readonly ImmutableArray<Type> Types;
public readonly ImmutableDictionary<string, Type> TypesByName; public readonly ImmutableDictionary<string, Type> TypesByName;
[MethodImpl(MethodImplOptions.NoOptimization)]
public AssemblyData(Assembly assembly, byte[] assemblyImage) public AssemblyData(Assembly assembly, byte[] assemblyImage)
{ {
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
@@ -669,6 +677,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type); TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type);
} }
[MethodImpl(MethodImplOptions.NoOptimization)]
public AssemblyData(Assembly assembly, string path) public AssemblyData(Assembly assembly, string path)
{ {
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
@@ -696,6 +705,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
HashCode = AssemblyName.GetHashCode(); HashCode = AssemblyName.GetHashCode();
} }
[MethodImpl(MethodImplOptions.NoOptimization)]
public AssemblyOrStringKey(string assemblyName) public AssemblyOrStringKey(string assemblyName)
{ {
if (assemblyName.IsNullOrWhiteSpace()) if (assemblyName.IsNullOrWhiteSpace())
@@ -89,6 +89,7 @@ public class PluginManagementService : IAssemblyManagementService
private ImmutableArray<MetadataReference> _baseMetadataReferences = ImmutableArray<MetadataReference>.Empty; private ImmutableArray<MetadataReference> _baseMetadataReferences = ImmutableArray<MetadataReference>.Empty;
private ImmutableArray<MetadataReference> _baseMetadataReferencesNonPublicized = ImmutableArray<MetadataReference>.Empty; private ImmutableArray<MetadataReference> _baseMetadataReferencesNonPublicized = ImmutableArray<MetadataReference>.Empty;
private static readonly int GC_COLLECT_WAIT_TIME = 2000;
private IEnumerable<MetadataReference> BaseMetadataReferences private IEnumerable<MetadataReference> BaseMetadataReferences
{ {
@@ -215,6 +216,7 @@ public class PluginManagementService : IAssemblyManagementService
private IEventService _pluginEventService; private IEventService _pluginEventService;
private Lazy<ILuaPatcher> _pluginLuaPatcherService; private Lazy<ILuaPatcher> _pluginLuaPatcherService;
private Func<IConsoleCommandsService> _consoleCommandServiceFactory; private Func<IConsoleCommandsService> _consoleCommandServiceFactory;
private readonly IConsoleCommandsService _internalConsoleCommandsService;
private ILuaCsInfoProvider _luaCsInfoProvider; private ILuaCsInfoProvider _luaCsInfoProvider;
private readonly ConcurrentDictionary<ContentPackage, IAssemblyLoaderService> _assemblyLoaders = new(); private readonly ConcurrentDictionary<ContentPackage, IAssemblyLoaderService> _assemblyLoaders = new();
private readonly ConcurrentDictionary<Type, ContentPackage> _pluginPackageLookup = new(); private readonly ConcurrentDictionary<Type, ContentPackage> _pluginPackageLookup = new();
@@ -244,6 +246,18 @@ public class PluginManagementService : IAssemblyManagementService
_pluginLuaPatcherService = pluginLuaPatcherService; _pluginLuaPatcherService = pluginLuaPatcherService;
_consoleCommandServiceFactory = consoleCommandServiceFactory; _consoleCommandServiceFactory = consoleCommandServiceFactory;
_luaCsInfoProvider = luaCsInfoProvider; _luaCsInfoProvider = luaCsInfoProvider;
_internalConsoleCommandsService = consoleCommandServiceFactory.Invoke();
RegisterCommands(_internalConsoleCommandsService);
}
private void RegisterCommands(IConsoleCommandsService cmdService)
{
cmdService.RegisterCommand("plugin_forcerungc", "Forces the GC to run", cmds =>
{
_logger.LogMessage("Forcing GC run.");
RunGC(true);
});
} }
private ServiceContainer CreatePluginServiceContainer() private ServiceContainer CreatePluginServiceContainer()
@@ -310,11 +324,13 @@ public class PluginManagementService : IAssemblyManagementService
} }
} }
[MethodImpl(MethodImplOptions.NoInlining)]
public bool TryGetPackageForPlugin<TPlugin>(out ContentPackage ownerPackage) public bool TryGetPackageForPlugin<TPlugin>(out ContentPackage ownerPackage)
{ {
return _pluginPackageLookup.TryGetValue(typeof(TPlugin), out ownerPackage); return _pluginPackageLookup.TryGetValue(typeof(TPlugin), out ownerPackage);
} }
[MethodImpl(MethodImplOptions.NoInlining)]
public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false, public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false,
bool includeDefaultContext = true) bool includeDefaultContext = true)
{ {
@@ -497,7 +513,7 @@ public class PluginManagementService : IAssemblyManagementService
} }
} }
[MethodImpl(MethodImplOptions.NoInlining)]
public FluentResults.Result LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resources) public FluentResults.Result LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resources)
{ {
if (resources.IsDefaultOrEmpty) if (resources.IsDefaultOrEmpty)
@@ -727,7 +743,7 @@ public class PluginManagementService : IAssemblyManagementService
.Replace(" Barotrauma.Networking.Client.ClientList", " ModUtils.Client.ClientList") .Replace(" Barotrauma.Networking.Client.ClientList", " ModUtils.Client.ClientList")
.Replace("ItemPrefab.GetItemPrefab", "ModUtils.ItemPrefab.GetItemPrefab"); .Replace("ItemPrefab.GetItemPrefab", "ModUtils.ItemPrefab.GetItemPrefab");
} }
private IntPtr OnAssemblyLoaderResolvingUnmanaged(Assembly callerAssembly, string targetAssemblyName) private IntPtr OnAssemblyLoaderResolvingUnmanaged(Assembly callerAssembly, string targetAssemblyName)
{ {
Guard.IsNull(callerAssembly, nameof(callerAssembly)); Guard.IsNull(callerAssembly, nameof(callerAssembly));
@@ -800,21 +816,58 @@ public class PluginManagementService : IAssemblyManagementService
{ {
_eventService?.Value?.PublishEvent<IEventAssemblyUnloading>(sub => sub.OnAssemblyUnloading(assembly)); _eventService?.Value?.PublishEvent<IEventAssemblyUnloading>(sub => sub.OnAssemblyUnloading(assembly));
} }
_unloadingAssemblyLoaders.Add(loader, loader.OwnerPackage);
} }
[MethodImpl(MethodImplOptions.NoOptimization)]
public FluentResults.Result UnloadManagedAssemblies() public FluentResults.Result UnloadManagedAssemblies()
{ {
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this); IService.CheckDisposed(this);
if (_assemblyLoaders.Count == 0)
{
return FluentResults.Result.Ok();
}
var results = new FluentResults.Result(); var results = new FluentResults.Result();
results.WithReasons(UnsafeDisposeManagedTypeInstances().Reasons); if (!_pluginInstances.IsEmpty)
{
foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value))
{
try
{
instance.Dispose();
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
continue;
}
}
_pluginInstances.Clear();
}
if (_pluginEventService is not null)
{
_eventService.Value.RemoveDispatcherEventService(_pluginEventService);
try
{
_pluginEventService.Dispose();
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
}
_pluginEventService = null;
}
try
{
_pluginInjectorContainer?.Dispose();
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
}
_pluginInjectorContainer = null;
ReflectionUtils.ResetCache(); ReflectionUtils.ResetCache();
foreach (var loaderService in _assemblyLoaders) foreach (var loaderService in _assemblyLoaders)
@@ -822,7 +875,6 @@ public class PluginManagementService : IAssemblyManagementService
try try
{ {
loaderService.Value.Dispose(); loaderService.Value.Dispose();
_unloadingAssemblyLoaders.Add(loaderService.Value, loaderService.Key);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -832,41 +884,7 @@ public class PluginManagementService : IAssemblyManagementService
_assemblyLoaders.Clear(); _assemblyLoaders.Clear();
_storageService.PurgeCache(); _storageService.PurgeCache();
GC.Collect(); _pluginPackageLookup.Clear();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
GC.WaitForPendingFinalizers();
GC.WaitForFullGCComplete(1000);
#if DEBUG
// Print still loaded assembly load ctx after giving some time
CoroutineManager.Invoke(() =>
{
if (!_unloadingAssemblyLoaders.Any())
{
return;
}
StringBuilder sb = new StringBuilder();
sb.AppendLine("The following ContentPackages have not unloaded their assemblies:");
foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray())
{
sb.AppendLine($"- '{kvp.Value.Name}'");
}
// Use DebugConsole in case logger is null by the time this executes.
if (_logger is null)
{
DebugConsole.LogError(sb.ToString());
}
else
{
_logger.LogWarning(sb.ToString());
}
}, 3.0f);
#endif
// clear native libraries // clear native libraries
if (_loadedNativeLibraries.Any()) if (_loadedNativeLibraries.Any())
@@ -886,49 +904,58 @@ public class PluginManagementService : IAssemblyManagementService
_loadedNativeLibraries.Clear(); _loadedNativeLibraries.Clear();
} }
RunGC(false);
return results; return results;
} }
private FluentResults.Result UnsafeDisposeManagedTypeInstances() private void RunGC(bool logResults)
{ {
var results = new FluentResults.Result(); int maxGen = GC.MaxGeneration;
GC.RegisterForFullGCNotification(maxGen, 10);
if (!_pluginInstances.IsEmpty) for (int gcGen = 0; gcGen < maxGen; gcGen++)
{ {
foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value)) GC.Collect(maxGen, GCCollectionMode.Aggressive, true, true);
var confirmationToken = GC.WaitForFullGCComplete(GC_COLLECT_WAIT_TIME);
if (logResults)
{ {
try _logger.LogWarning($"GC Pass # {gcGen} completed. Completion status: {confirmationToken.ToString()}");
{
instance.Dispose();
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
continue;
}
} }
} }
GC.CancelFullGCNotification();
if (_pluginEventService is not null) // Print still loaded assembly load ctx after giving some time
if (logResults)
{ {
_eventService.Value.RemoveDispatcherEventService(_pluginEventService); CoroutineManager.Invoke(() =>
_pluginEventService = null; {
if (!_unloadingAssemblyLoaders.Any())
{
return;
}
StringBuilder sb = new StringBuilder();
sb.AppendLine("The following ContentPackages have not unloaded their assemblies:");
foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray())
{
sb.AppendLine($"- '{kvp.Value.Name}'");
}
// Use DebugConsole in case logger is null by the time this executes.
if (_logger is null)
{
DebugConsole.LogError(sb.ToString());
}
else
{
_logger.LogWarning(sb.ToString());
}
}, GC.MaxGeneration * GC_COLLECT_WAIT_TIME/1000f);
} }
try
{
_pluginInjectorContainer.Dispose();
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
}
_pluginInjectorContainer = null;
_pluginInstances.Clear();
_pluginPackageLookup.Clear();
return results;
} }
public Result<Assembly> GetLoadedAssembly(OneOf<AssemblyName, string> assemblyName, in Guid[] excludedContexts) public Result<Assembly> GetLoadedAssembly(OneOf<AssemblyName, string> assemblyName, in Guid[] excludedContexts)