Skip to content

Commit

Permalink
[browser] Extension agnostic lazy assembly loading (#104793)
Browse files Browse the repository at this point in the history
  • Loading branch information
maraf authored Jul 23, 2024
1 parent 705a548 commit dc5a4a1
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 25 deletions.
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

0 comments on commit dc5a4a1

Please sign in to comment.