Skip to content
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

Explicit assembly loading #850

Merged
merged 27 commits into from
Aug 7, 2020
Merged

Explicit assembly loading #850

merged 27 commits into from
Aug 7, 2020

Conversation

tomasherceg
Copy link
Member

Since PR #812 contained a lot of changes and was difficult to merge, I've extracted part of the changes as a separate PR.

  • Some static methods (FindType, GetAllAssemblies etc.) were moved from ReflectionUtils to CompiledAssemblyCache and changed to be instance methods. The logic inside these methods was not changed.
  • Added experimental flag ExplicitAssemblyLoading which disables aggressive loading of all referenced assemblies (including transitive references) and uses only assemblies explicitly listed in config.Markup.Assemblies.
  • Added simple performance tracking mechanism IStartupTracer for the DotVVM startup code.
  • Since various tests and custom JSON converters relied on static ReflectionUtils, there is CompiledAssemblyCache.Instance which contains a singleton assembly cache. In tests, it is possible that multiple instances of this class are created, which is unfortunate, but tolerable. In real DotVVM web apps, there should be always only instance created by DI.

@tomasherceg tomasherceg added this to the Version 2.5 milestone Jun 14, 2020
@exyi
Copy link
Member

exyi commented Jun 16, 2020

Since various tests and custom JSON converters relied on static ReflectionUtils, there is CompiledAssemblyCache.Instance which contains a singleton assembly cache. ... In real DotVVM web apps, there should be always only instance created by DI.

No, the reason why we have everything in the DI is to allow multiple instances of DotVVM (and entire AspNetCore in a process). This approach will lead to collisions and fail horribly if I'd run multiple servers in the process.

However, it will be mostly fine to register all allowed assemblies in a single "cache" - have a union of both configurations.

@exyi
Copy link
Member

exyi commented Jun 16, 2020

The name CompiledAssemblyCache is terrible for a class that is used everywhere for doing reflection. I think should named SomethingContext, maybe DotvvmReflectionContext, but I don't know. But it is certainly not a cache of compiled assemblies, how "compiled"?

@tomasherceg
Copy link
Member Author

The name CompiledAssemblyCache is terrible for a class that is used everywhere for doing reflection. I think should named SomethingContext, maybe DotvvmReflectionContext, but I don't know. But it is certainly not a cache of compiled assemblies, how "compiled"?

I am not sure why it was named that - it is quite old. Basically it maintains a list of assemblies that are used in Roslyn compilation, and caches Roslyn MetadataReferences.
DotvvmReflectionContext seems fine to me, or maybe DotvvmCompilationContext.

@tomasherceg
Copy link
Member Author

No, the reason why we have everything in the DI is to allow multiple instances of DotVVM (and entire AspNetCore in a process). This approach will lead to collisions and fail horribly if I'd run multiple servers in the process.
Is there a way how to get things from DI from JsonConverter attributes?https://github.com/riganti/dotvvm/pull/850/files#diff-216e10f9b6e51de337bb86e303e2c5a4R19

Running two instances of DotVVM in a single app would be a nice thing, but I'm not sure how many people would even consider doing this.

startupTracer.TraceEvent(StartupTracingConstants.AddDotvvmStarted);

var config = DotvvmConfiguration.CreateDefault(s =>
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we format the { on the first line in expressions.

services.Services.AddScoped<IRequestTracer>(s => {
var config = s.GetRequiredService<DotvvmConfiguration>();
return (config.Debug ? (IRequestTracer)s.GetService<DiagnosticsRequestTracer>() : null) ?? new NullRequestTracer();
});

services.Services.AddScoped<DiagnosticsStartupTracer>();
services.Services.AddScoped<IStartupTracer>(s => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why Scoped? It is created once for the app startup anyway, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this - I haven't noticed that this method is meant for the Diagnostic Window feature in VS extension that is not finished and probably doesn't work.


namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
private static readonly DiagnosticsStartupTracer startupTracer = new DiagnosticsStartupTracer();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait, where does the information from this tracer go?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is added to the IServiceCollection container later (at the end of the file).
In ASP.NET Core, it needs to be a static field because we need it before the IServiceCollection is ready and because it is called from both ConfigureServices and Configure methods in Startup.
In OWIN it can be just a local variable as there is only one method for initialization of DotVVM.

@@ -56,7 +57,11 @@ public static DotvvmConfiguration UseDotVVM<TStartup>(this IAppBuilder app, stri

private static DotvvmConfiguration UseDotVVM(this IAppBuilder app, string applicationRootPath, bool useErrorPages, bool debug, IDotvvmServiceConfigurator configurator, IDotvvmStartup startup, Func<IServiceCollection, IServiceProvider> serviceProviderFactoryMethod = null, Action<DotvvmConfiguration> modifyConfiguration = null)
{
var config = DotvvmConfiguration.CreateDefault(s => {
var startupTracer = new DiagnosticsStartupTracer();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super confusing - one tracer is registered in the DI and there is another? Which one should be used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is correct, I removed the other one.

{
if (sentEventsCount < events.Count)
{
informationSender.SendInformationAsync(BuildDiagnosticsInformation(events.Skip(sentEventsCount).ToList()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we should rather use a loop with await Task.Delay(10sec) so that we can await this task.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a loop so it would never end. Or maybe I don't understand.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I don't understand if the timer fires only once or every 10sec. In any case, why not use Task.Delay in an async method? We could await the task returned from SendInformationAsync

public virtual CSharpCompilation CreateCompilation(string assemblyName)
{
return CSharpCompilation.Create(assemblyName, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(referencedAssembliesCache.Value.Select(a => assemblyCache.GetAssemblyMetadata(a)));
.AddReferences(assemblyCache.GetAllAssemblies().Select(a => assemblyCache.GetAssemblyMetadata(a)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to reference only some assemblies and now we reference everything when the experimental option is disabled, right? I don't know if that will break, but I'd guess there is a reson why the logic used to be so complex.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - I've distinguished between the referenced assemblies and "all" assemblies, and work only with referenced assemblies here.

@@ -167,7 +169,7 @@ public string EmitCreateObject(string typeName, object[] constructorArguments =
}

var typeSyntax = ReflectionUtils.IsFullName(typeName)
? ParseTypeName(ReflectionUtils.FindType(typeName))
? ParseTypeName(compiledAssemblyCache.FindType(typeName))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I see this, the type parsing should not be done by emitter, can we move it to the caller?

@@ -769,7 +771,7 @@ public IEnumerable<SyntaxTree> BuildTree(string namespaceName, string className,
UseType(BuilderDataContextType);

var controlType = ReflectionUtils.IsFullName(ResultControlType)
? ParseTypeName(ReflectionUtils.FindType(ResultControlType))
? ParseTypeName(compiledAssemblyCache.FindType(ResultControlType))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, type parsing should not be done by emitter.


public void Enable()
{
ThrowIfFrozen();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant.

@@ -36,7 +37,7 @@ protected virtual IEnumerable<Type> ResolveAllTypesDerivedFromIResource(string d

protected virtual IEnumerable<Assembly> GetAllAssembliesLoadedAssemblies()
{
return ReflectionUtils.GetAllAssemblies();
return AppDomain.CurrentDomain.GetAssemblies();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be unused, anyway. I don't know why it's virtual, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed together with ResolveAllTypesDerivedFromIResource that was unused too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compiler overrides this method for its purposes.

@exyi
Copy link
Member

exyi commented Jun 16, 2020

Running two instances of DotVVM in a single app would be a nice thing, but I'm not sure how many people would even consider doing this.

I did. It is what we do in the benchmarks. It is also a convenient way to run two separate apps that are on different domains in a single process (like admin + the actual app).

@tomasherceg
Copy link
Member Author

I did. It is what we do in the benchmarks. It is also a convenient way to run two separate apps that are on different domains in a single process (like admin + the actual app).

Cool, good to know.

src/DotVVM.Framework/Compilation/ViewCompilingVisitor.cs Outdated Show resolved Hide resolved
src/DotVVM.Framework/Compilation/ViewCompilingVisitor.cs Outdated Show resolved Hide resolved
{
if (sentEventsCount < events.Count)
{
informationSender.SendInformationAsync(BuildDiagnosticsInformation(events.Skip(sentEventsCount).ToList()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I don't understand if the timer fires only once or every 10sec. In any case, why not use Task.Delay in an async method? We could await the task returned from SendInformationAsync

@tomasherceg
Copy link
Member Author

Ok, I don't understand if the timer fires only once or every 10sec. In any case, why not use Task.Delay in an async method? We could await the task returned from SendInformationAsync

I've got rid of the timer completely. When the DotVVM initialization ends, the events are sent using the DiagnosticsInformationSender. In case of additional events reported after this sending, the LateInfoReported event is triggered and there is a handler which sends these late events. And it returns task in canse anyone is interested.

{new string('-', 70 + 15 + 15)}
{messages}";
}
File.AppendAllText(logFilePath, messages);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may run in parallel, we should have a lock around this line (I know it's just samples, but still)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right - fixed.

}

public bool IsAssemblyNamespace(string fullName)
=> GetAllNamespaces().Contains(fullName, StringComparer.Ordinal);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I know this is just copy paste from the old utils. However, it would make more sense to create the HashSet once with the Ordinal comparer, and then cache it in a Lazy field.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Member

@exyi exyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given we rename the CompiledAssemblyCache to something else in the next PR, this is ok :)

@tomasherceg tomasherceg merged commit 78c2f51 into master Aug 7, 2020
@tomasherceg tomasherceg deleted the explicit-assembly-loading branch August 7, 2020 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants