Skip to content

[Blazor] Improve auto render mode selection strategy #49858

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.web.js

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions src/Components/Web.JS/src/Boot.Server.Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { WebRendererId } from './Rendering/WebRendererId';
import { RootComponentManager } from './Services/RootComponentManager';

let renderingFailed = false;
let hasStarted = false;
let connection: HubConnection;
let circuit: CircuitDescriptor;
let dispatcher: DotNet.ICallDispatcher;
Expand All @@ -33,7 +34,13 @@ export function setCircuitOptions(circuitUserOptions?: Partial<CircuitStartOptio
userOptions = circuitUserOptions;
}

export async function startCircuit(components?: ServerComponentDescriptor[] | RootComponentManager): Promise<void> {
export async function startCircuit(components: RootComponentManager<ServerComponentDescriptor>): Promise<void> {
if (hasStarted) {
throw new Error('Blazor Server has already started.');
}

hasStarted = true;

// Establish options to be used
const options = resolveOptions(userOptions);
const jsInitializer = await fetchAndInvokeInitializers(options);
Expand Down Expand Up @@ -62,7 +69,7 @@ export async function startCircuit(components?: ServerComponentDescriptor[] | Ro
logger.log(LogLevel.Information, 'Starting up Blazor server-side application.');

const appState = discoverPersistedState(document);
circuit = new CircuitDescriptor(components || [], appState || '');
circuit = new CircuitDescriptor(components, appState || '');

// Configure navigation via SignalR
Blazor._internal.navigationManager.listenForNavigationEvents((uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
Expand Down
4 changes: 3 additions & 1 deletion src/Components/Web.JS/src/Boot.Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
import { setCircuitOptions, startCircuit } from './Boot.Server.Common';
import { ServerComponentDescriptor, discoverComponents } from './Services/ComponentDescriptorDiscovery';
import { DotNet } from '@microsoft/dotnet-js-interop';
import { InitialRootComponentsList } from './Services/InitialRootComponentsList';

let started = false;

Expand All @@ -19,7 +20,8 @@ function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
setCircuitOptions(userOptions);

const serverComponents = discoverComponents(document, 'server') as ServerComponentDescriptor[];
return startCircuit(serverComponents);
const components = new InitialRootComponentsList(serverComponents);
return startCircuit(components);
}

Blazor.start = boot;
Expand Down
134 changes: 13 additions & 121 deletions src/Components/Web.JS/src/Boot.Web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,152 +9,44 @@
// of interactive components

import { DotNet } from '@microsoft/dotnet-js-interop';
import { setCircuitOptions, startCircuit } from './Boot.Server.Common';
import { loadWebAssemblyPlatform, setWebAssemblyOptions, startWebAssembly } from './Boot.WebAssembly.Common';
import { setCircuitOptions } from './Boot.Server.Common';
import { setWebAssemblyOptions } from './Boot.WebAssembly.Common';
import { shouldAutoStart } from './BootCommon';
import { Blazor } from './GlobalExports';
import { WebStartOptions } from './Platform/WebStartOptions';
import { attachStreamingRenderingListener } from './Rendering/StreamingRendering';
import { NavigationEnhancementCallbacks, attachProgressivelyEnhancedNavigationListener, isPerformingEnhancedPageLoad } from './Services/NavigationEnhancement';
import { ComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
import { RootComponentManager, attachAutoModeResolver } from './Services/RootComponentManager';
import { DescriptorHandler, attachComponentDescriptorHandler, registerAllComponentDescriptors } from './Rendering/DomMerging/DomSync';
import { waitForRendererAttached } from './Rendering/WebRendererInteropMethods';
import { WebRendererId } from './Rendering/WebRendererId';

enum WebAssemblyLoadingState {
None = 0,
Loading = 1,
Loaded = 2,
Starting = 3,
Started = 4,
}
import { attachProgressivelyEnhancedNavigationListener } from './Services/NavigationEnhancement';
import { WebRootComponentManager } from './Services/WebRootComponentManager';
import { attachComponentDescriptorHandler, registerAllComponentDescriptors } from './Rendering/DomMerging/DomSync';

let started = false;
let hasCircuitStarted = false;
let webAssemblyLoadingState = WebAssemblyLoadingState.None;
let autoModeTimeoutState: undefined | 'waiting' | 'timed out';
const autoModeWebAssemblyTimeoutMilliseconds = 100;

const rootComponentManager = new RootComponentManager();
const rootComponentManager = new WebRootComponentManager();

function boot(options?: Partial<WebStartOptions>) : Promise<void> {
if (started) {
throw new Error('Blazor has already started.');
}

started = true;

Blazor._internal.loadWebAssemblyQuicklyTimeout = 3000;

setCircuitOptions(options?.circuit);
setWebAssemblyOptions(options?.webAssembly);

const navigationEnhancementCallbacks: NavigationEnhancementCallbacks = {
documentUpdated: handleUpdatedComponentDescriptors,
};

const descriptorHandler: DescriptorHandler = {
registerComponentDescriptor,
};

attachComponentDescriptorHandler(descriptorHandler);
attachStreamingRenderingListener(options?.ssr, navigationEnhancementCallbacks);
attachAutoModeResolver(resolveAutoMode);
attachComponentDescriptorHandler(rootComponentManager);
attachStreamingRenderingListener(options?.ssr, rootComponentManager);

if (!options?.ssr?.disableDomPreservation) {
attachProgressivelyEnhancedNavigationListener(navigationEnhancementCallbacks);
attachProgressivelyEnhancedNavigationListener(rootComponentManager);
}

registerAllComponentDescriptors(document);
handleUpdatedComponentDescriptors();
rootComponentManager.documentUpdated();

return Promise.resolve();
}

function resolveAutoMode(): 'server' | 'webassembly' | null {
if (webAssemblyLoadingState >= WebAssemblyLoadingState.Loaded) {
// The WebAssembly runtime has loaded or is actively starting, so we'll use
// WebAssembly for the component in question. We'll also start
// the WebAssembly runtime if it hasn't already.
startWebAssemblyIfNotStarted();
return 'webassembly';
}

if (autoModeTimeoutState === 'timed out') {
// We've waited too long for WebAssembly to initialize, so we'll use the Server
// render mode for the component in question. At some point if the WebAssembly
// runtime finishes loading, we'll start using it again due to the earlier
// check in this function.
startCircuitIfNotStarted();
return 'server';
}

if (autoModeTimeoutState === undefined) {
// The WebAssembly runtime hasn't loaded yet, and this is the first
// time auto mode is being requested.
// We'll wait a bit for the WebAssembly runtime to load before making
// a render mode decision.
autoModeTimeoutState = 'waiting';
setTimeout(() => {
autoModeTimeoutState = 'timed out';

// We want to ensure that we activate any markers whose render mode didn't get resolved
// earlier.
handleUpdatedComponentDescriptors();
}, autoModeWebAssemblyTimeoutMilliseconds);
}

return null;
}

function registerComponentDescriptor(descriptor: ComponentDescriptor) {
rootComponentManager.registerComponentDescriptor(descriptor);

if (descriptor.type === 'auto') {
startLoadingWebAssemblyIfNotStarted();
} else if (descriptor.type === 'server') {
startCircuitIfNotStarted();
} else if (descriptor.type === 'webassembly') {
startWebAssemblyIfNotStarted();
}
}

function handleUpdatedComponentDescriptors() {
const shouldAddNewRootComponents = !isPerformingEnhancedPageLoad();
rootComponentManager.handleUpdatedRootComponents(shouldAddNewRootComponents);
}

async function startCircuitIfNotStarted() {
if (hasCircuitStarted) {
return;
}

hasCircuitStarted = true;
await startCircuit(rootComponentManager);
await waitForRendererAttached(WebRendererId.Server);
handleUpdatedComponentDescriptors();
}

async function startLoadingWebAssemblyIfNotStarted() {
if (webAssemblyLoadingState >= WebAssemblyLoadingState.Loading) {
return;
}

webAssemblyLoadingState = WebAssemblyLoadingState.Loading;
await loadWebAssemblyPlatform();
webAssemblyLoadingState = WebAssemblyLoadingState.Loaded;
}

async function startWebAssemblyIfNotStarted() {
if (webAssemblyLoadingState >= WebAssemblyLoadingState.Starting) {
return;
}

webAssemblyLoadingState = WebAssemblyLoadingState.Starting;
await startWebAssembly(rootComponentManager);
await waitForRendererAttached(WebRendererId.WebAssembly);
webAssemblyLoadingState = WebAssemblyLoadingState.Started;
handleUpdatedComponentDescriptors();
}

Blazor.start = boot;
window['DotNet'] = DotNet;

Expand Down
28 changes: 22 additions & 6 deletions src/Components/Web.JS/src/Boot.WebAssembly.Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@ import { addDispatchEventMiddleware } from './Rendering/WebRendererInteropMethod
import { WebAssemblyComponentDescriptor, discoverPersistedState } from './Services/ComponentDescriptorDiscovery';
import { receiveDotNetDataStream } from './StreamingInterop';
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
import { MonoConfig } from 'dotnet';
import { RootComponentManager } from './Services/RootComponentManager';

let options: Partial<WebAssemblyStartOptions> | undefined;
let platformLoadPromise: Promise<void> | undefined;
let hasStarted = false;

let resolveBootConfigPromise: (value: MonoConfig) => void;
const bootConfigPromise = new Promise<MonoConfig>(resolve => {
resolveBootConfigPromise = resolve;
});

export function setWebAssemblyOptions(webAssemblyOptions?: Partial<WebAssemblyStartOptions>) {
if (options) {
Expand All @@ -27,13 +34,19 @@ export function setWebAssemblyOptions(webAssemblyOptions?: Partial<WebAssemblySt
options = webAssemblyOptions;
}

export async function startWebAssembly(components?: WebAssemblyComponentDescriptor[] | RootComponentManager): Promise<void> {
export async function startWebAssembly(components: RootComponentManager<WebAssemblyComponentDescriptor>): Promise<void> {
if (hasStarted) {
throw new Error('Blazor WebAssembly has already started.');
}

hasStarted = true;

if (inAuthRedirectIframe()) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await new Promise(() => { }); // See inAuthRedirectIframe for explanation
}

const platformLoadPromise = loadWebAssemblyPlatform();
const platformLoadPromise = loadWebAssemblyPlatformIfNotStarted();

addDispatchEventMiddleware((browserRendererId, eventHandlerId, continuation) => {
// It's extremely unusual, but an event can be raised while we're in the middle of synchronously applying a
Expand Down Expand Up @@ -98,10 +111,9 @@ export async function startWebAssembly(components?: WebAssemblyComponentDescript

// Leverage the time while we are loading boot.config.json from the network to discover any potentially registered component on
// the document.
const componentAttacher = new WebAssemblyComponentAttacher(components || []);
const componentAttacher = new WebAssemblyComponentAttacher(components);
Blazor._internal.registeredComponents = {
getRegisteredComponentsCount: () => componentAttacher.getCount(),
getId: (index) => componentAttacher.getId(index),
getAssembly: (id) => componentAttacher.getAssembly(id),
getTypeName: (id) => componentAttacher.getTypeName(id),
getParameterDefinitions: (id) => componentAttacher.getParameterDefinitions(id) || '',
Expand Down Expand Up @@ -134,8 +146,12 @@ export async function startWebAssembly(components?: WebAssemblyComponentDescript
api.invokeLibraryInitializers('afterStarted', [Blazor]);
}

export function loadWebAssemblyPlatform(): Promise<void> {
platformLoadPromise ??= monoPlatform.load(options ?? {});
export function waitForBootConfigLoaded(): Promise<MonoConfig> {
return bootConfigPromise;
}

export function loadWebAssemblyPlatformIfNotStarted(): Promise<void> {
platformLoadPromise ??= monoPlatform.load(options ?? {}, resolveBootConfigPromise);
return platformLoadPromise;
}

Expand Down
4 changes: 3 additions & 1 deletion src/Components/Web.JS/src/Boot.WebAssembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
import { setWebAssemblyOptions, startWebAssembly } from './Boot.WebAssembly.Common';
import { WebAssemblyComponentDescriptor, discoverComponents } from './Services/ComponentDescriptorDiscovery';
import { DotNet } from '@microsoft/dotnet-js-interop';
import { InitialRootComponentsList } from './Services/InitialRootComponentsList';

let started = false;

Expand All @@ -21,7 +22,8 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
setWebAssemblyOptions(options);

const webAssemblyComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[];
await startWebAssembly(webAssemblyComponents);
const components = new InitialRootComponentsList(webAssemblyComponents);
await startWebAssembly(components);
}

Blazor.start = boot;
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Web.JS/src/GlobalExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ interface IBlazor {
attachRootComponentToElement?: (arg0: any, arg1: any, arg2: any, arg3: any) => void;
registeredComponents?: {
getRegisteredComponentsCount: () => number;
getId: (index) => number;
getAssembly: (id) => string;
getTypeName: (id) => string;
getParameterDefinitions: (id) => string;
Expand All @@ -74,6 +73,7 @@ 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
15 changes: 5 additions & 10 deletions src/Components/Web.JS/src/Platform/Circuits/CircuitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import { RootComponentManager } from '../../Services/RootComponentManager';
export class CircuitDescriptor {
public circuitId?: string;

public components: ServerComponentDescriptor[] | RootComponentManager;
public componentManager: RootComponentManager<ServerComponentDescriptor>;

public applicationState: string;

public constructor(components: ServerComponentDescriptor[] | RootComponentManager, appState: string) {
public constructor(componentManager: RootComponentManager<ServerComponentDescriptor>, appState: string) {
this.circuitId = undefined;
this.applicationState = appState;
this.components = components;
this.componentManager = componentManager;
}

public reconnect(reconnection: signalR.HubConnection): Promise<boolean> {
Expand All @@ -45,10 +45,7 @@ export class CircuitDescriptor {
return false;
}

const componentsJson = this.components instanceof RootComponentManager
? '[]'
: JSON.stringify(this.components.map(c => descriptorToMarker(c)));

const componentsJson = JSON.stringify(this.componentManager.initialComponents.map(c => descriptorToMarker(c)));
const result = await connection.invoke<string>(
'StartCircuit',
navigationManagerFunctions.getBaseURI(),
Expand All @@ -75,9 +72,7 @@ export class CircuitDescriptor {
// ... or it may be a root component added by .NET
const parsedSequence = Number.parseInt(sequenceOrIdentifier);
if (!Number.isNaN(parsedSequence)) {
const descriptor = this.components instanceof RootComponentManager
? this.components.resolveRootComponent(parsedSequence, componentId)
: this.components[parsedSequence];
const descriptor = this.componentManager.resolveRootComponent(parsedSequence, componentId);
return toLogicalRootCommentElement(descriptor);
}

Expand Down
12 changes: 7 additions & 5 deletions src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ function getValueU64(ptr: number) {
}

export const monoPlatform: Platform = {
load: function load(options: Partial<WebAssemblyStartOptions>) {
return createRuntimeInstance(options);
load: function load(options: Partial<WebAssemblyStartOptions>, onConfigLoaded?: (loadedConfig: MonoConfig) => void) {
return createRuntimeInstance(options, onConfigLoaded);
},

start: function start() {
Expand Down Expand Up @@ -174,7 +174,7 @@ async function importDotnetJs(startOptions: Partial<WebAssemblyStartOptions>): P
return await import(/* webpackIgnore: true */ absoluteSrc);
}

function prepareRuntimeConfig(options: Partial<WebAssemblyStartOptions>): DotnetModuleConfig {
function prepareRuntimeConfig(options: Partial<WebAssemblyStartOptions>, onConfigLoadedCallback?: (loadedConfig: MonoConfig) => void): DotnetModuleConfig {
const config: MonoConfig = {
maxParallelDownloads: 1000000, // disable throttling parallel downloads
enableDownloadRetry: false, // disable retry downloads
Expand All @@ -192,6 +192,8 @@ function prepareRuntimeConfig(options: Partial<WebAssemblyStartOptions>): Dotnet

Blazor._internal.getApplicationEnvironment = () => loadedConfig.applicationEnvironment!;

onConfigLoadedCallback?.(loadedConfig);

const initializerArguments = [options, loadedConfig.resources?.extensions ?? {}];
await invokeLibraryInitializers('beforeStart', initializerArguments);
};
Expand All @@ -210,9 +212,9 @@ function prepareRuntimeConfig(options: Partial<WebAssemblyStartOptions>): Dotnet
return dotnetModuleConfig;
}

async function createRuntimeInstance(options: Partial<WebAssemblyStartOptions>): Promise<void> {
async function createRuntimeInstance(options: Partial<WebAssemblyStartOptions>, onConfigLoaded?: (loadedConfig: MonoConfig) => void): Promise<void> {
const { dotnet } = await importDotnetJs(options);
const moduleConfig = prepareRuntimeConfig(options);
const moduleConfig = prepareRuntimeConfig(options, onConfigLoaded);

if (options.applicationCulture) {
dotnet.withApplicationCulture(options.applicationCulture);
Expand Down
Loading