Skip to content

Commit 0588658

Browse files
[blazor] Use JSImport for lazy loading assemblies (#46437)
* use new generated JavaScript interop with [JSImport] Co-authored-by: Mackinnon Buck <[email protected]>
1 parent 7b5b67e commit 0588658

File tree

3 files changed

+73
-118
lines changed

3 files changed

+73
-118
lines changed

src/Components/Web.JS/src/GlobalExports.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,9 @@ interface IBlazor {
5353
renderBatch?: (browserRendererId: number, batchAddress: Pointer) => void,
5454
getConfig?: (dotNetFileName: System_String) => System_Object | undefined,
5555
getApplicationEnvironment?: () => System_String,
56-
readLazyAssemblies?: () => System_Array<System_Object>,
57-
readLazyPdbs?: () => System_Array<System_Object>,
5856
readSatelliteAssemblies?: () => System_Array<System_Object>,
59-
getLazyAssemblies?: any
6057
dotNetCriticalError?: any
58+
loadLazyAssembly?: any,
6159
getSatelliteAssemblies?: any,
6260
sendJSDataStream?: (data: any, streamId: number, chunkSize: number) => void,
6361
getJSDataStreamChunk?: (data: any, position: number, chunkSize: number) => Promise<Uint8Array>,

src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

Lines changed: 43 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Platform, System_Array, Pointer, System_Object, System_String, HeapLock
1212
import { WebAssemblyBootResourceType } from '../WebAssemblyStartOptions';
1313
import { BootJsonData, ICUDataMode } from '../BootConfig';
1414
import { Blazor } from '../../GlobalExports';
15-
import { RuntimeAPI, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, AssetEntry, ResourceRequest } from 'dotnet';
15+
import { RuntimeAPI, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, AssetEntry } from 'dotnet';
1616
import { BINDINGType, MONOType } from 'dotnet/dotnet-legacy';
1717

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

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

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

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

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

343-
Blazor._internal.dotNetCriticalError = (message) => printErr(message || '(null)');
345+
Blazor._internal.dotNetCriticalError = printErr;
346+
Blazor._internal.loadLazyAssembly = loadLazyAssembly;
344347

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

375-
const lazyResources: {
376-
assemblies?: (ArrayBuffer | null)[],
377-
pdbs?: (ArrayBuffer | null)[]
378-
} = {};
379-
Blazor._internal.getLazyAssemblies = (assembliesToLoadDotNetArray) => {
380-
const assembliesToLoad = BINDING.mono_array_to_js_array(assembliesToLoadDotNetArray);
381-
const lazyAssemblies = resourceLoader.bootConfig.resources.lazyAssembly;
382-
383-
if (!lazyAssemblies) {
384-
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.");
385-
}
386-
387-
const assembliesMarkedAsLazy = assembliesToLoad!.filter(assembly => lazyAssemblies.hasOwnProperty(assembly));
388-
389-
if (assembliesMarkedAsLazy.length !== assembliesToLoad!.length) {
390-
const notMarked = assembliesToLoad!.filter(assembly => !assembliesMarkedAsLazy.includes(assembly));
391-
throw new Error(`${notMarked.join()} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);
392-
}
378+
};
393379

394-
let pdbPromises: Promise<(ArrayBuffer | null)[]> | undefined;
395-
if (hasDebuggingEnabled()) {
396-
const pdbs = resourceLoader.bootConfig.resources.pdb;
397-
const pdbsToLoad = assembliesMarkedAsLazy.map(a => changeExtension(a, '.pdb'));
398-
if (pdbs) {
399-
pdbPromises = Promise.all(pdbsToLoad
400-
.map(pdb => lazyAssemblies.hasOwnProperty(pdb) ? resourceLoader.loadResource(pdb, `_framework/${pdb}`, lazyAssemblies[pdb], 'pdb') : null)
401-
.map(async resource => resource ? (await resource.response).arrayBuffer() : null));
402-
}
403-
}
380+
async function loadLazyAssembly(assemblyNameToLoad: string): Promise<{ dll: Uint8Array, pdb: Uint8Array | null }> {
381+
const lazyAssemblies = resources.lazyAssembly;
382+
if (!lazyAssemblies) {
383+
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.");
384+
}
404385

405-
const resourcePromises = Promise.all(assembliesMarkedAsLazy
406-
.map(assembly => resourceLoader.loadResource(assembly, `_framework/${assembly}`, lazyAssemblies[assembly], 'assembly'))
407-
.map(async resource => (await resource.response).arrayBuffer()));
408-
409-
410-
return BINDING.js_to_mono_obj(Promise.all([resourcePromises, pdbPromises]).then(values => {
411-
lazyResources['assemblies'] = values[0];
412-
lazyResources['pdbs'] = values[1];
413-
if (lazyResources['assemblies'].length) {
414-
Blazor._internal.readLazyAssemblies = () => {
415-
const { assemblies } = lazyResources;
416-
if (!assemblies) {
417-
return BINDING.mono_obj_array_new(0);
418-
}
419-
const assemblyBytes = BINDING.mono_obj_array_new(assemblies.length);
420-
for (let i = 0; i < assemblies.length; i++) {
421-
const assembly = assemblies[i] as ArrayBuffer;
422-
BINDING.mono_obj_array_set(assemblyBytes, i, BINDING.js_typed_array_to_array(new Uint8Array(assembly)));
423-
}
424-
return assemblyBytes as any;
425-
};
426-
427-
Blazor._internal.readLazyPdbs = () => {
428-
const { assemblies, pdbs } = lazyResources;
429-
if (!assemblies) {
430-
return BINDING.mono_obj_array_new(0);
431-
}
432-
const pdbBytes = BINDING.mono_obj_array_new(assemblies.length);
433-
for (let i = 0; i < assemblies.length; i++) {
434-
const pdb = pdbs && pdbs[i] ? new Uint8Array(pdbs[i] as ArrayBufferLike) : new Uint8Array();
435-
BINDING.mono_obj_array_set(pdbBytes, i, BINDING.js_typed_array_to_array(pdb));
436-
}
437-
return pdbBytes as any;
438-
};
439-
}
440-
441-
return lazyResources['assemblies'].length;
442-
}));
443-
};
444-
};
386+
const assemblyMarkedAsLazy = lazyAssemblies.hasOwnProperty(assemblyNameToLoad);
387+
if (!assemblyMarkedAsLazy) {
388+
throw new Error(`${assemblyNameToLoad} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);
389+
}
390+
const dllNameToLoad = changeExtension(assemblyNameToLoad, '.dll');
391+
const pdbNameToLoad = changeExtension(assemblyNameToLoad, '.pdb');
392+
const shouldLoadPdb = hasDebuggingEnabled() && resources.pdb && lazyAssemblies.hasOwnProperty(pdbNameToLoad);
393+
394+
const dllBytesPromise = resourceLoader.loadResource(dllNameToLoad, `_framework/${dllNameToLoad}`, lazyAssemblies[dllNameToLoad], 'assembly').response.then(response => response.arrayBuffer());
395+
if (shouldLoadPdb) {
396+
const pdbBytesPromise = await resourceLoader.loadResource(pdbNameToLoad, `_framework/${pdbNameToLoad}`, lazyAssemblies[pdbNameToLoad], 'pdb').response.then(response => response.arrayBuffer());
397+
const [dllBytes, pdbBytes] = await Promise.all([dllBytesPromise, pdbBytesPromise]);
398+
return {
399+
dll: new Uint8Array(dllBytes),
400+
pdb: new Uint8Array(pdbBytes),
401+
};
402+
} else {
403+
const dllBytes = await dllBytesPromise;
404+
return {
405+
dll: new Uint8Array(dllBytes),
406+
pdb: null,
407+
};
408+
}
409+
}
445410

446411
const postRun = () => {
447412
if (resourceLoader.bootConfig.debugBuild && resourceLoader.bootConfig.cacheBootResources) {

src/Components/WebAssembly/WebAssembly/src/Services/LazyAssemblyLoader.cs

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
using System.Diagnostics.CodeAnalysis;
55
using System.Reflection;
6+
using System.Runtime.InteropServices.JavaScript;
67
using System.Runtime.Loader;
78
using Microsoft.JSInterop;
9+
using System.Linq;
10+
using System.Runtime.Versioning;
811

912
namespace Microsoft.AspNetCore.Components.WebAssembly.Services;
1013

@@ -13,13 +16,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services;
1316
///
1417
/// Supports finding pre-loaded assemblies in a server or pre-rendering context.
1518
/// </summary>
16-
public sealed class LazyAssemblyLoader
19+
public sealed partial class LazyAssemblyLoader
1720
{
18-
internal const string GetLazyAssemblies = "window.Blazor._internal.getLazyAssemblies";
19-
internal const string ReadLazyAssemblies = "window.Blazor._internal.readLazyAssemblies";
20-
internal const string ReadLazyPDBs = "window.Blazor._internal.readLazyPdbs";
21-
22-
private readonly IJSRuntime _jsRuntime;
2321
private HashSet<string>? _loadedAssemblyCache;
2422

2523
/// <summary>
@@ -28,7 +26,6 @@ public sealed class LazyAssemblyLoader
2826
/// <param name="jsRuntime">The <see cref="IJSRuntime"/>.</param>
2927
public LazyAssemblyLoader(IJSRuntime jsRuntime)
3028
{
31-
_jsRuntime = jsRuntime;
3229
}
3330

3431
/// <summary>
@@ -70,6 +67,7 @@ private static Task<IEnumerable<Assembly>> LoadAssembliesInServerAsync(IEnumerab
7067
}
7168

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

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

114-
if (count == 0)
115-
{
116-
return Array.Empty<Assembly>();
117-
}
108+
await Task.WhenAll(pendingLoads);
109+
return loadedAssemblies;
110+
}
118111

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

125-
for (int i = 0; i < assemblies.Length; i++)
126-
{
127-
// The runtime loads assemblies into an isolated context by default. As a result,
128-
// assemblies that are loaded via Assembly.Load aren't available in the app's context
129-
// AKA the default context. To work around this, we explicitly load the assemblies
130-
// into the default app context.
131-
var assembly = assemblies[i];
132-
var pdb = pdbs[i];
133-
var loadedAssembly = pdb.Length == 0 ?
134-
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(assembly)) :
135-
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(assembly), new MemoryStream(pdb));
136-
loadedAssemblies.Add(loadedAssembly);
137-
_loadedAssemblyCache.Add(loadedAssembly.GetName().Name + ".dll");
138-
}
118+
var dllBytes = files.GetPropertyAsByteArray("dll")!;
119+
var pdbBytes = files.GetPropertyAsByteArray("pdb");
120+
Assembly loadedAssembly = pdbBytes == null
121+
? AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes))
122+
: AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes), new MemoryStream(pdbBytes));
139123

140-
return loadedAssemblies;
124+
loadedAssemblies.Add(loadedAssembly);
125+
_loadedAssemblyCache!.Add(assemblyToLoad);
126+
127+
}
128+
129+
private partial class LazyAssemblyLoaderInterop
130+
{
131+
[JSImport("Blazor._internal.loadLazyAssembly", "blazor-internal")]
132+
public static partial Task<JSObject> LoadLazyAssembly(string assemblyToLoad);
141133
}
142134
}

0 commit comments

Comments
 (0)