New GC algorithm (untested).

This commit is contained in:
Maplewheels
2026-05-30 22:19:37 -04:00
parent eb22fbddff
commit c452493555
@@ -90,10 +90,13 @@ 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 Thread _backgroundGCCleanupThread = null;
private long _backgroundGCWatchdogTicks = 0;
private static readonly int private static readonly int
GC_COLLECT_WAIT_TIME = 1000, GC_TASK_COMPLETION_TIMEOUT = 5000,
GC_BACKGND_MAXITERATIONS = 2, GC_BACKGND_MAXITERATIONS = 2,
GC_BACKGND_INTERVAL_MILLIS = 500, GC_BACKGND_INTERVAL_MILLIS = 200,
GC_BACKGND_GENERATION_WAIT_MILLIS = 100; GC_BACKGND_GENERATION_WAIT_MILLIS = 100;
private IEnumerable<MetadataReference> BaseMetadataReferences private IEnumerable<MetadataReference> BaseMetadataReferences
@@ -261,7 +264,10 @@ public class PluginManagementService : IAssemblyManagementService
cmdService.RegisterCommand("plugin_forcerungc", "Forces the GC to run", cmds => cmdService.RegisterCommand("plugin_forcerungc", "Forces the GC to run", cmds =>
{ {
_logger.LogMessage("Forcing GC run."); _logger.LogMessage("Forcing GC run.");
RunGC(true); Task.Factory.StartNew(async () =>
{
await RunGC(true, false);
});
}); });
} }
@@ -910,78 +916,105 @@ public class PluginManagementService : IAssemblyManagementService
_loadedNativeLibraries.Clear(); _loadedNativeLibraries.Clear();
} }
RunGC(false); // Immediate, Blocking GC Task.Factory.StartNew(async () =>
Task.Factory.StartNew(RunBackgroundGC); // Non-blocking background GC {
await RunGC(true, false);
});
return results; return results;
} }
private async Task RunBackgroundGC() private void SafeLogUnloadingPackages()
{ {
// must be lock-free and thus cannot reference class members. if (!_unloadingAssemblyLoaders.Any())
await Task.Delay(GC_BACKGND_INTERVAL_MILLIS);
GC.RegisterForFullGCNotification(1, 10);
for (int i = 0; i < GC_BACKGND_MAXITERATIONS; i++)
{ {
int maxGen = GC.MaxGeneration; return;
for (int gcGen = 0; gcGen < maxGen; gcGen++) }
{
GC.Collect(maxGen, GCCollectionMode.Forced, false, false); // mark objects, run finalizers, promote generation StringBuilder sb = new StringBuilder();
GC.WaitForFullGCComplete(GC_COLLECT_WAIT_TIME);
GC.Collect(maxGen, GCCollectionMode.Forced, false, true); // free mem sb.AppendLine("The following ContentPackages have not unloaded their assemblies:");
GC.WaitForFullGCComplete(GC_COLLECT_WAIT_TIME);
await Task.Delay(GC_BACKGND_GENERATION_WAIT_MILLIS); foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray())
} {
await Task.Delay(GC_BACKGND_INTERVAL_MILLIS); sb.AppendLine($"- '{kvp.Value.Name}'");
}
// Use DebugConsole in case logger is null by the time this executes.
if (_logger is null)
{
DebugConsole.Log(sb.ToString());
}
else
{
_logger.LogWarning(sb.ToString());
} }
GC.CancelFullGCNotification();
} }
private void RunGC(bool logResults) private void GCCleanupTask(TaskCompletionSource<bool> completionSuccess)
{ {
int maxGen = GC.MaxGeneration; GC.RegisterForFullGCNotification(1, 1);
GC.RegisterForFullGCNotification(maxGen, 10); try
for (int gcGen = 0; gcGen < maxGen; gcGen++)
{ {
GC.Collect(maxGen, GCCollectionMode.Aggressive, true, true); for (int iter = 0; iter < GC_BACKGND_MAXITERATIONS; iter++)
var confirmationToken = GC.WaitForFullGCComplete(GC_COLLECT_WAIT_TIME); {
int maxGen = GC.MaxGeneration;
for (int currGen = 0; currGen < maxGen; currGen++)
{
GC.Collect(currGen, GCCollectionMode.Forced, false, false); // marking pass
GC.WaitForFullGCComplete(GC_BACKGND_GENERATION_WAIT_MILLIS);
GC.Collect(currGen); // generation cleanup
}
Thread.Sleep(GC_BACKGND_INTERVAL_MILLIS);
}
completionSuccess.SetResult(true);
}
catch (ThreadInterruptedException tie)
{
completionSuccess.SetResult(false);
}
catch (Exception e)
{
completionSuccess.SetException(e);
}
finally
{
GC.CancelFullGCNotification();
}
}
private async Task RunGC(bool logResults, bool runOnMainThread)
{
var gcCompletionSuccess = new TaskCompletionSource<bool>();
if (runOnMainThread)
{
GCCleanupTask(gcCompletionSuccess);
if (logResults) if (logResults)
{ {
_logger.LogWarning($"GC Pass # {gcGen} completed. Completion status: {confirmationToken.ToString()}"); SafeLogUnloadingPackages();
} }
}
GC.CancelFullGCNotification();
// Print still loaded assembly load ctx after giving some time return;
}
var gcThread = new Thread(() =>
{
GCCleanupTask(gcCompletionSuccess);
}) { IsBackground = true };
gcThread.Start();
try
{
await gcCompletionSuccess.Task.WaitAsync(TimeSpan.FromMilliseconds(GC_TASK_COMPLETION_TIMEOUT));
}
catch (TimeoutException te)
{
_logger.LogError($"{nameof(RunGC)}: The GC task thread has timed out.");
}
if (logResults) if (logResults)
{ {
CoroutineManager.Invoke(() => SafeLogUnloadingPackages();
{
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);
} }
} }