diff --git a/eng/testing/scenarios/BuildWasmAppsJobsList.txt b/eng/testing/scenarios/BuildWasmAppsJobsList.txt index 6b41da61285c23..818ff9e8edd3c2 100644 --- a/eng/testing/scenarios/BuildWasmAppsJobsList.txt +++ b/eng/testing/scenarios/BuildWasmAppsJobsList.txt @@ -39,6 +39,7 @@ Wasm.Build.Tests.TestAppScenarios.LazyLoadingTests Wasm.Build.Tests.TestAppScenarios.LibraryInitializerTests Wasm.Build.Tests.TestAppScenarios.SatelliteLoadingTests Wasm.Build.Tests.TestAppScenarios.ModuleConfigTests +Wasm.Build.Tests.TestAppScenarios.MemoryTests Wasm.Build.Tests.AspNetCore.SignalRClientTests Wasm.Build.Tests.WasmBuildAppTest Wasm.Build.Tests.WasmNativeDefaultsTests diff --git a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs new file mode 100644 index 00000000000000..a1f7cd8d72ce93 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs @@ -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")); + } +} diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs b/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs new file mode 100644 index 00000000000000..f59915b0be5eaa --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs @@ -0,0 +1,74 @@ +// 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.Text; +using System.Runtime.InteropServices.JavaScript; + +public partial class MemoryTest // ?test=AllocateLargeHeapThenInterop +{ + [JSImport("countChars", "main.js")] + internal static partial int CountChars(string testArray); + + [JSExport] + internal static void Run() + { + // Allocate over 2GB space, 2 621 440 000 bytes + const int arrayCnt = 25; + int[][] arrayHolder = new int[arrayCnt][]; + string errors = ""; + TestOutput.WriteLine("Starting over 2GB array allocation"); + 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}"; + } + } + TestOutput.WriteLine("Finished over 2GB array allocation"); + + // call a method many times to trigger tier-up optimization + string randomString = GenerateRandomString(1000); + try + { + for (int i = 0; i < 10000; i++) + { + int count = CountChars(randomString); + if (count != randomString.Length) + errors += $"CountChars returned {count} instead of {randomString.Length} for {i}-th string."; + } + } + catch (Exception ex) + { + errors += $"Exception {ex} was thrown when CountChars was called in a loop"; + } + if (!string.IsNullOrEmpty(errors)) + { + TestOutput.WriteLine(errors); + throw new Exception(errors); + } + else + { + TestOutput.WriteLine("Great success, MemoryTest finished without errors."); + } + } + + private static Random random = new Random(); + + private static string GenerateRandomString(int stringLength) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + var stringBuilder = new StringBuilder(stringLength); + for (int i = 0; i < stringLength; i++) + { + stringBuilder.Append(chars[random.Next(chars.Length)]); + } + return stringBuilder.ToString(); + } +} diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js index 0023e28f4c3338..72235f9dc9ac92 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js @@ -14,6 +14,12 @@ function testOutput(msg) { console.log(`TestOutput -> ${msg}`); } +function countChars(str) { + const length = str.length; + testOutput(`JS received str of ${length} length`); + return length; +} + // Prepare base runtime parameters dotnet .withElementOnExit() @@ -168,6 +174,13 @@ try { case "MaxParallelDownloads": exit(0); break; + case "AllocateLargeHeapThenInterop": + setModuleImports('main.js', { + countChars + }); + exports.MemoryTest.Run(); + exit(0); + break; default: console.error(`Unknown test case: ${testCase}`); exit(3);