Skip to content

[blazor] Use JSImport for lazy loading assemblies #46437

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
Feb 7, 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
4 changes: 1 addition & 3 deletions src/Components/Web.JS/src/GlobalExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,9 @@ interface IBlazor {
renderBatch?: (browserRendererId: number, batchAddress: Pointer) => void,
getConfig?: (dotNetFileName: System_String) => System_Object | undefined,
getApplicationEnvironment?: () => System_String,
readLazyAssemblies?: () => System_Array<System_Object>,
readLazyPdbs?: () => System_Array<System_Object>,
readSatelliteAssemblies?: () => System_Array<System_Object>,
getLazyAssemblies?: any
dotNetCriticalError?: any
loadLazyAssembly?: any,
getSatelliteAssemblies?: any,
sendJSDataStream?: (data: any, streamId: number, chunkSize: number) => void,
getJSDataStreamChunk?: (data: any, position: number, chunkSize: number) => Promise<Uint8Array>,
Expand Down
121 changes: 43 additions & 78 deletions src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Platform, System_Array, Pointer, System_Object, System_String, HeapLock
import { WebAssemblyBootResourceType } from '../WebAssemblyStartOptions';
import { BootJsonData, ICUDataMode } from '../BootConfig';
import { Blazor } from '../../GlobalExports';
import { RuntimeAPI, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, AssetEntry, ResourceRequest } from 'dotnet';
import { RuntimeAPI, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, AssetEntry } from 'dotnet';
import { BINDINGType, MONOType } from 'dotnet/dotnet-legacy';

// initially undefined and only fully initialized after createEmscriptenModuleInstance()
Expand Down Expand Up @@ -225,20 +225,20 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc
// If anything writes to stderr, treat it as a critical exception. The underlying runtime writes
// to stderr if a truly critical problem occurs outside .NET code. Note that .NET unhandled
// exceptions also reach this, but via a different code path - see dotNetCriticalError below.
console.error(line);
console.error(line || '(null)');
showErrorNotification();
};
const existingPreRun = moduleConfig.preRun || [] as any;
const existingPostRun = moduleConfig.postRun || [] as any;
(moduleConfig as any).preloadPlugins = [];

let resourcesLoaded = 0;
function setProgress(){
resourcesLoaded++;
const percentage = resourcesLoaded / totalResources.length * 100;
document.documentElement.style.setProperty('--blazor-load-percentage', `${percentage}%`);
document.documentElement.style.setProperty('--blazor-load-percentage-text', `"${Math.floor(percentage)}%"`);
}
function setProgress() {
resourcesLoaded++;
const percentage = resourcesLoaded / totalResources.length * 100;
document.documentElement.style.setProperty('--blazor-load-percentage', `${percentage}%`);
document.documentElement.style.setProperty('--blazor-load-percentage-text', `"${Math.floor(percentage)}%"`);
}

const monoToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | undefined } = {
'assembly': 'assembly',
Expand Down Expand Up @@ -280,7 +280,9 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc

// Begin loading the .dll/.pdb/.wasm files, but don't block here. Let other loading processes run in parallel.
const assembliesBeingLoaded = resourceLoader.loadResources(resources.assembly, filename => `_framework/${filename}`, 'assembly');
const pdbsBeingLoaded = resourceLoader.loadResources(resources.pdb || {}, filename => `_framework/${filename}`, 'pdb');
const pdbsBeingLoaded = hasDebuggingEnabled()
? resourceLoader.loadResources(resources.pdb || {}, filename => `_framework/${filename}`, 'pdb')
: [];
const totalResources = assembliesBeingLoaded.concat(pdbsBeingLoaded, runtimeAssetsBeingLoaded);

const dotnetTimeZoneResourceName = 'dotnet.timezones.blat';
Expand Down Expand Up @@ -340,7 +342,8 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc
assembliesBeingLoaded.forEach(r => addResourceAsAssembly(r, changeExtension(r.name, '.dll')));
pdbsBeingLoaded.forEach(r => addResourceAsAssembly(r, r.name));

Blazor._internal.dotNetCriticalError = (message) => printErr(message || '(null)');
Blazor._internal.dotNetCriticalError = printErr;
Blazor._internal.loadLazyAssembly = loadLazyAssembly;

// Wire-up callbacks for satellite assemblies. Blazor will call these as part of the application
// startup sequence to load satellite assemblies for the application's culture.
Expand Down Expand Up @@ -372,76 +375,38 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc
return BINDING.js_to_mono_obj(Promise.resolve(0));
};

const lazyResources: {
assemblies?: (ArrayBuffer | null)[],
pdbs?: (ArrayBuffer | null)[]
} = {};
Blazor._internal.getLazyAssemblies = (assembliesToLoadDotNetArray) => {
const assembliesToLoad = BINDING.mono_array_to_js_array(assembliesToLoadDotNetArray);
const lazyAssemblies = resourceLoader.bootConfig.resources.lazyAssembly;

if (!lazyAssemblies) {
throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly.");
}

const assembliesMarkedAsLazy = assembliesToLoad!.filter(assembly => lazyAssemblies.hasOwnProperty(assembly));

if (assembliesMarkedAsLazy.length !== assembliesToLoad!.length) {
const notMarked = assembliesToLoad!.filter(assembly => !assembliesMarkedAsLazy.includes(assembly));
throw new Error(`${notMarked.join()} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);
}
};

let pdbPromises: Promise<(ArrayBuffer | null)[]> | undefined;
if (hasDebuggingEnabled()) {
const pdbs = resourceLoader.bootConfig.resources.pdb;
const pdbsToLoad = assembliesMarkedAsLazy.map(a => changeExtension(a, '.pdb'));
if (pdbs) {
pdbPromises = Promise.all(pdbsToLoad
.map(pdb => lazyAssemblies.hasOwnProperty(pdb) ? resourceLoader.loadResource(pdb, `_framework/${pdb}`, lazyAssemblies[pdb], 'pdb') : null)
.map(async resource => resource ? (await resource.response).arrayBuffer() : null));
}
}
async function loadLazyAssembly(assemblyNameToLoad: string): Promise<{ dll: Uint8Array, pdb: Uint8Array | null }> {
const lazyAssemblies = resources.lazyAssembly;
if (!lazyAssemblies) {
throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly.");
}

const resourcePromises = Promise.all(assembliesMarkedAsLazy
.map(assembly => resourceLoader.loadResource(assembly, `_framework/${assembly}`, lazyAssemblies[assembly], 'assembly'))
.map(async resource => (await resource.response).arrayBuffer()));


return BINDING.js_to_mono_obj(Promise.all([resourcePromises, pdbPromises]).then(values => {
lazyResources['assemblies'] = values[0];
lazyResources['pdbs'] = values[1];
if (lazyResources['assemblies'].length) {
Blazor._internal.readLazyAssemblies = () => {
const { assemblies } = lazyResources;
if (!assemblies) {
return BINDING.mono_obj_array_new(0);
}
const assemblyBytes = BINDING.mono_obj_array_new(assemblies.length);
for (let i = 0; i < assemblies.length; i++) {
const assembly = assemblies[i] as ArrayBuffer;
BINDING.mono_obj_array_set(assemblyBytes, i, BINDING.js_typed_array_to_array(new Uint8Array(assembly)));
}
return assemblyBytes as any;
};

Blazor._internal.readLazyPdbs = () => {
const { assemblies, pdbs } = lazyResources;
if (!assemblies) {
return BINDING.mono_obj_array_new(0);
}
const pdbBytes = BINDING.mono_obj_array_new(assemblies.length);
for (let i = 0; i < assemblies.length; i++) {
const pdb = pdbs && pdbs[i] ? new Uint8Array(pdbs[i] as ArrayBufferLike) : new Uint8Array();
BINDING.mono_obj_array_set(pdbBytes, i, BINDING.js_typed_array_to_array(pdb));
}
return pdbBytes as any;
};
}

return lazyResources['assemblies'].length;
}));
};
};
const assemblyMarkedAsLazy = lazyAssemblies.hasOwnProperty(assemblyNameToLoad);
if (!assemblyMarkedAsLazy) {
throw new Error(`${assemblyNameToLoad} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);
}
const dllNameToLoad = changeExtension(assemblyNameToLoad, '.dll');
const pdbNameToLoad = changeExtension(assemblyNameToLoad, '.pdb');
const shouldLoadPdb = hasDebuggingEnabled() && resources.pdb && lazyAssemblies.hasOwnProperty(pdbNameToLoad);

const dllBytesPromise = resourceLoader.loadResource(dllNameToLoad, `_framework/${dllNameToLoad}`, lazyAssemblies[dllNameToLoad], 'assembly').response.then(response => response.arrayBuffer());
if (shouldLoadPdb) {
const pdbBytesPromise = await resourceLoader.loadResource(pdbNameToLoad, `_framework/${pdbNameToLoad}`, lazyAssemblies[pdbNameToLoad], 'pdb').response.then(response => response.arrayBuffer());
const [dllBytes, pdbBytes] = await Promise.all([dllBytesPromise, pdbBytesPromise]);
return {
dll: new Uint8Array(dllBytes),
pdb: new Uint8Array(pdbBytes),
};
} else {
const dllBytes = await dllBytesPromise;
return {
dll: new Uint8Array(dllBytes),
pdb: null,
};
}
}

const postRun = () => {
if (resourceLoader.bootConfig.debugBuild && resourceLoader.bootConfig.cacheBootResources) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Loader;
using Microsoft.JSInterop;
using System.Linq;
using System.Runtime.Versioning;

namespace Microsoft.AspNetCore.Components.WebAssembly.Services;

Expand All @@ -13,13 +16,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services;
///
/// Supports finding pre-loaded assemblies in a server or pre-rendering context.
/// </summary>
public sealed class LazyAssemblyLoader
public sealed partial class LazyAssemblyLoader
{
internal const string GetLazyAssemblies = "window.Blazor._internal.getLazyAssemblies";
internal const string ReadLazyAssemblies = "window.Blazor._internal.readLazyAssemblies";
internal const string ReadLazyPDBs = "window.Blazor._internal.readLazyPdbs";

private readonly IJSRuntime _jsRuntime;
private HashSet<string>? _loadedAssemblyCache;

/// <summary>
Expand All @@ -28,7 +26,6 @@ public sealed class LazyAssemblyLoader
/// <param name="jsRuntime">The <see cref="IJSRuntime"/>.</param>
public LazyAssemblyLoader(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}

/// <summary>
Expand Down Expand Up @@ -70,6 +67,7 @@ private static Task<IEnumerable<Assembly>> LoadAssembliesInServerAsync(IEnumerab
}

[RequiresUnreferencedCode("Types and members the loaded assemblies depend on might be removed")]
[SupportedOSPlatform("browser")]
private async Task<IEnumerable<Assembly>> LoadAssembliesInClientAsync(IEnumerable<string> assembliesToLoad)
{
if (_loadedAssemblyCache is null)
Expand Down Expand Up @@ -104,39 +102,33 @@ private async Task<IEnumerable<Assembly>> LoadAssembliesInClientAsync(IEnumerabl
return Array.Empty<Assembly>();
}

var jsRuntime = (IJSUnmarshalledRuntime)_jsRuntime;
#pragma warning disable CS0618 // Type or member is obsolete
var count = (int)await jsRuntime.InvokeUnmarshalled<string[], Task<object>>(
GetLazyAssemblies,
newAssembliesToLoad.ToArray());
#pragma warning restore CS0618 // Type or member is obsolete
var loadedAssemblies = new List<Assembly>();
var pendingLoads = newAssembliesToLoad.Select(assemblyToLoad => LoadAssembly(assemblyToLoad, loadedAssemblies));

if (count == 0)
{
return Array.Empty<Assembly>();
}
await Task.WhenAll(pendingLoads);
return loadedAssemblies;
}

var loadedAssemblies = new List<Assembly>();
#pragma warning disable CS0618 // Type or member is obsolete
var assemblies = jsRuntime.InvokeUnmarshalled<byte[][]>(ReadLazyAssemblies);
var pdbs = jsRuntime.InvokeUnmarshalled<byte[][]>(ReadLazyPDBs);
#pragma warning restore CS0618 // Type or member is obsolete
[RequiresUnreferencedCode("Types and members the loaded assemblies depend on might be removed")]
[SupportedOSPlatform("browser")]
private async Task LoadAssembly(string assemblyToLoad, List<Assembly> loadedAssemblies)
{
using var files = await LazyAssemblyLoaderInterop.LoadLazyAssembly(assemblyToLoad);

for (int i = 0; i < assemblies.Length; i++)
{
// The runtime loads assemblies into an isolated context by default. As a result,
// assemblies that are loaded via Assembly.Load aren't available in the app's context
// AKA the default context. To work around this, we explicitly load the assemblies
// into the default app context.
var assembly = assemblies[i];
var pdb = pdbs[i];
var loadedAssembly = pdb.Length == 0 ?
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(assembly)) :
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(assembly), new MemoryStream(pdb));
loadedAssemblies.Add(loadedAssembly);
_loadedAssemblyCache.Add(loadedAssembly.GetName().Name + ".dll");
}
var dllBytes = files.GetPropertyAsByteArray("dll")!;
var pdbBytes = files.GetPropertyAsByteArray("pdb");
Assembly loadedAssembly = pdbBytes == null
? AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes))
: AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes), new MemoryStream(pdbBytes));

return loadedAssemblies;
loadedAssemblies.Add(loadedAssembly);
_loadedAssemblyCache!.Add(assemblyToLoad);

}

private partial class LazyAssemblyLoaderInterop
{
[JSImport("Blazor._internal.loadLazyAssembly", "blazor-internal")]
public static partial Task<JSObject> LoadLazyAssembly(string assemblyToLoad);
}
}