diff --git a/src/Components/Web.JS/src/Boot.Web.ts b/src/Components/Web.JS/src/Boot.Web.ts index 01cadd91a941..488a2faabc00 100644 --- a/src/Components/Web.JS/src/Boot.Web.ts +++ b/src/Components/Web.JS/src/Boot.Web.ts @@ -37,7 +37,6 @@ function boot(options?: Partial) : Promise { started = true; options = options || {}; options.logLevel ??= LogLevel.Error; - Blazor._internal.loadWebAssemblyQuicklyTimeout = 3000; // Defined here to avoid inadvertently imported enhanced navigation // related APIs in WebAssembly or Blazor Server contexts. diff --git a/src/Components/Web.JS/src/GlobalExports.ts b/src/Components/Web.JS/src/GlobalExports.ts index fad0d11cc42e..6bc48be0bb40 100644 --- a/src/Components/Web.JS/src/GlobalExports.ts +++ b/src/Components/Web.JS/src/GlobalExports.ts @@ -78,7 +78,6 @@ export interface IBlazor { receiveWebAssemblyDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void; receiveWebViewDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void; attachWebRendererInterop?: typeof attachWebRendererInterop; - loadWebAssemblyQuicklyTimeout?: number; // JSExport APIs dotNetExports?: { diff --git a/src/Components/Web.JS/src/Services/WebRootComponentManager.ts b/src/Components/Web.JS/src/Services/WebRootComponentManager.ts index 59898d6d9db7..b1e3e3de814d 100644 --- a/src/Components/Web.JS/src/Services/WebRootComponentManager.ts +++ b/src/Components/Web.JS/src/Services/WebRootComponentManager.ts @@ -9,7 +9,6 @@ import { disposeCircuit, hasStartedServer, isCircuitAvailable, startCircuit, sta import { hasLoadedWebAssemblyPlatform, hasStartedLoadingWebAssemblyPlatform, hasStartedWebAssembly, isFirstUpdate, loadWebAssemblyPlatformIfNotStarted, resolveInitialUpdate, setWaitForRootComponents, startWebAssembly, updateWebAssemblyRootComponents, waitForBootConfigLoaded } from '../Boot.WebAssembly.Common'; import { MonoConfig } from 'dotnet-runtime'; import { RootComponentManager } from './RootComponentManager'; -import { Blazor } from '../GlobalExports'; import { getRendererer } from '../Rendering/Renderer'; import { isPageLoading } from './NavigationEnhancement'; import { setShouldPreserveContentOnInteractiveComponentDisposal } from '../Rendering/BrowserRenderer'; @@ -100,12 +99,18 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent return; } - if (descriptor.type === 'auto' || descriptor.type === 'webassembly') { - // Eagerly start loading the WebAssembly runtime, even though we're not - // activating the component yet. This is becuase WebAssembly resources - // may take a long time to load, so starting to load them now potentially reduces - // the time to interactvity. + // When encountering a component with a WebAssembly or Auto render mode, + // start loading the WebAssembly runtime, even though we're not + // activating the component yet. This is becuase WebAssembly resources + // may take a long time to load, so starting to load them now potentially reduces + // the time to interactvity. + if (descriptor.type === 'webassembly') { this.startLoadingWebAssemblyIfNotStarted(); + } else if (descriptor.type === 'auto') { + // If the WebAssembly runtime starts downloading because an Auto component was added to + // the page, we limit the maximum number of parallel WebAssembly resource downloads to 1 + // so that the performance of any Blazor Server circuit is minimally impacted. + this.startLoadingWebAssemblyIfNotStarted(/* maxParallelDownloadsOverride */ 1); } const ssrComponentId = this._nextSsrComponentId++; @@ -120,7 +125,7 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent this.circuitMayHaveNoRootComponents(); } - private async startLoadingWebAssemblyIfNotStarted() { + private async startLoadingWebAssemblyIfNotStarted(maxParallelDownloadsOverride?: number) { if (hasStartedLoadingWebAssemblyPlatform()) { return; } @@ -128,18 +133,12 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent setWaitForRootComponents(); const loadWebAssemblyPromise = loadWebAssemblyPlatformIfNotStarted(); - - // If WebAssembly resources can't be loaded within some time limit, - // we take note of this fact so that "auto" components fall back - // to using Blazor Server. - setTimeout(() => { - if (!hasLoadedWebAssemblyPlatform()) { - this.onWebAssemblyFailedToLoadQuickly(); - } - }, Blazor._internal.loadWebAssemblyQuicklyTimeout); - const bootConfig = await waitForBootConfigLoaded(); + if (maxParallelDownloadsOverride !== undefined) { + bootConfig.maxParallelDownloads = maxParallelDownloadsOverride; + } + if (!areWebAssemblyResourcesLikelyCached(bootConfig)) { // Since WebAssembly resources aren't likely cached, // they will probably need to be fetched over the network. @@ -299,6 +298,8 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent this.updateWebAssemblyRootComponents(batchJson); } } + + this.circuitMayHaveNoRootComponents(); } private updateWebAssemblyRootComponents(operationsJson: string) { diff --git a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs index d16ae8cf226b..ebe2db4675ce 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs @@ -597,22 +597,6 @@ public void DynamicallyAddedSsrComponents_CanGetRemoved_BeforeStreamingRendering AssertBrowserLogDoesNotContainErrors(); } - [Fact] - public void AutoRenderMode_UsesBlazorServer_IfWebAssemblyResourcesTakeTooLongToLoad() - { - Navigate(ServerPathBase); - Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text); - ForceWebAssemblyResourceCacheMiss(); - BlockWebAssemblyResourceLoad(); - - Navigate($"{ServerPathBase}/streaming-interactivity"); - Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text); - - Browser.Click(By.Id(AddAutoPrerenderedId)); - Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text); - Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-0")).Text); - } - [Fact] public void AutoRenderMode_UsesBlazorWebAssembly_AfterAddingWebAssemblyRootComponent() { @@ -659,8 +643,6 @@ public void AutoRenderMode_UsesBlazorServerOnFirstLoad_ThenWebAssemblyOnSuccessi Navigate(ServerPathBase); Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text); BlockWebAssemblyResourceLoad(); - UseLongWebAssemblyLoadTimeout(); - ForceWebAssemblyResourceCacheMiss(); Navigate($"{ServerPathBase}/streaming-interactivity"); Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text); @@ -697,8 +679,6 @@ public void AutoRenderMode_UsesBlazorServer_IfBootResourceHashChanges() Navigate(ServerPathBase); Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text); BlockWebAssemblyResourceLoad(); - UseLongWebAssemblyLoadTimeout(); - ForceWebAssemblyResourceCacheMiss(); Navigate($"{ServerPathBase}/streaming-interactivity"); Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text); @@ -715,14 +695,11 @@ public void AutoRenderMode_UsesBlazorServer_IfBootResourceHashChanges() Browser.Click(By.Id($"remove-counter-link-1")); Browser.DoesNotExist(By.Id("is-interactive-1")); - UseLongWebAssemblyLoadTimeout(); Browser.Navigate().Refresh(); Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text); Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-0")).Text); - BlockWebAssemblyResourceLoad(); - UseLongWebAssemblyLoadTimeout(); ForceWebAssemblyResourceCacheMiss("dummy hash"); Browser.Navigate().Refresh(); @@ -766,8 +743,6 @@ public void AutoRenderMode_CanUseBlazorServer_WhenMultipleAutoComponentsAreAdded Navigate(ServerPathBase); Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text); BlockWebAssemblyResourceLoad(); - UseLongWebAssemblyLoadTimeout(); - ForceWebAssemblyResourceCacheMiss(); Navigate($"{ServerPathBase}/streaming-interactivity"); Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text); @@ -911,6 +886,36 @@ public void AutoRenderMode_UsesBlazorServer_AfterWebAssemblyComponentsNoLongerEx Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-3")).Text); } + [Fact] + public void WebAssemblyRenderMode_DownloadsWebAssemblyResourcesInParallel() + { + Navigate($"{ServerPathBase}/streaming-interactivity?ClearSiteData=True"); + Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text); + + Browser.Click(By.Id(AddWebAssemblyPrerenderedId)); + Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text); + Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-0")).Text); + + Browser.True(() => GetMaxParallelWebAssemblyResourceDownloadCount() > 1); + } + + [Fact] + public void AutoRenderMode_DoesNotDownloadWebAssemblyResourcesInParallel() + { + Navigate($"{ServerPathBase}/streaming-interactivity?ClearSiteData=True"); + Browser.Equal("Not streaming", () => Browser.FindElement(By.Id("status")).Text); + + Browser.Click(By.Id(AddAutoPrerenderedId)); + Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-0")).Text); + Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-0")).Text); + + Browser.Click(By.Id(AddWebAssemblyPrerenderedId)); + Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-1")).Text); + Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-1")).Text); + + Browser.Equal(1, GetMaxParallelWebAssemblyResourceDownloadCount); + } + [Fact] public void Circuit_ShutsDown_WhenAllBlazorServerComponentsGetRemoved() { @@ -1135,6 +1140,9 @@ public void NavigationManagerCanRefreshSSRPageWhenServerInteractivityEnabled() private void BlockWebAssemblyResourceLoad() { + // Force a WebAssembly resource cache miss so that we can fall back to using server interactivity + ForceWebAssemblyResourceCacheMiss(); + ((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('block-load-boot-resource', 'true')"); // Clear caches so that we can block the resource load @@ -1146,11 +1154,6 @@ private void UnblockWebAssemblyResourceLoad() ((IJavaScriptExecutor)Browser).ExecuteScript("window.unblockLoadBootResource()"); } - private void UseLongWebAssemblyLoadTimeout() - { - ((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('use-long-auto-timeout', 'true')"); - } - private void ForceWebAssemblyResourceCacheMiss(string resourceHash = null) { if (resourceHash is not null) @@ -1164,6 +1167,11 @@ private void ForceWebAssemblyResourceCacheMiss(string resourceHash = null) } } + private long GetMaxParallelWebAssemblyResourceDownloadCount() + { + return (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window['__aspnetcore__testing__max__parallel__resource__download__count'] || 0;"); + } + private string InteractiveCallsiteUrl(bool prerender, int? serverIncrement = default, int? webAssemblyIncrement = default) { var result = $"{ServerPathBase}/interactive-callsite?suppress-autostart&prerender={prerender}"; diff --git a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs index be45f9d58423..615fafe6aefb 100644 --- a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs +++ b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs @@ -31,7 +31,7 @@ public StatePersistenceTest( public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext + _nextStreamingIdContext++); - // Validates that we can use persisted state across server, webasembly, and auto modes, with and without + // Validates that we can use persisted state across server, webassembly, and auto modes, with and without // streaming rendering. // For streaming rendering, we validate that the state is captured and restored after streaming completes. // For enhanced navigation we validate that the state is captured at the time components are rendered for @@ -101,6 +101,12 @@ public void CanRenderComponentWithPersistedState(bool suppressEnhancedNavigation RenderComponentsWithPersistentStateAndValidate(suppressEnhancedNavigation, mode, renderMode, streaming, interactiveRuntime: "server"); UnblockWebAssemblyResourceLoad(); + + if (suppressEnhancedNavigation) + { + RenderWebAssemblyComponentAndWaitForWebAssemblyRuntime(returnUrl: Browser.Url); + } + Browser.Navigate().Refresh(); RenderComponentsWithPersistentStateAndValidate(suppressEnhancedNavigation, mode, renderMode, streaming, interactiveRuntime: "wasm"); @@ -123,16 +129,19 @@ public async Task StateIsProvidedEveryTimeACircuitGetsCreated(string streaming) } Browser.Click(By.Id("page-with-components-link")); - RenderComponentsWithPersistentStateAndValidate(suppresEnhancedNavigation: false, mode, typeof(InteractiveServerRenderMode), streaming); + RenderComponentsWithPersistentStateAndValidate(suppressEnhancedNavigation: false, mode, typeof(InteractiveServerRenderMode), streaming); Browser.Click(By.Id("page-no-components-link")); // Ensure that the circuit is gone. await Task.Delay(1000); Browser.Click(By.Id("page-with-components-link-and-state")); - RenderComponentsWithPersistentStateAndValidate(suppresEnhancedNavigation: false, mode, typeof(InteractiveServerRenderMode), streaming, stateValue: "other"); + RenderComponentsWithPersistentStateAndValidate(suppressEnhancedNavigation: false, mode, typeof(InteractiveServerRenderMode), streaming, stateValue: "other"); } private void BlockWebAssemblyResourceLoad() { + // Clear local storage so that the resource hash is not found + ((IJavaScriptExecutor)Browser).ExecuteScript("localStorage.clear()"); + ((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('block-load-boot-resource', 'true')"); // Clear caches so that we can block the resource load @@ -145,7 +154,7 @@ private void UnblockWebAssemblyResourceLoad() } private void RenderComponentsWithPersistentStateAndValidate( - bool suppresEnhancedNavigation, + bool suppressEnhancedNavigation, string mode, Type renderMode, string streaming, @@ -154,7 +163,7 @@ private void RenderComponentsWithPersistentStateAndValidate( { stateValue ??= "restored"; // No need to navigate if we are using enhanced navigation, the tests will have already navigated to the page via a link. - if (suppresEnhancedNavigation) + if (suppressEnhancedNavigation) { // In this case we suppress auto start to check some server side state before we boot Blazor. if (streaming == null) @@ -232,6 +241,18 @@ private void AssertPageState( } } + private void RenderWebAssemblyComponentAndWaitForWebAssemblyRuntime(string returnUrl = null) + { + Navigate("subdir/persistent-state/page-with-webassembly-interactivity"); + + Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-counter")).Text); + + if (returnUrl is not null) + { + Navigate(returnUrl); + } + } + private void SuppressEnhancedNavigation(bool shouldSuppress) => EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, shouldSuppress); } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index 925d2f4a0817..befd36e52a53 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -21,10 +21,8 @@ const enableClassicInitializers = sessionStorage.getItem('enable-classic-initializers') === 'true'; const suppressEnhancedNavigation = sessionStorage.getItem('suppress-enhanced-navigation') === 'true'; const blockLoadBootResource = sessionStorage.getItem('block-load-boot-resource') === 'true'; - const useLongWebAssemblyTimeout = sessionStorage.getItem('use-long-auto-timeout') === 'true'; sessionStorage.removeItem('suppress-enhanced-navigation'); sessionStorage.removeItem('block-load-boot-resource'); - sessionStorage.removeItem('use-long-auto-timeout'); sessionStorage.removeItem('enable-classic-initializers'); let loadBootResourceUnblocked = null; @@ -34,6 +32,9 @@ }); } + let maxParallelResourceDownloadCount = 0; + let currentParallelResourceDownloadCount = 0; + function callBlazorStart() { Blazor.start({ ssr: { @@ -55,19 +56,21 @@ // The following allows us to arbitrarily delay the loading of WebAssembly resources. // This is useful for guaranteeing that auto mode components will fall back on // using Blazor server. + currentParallelResourceDownloadCount++; return fetch(`${document.baseURI}WasmMinimal/_framework/${name}?`, { method: "GET", }).then(async (response) => { + if (currentParallelResourceDownloadCount > maxParallelResourceDownloadCount) { + maxParallelResourceDownloadCount = currentParallelResourceDownloadCount; + window['__aspnetcore__testing__max__parallel__resource__download__count'] = maxParallelResourceDownloadCount; + } + currentParallelResourceDownloadCount--; await loadBootResourceUnblocked; return response; }); } }, }, - }).then(() => { - if (useLongWebAssemblyTimeout) { - Blazor._internal.loadWebAssemblyQuicklyTimeout = 10000000; - } }).then(() => { const startedParagraph = document.createElement('p'); startedParagraph.id = 'blazor-started'; diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/InteractiveStreamingRenderingComponent.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/InteractiveStreamingRenderingComponent.razor index 9017ffe1023e..de9d5114a204 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/InteractiveStreamingRenderingComponent.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/InteractiveStreamingRenderingComponent.razor @@ -100,6 +100,9 @@ else ComponentState _state = new(ImmutableArray.Empty, NextCounterId: 0); bool _isStreaming = false; + [CascadingParameter] + public HttpContext HttpContext { get; set; } + [SupplyParameterFromQuery] public string? InitialState { get; set; } @@ -109,6 +112,9 @@ else [SupplyParameterFromQuery] public bool DisableKeys { get; set; } + [SupplyParameterFromQuery] + public bool ClearSiteData { get; set; } + protected override async Task OnInitializedAsync() { if (InitialState is not null) @@ -116,6 +122,11 @@ else _state = ReadStateFromJson(InitialState); } + if (ClearSiteData) + { + HttpContext.Response.Headers["Clear-Site-Data"] = "\"*\""; + } + if (ShouldStream) { _isStreaming = true; diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithWebAssemblyInteractivity.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithWebAssemblyInteractivity.razor new file mode 100644 index 000000000000..0d24a1dfcac2 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithWebAssemblyInteractivity.razor @@ -0,0 +1,8 @@ +@page "/persistent-state/page-with-webassembly-interactivity" + +

+ This page is used to ensure that the WebAssembly runtime is downloaded and available + so that WebAssembly interactivity will get used for components with the Auto render mode +

+ +