[browser] Allow download retry+throttling in unified code with Blazor (#88910)
authorMarek Fišera <mara@neptuo.com>
Thu, 20 Jul 2023 12:51:07 +0000 (14:51 +0200)
committerGitHub <noreply@github.com>
Thu, 20 Jul 2023 12:51:07 +0000 (14:51 +0200)
eng/testing/scenarios/BuildWasmAppsJobsList.txt
src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/AppTestBase.cs
src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/DownloadResourceProgressTests.cs [new file with mode: 0644]
src/mono/wasm/runtime/dotnet.d.ts
src/mono/wasm/runtime/loader/assets.ts
src/mono/wasm/runtime/loader/blazor/_Integration.ts
src/mono/wasm/runtime/types/export-types.ts
src/mono/wasm/testassets/WasmBasicTestApp/wwwroot/main.js

index 917d402..356cf2e 100644 (file)
@@ -34,3 +34,4 @@ Wasm.Build.Tests.WasmRunOutOfAppBundleTests
 Wasm.Build.Tests.WasmSIMDTests
 Wasm.Build.Tests.WasmTemplateTests
 Wasm.Build.Tests.WorkloadTests
+Wasm.Build.Tests.TestAppScenarios.DownloadResourceProgressTests
index 132d50d..71ce260 100644 (file)
@@ -77,7 +77,12 @@ public abstract class AppTestBase : BlazorWasmTestBase
         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);
+        page = await runner.RunAsync(runCommand, runArgs, onConsoleMessage: OnConsoleMessage, modifyBrowserUrl: url => 
+        {
+            url += queryString;
+            _testOutput.WriteLine($"Opening browser at {url}");
+            return url;
+        });
 
         void OnConsoleMessage(IConsoleMessage msg)
         {
diff --git a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/DownloadResourceProgressTests.cs b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/DownloadResourceProgressTests.cs
new file mode 100644 (file)
index 0000000..0015476
--- /dev/null
@@ -0,0 +1,58 @@
+// 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 DownloadResourceProgressTests : AppTestBase
+{
+    public DownloadResourceProgressTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
+        : base(output, buildContext)
+    {
+    }
+
+    [Theory]
+    [InlineData(false)]
+    [InlineData(true)]
+    public async Task DownloadProgressFinishes(bool failAssemblyDownload)
+    {
+        CopyTestAsset("WasmBasicTestApp", $"DownloadResourceProgressTests_{failAssemblyDownload}");
+        PublishProject("Debug");
+
+        var result = await RunSdkStyleApp(new(
+            Configuration: "Debug",
+            ForPublish: true,
+            TestScenario: "DownloadResourceProgressTest",
+            BrowserQueryString: new Dictionary<string, string> { ["failAssemblyDownload"] = failAssemblyDownload.ToString().ToLowerInvariant() }
+        ));
+        Assert.True(
+            result.TestOutput.Any(m => m.Contains("DownloadResourceProgress: Finished")),
+            "The download progress test didn't emit expected error message"
+        );
+        Assert.True(
+            result.ConsoleOutput.Any(m => m.Contains("Retrying download")) == failAssemblyDownload,
+            failAssemblyDownload
+                ? "The download progress test didn't emit expected message about retrying download"
+                : "The download progress test did emit unexpected message about retrying download"
+        );
+        Assert.False(
+            result.ConsoleOutput.Any(m => m.Contains("Retrying download (2)")),
+            "The download progress test did emit unexpected message about second download retry"
+        );
+        Assert.True(
+            result.TestOutput.Any(m => m.Contains("Throw error instead of downloading resource") == failAssemblyDownload),
+            failAssemblyDownload
+                ? "The download progress test didn't emit expected message about failing download"
+                : "The download progress test did emit unexpected message about failing download"
+        );
+    }
+}
index 373d031..e42d6e1 100644 (file)
@@ -436,4 +436,4 @@ declare global {
 }
 declare const createDotnetRuntime: CreateDotnetRuntimeType;
 
-export { AssetEntry, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit };
+export { AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit };
index c6f0ddf..5cf6ec9 100644 (file)
@@ -230,11 +230,14 @@ export async function start_asset_download(asset: AssetEntryInternal): Promise<A
         // second attempt only after all first attempts are queued
         await loaderHelpers.allDownloadsQueued.promise;
         try {
+            mono_log_debug(`Retrying download '${asset.name}'`);
             return await start_asset_download_with_throttle(asset);
         } catch (err) {
             asset.pendingDownloadInternal = undefined;
             // third attempt after small delay
             await delay(100);
+
+            mono_log_debug(`Retrying download (2) '${asset.name}' after delay`);
             return await start_asset_download_with_throttle(asset);
         }
     }
index 8e58797..2cfdaac 100644 (file)
@@ -30,7 +30,7 @@ export async function initializeBootConfig(bootConfigResult: BootConfigResult, m
 }
 
 let resourcesLoaded = 0;
-let totalResources = 0;
+const totalResources = new Set<string>();
 
 const behaviorByName = (name: string): AssetBehaviours | "other" => {
     return name === "dotnet.native.wasm" ? "dotnetwasm"
@@ -61,13 +61,12 @@ export function setupModuleForBlazor(module: DotnetModuleInternal) {
         const type = monoToBlazorAssetTypeMap[asset.behavior];
         if (type !== undefined) {
             const res = resourceLoader.loadResource(asset.name, asset.resolvedUrl!, asset.hash!, type);
-            asset.pendingDownload = res;
 
-            totalResources++;
+            totalResources.add(asset.name!);
             res.response.then(() => {
                 resourcesLoaded++;
                 if (module.onDownloadResourceProgress)
-                    module.onDownloadResourceProgress(resourcesLoaded, totalResources);
+                    module.onDownloadResourceProgress(resourcesLoaded, totalResources.size);
             });
 
             return res;
index aa7f7e0..af00c84 100644 (file)
@@ -2,7 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 import type { IMemoryView } from "../marshal";
-import type { CreateDotnetRuntimeType, DotnetModuleConfig, RuntimeAPI, MonoConfig, ModuleAPI, AssetEntry, ResourceRequest, GlobalizationMode } from ".";
+import type { CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, MonoConfig, ModuleAPI, AssetEntry, ResourceRequest, GlobalizationMode } from ".";
 import type { EmscriptenModule } from "./emscripten";
 import type { dotnet, exit } from "../loader/index";
 
@@ -21,6 +21,6 @@ export default createDotnetRuntime;
 
 export {
     EmscriptenModule,
-    RuntimeAPI, ModuleAPI, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, ResourceRequest, GlobalizationMode,
+    RuntimeAPI, ModuleAPI, DotnetHostBuilder, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, ResourceRequest, GlobalizationMode,
     dotnet, exit
 };
index 6c1ec09..7842472 100644 (file)
@@ -10,6 +10,10 @@ if (testCase == null) {
     exit(2, new Error("Missing test scenario. Supply query argument 'test'."));
 }
 
+function testOutput(msg) {
+    console.log(`TestOutput -> ${msg}`);
+}
+
 // Prepare base runtime parameters
 dotnet
     .withElementOnExit()
@@ -21,6 +25,37 @@ switch (testCase) {
     case "AppSettingsTest":
         dotnet.withApplicationEnvironment(params.get("applicationEnvironment"));
         break;
+    case "DownloadResourceProgressTest":
+        if (params.get("failAssemblyDownload") === "true") {
+            let assemblyCounter = 0;
+            let failAtAssemblyNumbers = [
+                Math.floor(Math.random() * 5),
+                Math.floor(Math.random() * 5) + 5,
+                Math.floor(Math.random() * 5) + 10
+            ];
+            dotnet.withDiagnosticTracing(true).withResourceLoader((type, name, defaultUri, integrity) => {
+                if (type !== "assembly")
+                    return defaultUri;
+
+                assemblyCounter++;
+                if (!failAtAssemblyNumbers.includes(assemblyCounter))
+                    return defaultUri;
+
+                testOutput("Throw error instead of downloading resource");
+                const error = new Error("Simulating a failed fetch");
+                error.silent = true;
+                throw error;
+            });
+        }
+        dotnet.withModuleConfig({
+            onDownloadResourceProgress: (loaded, total) => {
+                console.log(`DownloadResourceProgress: ${loaded} / ${total}`);
+                if (loaded === total && loaded !== 0) {
+                    testOutput("DownloadResourceProgress: Finished");
+                }
+            }
+        });
+        break;
 }
 
 const { getAssemblyExports, getConfig, INTERNAL } = await dotnet.create();
@@ -48,9 +83,13 @@ try {
             exports.AppSettingsTest.Run();
             exit(0);
             break;
+        case "DownloadResourceProgressTest":
+            exit(0);
+            break;
         default:
             console.error(`Unknown test case: ${testCase}`);
             exit(3);
+            break;
     }
 } catch (e) {
     exit(1, e);