Skip to content

[release/9.0-preview7] [browser] Extension agnostic lazy assembly loading #105298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
30 changes: 17 additions & 13 deletions src/mono/browser/runtime/lazyLoading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,38 @@ import { AssetEntry } from "./types";

export async function loadLazyAssembly (assemblyNameToLoad: string): Promise<boolean> {
const resources = loaderHelpers.config.resources!;
const originalAssemblyName = assemblyNameToLoad;
const lazyAssemblies = resources.lazyAssembly;
if (!lazyAssemblies) {
throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly.");
}

let assemblyNameWithoutExtension = assemblyNameToLoad;
if (assemblyNameToLoad.endsWith(".dll"))
assemblyNameWithoutExtension = assemblyNameToLoad.substring(0, assemblyNameToLoad.length - 4);
else if (assemblyNameToLoad.endsWith(".wasm"))
assemblyNameWithoutExtension = assemblyNameToLoad.substring(0, assemblyNameToLoad.length - 5);

const assemblyNameToLoadDll = assemblyNameWithoutExtension + ".dll";
const assemblyNameToLoadWasm = assemblyNameWithoutExtension + ".wasm";
if (loaderHelpers.config.resources!.fingerprinting) {
const map = loaderHelpers.config.resources!.fingerprinting;
for (const fingerprintedName in map) {
const nonFingerprintedName = map[fingerprintedName];
if (nonFingerprintedName == assemblyNameToLoad) {
if (nonFingerprintedName == assemblyNameToLoadDll || nonFingerprintedName == assemblyNameToLoadWasm) {
assemblyNameToLoad = fingerprintedName;
break;
}
}
}

if (!lazyAssemblies[assemblyNameToLoad]) {
throw new Error(`${assemblyNameToLoad} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);
if (lazyAssemblies[assemblyNameToLoadDll]) {
assemblyNameToLoad = assemblyNameToLoadDll;
} else if (lazyAssemblies[assemblyNameToLoadWasm]) {
assemblyNameToLoad = assemblyNameToLoadWasm;
} else {
throw new Error(`${assemblyNameToLoad} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);
}
}

const dllAsset: AssetEntry = {
Expand All @@ -38,7 +51,7 @@ export async function loadLazyAssembly (assemblyNameToLoad: string): Promise<boo
return false;
}

let pdbNameToLoad = changeExtension(originalAssemblyName, ".pdb");
let pdbNameToLoad = assemblyNameWithoutExtension + ".pdb";
let shouldLoadPdb = false;
if (loaderHelpers.config.debugLevel != 0 && loaderHelpers.isDebuggingSupported()) {
shouldLoadPdb = Object.prototype.hasOwnProperty.call(lazyAssemblies, pdbNameToLoad);
Expand Down Expand Up @@ -81,12 +94,3 @@ export async function loadLazyAssembly (assemblyNameToLoad: string): Promise<boo
load_lazy_assembly(dll, pdb);
return true;
}

function changeExtension (filename: string, newExtensionWithLeadingDot: string) {
const lastDotIndex = filename.lastIndexOf(".");
if (lastDotIndex < 0) {
throw new Error(`No extension to replace in '${filename}'`);
}

return filename.substring(0, lastDotIndex) + newExtensionWithLeadingDot;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,32 @@ public LazyLoadingTests(ITestOutputHelper output, SharedBuildPerTestClassFixture
{
}

[Fact, TestCategory("no-fingerprinting")]
public async Task LoadLazyAssemblyBeforeItIsNeeded()
public static IEnumerable<object?[]> LoadLazyAssemblyBeforeItIsNeededData()
{
string[] data = ["wasm", "dll", "NoExtension"];
return data.Select(d => new object[] { d, data });
}

[Theory, TestCategory("no-fingerprinting")]
[MemberData(nameof(LoadLazyAssemblyBeforeItIsNeededData))]
public async Task LoadLazyAssemblyBeforeItIsNeeded(string lazyLoadingTestExtension, string[] allLazyLoadingTestExtensions)
{
CopyTestAsset("WasmBasicTestApp", "LazyLoadingTests", "App");
BuildProject("Debug");
BuildProject("Debug", extraArgs: $"-p:LazyLoadingTestExtension={lazyLoadingTestExtension}");

var result = await RunSdkStyleAppForBuild(new(Configuration: "Debug", TestScenario: "LazyLoadingTest"));
// We are running the app and passing all possible lazy extensions to test matrix of all possibilities.
// We don't need to rebuild the application to test how client is trying to load the assembly.
foreach (var clientLazyLoadingTestExtension in allLazyLoadingTestExtensions)
{
var result = await RunSdkStyleAppForBuild(new(
Configuration: "Debug",
TestScenario: "LazyLoadingTest",
BrowserQueryString: new Dictionary<string, string> { ["lazyLoadingTestExtension"] = clientLazyLoadingTestExtension }
));

Assert.True(result.TestOutput.Any(m => m.Contains("FirstName")), "The lazy loading test didn't emit expected message with JSON");
Assert.True(result.ConsoleOutput.Any(m => m.Contains("Attempting to download") && m.Contains("_framework/Json.") && m.Contains(".pdb")), "The lazy loading test didn't load PDB");
Assert.True(result.TestOutput.Any(m => m.Contains("FirstName")), "The lazy loading test didn't emit expected message with JSON");
Assert.True(result.ConsoleOutput.Any(m => m.Contains("Attempting to download") && m.Contains("_framework/Json.") && m.Contains(".pdb")), "The lazy loading test didn't load PDB");
}
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<!-- Lazy loading test various extensions -->
<PropertyGroup>
<LazyAssemblyExtension Condition="'$(LazyLoadingTestExtension)' == 'dll'">.dll</LazyAssemblyExtension>
<LazyAssemblyExtension Condition="'$(LazyLoadingTestExtension)' == 'wasm'">.wasm</LazyAssemblyExtension>
<LazyAssemblyExtension Condition="'$(LazyLoadingTestExtension)' == 'NoExtension'"></LazyAssemblyExtension>
<LazyAssemblyExtension Condition="'$(LazyLoadingTestExtension)' == ''">$(WasmAssemblyExtension)</LazyAssemblyExtension>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Library\Json.csproj" />
</ItemGroup>

<ItemGroup>
<BlazorWebAssemblyLazyLoad Include="Json$(WasmAssemblyExtension)" />
<BlazorWebAssemblyLazyLoad Include="Json$(LazyAssemblyExtension)" />
</ItemGroup>
</Project>
18 changes: 17 additions & 1 deletion src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,23 @@ try {
break;
case "LazyLoadingTest":
if (params.get("loadRequiredAssembly") !== "false") {
await INTERNAL.loadLazyAssembly(`Json${assemblyExtension}`);
let lazyAssemblyExtension = assemblyExtension;
switch (params.get("lazyLoadingTestExtension")) {
case "wasm":
lazyAssemblyExtension = ".wasm";
break;
case "dll":
lazyAssemblyExtension = ".dll";
break;
case "NoExtension":
lazyAssemblyExtension = "";
break;
default:
lazyAssemblyExtension = assemblyExtension;
break;
}

await INTERNAL.loadLazyAssembly(`Json${lazyAssemblyExtension}`);
}
exports.LazyLoadingTest.Run();
exit(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,15 @@ public void WriteBootJson(Stream output, string entryAssemblyName)
{
var endpointByAsset = Endpoints.ToDictionary(e => e.GetMetadata("AssetFile"));

var lazyLoadAssembliesWithoutExtension = (LazyLoadedAssemblies ?? Array.Empty<ITaskItem>()).ToDictionary(l =>
{
var extension = Path.GetExtension(l.ItemSpec);
if (extension == ".dll" || extension == Utils.WebcilInWasmExtension)
return Path.GetFileNameWithoutExtension(l.ItemSpec);

return l.ItemSpec;
});

var remainingLazyLoadAssemblies = new List<ITaskItem>(LazyLoadedAssemblies ?? Array.Empty<ITaskItem>());
var resourceData = result.resources;

Expand All @@ -194,7 +203,7 @@ public void WriteBootJson(Stream output, string entryAssemblyName)
var resourceName = Path.GetFileName(resource.GetMetadata("OriginalItemSpec"));
var resourceRoute = Path.GetFileName(endpointByAsset[resource.ItemSpec].ItemSpec);

if (TryGetLazyLoadedAssembly(resourceName, out var lazyLoad))
if (TryGetLazyLoadedAssembly(lazyLoadAssembliesWithoutExtension, resourceName, out var lazyLoad))
{
MapFingerprintedAsset(resourceData, resourceRoute, resourceName);
Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a lazy loaded assembly.", resource.ItemSpec);
Expand All @@ -220,7 +229,7 @@ public void WriteBootJson(Stream output, string entryAssemblyName)
else if (string.Equals("symbol", assetTraitValue, StringComparison.OrdinalIgnoreCase))
{
MapFingerprintedAsset(resourceData, resourceRoute, resourceName);
if (TryGetLazyLoadedAssembly($"{fileName}.dll", out _) || TryGetLazyLoadedAssembly($"{fileName}{Utils.WebcilInWasmExtension}", out _))
if (TryGetLazyLoadedAssembly(lazyLoadAssembliesWithoutExtension, fileName, out _))
{
Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a lazy loaded symbols file.", resource.ItemSpec);
resourceData.lazyAssembly ??= new ResourceHashesByNameDictionary();
Expand Down Expand Up @@ -463,9 +472,13 @@ private void AddToAdditionalResources(ITaskItem resource, Dictionary<string, Add
}
}

private bool TryGetLazyLoadedAssembly(string fileName, out ITaskItem lazyLoadedAssembly)
private static bool TryGetLazyLoadedAssembly(Dictionary<string, ITaskItem> lazyLoadAssembliesNoExtension, string fileName, out ITaskItem lazyLoadedAssembly)
{
return (lazyLoadedAssembly = LazyLoadedAssemblies?.SingleOrDefault(a => a.ItemSpec == fileName)) != null;
var extension = Path.GetExtension(fileName);
if (extension == ".dll" || extension == Utils.WebcilInWasmExtension)
fileName = Path.GetFileNameWithoutExtension(fileName);

return lazyLoadAssembliesNoExtension.TryGetValue(fileName, out lazyLoadedAssembly);
}

private Version? parsedTargetFrameworkVersion;
Expand Down
Loading