Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions Jint.Tests/Runtime/GarbageCollectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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;
}
}
}
48 changes: 45 additions & 3 deletions Jint/Collections/RefStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,47 @@ public bool TryPeek([NotNullWhen(true)] out T item)
return false;
}

/// <summary>
/// Returns the last added item from the stack and decreases the stack size by one.
/// </summary>
/// <exception cref="InvalidOperationException">Is thrown if the stack is empty.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref readonly T Pop()
public T Pop()
{
if (_size == 0)
{
Throw.InvalidOperationException("stack is empty");
}

_size--;
return ref _array[_size];
// Create a copy of the value because it will get overridden
// in the statement.
var result = _array[_size];
// Ensure that the item can get garbage collected if possible.
_array[_size] = default;
return result;
}

/// <summary>
/// Same as <see cref="Pop"/> but without copying the value.
/// </summary>
/// <remarks>
/// Use this method when the return value is not used and
/// <typeparamref name="T"/> is a value type. In that case, this methods
/// avoids one value type copy compared to <see cref="Pop"/>.
/// </remarks>
/// <exception cref="InvalidOperationException">Is thrown if the stack is empty.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void PopAndDiscard()
{
if (_size == 0)
{
Throw.InvalidOperationException("stack is empty");
}

_size--;
// Ensure that the item can get garbage collected if possible.
_array[_size] = default;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down Expand Up @@ -113,7 +144,18 @@ private void Resize(int value)

public void Clear()
{
_size = 0;
if (_size > 0)
{
#if NETFRAMEWORK || NETSTANDARD2_0
for (var i = 0; i < _size; i++)
{
_array[i] = default;
}
#else
Array.Fill(_array, default, 0, _size);
#endif
_size = 0;
}
}

public Enumerator GetEnumerator()
Expand Down
10 changes: 10 additions & 0 deletions Jint/Pooling/JsValueArrayPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,26 @@ public JsValue[] RentArray(int size)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReturnArray(JsValue[] array)
{
// Ensure that the array contents are cleared
// to allow garbage collecting the values of the
// array if possible. Only the array itself
// should be cached.
if (array.Length == 1)
{
array[0] = null!;
_poolArray1.Free(array);
}
else if (array.Length == 2)
{
array[0] = null!;
array[1] = null!;
_poolArray2.Free(array);
}
else if (array.Length == 3)
{
array[0] = null!;
array[1] = null!;
array[2] = null!;
_poolArray3.Free(array);
}
}
Expand Down
2 changes: 1 addition & 1 deletion Jint/Runtime/CallStack/JintCallStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public int Push(Function function, JintExpression? expression, in ExecutionConte

public CallStackElement Pop()
{
ref readonly var item = ref _stack.Pop();
var item = _stack.Pop();
if (_statistics is not null)
{
if (_statistics[item] == 0)
Expand Down
4 changes: 2 additions & 2 deletions Jint/Runtime/ExecutionContextStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public ref readonly ExecutionContext ReplaceTopGenerator(GeneratorInstance newEn
public void Push(in ExecutionContext context) => _stack.Push(in context);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref readonly ExecutionContext Pop() => ref _stack.Pop();
public void Pop() => _stack.PopAndDiscard();

public IScriptOrModule? GetActiveScriptOrModule()
{
Expand Down Expand Up @@ -91,4 +91,4 @@ public ref readonly ExecutionContext ReplaceTopGenerator(GeneratorInstance newEn

return null;
}
}
}