Skip to content

Commit 1aafc6f

Browse files
authored
[browser] Allow download retry+throttling in unified code with Blazor (#88910)
1 parent 342bcdb commit 1aafc6f

File tree

8 files changed

+113
-8
lines changed

8 files changed

+113
-8
lines changed

eng/testing/scenarios/BuildWasmAppsJobsList.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ Wasm.Build.Tests.WasmRunOutOfAppBundleTests
3434
Wasm.Build.Tests.WasmSIMDTests
3535
Wasm.Build.Tests.WasmTemplateTests
3636
Wasm.Build.Tests.WorkloadTests
37+
Wasm.Build.Tests.TestAppScenarios.DownloadResourceProgressTests

src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/AppTestBase.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ protected async Task<RunResult> RunSdkStyleApp(RunOptions options)
7777
if (options.BrowserQueryString != null)
7878
queryString += "&" + string.Join("&", options.BrowserQueryString.Select(kvp => $"{kvp.Key}={kvp.Value}"));
7979

80-
page = await runner.RunAsync(runCommand, runArgs, onConsoleMessage: OnConsoleMessage, modifyBrowserUrl: url => url + queryString);
80+
page = await runner.RunAsync(runCommand, runArgs, onConsoleMessage: OnConsoleMessage, modifyBrowserUrl: url =>
81+
{
82+
url += queryString;
83+
_testOutput.WriteLine($"Opening browser at {url}");
84+
return url;
85+
});
8186

8287
void OnConsoleMessage(IConsoleMessage msg)
8388
{
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Xunit;
10+
using Xunit.Abstractions;
11+
12+
#nullable enable
13+
14+
namespace Wasm.Build.Tests.TestAppScenarios;
15+
16+
public class DownloadResourceProgressTests : AppTestBase
17+
{
18+
public DownloadResourceProgressTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
19+
: base(output, buildContext)
20+
{
21+
}
22+
23+
[Theory]
24+
[InlineData(false)]
25+
[InlineData(true)]
26+
public async Task DownloadProgressFinishes(bool failAssemblyDownload)
27+
{
28+
CopyTestAsset("WasmBasicTestApp", $"DownloadResourceProgressTests_{failAssemblyDownload}");
29+
PublishProject("Debug");
30+
31+
var result = await RunSdkStyleApp(new(
32+
Configuration: "Debug",
33+
ForPublish: true,
34+
TestScenario: "DownloadResourceProgressTest",
35+
BrowserQueryString: new Dictionary<string, string> { ["failAssemblyDownload"] = failAssemblyDownload.ToString().ToLowerInvariant() }
36+
));
37+
Assert.True(
38+
result.TestOutput.Any(m => m.Contains("DownloadResourceProgress: Finished")),
39+
"The download progress test didn't emit expected error message"
40+
);
41+
Assert.True(
42+
result.ConsoleOutput.Any(m => m.Contains("Retrying download")) == failAssemblyDownload,
43+
failAssemblyDownload
44+
? "The download progress test didn't emit expected message about retrying download"
45+
: "The download progress test did emit unexpected message about retrying download"
46+
);
47+
Assert.False(
48+
result.ConsoleOutput.Any(m => m.Contains("Retrying download (2)")),
49+
"The download progress test did emit unexpected message about second download retry"
50+
);
51+
Assert.True(
52+
result.TestOutput.Any(m => m.Contains("Throw error instead of downloading resource") == failAssemblyDownload),
53+
failAssemblyDownload
54+
? "The download progress test didn't emit expected message about failing download"
55+
: "The download progress test did emit unexpected message about failing download"
56+
);
57+
}
58+
}

src/mono/wasm/runtime/dotnet.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,4 +436,4 @@ declare global {
436436
}
437437
declare const createDotnetRuntime: CreateDotnetRuntimeType;
438438

439-
export { AssetEntry, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit };
439+
export { AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit };

src/mono/wasm/runtime/loader/assets.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,14 @@ export async function start_asset_download(asset: AssetEntryInternal): Promise<A
230230
// second attempt only after all first attempts are queued
231231
await loaderHelpers.allDownloadsQueued.promise;
232232
try {
233+
mono_log_debug(`Retrying download '${asset.name}'`);
233234
return await start_asset_download_with_throttle(asset);
234235
} catch (err) {
235236
asset.pendingDownloadInternal = undefined;
236237
// third attempt after small delay
237238
await delay(100);
239+
240+
mono_log_debug(`Retrying download (2) '${asset.name}' after delay`);
238241
return await start_asset_download_with_throttle(asset);
239242
}
240243
}

src/mono/wasm/runtime/loader/blazor/_Integration.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export async function initializeBootConfig(bootConfigResult: BootConfigResult, m
3030
}
3131

3232
let resourcesLoaded = 0;
33-
let totalResources = 0;
33+
const totalResources = new Set<string>();
3434

3535
const behaviorByName = (name: string): AssetBehaviours | "other" => {
3636
return name === "dotnet.native.wasm" ? "dotnetwasm"
@@ -61,13 +61,12 @@ export function setupModuleForBlazor(module: DotnetModuleInternal) {
6161
const type = monoToBlazorAssetTypeMap[asset.behavior];
6262
if (type !== undefined) {
6363
const res = resourceLoader.loadResource(asset.name, asset.resolvedUrl!, asset.hash!, type);
64-
asset.pendingDownload = res;
6564

66-
totalResources++;
65+
totalResources.add(asset.name!);
6766
res.response.then(() => {
6867
resourcesLoaded++;
6968
if (module.onDownloadResourceProgress)
70-
module.onDownloadResourceProgress(resourcesLoaded, totalResources);
69+
module.onDownloadResourceProgress(resourcesLoaded, totalResources.size);
7170
});
7271

7372
return res;

src/mono/wasm/runtime/types/export-types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
import type { IMemoryView } from "../marshal";
5-
import type { CreateDotnetRuntimeType, DotnetModuleConfig, RuntimeAPI, MonoConfig, ModuleAPI, AssetEntry, ResourceRequest, GlobalizationMode } from ".";
5+
import type { CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, MonoConfig, ModuleAPI, AssetEntry, ResourceRequest, GlobalizationMode } from ".";
66
import type { EmscriptenModule } from "./emscripten";
77
import type { dotnet, exit } from "../loader/index";
88

@@ -21,6 +21,6 @@ export default createDotnetRuntime;
2121

2222
export {
2323
EmscriptenModule,
24-
RuntimeAPI, ModuleAPI, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, ResourceRequest, GlobalizationMode,
24+
RuntimeAPI, ModuleAPI, DotnetHostBuilder, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, ResourceRequest, GlobalizationMode,
2525
dotnet, exit
2626
};

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ if (testCase == null) {
1010
exit(2, new Error("Missing test scenario. Supply query argument 'test'."));
1111
}
1212

13+
function testOutput(msg) {
14+
console.log(`TestOutput -> ${msg}`);
15+
}
16+
1317
// Prepare base runtime parameters
1418
dotnet
1519
.withElementOnExit()
@@ -21,6 +25,37 @@ switch (testCase) {
2125
case "AppSettingsTest":
2226
dotnet.withApplicationEnvironment(params.get("applicationEnvironment"));
2327
break;
28+
case "DownloadResourceProgressTest":
29+
if (params.get("failAssemblyDownload") === "true") {
30+
let assemblyCounter = 0;
31+
let failAtAssemblyNumbers = [
32+
Math.floor(Math.random() * 5),
33+
Math.floor(Math.random() * 5) + 5,
34+
Math.floor(Math.random() * 5) + 10
35+
];
36+
dotnet.withDiagnosticTracing(true).withResourceLoader((type, name, defaultUri, integrity) => {
37+
if (type !== "assembly")
38+
return defaultUri;
39+
40+
assemblyCounter++;
41+
if (!failAtAssemblyNumbers.includes(assemblyCounter))
42+
return defaultUri;
43+
44+
testOutput("Throw error instead of downloading resource");
45+
const error = new Error("Simulating a failed fetch");
46+
error.silent = true;
47+
throw error;
48+
});
49+
}
50+
dotnet.withModuleConfig({
51+
onDownloadResourceProgress: (loaded, total) => {
52+
console.log(`DownloadResourceProgress: ${loaded} / ${total}`);
53+
if (loaded === total && loaded !== 0) {
54+
testOutput("DownloadResourceProgress: Finished");
55+
}
56+
}
57+
});
58+
break;
2459
}
2560

2661
const { getAssemblyExports, getConfig, INTERNAL } = await dotnet.create();
@@ -48,9 +83,13 @@ try {
4883
exports.AppSettingsTest.Run();
4984
exit(0);
5085
break;
86+
case "DownloadResourceProgressTest":
87+
exit(0);
88+
break;
5189
default:
5290
console.error(`Unknown test case: ${testCase}`);
5391
exit(3);
92+
break;
5493
}
5594
} catch (e) {
5695
exit(1, e);

0 commit comments

Comments
 (0)