Skip to content

Commit

Permalink
feature: Lazy Loading of Transitive Shared Types (#164)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sewer56 committed Jan 31, 2021
1 parent 5cd23e7 commit 6f40206
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 24 deletions.
65 changes: 55 additions & 10 deletions src/Plugins/Loader/AssemblyLoadContextBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class AssemblyLoadContextBuilder
private AssemblyLoadContext _defaultLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
private string? _mainAssemblyPath;
private bool _preferDefaultLoadContext;
private bool _lazyLoadReferences;

#if FEATURE_UNLOAD
private bool _isCollectible;
Expand Down Expand Up @@ -63,6 +64,7 @@ public AssemblyLoadContext Build()
resourceProbingPaths,
_defaultLoadContext,
_preferDefaultLoadContext,
_lazyLoadReferences,
#if FEATURE_UNLOAD
_isCollectible,
_loadInMemory,
Expand Down Expand Up @@ -148,21 +150,46 @@ public AssemblyLoadContextBuilder PreferLoadContextAssembly(AssemblyName assembl
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName)
{
if (assemblyName.Name == null || _defaultAssemblies.Contains(assemblyName.Name))
// Lazy loaded references have dependencies resolved as they are loaded inside the actual Load Context.
if (_lazyLoadReferences)
{
// base cases
if (assemblyName.Name != null && !_defaultAssemblies.Contains(assemblyName.Name))
{
_defaultAssemblies.Add(assemblyName.Name);
var assembly = _defaultLoadContext.LoadFromAssemblyName(assemblyName);
foreach (var reference in assembly.GetReferencedAssemblies())
{
if (reference.Name != null)
{
_defaultAssemblies.Add(reference.Name);
}
}
}

return this;
}

_defaultAssemblies.Add(assemblyName.Name);

// Recursively load and find all dependencies of default assemblies.
// This sacrifices some performance for determinism in how transitive
// dependencies will be shared between host and plugin.
var assembly = _defaultLoadContext.LoadFromAssemblyName(assemblyName);
foreach (var reference in assembly.GetReferencedAssemblies())
var names = new Queue<AssemblyName>();
names.Enqueue(assemblyName);
while (names.TryDequeue(out var name))
{
PreferDefaultLoadContextAssembly(reference);
if (name.Name == null || _defaultAssemblies.Contains(name.Name))
{
// base cases
continue;
}

_defaultAssemblies.Add(name.Name);

// Load and find all dependencies of default assemblies.
// This sacrifices some performance for determinism in how transitive
// dependencies will be shared between host and plugin.
var assembly = _defaultLoadContext.LoadFromAssemblyName(name);

foreach (var reference in assembly.GetReferencedAssemblies())
{
names.Enqueue(reference);
}
}

return this;
Expand All @@ -187,6 +214,24 @@ public AssemblyLoadContextBuilder PreferDefaultLoadContext(bool preferDefaultLoa
return this;
}

/// <summary>
/// Instructs the load context to lazy load dependencies of all shared assemblies.
/// Reduces plugin load time at the expense of non-determinism in how transitive dependencies are loaded
/// between the plugin and the host.
///
/// Please be aware of the danger of using this option:
/// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/pull/164#issuecomment-751557873">
/// https://github.com/natemcmaster/DotNetCorePlugins/pull/164#issuecomment-751557873
/// </seealso>
/// </summary>
/// <param name="isLazyLoaded">True to lazy load, else false.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder IsLazyLoaded(bool isLazyLoaded)
{
_lazyLoadReferences = isLazyLoaded;
return this;
}

/// <summary>
/// Add a managed library to the load context.
/// </summary>
Expand Down
20 changes: 18 additions & 2 deletions src/Plugins/Loader/ManagedLoadContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ internal class ManagedLoadContext : AssemblyLoadContext
private readonly IReadOnlyDictionary<string, ManagedLibrary> _managedAssemblies;
private readonly IReadOnlyDictionary<string, NativeLibrary> _nativeLibraries;
private readonly IReadOnlyCollection<string> _privateAssemblies;
private readonly IReadOnlyCollection<string> _defaultAssemblies;
private readonly ICollection<string> _defaultAssemblies;
private readonly IReadOnlyCollection<string> _additionalProbingPaths;
private readonly bool _preferDefaultLoadContext;
private readonly string[] _resourceRoots;
private readonly bool _loadInMemory;
private readonly bool _lazyLoadReferences;
private readonly AssemblyLoadContext _defaultLoadContext;
#if FEATURE_NATIVE_RESOLVER
private readonly AssemblyDependencyResolver _dependencyResolver;
Expand All @@ -45,6 +46,7 @@ internal class ManagedLoadContext : AssemblyLoadContext
IReadOnlyCollection<string> resourceProbingPaths,
AssemblyLoadContext defaultLoadContext,
bool preferDefaultLoadContext,
bool lazyLoadReferences,
bool isCollectible,
bool loadInMemory,
bool shadowCopyNativeLibraries)
Expand All @@ -64,12 +66,13 @@ internal class ManagedLoadContext : AssemblyLoadContext
_basePath = Path.GetDirectoryName(mainAssemblyPath) ?? throw new ArgumentException(nameof(mainAssemblyPath));
_managedAssemblies = managedAssemblies ?? throw new ArgumentNullException(nameof(managedAssemblies));
_privateAssemblies = privateAssemblies ?? throw new ArgumentNullException(nameof(privateAssemblies));
_defaultAssemblies = defaultAssemblies ?? throw new ArgumentNullException(nameof(defaultAssemblies));
_defaultAssemblies = defaultAssemblies != null ? defaultAssemblies.ToList() : throw new ArgumentNullException(nameof(defaultAssemblies));
_nativeLibraries = nativeLibraries ?? throw new ArgumentNullException(nameof(nativeLibraries));
_additionalProbingPaths = additionalProbingPaths ?? throw new ArgumentNullException(nameof(additionalProbingPaths));
_defaultLoadContext = defaultLoadContext;
_preferDefaultLoadContext = preferDefaultLoadContext;
_loadInMemory = loadInMemory;
_lazyLoadReferences = lazyLoadReferences;

_resourceRoots = new[] { _basePath }
.Concat(resourceProbingPaths)
Expand Down Expand Up @@ -105,6 +108,19 @@ internal class ManagedLoadContext : AssemblyLoadContext
var defaultAssembly = _defaultLoadContext.LoadFromAssemblyName(assemblyName);
if (defaultAssembly != null)
{
// Add referenced assemblies to the list of default assemblies.
// This is basically lazy loading
if (_lazyLoadReferences)
{
foreach (var reference in defaultAssembly.GetReferencedAssemblies())
{
if (reference.Name != null && !_defaultAssemblies.Contains(reference.Name))
{
_defaultAssemblies.Add(reference.Name);
}
}
}

// Older versions used to return null here such that returned assembly would be resolved from the default ALC.
// However, with the addition of custom default ALCs, the Default ALC may not be the user's chosen ALC when
// this context was built. As such, we simply return the Assembly from the user's chosen default load context.
Expand Down
20 changes: 16 additions & 4 deletions src/Plugins/PluginConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,22 @@ public PluginConfig(string mainAssemblyPath)
/// </summary>
public bool PreferSharedTypes { get; set; }

/// <summary>
/// <summary>
/// If enabled, will lazy load dependencies of all shared assemblies.
/// Reduces plugin load time at the expense of non-determinism in how transitive dependencies are loaded
/// between the plugin and the host.
///
/// Please be aware of the danger of using this option:
/// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/pull/164#issuecomment-751557873">
/// https://github.com/natemcmaster/DotNetCorePlugins/pull/164#issuecomment-751557873
/// </seealso>
/// </summary>
public bool IsLazyLoaded { get; set; } = false;

/// <summary>
/// If set, replaces the default <see cref="AssemblyLoadContext"/> used by the <see cref="PluginLoader"/>.
/// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
/// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>
/// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>
/// </summary>
public AssemblyLoadContext DefaultContext { get; set; } = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;

Expand Down Expand Up @@ -104,9 +116,9 @@ public bool LoadInMemory
/// </remarks>
public bool EnableHotReload { get; set; }

/// <summary>
/// <summary>
/// Specifies the delay to reload a plugin, after file changes have been detected.
/// Default value is 200 milliseconds.
/// Default value is 200 milliseconds.
/// </summary>
public TimeSpan ReloadDelay { get; set; } = TimeSpan.FromMilliseconds(200);
#endif
Expand Down
10 changes: 9 additions & 1 deletion src/Plugins/PluginLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
Expand Down Expand Up @@ -112,9 +113,15 @@ public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Type[] sh
{
if (sharedTypes != null)
{
var uniqueAssemblies = new HashSet<Assembly>();
foreach (var type in sharedTypes)
{
config.SharedAssemblies.Add(type.Assembly.GetName());
uniqueAssemblies.Add(type.Assembly);
}
foreach (var assembly in uniqueAssemblies)
{
config.SharedAssemblies.Add(assembly.GetName());
}
}
configure(config);
Expand Down Expand Up @@ -374,6 +381,7 @@ private static AssemblyLoadContextBuilder CreateLoadContextBuilder(PluginConfig
}
#endif

builder.IsLazyLoaded(config.IsLazyLoaded);
foreach (var assemblyName in config.SharedAssemblies)
{
builder.PreferDefaultLoadContextAssembly(assemblyName);
Expand Down
3 changes: 3 additions & 0 deletions src/Plugins/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
McMaster.NETCore.Plugins.PluginConfig.IsLazyLoaded.get -> bool
McMaster.NETCore.Plugins.PluginConfig.IsLazyLoaded.set -> void
McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.IsLazyLoaded(bool isLazyLoaded) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
3 changes: 2 additions & 1 deletion src/Plugins/releasenotes.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
<PropertyGroup>
<PackageReleaseNotes Condition="'$(VersionPrefix)' == '1.4.0'">
Changes:
* @Sewer56 - search in additional probing paths (PR #172)
* @Sewer56 - feature: add option to support lazy loading of transitive dependencies to increase performance (PR #164)
* @Sewer56 - bugfix: search in additional probing paths (PR #172)
</PackageReleaseNotes>
<PackageReleaseNotes Condition="'$(VersionPrefix)' == '1.3.1'">
Changes:
Expand Down
11 changes: 5 additions & 6 deletions test/Plugins.Tests/SharedTypesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,12 @@ public void PluginsCanForceSharedTypes()
/// accounted for. Without this, the order in which code loads
/// could cause different assembly versions to be loaded.
/// </summary>
[Fact]
public void TransitiveAssembliesOfSharedTypesAreResolved()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void TransitiveAssembliesOfSharedTypesAreResolved(bool isLazyLoaded)
{
using var loader = PluginLoader.CreateFromAssemblyFile(
TestResources.GetTestProjectAssembly("TransitivePlugin"),
sharedTypes: new[] { typeof(SharedType) });

using var loader = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("TransitivePlugin"), sharedTypes: new[] { typeof(SharedType) }, config => config.IsLazyLoaded = isLazyLoaded);
var assembly = loader.LoadDefaultAssembly();
var configType = assembly.GetType("TransitivePlugin.PluginConfig", throwOnError: true)!;
var config = Activator.CreateInstance(configType);
Expand Down

0 comments on commit 6f40206

Please sign in to comment.