Skip to content

Commit

Permalink
[Blazor] Auto render mode improvements (#53159)
Browse files Browse the repository at this point in the history
  • Loading branch information
MackinnonBuck committed Jan 10, 2024
1 parent 71ecce4 commit d5a8d01
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 60 deletions.
1 change: 0 additions & 1 deletion src/Components/Web.JS/src/Boot.Web.ts
Expand Up @@ -37,7 +37,6 @@ function boot(options?: Partial<WebStartOptions>) : Promise<void> {
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.
Expand Down
1 change: 0 additions & 1 deletion src/Components/Web.JS/src/GlobalExports.ts
Expand Up @@ -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?: {
Expand Down
35 changes: 18 additions & 17 deletions src/Components/Web.JS/src/Services/WebRootComponentManager.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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++;
Expand All @@ -120,26 +125,20 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
this.circuitMayHaveNoRootComponents();
}

private async startLoadingWebAssemblyIfNotStarted() {
private async startLoadingWebAssemblyIfNotStarted(maxParallelDownloadsOverride?: number) {
if (hasStartedLoadingWebAssemblyPlatform()) {
return;
}

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.
Expand Down Expand Up @@ -299,6 +298,8 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
this.updateWebAssemblyRootComponents(batchJson);
}
}

this.circuitMayHaveNoRootComponents();
}

private updateWebAssemblyRootComponents(operationsJson: string) {
Expand Down
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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();

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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}";
Expand Down
31 changes: 26 additions & 5 deletions src/Components/test/E2ETest/Tests/StatePersistenceTest.cs
Expand Up @@ -31,7 +31,7 @@ public class StatePersistenceTest : ServerTestBase<BasicTestAppServerSiteFixture
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
Expand Down Expand Up @@ -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");
Expand All @@ -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
Expand All @@ -145,7 +154,7 @@ private void UnblockWebAssemblyResourceLoad()
}

private void RenderComponentsWithPersistentStateAndValidate(
bool suppresEnhancedNavigation,
bool suppressEnhancedNavigation,
string mode,
Type renderMode,
string streaming,
Expand All @@ -154,7 +163,7 @@ private void UnblockWebAssemblyResourceLoad()
{
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)
Expand Down Expand Up @@ -232,6 +241,18 @@ private void UnblockWebAssemblyResourceLoad()
}
}

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);
}
Expand Up @@ -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;
Expand All @@ -34,6 +32,9 @@
});
}
let maxParallelResourceDownloadCount = 0;
let currentParallelResourceDownloadCount = 0;
function callBlazorStart() {
Blazor.start({
ssr: {
Expand All @@ -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';
Expand Down
Expand Up @@ -100,6 +100,9 @@ else
ComponentState _state = new(ImmutableArray<CounterInfo>.Empty, NextCounterId: 0);
bool _isStreaming = false;

[CascadingParameter]
public HttpContext HttpContext { get; set; }

[SupplyParameterFromQuery]
public string? InitialState { get; set; }

Expand All @@ -109,13 +112,21 @@ else
[SupplyParameterFromQuery]
public bool DisableKeys { get; set; }

[SupplyParameterFromQuery]
public bool ClearSiteData { get; set; }

protected override async Task OnInitializedAsync()
{
if (InitialState is not null)
{
_state = ReadStateFromJson(InitialState);
}

if (ClearSiteData)
{
HttpContext.Response.Headers["Clear-Site-Data"] = "\"*\"";
}

if (ShouldStream)
{
_isStreaming = true;
Expand Down
@@ -0,0 +1,8 @@
@page "/persistent-state/page-with-webassembly-interactivity"

<p>
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
</p>

<TestContentPackage.Counter @rendermode="RenderMode.InteractiveWebAssembly" IncrementAmount="1" IdSuffix="counter" />

0 comments on commit d5a8d01

Please sign in to comment.