Skip to content

Commit

Permalink
[browser] Migrate more Blazor features, prepare JavaScript API for Bl…
Browse files Browse the repository at this point in the history
…azor cleanup (#87959)

* Lazy assembly loading
* Satellite assembly loading
* Library initializers
* API cleanup
* WBT for new features
  • Loading branch information
maraf authored Jul 13, 2023
1 parent 4d5d2dc commit acccc01
Show file tree
Hide file tree
Showing 54 changed files with 1,646 additions and 196 deletions.
3 changes: 3 additions & 0 deletions eng/testing/scenarios/BuildWasmAppsJobsList.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ Wasm.Build.Tests.WasmNativeDefaultsTests
Wasm.Build.Tests.WasmRunOutOfAppBundleTests
Wasm.Build.Tests.WasmSIMDTests
Wasm.Build.Tests.WasmTemplateTests
Wasm.Build.Tests.TestAppScenarios.LazyLoadingTests
Wasm.Build.Tests.TestAppScenarios.LibraryInitializerTests
Wasm.Build.Tests.TestAppScenarios.SatelliteLoadingTests
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
<Reference Include="System.Net.Primitives" />
<Reference Include="System.Runtime" />
<Reference Include="System.Runtime.InteropServices" />
<Reference Include="System.Runtime.Loader" />
<Reference Include="System.Threading" />
<Reference Include="System.Threading.Thread" />
<Reference Include="System.Threading.Channels" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
Expand Down Expand Up @@ -93,6 +95,41 @@ public static void CallEntrypoint(JSMarshalerArgument* arguments_buffer)
}
}

public static void LoadLazyAssembly(JSMarshalerArgument* arguments_buffer)
{
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0];
ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];
ref JSMarshalerArgument arg_2 = ref arguments_buffer[3];
try
{
arg_1.ToManaged(out byte[]? dllBytes);
arg_2.ToManaged(out byte[]? pdbBytes);

if (dllBytes != null)
JSHostImplementation.LoadLazyAssembly(dllBytes, pdbBytes);
}
catch (Exception ex)
{
arg_exc.ToJS(ex);
}
}

public static void LoadSatelliteAssembly(JSMarshalerArgument* arguments_buffer)
{
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0];
ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];
try
{
arg_1.ToManaged(out byte[]? dllBytes);

if (dllBytes != null)
JSHostImplementation.LoadSatelliteAssembly(dllBytes);
}
catch (Exception ex)
{
arg_exc.ToJS(ex);
}
}

// The JS layer invokes this method when the JS wrapper for a JS owned object
// has been collected by the JS garbage collector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,10 @@ internal static unsafe partial class JavaScriptImports
public static partial JSObject GetDotnetInstance();
[JSImport("INTERNAL.dynamic_import")]
public static partial Task<JSObject> DynamicImport(string moduleName, string moduleUrl);

#if DEBUG
[JSImport("globalThis.console.log")]
public static partial void Log([JSMarshalAs<JSType.String>] string message);
#endif
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.Tasks;
using System.Reflection;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Threading;
using System.Threading.Tasks;

namespace System.Runtime.InteropServices.JavaScript
{
Expand Down Expand Up @@ -198,6 +200,21 @@ public static JSObject CreateCSOwnedProxy(nint jsHandle)
return res;
}

[Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")]
public static void LoadLazyAssembly(byte[] dllBytes, byte[]? pdbBytes)
{
if (pdbBytes == null)
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes));
else
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes), new MemoryStream(pdbBytes));
}

[Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")]
public static void LoadSatelliteAssembly(byte[] dllBytes)
{
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes));
}

#if FEATURE_WASM_THREADS
public static void InstallWebWorkerInterop(bool installJSSynchronizationContext, bool isMainThread)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ Copyright (c) .NET Foundation. All rights reserved.

<Target Name="_ResolveWasmConfiguration" DependsOnTargets="_ResolveGlobalizationConfiguration">
<PropertyGroup>
<_TargetingNET80OrLater>$([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '8.0'))</_TargetingNET80OrLater>
<_TargetingNET80OrLater>false</_TargetingNET80OrLater>
<_TargetingNET80OrLater Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '8.0'))">true</_TargetingNET80OrLater>

<_BlazorEnableTimeZoneSupport>$(BlazorEnableTimeZoneSupport)</_BlazorEnableTimeZoneSupport>
<_BlazorEnableTimeZoneSupport Condition="'$(_BlazorEnableTimeZoneSupport)' == ''">true</_BlazorEnableTimeZoneSupport>
Expand All @@ -180,11 +181,14 @@ Copyright (c) .NET Foundation. All rights reserved.
<_WasmEnableThreads Condition="'$(_WasmEnableThreads)' == ''">false</_WasmEnableThreads>

<_WasmEnableWebcil>$(WasmEnableWebcil)</_WasmEnableWebcil>
<_WasmEnableWebcil Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp' or '$(_TargetingNET80OrLater)' != 'true'">false</_WasmEnableWebcil>
<_WasmEnableWebcil Condition="'$(_TargetingNET80OrLater)' != 'true'">false</_WasmEnableWebcil>
<_WasmEnableWebcil Condition="'$(_WasmEnableWebcil)' == ''">true</_WasmEnableWebcil>
<_BlazorWebAssemblyStartupMemoryCache>$(BlazorWebAssemblyStartupMemoryCache)</_BlazorWebAssemblyStartupMemoryCache>
<_BlazorWebAssemblyJiterpreter>$(BlazorWebAssemblyJiterpreter)</_BlazorWebAssemblyJiterpreter>
<_BlazorWebAssemblyRuntimeOptions>$(BlazorWebAssemblyRuntimeOptions)</_BlazorWebAssemblyRuntimeOptions>
<_WasmDebugLevel>$(WasmDebugLevel)</_WasmDebugLevel>
<_WasmDebugLevel Condition="'$(_WasmDebugLevel)' == ''">0</_WasmDebugLevel>
<_WasmDebugLevel Condition="'$(_WasmDebugLevel)' == '0' and ('$(DebuggerSupport)' == 'true' or '$(Configuration)' == 'Debug')">-1</_WasmDebugLevel>

<!-- Workaround for https://github.com/dotnet/sdk/issues/12114-->
<PublishDir Condition="'$(AppendRuntimeIdentifierToOutputPath)' != 'true' AND '$(PublishDir)' == '$(OutputPath)$(RuntimeIdentifier)\$(PublishDirName)\'">$(OutputPath)$(PublishDirName)\</PublishDir>
Expand Down Expand Up @@ -343,6 +347,7 @@ Copyright (c) .NET Foundation. All rights reserved.
AssemblyPath="@(IntermediateAssembly)"
Resources="@(_WasmOutputWithHash)"
DebugBuild="true"
DebugLevel="$(_WasmDebugLevel)"
LinkerEnabled="false"
CacheBootResources="$(BlazorCacheBootResources)"
OutputPath="$(_WasmBuildBootJsonPath)"
Expand All @@ -355,7 +360,10 @@ Copyright (c) .NET Foundation. All rights reserved.
StartupMemoryCache="$(_BlazorWebAssemblyStartupMemoryCache)"
Jiterpreter="$(_BlazorWebAssemblyJiterpreter)"
RuntimeOptions="$(_BlazorWebAssemblyRuntimeOptions)"
Extensions="@(WasmBootConfigExtension)" />
Extensions="@(WasmBootConfigExtension)"
TargetFrameworkVersion="$(TargetFrameworkVersion)"
LibraryInitializerOnRuntimeConfigLoaded="@(WasmLibraryInitializerOnRuntimeConfigLoaded)"
LibraryInitializerOnRuntimeReady="@(WasmLibraryInitializerOnRuntimeReady)" />

<ItemGroup>
<FileWrites Include="$(_WasmBuildBootJsonPath)" />
Expand Down Expand Up @@ -530,6 +538,7 @@ Copyright (c) .NET Foundation. All rights reserved.
AssemblyPath="@(IntermediateAssembly)"
Resources="@(_WasmPublishBootResourceWithHash)"
DebugBuild="false"
DebugLevel="$(_WasmDebugLevel)"
LinkerEnabled="$(PublishTrimmed)"
CacheBootResources="$(BlazorCacheBootResources)"
OutputPath="$(IntermediateOutputPath)blazor.publish.boot.json"
Expand All @@ -542,7 +551,10 @@ Copyright (c) .NET Foundation. All rights reserved.
StartupMemoryCache="$(_BlazorWebAssemblyStartupMemoryCache)"
Jiterpreter="$(_BlazorWebAssemblyJiterpreter)"
RuntimeOptions="$(_BlazorWebAssemblyRuntimeOptions)"
Extensions="@(WasmBootConfigExtension)" />
Extensions="@(WasmBootConfigExtension)"
TargetFrameworkVersion="$(TargetFrameworkVersion)"
LibraryInitializerOnRuntimeConfigLoaded="@(WasmLibraryInitializerOnRuntimeConfigLoaded)"
LibraryInitializerOnRuntimeReady="@(WasmLibraryInitializerOnRuntimeReady)" />

<ItemGroup>
<FileWrites Include="$(IntermediateOutputPath)blazor.publish.boot.json" />
Expand Down
8 changes: 6 additions & 2 deletions src/mono/wasm/Wasm.Build.Tests/BrowserRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ internal class BrowserRunner : IAsyncDisposable
public BrowserRunner(ITestOutputHelper testOutput) => _testOutput = testOutput;

// FIXME: options
public async Task<IPage> RunAsync(ToolCommand cmd, string args, bool headless = true, Action<IConsoleMessage>? onConsoleMessage = null)
public async Task<IPage> RunAsync(ToolCommand cmd, string args, bool headless = true, Action<IConsoleMessage>? onConsoleMessage = null, Func<string, string>? modifyBrowserUrl = null)
{
TaskCompletionSource<string> urlAvailable = new();
Action<string?> outputHandler = msg =>
Expand Down Expand Up @@ -89,10 +89,14 @@ public async Task<IPage> RunAsync(ToolCommand cmd, string args, bool headless =
Args = chromeArgs
});

string browserUrl = urlAvailable.Task.Result;
if (modifyBrowserUrl != null)
browserUrl = modifyBrowserUrl(browserUrl);

IPage page = await Browser.NewPageAsync();
if (onConsoleMessage is not null)
page.Console += (_, msg) => onConsoleMessage(msg);
await page.GotoAsync(urlAvailable.Task.Result);
await page.GotoAsync(browserUrl);
RunTask = runTask;
return page;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;

#nullable enable

namespace Wasm.Build.Tests.TestAppScenarios;

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

[Theory]
[InlineData("Development")]
[InlineData("Production")]
public async Task LoadAppSettingsBasedOnApplicationEnvironment(string applicationEnvironment)
{
CopyTestAsset("WasmBasicTestApp", "AppSettingsTests");
PublishProject("Debug");

var result = await RunSdkStyleApp(new(
Configuration: "Debug",
ForPublish: true,
TestScenario: "AppSettingsTest",
BrowserQueryString: new Dictionary<string, string> { ["applicationEnvironment"] = applicationEnvironment }
));
Assert.Collection(
result.TestOutput,
m => Assert.Equal(GetFileExistenceMessage("/appsettings.json", true), m),
m => Assert.Equal(GetFileExistenceMessage("/appsettings.Development.json", applicationEnvironment == "Development"), m),
m => Assert.Equal(GetFileExistenceMessage("/appsettings.Production.json", applicationEnvironment == "Production"), m)
);
}

// Synchronize with AppSettingsTest
private static string GetFileExistenceMessage(string path, bool expected) => $"'{path}' exists '{expected}'";
}
133 changes: 133 additions & 0 deletions src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/AppTestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// 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.IO;
using System.Linq;
using System.Security.Authentication.ExtendedProtection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Playwright;
using Xunit.Abstractions;

namespace Wasm.Build.Tests.TestAppScenarios;

public abstract class AppTestBase : BuildTestBase
{
protected AppTestBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
: base(output, buildContext)
{
}

protected string Id { get; set; }
protected string LogPath { get; set; }

protected void CopyTestAsset(string assetName, string generatedProjectNamePrefix = null)
{
Id = $"{generatedProjectNamePrefix ?? assetName}_{Path.GetRandomFileName()}";
InitBlazorWasmProjectDir(Id);

LogPath = Path.Combine(s_buildEnv.LogRootPath, Id);
Utils.DirectoryCopy(Path.Combine(BuildEnvironment.TestAssetsPath, assetName), Path.Combine(_projectDir!));
}

protected void BuildProject(string configuration)
{
CommandResult result = CreateDotNetCommand().ExecuteWithCapturedOutput("build", $"-bl:{GetBinLogFilePath()}", $"-p:Configuration={configuration}");
result.EnsureSuccessful();
}

protected void PublishProject(string configuration)
{
CommandResult result = CreateDotNetCommand().ExecuteWithCapturedOutput("publish", $"-bl:{GetBinLogFilePath()}", $"-p:Configuration={configuration}");
result.EnsureSuccessful();
}

protected string GetBinLogFilePath(string suffix = null)
{
if (!string.IsNullOrEmpty(suffix))
suffix = "_" + suffix;

return Path.Combine(LogPath, $"{Id}{suffix}.binlog");
}

protected ToolCommand CreateDotNetCommand() => new DotNetCommand(s_buildEnv, _testOutput)
.WithWorkingDirectory(_projectDir!)
.WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir);

protected async Task<RunResult> RunSdkStyleApp(RunOptions options)
{
string runArgs = $"{s_xharnessRunnerCommand} wasm webserver --app=. --web-server-use-default-files";
string workingDirectory = Path.GetFullPath(Path.Combine(FindBlazorBinFrameworkDir(options.Configuration, forPublish: options.ForPublish), ".."));

using var runCommand = new RunCommand(s_buildEnv, _testOutput)
.WithWorkingDirectory(workingDirectory);

var tcs = new TaskCompletionSource<int>();

List<string> testOutput = new();
List<string> consoleOutput = new();
Regex exitRegex = new Regex("WASM EXIT (?<exitCode>[0-9]+)$");

await using var runner = new BrowserRunner(_testOutput);

IPage page = null;

string queryString = "?test=" + options.TestScenario;
if (options.BrowserQueryString != null)
queryString += "&" + string.Join("&", options.BrowserQueryString.Select(kvp => $"{kvp.Key}={kvp.Value}"));

page = await runner.RunAsync(runCommand, runArgs, onConsoleMessage: OnConsoleMessage, modifyBrowserUrl: url => url + queryString);

void OnConsoleMessage(IConsoleMessage msg)
{
if (EnvironmentVariables.ShowBuildOutput)
Console.WriteLine($"[{msg.Type}] {msg.Text}");

_testOutput.WriteLine($"[{msg.Type}] {msg.Text}");
consoleOutput.Add(msg.Text);

const string testOutputPrefix = "TestOutput -> ";
if (msg.Text.StartsWith(testOutputPrefix))
testOutput.Add(msg.Text.Substring(testOutputPrefix.Length));

var exitMatch = exitRegex.Match(msg.Text);
if (exitMatch.Success)
tcs.TrySetResult(int.Parse(exitMatch.Groups["exitCode"].Value));

if (msg.Text.StartsWith("Error: Missing test scenario"))
throw new Exception(msg.Text);

if (options.OnConsoleMessage != null)
options.OnConsoleMessage(msg, page);
}

TimeSpan timeout = TimeSpan.FromMinutes(2);
await Task.WhenAny(tcs.Task, Task.Delay(timeout));
if (!tcs.Task.IsCompleted)
throw new Exception($"Timed out after {timeout.TotalSeconds}s waiting for process to exit");

int wasmExitCode = tcs.Task.Result;
if (options.ExpectedExitCode != null && wasmExitCode != options.ExpectedExitCode)
throw new Exception($"Expected exit code {options.ExpectedExitCode} but got {wasmExitCode}");

return new(wasmExitCode, testOutput, consoleOutput);
}

protected record RunOptions(
string Configuration,
string TestScenario,
Dictionary<string, string> BrowserQueryString = null,
bool ForPublish = false,
Action<IConsoleMessage, IPage> OnConsoleMessage = null,
int? ExpectedExitCode = 0
);

protected record RunResult(
int ExitCode,
IReadOnlyCollection<string> TestOutput,
IReadOnlyCollection<string> ConsoleOutput
);
}
Loading

0 comments on commit acccc01

Please sign in to comment.