namespace Jint.Tests.Runtime; // This needs to run without any parallelization because it uses // garbage collector metrics which cannot be isolated. [CollectionDefinition(nameof(GarbageCollectionTests), DisableParallelization = true)] [Collection(nameof(GarbageCollectionTests))] public class GarbageCollectionTests { [Fact] public void InternalCachingDoesNotPreventGarbageCollection() { // This test ensures that memory allocated within functions // can be garbage collected by the .NET runtime. To test that, // the "allocate" functions allocates a big chunk of memory, // which is not used anywhere. So the GC should have no problem // releasing that memory after the "allocate" function leaves. // Arrange var engine = new Engine(); const string script = """ function allocate(runAllocation) { if (runAllocation) { // Allocate ~200 MB of data (not 100 because .NET uses UTF-16 for strings) var test = Array.from({ length: 100 }) .map(() => ' '.repeat(1 * 1024 * 1024)); } return 2; } """; engine.Evaluate(script); // Create a baseline for memory usage. engine.Evaluate("allocate(false);"); var usedMemoryBytesBaseline = CurrentlyUsedMemory(); // Act engine.Evaluate("allocate(true);"); // Assert var usedMemoryBytesAfterJsScript = CurrentlyUsedMemory(); var epsilon = 10 * 1024 * 1024; // allowing up to 10 MB of other allocations should be enough to prevent false positives Assert.True( usedMemoryBytesAfterJsScript - usedMemoryBytesBaseline < epsilon, userMessage: $""" The garbage collector did not free the allocated but unreachable 200 MB from the script.; Before Call : {BytesToString(usedMemoryBytesBaseline)} After Call : {BytesToString(usedMemoryBytesAfterJsScript)} --- Acceptable : {BytesToString(usedMemoryBytesBaseline + epsilon)} """); return; static string BytesToString(long bytes) => $"{(bytes / 1024.0 / 1024.0),6:0.0} MB"; static long CurrentlyUsedMemory() { // Just try to ensure that everything possible gets collected. GC.Collect(2, GCCollectionMode.Forced, blocking: true); var currentlyUsed = GC.GetTotalMemory(forceFullCollection: true); return currentlyUsed; } } }