Skip to content

Commit 4e663eb

Browse files
authored
[browser][coreCLR] idempotent lazy download of dll/pdb (#129666)
1 parent afb6588 commit 4e663eb

3 files changed

Lines changed: 45 additions & 9 deletions

File tree

src/mono/wasm/Wasm.Build.Tests/LazyLoadingTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ public async Task LoadLazyAssemblyBeforeItIsNeeded(string lazyLoadingTestExtensi
5050
}
5151
}
5252

53+
[Fact]
54+
public async Task LoadLazyAssemblyTwiceIsIdempotent()
55+
{
56+
// Regression test for the lazy loader rewriting an asset's virtualPath in place while
57+
// fetching it, which broke the lookup (and dedup) on a subsequent load of the same
58+
// assembly - e.g. when Blazor fires OnNavigate more than once - and surfaced as
59+
// "<assembly> must be marked with 'BlazorWebAssemblyLazyLoad' item group ...".
60+
Configuration config = Configuration.Debug;
61+
ProjectInfo info = CopyTestAsset(config, false, TestAsset.WasmBasicTestApp, "LazyLoadingTests");
62+
BuildProject(info, config, new BuildOptions(ExtraMSBuildArgs: "-p:TestLazyLoading=true"));
63+
64+
RunResult result = await RunForBuildWithDotnetRun(new BrowserRunOptions(
65+
config,
66+
TestScenario: "LazyLoadingTest",
67+
BrowserQueryString: new NameValueCollection { { "loadLazyAssemblyTwice", "true" } }
68+
));
69+
70+
Assert.True(result.TestOutput.Any(m => m.Contains("firstJsonLoad=true")), "The first lazy load should report that it loaded the assembly");
71+
Assert.True(result.TestOutput.Any(m => m.Contains("secondJsonLoad=false")), "The second lazy load of the same assembly should be an idempotent no-op");
72+
Assert.True(result.TestOutput.Any(m => m.Contains("FirstName")), "The lazy loading test didn't emit expected message with JSON");
73+
Assert.False(result.ConsoleOutput.Any(m => m.Contains("must be marked with 'BlazorWebAssemblyLazyLoad'")), "Reloading an already-loaded lazy assembly must not throw the 'must be marked' error");
74+
}
75+
5376
[Fact]
5477
public async Task FailOnMissingLazyAssembly()
5578
{

src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,15 @@ try {
270270
break;
271271
}
272272

273-
await INTERNAL.loadLazyAssembly(`Json${lazyAssemblyExtension}`);
273+
const firstJsonLoad = await INTERNAL.loadLazyAssembly(`Json${lazyAssemblyExtension}`);
274+
testOutput(`firstJsonLoad=${firstJsonLoad}`);
275+
if (params.get("loadLazyAssemblyTwice") === "true") {
276+
// Regression test: loading the same lazy assembly a second time must be an
277+
// idempotent no-op (returns false) and must not throw
278+
// "must be marked with 'BlazorWebAssemblyLazyLoad'".
279+
const secondJsonLoad = await INTERNAL.loadLazyAssembly(`Json${lazyAssemblyExtension}`);
280+
testOutput(`secondJsonLoad=${secondJsonLoad}`);
281+
}
274282
exports.LazyLoadingTest.Run();
275283
await INTERNAL.loadLazyAssembly(`LazyLibrary${lazyAssemblyExtension}`);
276284
const { LazyLibrary } = await getAssemblyExports("LazyLibrary");

src/native/libs/Common/JavaScript/loader/assets.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,10 @@ export async function fetchSatelliteAssemblies(culturesToLoad: string[]): Promis
261261
await Promise.all(promises);
262262
}
263263

264+
function lazyAssetFileName(virtualPath: string): string {
265+
return virtualPath.substring(virtualPath.lastIndexOf("/") + 1);
266+
}
267+
264268
export async function fetchLazyAssembly(assemblyNameToLoad: string): Promise<boolean> {
265269
const lazyAssemblies = loaderConfig.resources?.lazyAssembly;
266270
if (!lazyAssemblies) {
@@ -273,12 +277,17 @@ export async function fetchLazyAssembly(assemblyNameToLoad: string): Promise<boo
273277
else if (assemblyNameToLoad.endsWith(".wasm"))
274278
assemblyNameWithoutExtension = assemblyNameToLoad.substring(0, assemblyNameToLoad.length - 5);
275279

280+
if (loadedLazyAssemblies.has(assemblyNameWithoutExtension)) {
281+
return false;
282+
}
283+
276284
const assemblyNameToLoadDll = assemblyNameWithoutExtension + ".dll";
277285
const assemblyNameToLoadWasm = assemblyNameWithoutExtension + ".wasm";
278286

279287
let dllAsset: AssemblyAsset | null = null;
280288
for (const asset of lazyAssemblies) {
281-
if (asset.virtualPath === assemblyNameToLoadDll || asset.virtualPath === assemblyNameToLoadWasm) {
289+
const fileName = lazyAssetFileName(asset.virtualPath);
290+
if (fileName === assemblyNameToLoadDll || fileName === assemblyNameToLoadWasm) {
282291
dllAsset = asset;
283292
break;
284293
}
@@ -288,28 +297,24 @@ export async function fetchLazyAssembly(assemblyNameToLoad: string): Promise<boo
288297
throw new Error(`${assemblyNameToLoad} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);
289298
}
290299

291-
if (loadedLazyAssemblies.has(dllAsset.virtualPath)) {
292-
return false;
293-
}
294-
295300
await fetchAssembly(dllAsset);
296-
loadedLazyAssemblies.add(dllAsset.virtualPath);
301+
loadedLazyAssemblies.add(assemblyNameWithoutExtension);
297302

298303
if (loaderConfig.debugLevel !== 0) {
299304
const pdbNameToLoad = assemblyNameWithoutExtension + ".pdb";
300305
const pdbAssets = loaderConfig.resources?.pdb;
301306
let pdbAssetToLoad: AssemblyAsset | undefined;
302307
if (pdbAssets) {
303308
for (const pdbAsset of pdbAssets) {
304-
if (pdbAsset.virtualPath === pdbNameToLoad) {
309+
if (lazyAssetFileName(pdbAsset.virtualPath) === pdbNameToLoad) {
305310
pdbAssetToLoad = pdbAsset;
306311
break;
307312
}
308313
}
309314
}
310315
if (!pdbAssetToLoad) {
311316
for (const lazyAsset of lazyAssemblies) {
312-
if (lazyAsset.virtualPath === pdbNameToLoad) {
317+
if (lazyAssetFileName(lazyAsset.virtualPath) === pdbNameToLoad) {
313318
pdbAssetToLoad = lazyAsset as AssemblyAsset;
314319
break;
315320
}

0 commit comments

Comments
 (0)