Reputation: 1370
We have a Windows service that loads a bunch of plugins (assemblies) in to their own AppDomain. Each plugin is aligned to a "service boundary" in the SOA sense, and so is responsible for accessing its own database. We have noticed that EF is 3 to 5 times slower when in a separate AppDomain.
I know that the first time EF creates a DbContext and hits the database, it has to do some setup work which has to be repeated per AppDomain (i.e. not cached across AppDomains). Considering that the EF code is entirely self-contained to the plugin (and hence self-contained to the AppDomain), I would have expected the timings to be comparable to the timings from the parent AppDomain. Why are they different?
Have tried targeting both .NET 4/EF 4.4 and .NET 4.5/EF 5.
class Program
{
static void Main(string[] args)
{
var watch = Stopwatch.StartNew();
var context = new Plugin.MyContext();
watch.Stop();
Console.WriteLine("outside plugin - new MyContext() : " + watch.ElapsedMilliseconds);
watch = Stopwatch.StartNew();
var posts = context.Posts.FirstOrDefault();
watch.Stop();
Console.WriteLine("outside plugin - FirstOrDefault(): " + watch.ElapsedMilliseconds);
var pluginDll = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF.Plugin\bin\Debug\EF.Plugin.dll");
var domain = AppDomain.CreateDomain("other");
var plugin = (IPlugin) domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");
plugin.FirstPost();
Console.ReadLine();
}
}
public interface IPlugin
{
void FirstPost();
}
public class MyContext : DbContext
{
public IDbSet<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
}
public class SamplePlugin : MarshalByRefObject, IPlugin
{
public void FirstPost()
{
var watch = Stopwatch.StartNew();
var context = new MyContext();
watch.Stop();
Console.WriteLine(" inside plugin - new MyContext() : " + watch.ElapsedMilliseconds);
watch = Stopwatch.StartNew();
var posts = context.Posts.FirstOrDefault();
watch.Stop();
Console.WriteLine(" inside plugin - FirstOrDefault(): " + watch.ElapsedMilliseconds);
}
}
Notes:
outside plugin - new MyContext() : 55 outside plugin - FirstOrDefault(): 783 inside plugin - new MyContext() : 352 inside plugin - FirstOrDefault(): 2675
outside plugin - new MyContext() : 53 outside plugin - FirstOrDefault(): 798 inside plugin - new MyContext() : 355 inside plugin - FirstOrDefault(): 2687
outside plugin - new MyContext() : 45 outside plugin - FirstOrDefault(): 778 inside plugin - new MyContext() : 355 inside plugin - FirstOrDefault(): 2683
After some further research in to the cost of AppDomains, there seems to be a suggestion that subsequent AppDomains have to re-JIT system DLLs and so there is an inherent start-up cost in creating an AppDomain. Is that what is happening here? I would have expected that the JIT-ing would have been on AppDomain creation, but perhaps it is EF JIT-ing when it is called?
Reference for re-JIT: http://msdn.microsoft.com/en-us/magazine/cc163655.aspx#S8
Timings sounds similar, but not sure if related: First WCF connection made in new AppDomain is very slow
Based on @Yasser's suggestion that there is EF communication across the AppDomains, I tried to isolate this further. I don't believe this to be the case.
I have completely removed any EF reference from EF.csproj. I now have enough rep to post images, so this is the solution structure:
As you can see, only the plugin has a reference to Entity Framework. I have also verified that only the plugin has a bin folder with an EntityFramework.dll.
I have added a helper to verify if the EF assembly has been loaded in the AppDomain. I have also verified (not shown) that after the call to the database, additional EF assemblies (e.g. dynamic proxy) are also loaded.
So, checking if EF has loaded at various points:
... produces:
Main - IsEFLoaded: False Plugin - IsEFLoaded: True Plugin - new MyContext() : 367 Plugin - FirstOrDefault(): 2693 Plugin - IsEFLoaded: True Main - IsEFLoaded: False
So it seems that the AppDomains are fully isolated (as expected) and the timings are the same inside the plugin.
class Program
{
static void Main(string[] args)
{
var dir = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF.Plugin\bin\Debug");
var evidence = new Evidence();
var setup = new AppDomainSetup { ApplicationBase = dir };
var domain = AppDomain.CreateDomain("other", evidence, setup);
var pluginDll = Path.Combine(dir, "EF.Plugin.dll");
var plugin = (IPlugin) domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");
Console.WriteLine("Main - IsEFLoaded: " + Helper.IsEFLoaded());
plugin.FirstPost();
Console.WriteLine("Main - IsEFLoaded: " + Helper.IsEFLoaded());
Console.ReadLine();
}
}
(Yeah, I wasn’t going to add another project for this…)
public static class Helper
{
public static bool IsEFLoaded()
{
return AppDomain.CurrentDomain
.GetAssemblies()
.Any(a => a.FullName.StartsWith("EntityFramework"));
}
}
public class SamplePlugin : MarshalByRefObject, IPlugin
{
public void FirstPost()
{
Console.WriteLine("Plugin - IsEFLoaded: " + Helper.IsEFLoaded());
var watch = Stopwatch.StartNew();
var context = new MyContext();
watch.Stop();
Console.WriteLine("Plugin - new MyContext() : " + watch.ElapsedMilliseconds);
watch = Stopwatch.StartNew();
var posts = context.Posts.FirstOrDefault();
watch.Stop();
Console.WriteLine("Plugin - FirstOrDefault(): " + watch.ElapsedMilliseconds);
Console.WriteLine("Plugin - IsEFLoaded: " + Helper.IsEFLoaded());
}
}
@Yasser: System.Data.Entity is loaded in to the plugin only after hitting the database. Initially only the EntityFramework.dll is loaded in the plugin, but post-database other EF assemblies are loaded too:
Zipped solution. The site only keeps files for 30 days. Feel free to suggest a better file sharing site.
Also, I am interested to know if you can verify my findings by referencing EF in the main project and seeing if the timings pattern from the original sample are reproducible.
To be clear, it is first call timings that I am interested in analyzing which includes EF startup. On first call, going from ~800ms in a parent AppDomain to ~2700ms in a child AppDomain is very noticeable. On subsequent calls, going from ~1ms to ~3ms is hardly noticeable at all. Why is the first call (including EF startup) so much more expensive inside child AppDomains?
I’ve updated the sample to focus just on a FirstOrDefault()
call to reduce the noise. Some timings for running in the parent AppDomain and running in 3 child AppDomains:
EF.vshost.exe|0|FirstOrDefault(): 768 EF.vshost.exe|1|FirstOrDefault(): 1 EF.vshost.exe|2|FirstOrDefault(): 1 AppDomain0|0|FirstOrDefault(): 2623 AppDomain0|1|FirstOrDefault(): 2 AppDomain0|2|FirstOrDefault(): 1 AppDomain1|0|FirstOrDefault(): 2669 AppDomain1|1|FirstOrDefault(): 2 AppDomain1|2|FirstOrDefault(): 1 AppDomain2|0|FirstOrDefault(): 2760 AppDomain2|1|FirstOrDefault(): 3 AppDomain2|2|FirstOrDefault(): 1
static void Main(string[] args)
{
var mainPlugin = new SamplePlugin();
for (var i = 0; i < 3; i++)
mainPlugin.Do(i);
Console.WriteLine();
for (var i = 0; i < 3; i++)
{
var plugin = CreatePluginForAppDomain("AppDomain" + i);
for (var j = 0; j < 3; j++)
plugin.Do(j);
Console.WriteLine();
}
Console.ReadLine();
}
private static IPlugin CreatePluginForAppDomain(string appDomainName)
{
var dir = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF.Plugin\bin\Debug");
var evidence = new Evidence();
var setup = new AppDomainSetup { ApplicationBase = dir };
var domain = AppDomain.CreateDomain(appDomainName, evidence, setup);
var pluginDll = Path.Combine(dir, "EF.Plugin.dll");
return (IPlugin) domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");
}
public class SamplePlugin : MarshalByRefObject, IPlugin
{
public void Do(int i)
{
var context = new MyContext();
var watch = Stopwatch.StartNew();
var posts = context.Posts.FirstOrDefault();
watch.Stop();
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + "|" + i + "|FirstOrDefault(): " + watch.ElapsedMilliseconds);
}
}
Zipped solution. The site only keeps files for 30 days. Feel free to suggest a better file sharing site.
Upvotes: 27
Views: 2155
Reputation: 1370
This seems to be just the cost of child AppDomains. A rather ancient post (which may no longer be relevant) suggests that there could be other considerations outside of just having to JIT-compile each child AppDomain, e.g. evaluating security policies.
Entity Framework does have a relatively high startup cost so the effects are magnified, but for comparision calling other parts of System.Data (e.g. a straight SqlDataReader
) is just as horrible:
EF.vshost.exe|0|SqlDataReader: 67 EF.vshost.exe|1|SqlDataReader: 0 EF.vshost.exe|2|SqlDataReader: 0 AppDomain0|0|SqlDataReader: 313 AppDomain0|1|SqlDataReader: 2 AppDomain0|2|SqlDataReader: 0 AppDomain1|0|SqlDataReader: 290 AppDomain1|1|SqlDataReader: 3 AppDomain1|2|SqlDataReader: 0 AppDomain2|0|SqlDataReader: 316 AppDomain2|1|SqlDataReader: 2 AppDomain2|2|SqlDataReader: 0
public class SamplePlugin : MarshalByRefObject, IPlugin
{
public void Do(int i)
{
var watch = Stopwatch.StartNew();
using (var connection = new SqlConnection("Data Source=.\\sqlexpress;Initial Catalog=EF.Plugin.MyContext;Integrated Security=true"))
{
var command = new SqlCommand("SELECT * from Posts;", connection);
connection.Open();
var reader = command.ExecuteReader();
reader.Close();
}
watch.Stop();
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + "|" + i + "|SqlDataReader: " + watch.ElapsedMilliseconds);
}
}
Even newing up a humble DataTable
is inflated:
EF.vshost.exe|0|DataTable: 0 EF.vshost.exe|1|DataTable: 0 EF.vshost.exe|2|DataTable: 0 AppDomain0|0|DataTable: 12 AppDomain0|1|DataTable: 0 AppDomain0|2|DataTable: 0 AppDomain1|0|DataTable: 11 AppDomain1|1|DataTable: 0 AppDomain1|2|DataTable: 0 AppDomain2|0|DataTable: 10 AppDomain2|1|DataTable: 0 AppDomain2|2|DataTable: 0
public class SamplePlugin : MarshalByRefObject, IPlugin
{
public void Do(int i)
{
var watch = Stopwatch.StartNew();
var table = new DataTable("");
watch.Stop();
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + "|" + i + "|DataTable: " + watch.ElapsedMilliseconds);
}
}
Upvotes: 4
Reputation: 3317
Maybe I'm wrong but with following code :
public class SamplePlugin : MarshalByRefObject, IPlugin
{
public void Do()
{
using (AppDb db = new AppDb())
{
db.Posts.FirstOrDefault();
}
}
}
and these codes :
[LoaderOptimization(LoaderOptimization.MultiDomain)]
static void Main(String[] args)
{
AppDomain.CurrentDomain.AssemblyLoad += CurrentDomain_AssemblyLoad;
var dir = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF\bin\Debug");
var evidence = new Evidence();
var setup = new AppDomainSetup { ApplicationBase = dir };
var domain = AppDomain.CreateDomain("Plugin", evidence, setup);
domain.AssemblyLoad += domain_AssemblyLoad;
var pluginDll = Path.Combine(dir, "EF.Plugin.dll");
var anotherDomainPlugin = (IPlugin)domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");
var mainDomainPlugin = new SamplePlugin();
mainDomainPlugin.Do(); // To prevent side effects of entity framework startup from our test
anotherDomainPlugin.Do(); // To prevent side effects of entity framework startup from our test
Stopwatch watch = Stopwatch.StartNew();
mainDomainPlugin.Do();
watch.Stop();
Console.WriteLine("Main Application Domain -------------------------- " + watch.ElapsedMilliseconds.ToString());
watch.Restart();
anotherDomainPlugin.Do();
watch.Stop();
Console.WriteLine("Another Application Domain -------------------------- " + watch.ElapsedMilliseconds.ToString());
Console.ReadLine();
}
static void CurrentDomain_AssemblyLoad(Object sender, AssemblyLoadEventArgs args)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Main Domain : " + args.LoadedAssembly.FullName);
}
static void domain_AssemblyLoad(Object sender, AssemblyLoadEventArgs args)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Another Domain : " + args.LoadedAssembly.FullName);
}
There is no real performance difference between main application domain and another application domain in this scenario, You get different results because your tests are wrong (-: ( At least I think they are wrong ), I've also tested main application domain by directly invoking DbContext and first or default , My times are the same and difference is between 1 - 2 milliseconds , I can't understand why my results are different than your results
Upvotes: 0
Reputation: 3317
You should run that test several times when you start your application
After first time , the performance difference is all about serialization of objects between your main application domain and the plugin application domain.
Notice that each communication between application domains need serialization & deserialization which costs too much.
You can see this problem while developing applications on [SQL Server / .NET CLR] stored procedures which run in separated application domain rather than sql server engine.
Upvotes: 3