Skip to content
37 changes: 37 additions & 0 deletions src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit;

#nullable enable

namespace Wasm.Build.Tests.TestAppScenarios;

public class MemoryTests : AppTestBase
{
public MemoryTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
: base(output, buildContext)
{
}

[Theory]
[InlineData("Release", true)]
[InlineData("Release", false)]
public async Task AllocateLargeHeapThenRepeatedlyInterop(string config, bool buildNative)
{
// native build triggers passing value form EmccMaximumHeapSize to MAXIMUM_MEMORY that is set in emscripten
// in non-native build EmccMaximumHeapSize does not have an effect, so the test will fail with "out of memory"
CopyTestAsset("WasmBasicTestApp", "MemoryTests", "App");
string extraArgs = $"-p:EmccMaximumHeapSize=4294901760 -p:WasmBuildNative={buildNative}";
BuildProject(config, assertAppBundle: false, extraArgs: extraArgs);

var result = await RunSdkStyleAppForBuild(new (Configuration: config, TestScenario: "AllocateLargeHeapThenInterop", ExpectedExitCode: buildNative ? 0 : 1));
if (!buildNative)
Assert.Contains(result.TestOutput, item => item.Contains("Exception System.OutOfMemoryException: Out of memory"));
}
}
58 changes: 58 additions & 0 deletions src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Text.Json;
using System.Runtime.InteropServices.JavaScript;

public partial class MemoryTest
{
[JSImport("joinStringArray", "main.js")]
internal static partial string JoinStringArray(string[] testArray);

[JSExport]
internal static void Run()
{
// Allocate a 2GB space (20 int arrays of 100MB, 100MB = 4 * 1024 * 1024 * 25)
const int arrayCnt = 20;
int[][] arrayHolder = new int[arrayCnt][];
string errors = "";
for (int i = 0; i < arrayCnt; i++)
{
try
{
arrayHolder[i] = new int[1024 * 1024 * 25];
}
catch (Exception ex)
{
errors += $"Exception {ex} was thrown on i={i}";
}
}

// marshall string array to JS
string [] testArray = new [] { "M", "e", "m", "o", "r", "y", "T", "e", "s", "t" };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make each string here random (constants are not allocated) and large enough, so that the JS side is forced to make allocations above 2GB mark. And overflow to negative numbers. Maybe run this in a loop.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then let's use the arrayHolder that is already prepared.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2GB string allocation with random data is taking too long, not only to run WBT but to even debug it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we cannot just take arrayHolder and send it to JS to force JS to allocate 2GB because interop does not support array of arrays. Allocating flat array of 2GB requires more linear memory and can be problematic from other reasons than investigated in the issue. Allocating an array of strings theoretically should work but as I already mentioned, it takes tens of minutes to finish the allocation, even using StringBuilder.
@maraf, do you have a good idea how to fulfill the requirements?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind, I realized that the key point was "allocations above 2GB mark", not "allocations of 2GB".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're allocating 2 621 440 000 bytes so more than max int. There are no traces of overflow. Let's merge this to take care of the relink fix PR that is built on top of this.

string response = JoinStringArray(testArray);

bool correct = AssertJoinCorrect(testArray, response);
if (!correct)
errors += $"expected response: {response}, testArray: {string.Join("", testArray)}";

// call a method many times to trigger tier-up optimization
for (int i = 0; i < 10000; i++)
{
AssertJoinCorrect(testArray, response);
}
if (!string.IsNullOrEmpty(errors))
{
TestOutput.WriteLine(errors);
throw new Exception(errors);
}
}

private static bool AssertJoinCorrect(string[] testArray, string expected)
{
string joinedArray = string.Join("", testArray);
return joinedArray.Equals(expected);
}
}
11 changes: 11 additions & 0 deletions src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ function testOutput(msg) {
console.log(`TestOutput -> ${msg}`);
}

function joinStringArray(stringArr) {
testOutput(`JS received array: ${JSON.stringify(stringArr)}`);
return stringArr.join("");
}

// Prepare base runtime parameters
dotnet
.withElementOnExit()
Expand Down Expand Up @@ -168,6 +173,12 @@ try {
case "MaxParallelDownloads":
exit(0);
break;
case "AllocateLargeHeapThenInterop":
setModuleImports('main.js', {
joinStringArray
});
exports.MemoryTest.Run();
break;
default:
console.error(`Unknown test case: ${testCase}`);
exit(3);
Expand Down