Skip to content

Commit

Permalink
[browser] More meaningful code in browser template (#101287)
Browse files Browse the repository at this point in the history
  • Loading branch information
maraf authored Apr 24, 2024
1 parent e58be47 commit 0549076
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 80 deletions.
2 changes: 1 addition & 1 deletion eng/testing/scenarios/BuildWasmAppsJobsList.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Wasm.Build.NativeRebuild.Tests.NoopNativeRebuildTest
Wasm.Build.NativeRebuild.Tests.OptimizationFlagChangeTests
Wasm.Build.NativeRebuild.Tests.ReferenceNewAssemblyRebuildTest
Wasm.Build.NativeRebuild.Tests.SimpleSourceChangeRebuildTest
Wasm.Build.Templates.Tests.InterpPgoTests
Wasm.Build.Tests.TestAppScenarios.InterpPgoTests
Wasm.Build.Templates.Tests.NativeBuildTests
Wasm.Build.Tests.Blazor.AppsettingsTests
Wasm.Build.Tests.Blazor.BuildPublishTests
Expand Down
78 changes: 55 additions & 23 deletions src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,25 @@ public WasmTemplateTests(ITestOutputHelper output, SharedBuildPerTestClassFixtur
{
}

private void UpdateProgramCS()
private string StringReplaceWithAssert(string oldContent, string oldValue, string newValue)
{
string newContent = oldContent.Replace(oldValue, newValue);
if (oldValue != newValue && oldContent == newContent)
throw new XunitException($"Replacing '{oldValue}' with '{newValue}' did not change the content '{oldContent}'");

return newContent;
}

private void UpdateBrowserProgramCs()
{
var path = Path.Combine(_projectDir!, "Program.cs");
string text = File.ReadAllText(path);
text = StringReplaceWithAssert(text, "while(true)", $"int i = 0;{Environment.NewLine}while(i++ < 10)");
text = StringReplaceWithAssert(text, "partial class StopwatchSample", $"return 42;{Environment.NewLine}partial class StopwatchSample");
File.WriteAllText(path, text);
}

private void UpdateConsoleProgramCs()
{
string programText = """
Console.WriteLine("Hello, Console!");
Expand All @@ -30,34 +48,44 @@ private void UpdateProgramCS()
""";
var path = Path.Combine(_projectDir!, "Program.cs");
string text = File.ReadAllText(path);
text = text.Replace(@"Console.WriteLine(""Hello, Console!"");", programText);
text = text.Replace("return 0;", "return 42;");
text = StringReplaceWithAssert(text, @"Console.WriteLine(""Hello, Console!"");", programText);
text = StringReplaceWithAssert(text, "return 0;", "return 42;");
File.WriteAllText(path, text);
}

private void UpdateBrowserMainJs(string targetFramework, string runtimeAssetsRelativePath = DefaultRuntimeAssetsRelativePath)
{
base.UpdateBrowserMainJs((mainJsContent) => {
// .withExitOnUnhandledError() is available only only >net7.0
mainJsContent = mainJsContent.Replace(".create()",
base.UpdateBrowserMainJs(
(mainJsContent) =>
{
// .withExitOnUnhandledError() is available only only >net7.0
mainJsContent = StringReplaceWithAssert(
mainJsContent,
".create()",
(targetFramework == "net8.0" || targetFramework == "net9.0")
? ".withConsoleForwarding().withElementOnExit().withExitCodeLogging().withExitOnUnhandledError().create()"
: ".withConsoleForwarding().withElementOnExit().withExitCodeLogging().create()");
: ".withConsoleForwarding().withElementOnExit().withExitCodeLogging().create()"
);
mainJsContent = mainJsContent.Replace("runMain()", "dotnet.run()");
mainJsContent = mainJsContent.Replace("from './_framework/dotnet.js'", $"from '{runtimeAssetsRelativePath}dotnet.js'");
// dotnet.run() is already used in <= net8.0
if (targetFramework != "net8.0")
mainJsContent = StringReplaceWithAssert(mainJsContent, "runMain()", "dotnet.run()");
return mainJsContent;
}, targetFramework, runtimeAssetsRelativePath);
mainJsContent = StringReplaceWithAssert(mainJsContent, "from './_framework/dotnet.js'", $"from '{runtimeAssetsRelativePath}dotnet.js'");
return mainJsContent;
},
targetFramework,
runtimeAssetsRelativePath
);
}

private void UpdateConsoleMainJs()
{
string mainJsPath = Path.Combine(_projectDir!, "main.mjs");
string mainJsContent = File.ReadAllText(mainJsPath);

mainJsContent = mainJsContent
.Replace(".create()", ".withConsoleForwarding().create()");
mainJsContent = StringReplaceWithAssert(mainJsContent, ".create()", ".withConsoleForwarding().create()");

File.WriteAllText(mainJsPath, mainJsContent);
}
Expand All @@ -73,8 +101,7 @@ private void UpdateMainJsEnvironmentVariables(params (string key, string value)[
js.Append($".withEnvironmentVariable(\"{variable.key}\", \"{variable.value}\")");
}

mainJsContent = mainJsContent
.Replace(".create()", js.ToString() + ".create()");
mainJsContent = StringReplaceWithAssert(mainJsContent, ".create()", js.ToString() + ".create()");

File.WriteAllText(mainJsPath, mainJsContent);
}
Expand All @@ -88,6 +115,7 @@ public void BrowserBuildThenPublish(string config)
string projectFile = CreateWasmTemplateProject(id, "wasmbrowser");
string projectName = Path.GetFileNameWithoutExtension(projectFile);

UpdateBrowserProgramCs();
UpdateBrowserMainJs(DefaultTargetFramework);

var buildArgs = new BuildArgs(projectName, config, false, id, null);
Expand All @@ -96,10 +124,10 @@ public void BrowserBuildThenPublish(string config)
atTheEnd:
"""
<Target Name="CheckLinkedFiles" AfterTargets="ILLink">
<ItemGroup>
<_LinkedOutFile Include="$(IntermediateOutputPath)\linked\*.dll" />
</ItemGroup>
<Error Text="No file was linked-out. Trimming probably doesn't work (PublishTrimmed=$(PublishTrimmed))" Condition="@(_LinkedOutFile->Count()) == 0" />
<ItemGroup>
<_LinkedOutFile Include="$(IntermediateOutputPath)\linked\*.dll" />
</ItemGroup>
<Error Text="No file was linked-out. Trimming probably doesn't work (PublishTrimmed=$(PublishTrimmed))" Condition="@(_LinkedOutFile->Count()) == 0" />
</Target>
"""
);
Expand Down Expand Up @@ -212,7 +240,7 @@ private void ConsoleBuildAndRun(string config, bool relinking, string extraNewAr
string projectFile = CreateWasmTemplateProject(id, "wasmconsole", extraNewArgs, addFrameworkArg: addFrameworkArg);
string projectName = Path.GetFileNameWithoutExtension(projectFile);

UpdateProgramCS();
UpdateConsoleProgramCs();
UpdateConsoleMainJs();
if (relinking)
AddItemsPropertiesToProject(projectFile, "<WasmBuildNative>true</WasmBuildNative>");
Expand Down Expand Up @@ -277,6 +305,7 @@ private async Task BrowserRunTwiceWithAndThenWithoutBuildAsync(string config, st
string id = $"browser_{config}_{GetRandomId()}";
string projectFile = CreateWasmTemplateProject(id, "wasmbrowser");

UpdateBrowserProgramCs();
UpdateBrowserMainJs(DefaultTargetFramework);

if (!string.IsNullOrEmpty(extraProperties))
Expand Down Expand Up @@ -310,7 +339,7 @@ private Task ConsoleRunWithAndThenWithoutBuildAsync(string config, string extraP
string id = $"console_{config}_{GetRandomId()}";
string projectFile = CreateWasmTemplateProject(id, "wasmconsole");

UpdateProgramCS();
UpdateConsoleProgramCs();
UpdateConsoleMainJs();

if (!string.IsNullOrEmpty(extraProperties))
Expand Down Expand Up @@ -374,7 +403,7 @@ public void ConsolePublishAndRun(string config, bool aot, bool relinking)
string projectFile = CreateWasmTemplateProject(id, "wasmconsole");
string projectName = Path.GetFileNameWithoutExtension(projectFile);

UpdateProgramCS();
UpdateConsoleProgramCs();
UpdateConsoleMainJs();

if (aot)
Expand Down Expand Up @@ -436,6 +465,9 @@ public async Task BrowserBuildAndRun(string extraNewArgs, string targetFramework
string id = $"browser_{config}_{GetRandomId()}";
CreateWasmTemplateProject(id, "wasmbrowser", extraNewArgs, addFrameworkArg: extraNewArgs.Length == 0);

if (targetFramework != "net8.0")
UpdateBrowserProgramCs();

UpdateBrowserMainJs(targetFramework, runtimeAssetsRelativePath);

new DotNetCommand(s_buildEnv, _testOutput)
Expand Down Expand Up @@ -518,7 +550,7 @@ public void Test_WasmStripILAfterAOT(string stripILAfterAOT, bool expectILStripp
string projectDirectory = Path.GetDirectoryName(projectFile)!;
bool aot = true;

UpdateProgramCS();
UpdateConsoleProgramCs();
UpdateConsoleMainJs();

string extraProperties = "<RunAOTCompilation>true</RunAOTCompilation>";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@

#nullable enable

namespace Wasm.Build.Templates.Tests;
namespace Wasm.Build.Tests.TestAppScenarios;

public class InterpPgoTests : WasmTemplateTestBase
public class InterpPgoTests : AppTestBase
{
public InterpPgoTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
: base(output, buildContext)
Expand All @@ -32,41 +32,11 @@ public async Task FirstRunGeneratesTableAndSecondRunLoadsIt(string config)
// Invoking it too many times makes the test meaningfully slower.
const int iterationCount = 70;

string id = $"browser_{config}_{GetRandomId()}";
_testOutput.WriteLine("/// Creating project");
string projectFile = CreateWasmTemplateProject(id, "wasmbrowser", extraProperties: "<WasmDebugLevel>0</WasmDebugLevel>");

_testOutput.WriteLine("/// Updating JS");
UpdateBrowserMainJs((js) => {
// We need to capture INTERNAL so we can explicitly save the PGO table
js = js.Replace(
"const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotnet",
"const { setModuleImports, getAssemblyExports, getConfig, runMain, INTERNAL } = await dotnet"
);
// Enable interpreter PGO + interpreter PGO logging + console output capturing
js = js.Replace(
".create()",
".withConsoleForwarding().withElementOnExit().withExitCodeLogging().withExitOnUnhandledError().withRuntimeOptions(['--interp-pgo-logging']).withInterpreterPgo(true).create()"
);
js = js.Replace("runMain()", "dotnet.run()");
// Call Greeting in a loop to exercise enough code to cause something to tier,
// then call INTERNAL.interp_pgo_save_data() to save the interp PGO table
js = js.Replace(
"const text = exports.MyClass.Greeting();",
"console.log(`WASM debug level ${getConfig().debugLevel}`);\n" +
"let text = '';\n" +
$"for (let i = 0; i < {iterationCount}; i++) {{ text = exports.MyClass.Greeting(); }};\n" +
"await INTERNAL.interp_pgo_save_data();"
);
return js;
}, DefaultTargetFramework);
CopyTestAsset("WasmBasicTestApp", "InterpPgoTest", "App");

_testOutput.WriteLine("/// Building");

new DotNetCommand(s_buildEnv, _testOutput)
.WithWorkingDirectory(_projectDir!)
.Execute($"build -c {config} -bl:{Path.Combine(s_buildEnv.LogRootPath, $"{id}.binlog")}")
.EnsureSuccessful();
BuildProject(config, extraArgs: "-p:WasmDebugLevel=0");

_testOutput.WriteLine("/// Starting server");

Expand All @@ -75,19 +45,23 @@ public async Task FirstRunGeneratesTableAndSecondRunLoadsIt(string config)
using var runCommand = new RunCommand(s_buildEnv, _testOutput)
.WithWorkingDirectory(_projectDir!);
await using var runner = new BrowserRunner(_testOutput);
var url = await runner.StartServerAndGetUrlAsync(runCommand, $"run --no-silent -c {config} --no-build --project \"{projectFile}\" --forward-console");
var url = await runner.StartServerAndGetUrlAsync(runCommand, $"run --no-silent -c {config} --no-build --project \"{_projectDir!}\" --forward-console");
url = $"{url}?test=InterpPgoTest&iterationCount={iterationCount}";

_testOutput.WriteLine($"/// Spawning browser at URL {url}");

IBrowser browser = await runner.SpawnBrowserAsync(url);
IBrowserContext context = await browser.NewContextAsync();

string output;
{
_testOutput.WriteLine("/// First run");
var page = await runner.RunAsync(context, url);
await runner.WaitForExitMessageAsync(TimeSpan.FromSeconds(30));
await runner.WaitForExitMessageAsync(TimeSpan.FromSeconds(6 * 30));
lock (runner.OutputLines)
output = string.Join(Environment.NewLine, runner.OutputLines);

Assert.Contains("Hello, Browser!", output);
Assert.Contains("Hello, World!", output);
// Verify that no PGO table was located in cache
Assert.Contains("Failed to load interp_pgo table", output);
// Verify that the table was saved after the app ran
Expand All @@ -107,7 +81,7 @@ public async Task FirstRunGeneratesTableAndSecondRunLoadsIt(string config)
lock (runner.OutputLines)
output = string.Join(Environment.NewLine, runner.OutputLines);

Assert.Contains("Hello, Browser!", output);
Assert.Contains("Hello, World!", output);
// Verify that table data was loaded from cache
// if this breaks, it could be caused by change in config which affects the config hash and the cache storage hash key
Assert.Contains(" bytes of interp_pgo data (table size == ", output);
Expand Down
51 changes: 44 additions & 7 deletions src/mono/wasm/templates/templates/browser/Program.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,55 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

Console.WriteLine("Hello, Browser!");

public partial class MyClass
if (args.Length == 1 && args[0] == "start")
StopwatchSample.Start();

while(true)
{
StopwatchSample.Render();
await Task.Delay(1000);
}

partial class StopwatchSample
{
private static Stopwatch stopwatch = new();

public static void Start() => stopwatch.Start();
public static void Render() => SetInnerText("#time", stopwatch.Elapsed.ToString(@"mm\:ss"));

[JSImport("dom.setInnerText", "main.js")]
internal static partial void SetInnerText(string selector, string content);

[JSExport]
internal static string Greeting()
internal static bool Toggle()
{
var text = $"Hello, World! Greetings from {GetHRef()}";
Console.WriteLine(text);
return text;
if (stopwatch.IsRunning)
{
stopwatch.Stop();
return false;
}
else
{
stopwatch.Start();
return true;
}
}

[JSImport("window.location.href", "main.js")]
internal static partial string GetHRef();
[JSExport]
internal static void Reset()
{
if (stopwatch.IsRunning)
stopwatch.Restart();
else
stopwatch.Reset();

Render();
}

[JSExport]
internal static bool IsRunning() => stopwatch.IsRunning;
}
9 changes: 8 additions & 1 deletion src/mono/wasm/templates/templates/browser/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@
</head>

<body>
<span id="out"></span>
<h1>Stopwatch</h1>
<p>
Time elapsed in .NET is <span id="time"><i>loading...</i></span>
</p>
<p>
<button id="pause">Pause</button>
<button id="reset">Reset</button>
</p>
</body>

</html>
23 changes: 14 additions & 9 deletions src/mono/wasm/templates/templates/browser/wwwroot/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,29 @@
import { dotnet } from './_framework/dotnet.js'

const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotnet
.withDiagnosticTracing(false)
.withApplicationArgumentsFromQuery()
.withApplicationArguments("start")
.create();

setModuleImports('main.js', {
window: {
location: {
href: () => globalThis.window.location.href
}
dom: {
setInnerText: (selector, time) => document.querySelector(selector).innerText = time
}
});

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
const text = exports.MyClass.Greeting();
console.log(text);

document.getElementById('out').innerHTML = text;
document.getElementById('reset').addEventListener('click', e => {
exports.StopwatchSample.Reset();
e.preventDefault();
});

const pauseButton = document.getElementById('pause');
pauseButton.addEventListener('click', e => {
const isRunning = exports.StopwatchSample.Toggle();
pauseButton.innerText = isRunning ? 'Pause' : 'Start';
e.preventDefault();
});

// run the C# Main() method and keep the runtime process running and executing further API calls
await runMain();
Loading

0 comments on commit 0549076

Please sign in to comment.