У нас есть служба Windows, которая загружает кучу плагинов (сборок) в свой собственный AppDomain. Каждый плагин привязан к "границе службы" в смысле SOA и поэтому отвечает за доступ к своей собственной базе данных. Мы заметили, что EF в 3 - 5 раз медленнее, когда в отдельном AppDomain.
Я знаю, что в первый раз, когда EF создает DbContext и попадает в базу данных, он должен выполнять некоторую работу по настройке, которая должна повторяться в AppDomain (т.е. не кэшироваться через AppDomains). Учитывая, что EF-код полностью автономный для плагина (и, следовательно, автономный для AppDomain), я ожидал, что тайминги будут сопоставимы с таймингами из родительского AppDomain. Почему они разные?
Попробовали настроить как .NET 4/EF 4.4, так и .NET 4.5/EF 5.
Пример кода
EF.csproj
Program.cs
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();
}
}
EF.Interfaces.csproj
IPlugin.cs
public interface IPlugin
{
void FirstPost();
}
EF.Plugin.csproj
MyContext.cs
public class MyContext : DbContext
{
public IDbSet<Post> Posts { get; set; }
}
Post.cs
public class Post
{
public int Id { get; set; }
}
SamplePlugin.cs
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);
}
}
Пример времени
Примечания:
- Это запрос к пустой таблице базы данных - 0 строк.
- Сроки преднамеренно смотрят только на первые вызовы. Последующие вызовы выполняются намного быстрее, но все же относительно 3 - 5 раз медленнее в дочернем AppDomain и родительском AppDomain.
Выполнить 1
outside plugin - new MyContext() : 55 outside plugin - FirstOrDefault(): 783 inside plugin - new MyContext() : 352 inside plugin - FirstOrDefault(): 2675
Выполнить 2
outside plugin - new MyContext() : 53 outside plugin - FirstOrDefault(): 798 inside plugin - new MyContext() : 355 inside plugin - FirstOrDefault(): 2687
Выполнить 3
outside plugin - new MyContext() : 45 outside plugin - FirstOrDefault(): 778 inside plugin - new MyContext() : 355 inside plugin - FirstOrDefault(): 2683
Исследование AppDomain
После некоторого дальнейшего исследования стоимости AppDomains, похоже, есть предположение, что последующие AppDomains должны повторно использовать системные DLL-системы, и поэтому при создании AppDomain существует первоначальная стоимость запуска. Это то, что здесь происходит? Я бы ожидал, что JIT-ing был бы на создании AppDomain, но, возможно, это EF JIT-ing, когда он вызывается?
Ссылка для повторной JIT: http://msdn.microsoft.com/en-us/magazine/cc163655.aspx#S8
Сроки звучат одинаково, но не уверены, связаны ли они: Первое соединение WCF, созданное в новом AppDomain, очень медленное
Обновление 1
Основываясь на предположении @Yasser о наличии связи EF через AppDomains, я попытался выделить это дальше. Я не считаю, что это так.
Я полностью удалил ссылку EF из EF.csproj. У меня теперь достаточно репутации для отправки изображений, поэтому это структура решения:
Как вы можете видеть, только плагин имеет ссылку на Entity Framework. Я также подтвердил, что только плагин имеет папку bin с EntityFramework.dll.
Я добавил помощника, чтобы проверить, была ли загружена сборка EF в AppDomain. Я также проверил (не показан), что после вызова в базу данных также загружаются дополнительные сборки EF (например, динамический прокси).
Итак, проверяя, загружен ли EF в разных точках:
- В Main перед вызовом плагина
- В плагине перед удалением базы данных
- В плагине после попадания в базу данных
- В Main после вызова плагина
... производит:
Main - IsEFLoaded: False Plugin - IsEFLoaded: True Plugin - new MyContext() : 367 Plugin - FirstOrDefault(): 2693 Plugin - IsEFLoaded: True Main - IsEFLoaded: False
Итак, кажется, что AppDomains полностью изолированы (как и ожидалось), а тайминги одинаковы внутри плагина.
Обновленный пример кода
Program.cs
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();
}
}
Helper.cs
(Да, я не собирался добавлять еще один проект для этого...)
public static class Helper
{
public static bool IsEFLoaded()
{
return AppDomain.CurrentDomain
.GetAssemblies()
.Any(a => a.FullName.StartsWith("EntityFramework"));
}
}
SamplePlugin.cs
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());
}
}
Обновление 2
@Yasser: System.Data.Entity загружается в плагин только после попадания в базу данных. Изначально в плагин загружается только EntityFramework.dll, но также загружаются другие сборки EF после базы данных:
Zipped solution. Сайт хранит файлы только в течение 30 дней. Не стесняйтесь предлагать лучший сайт для обмена файлами.
Кроме того, мне интересно узнать, можете ли вы проверить мои результаты, указав EF в главном проекте и посмотреть, воспроизводится ли диаграмма таймингов из исходного образца.
Обновление 3
Чтобы быть ясным, это первые тайминги звонков, которые меня интересуют в анализе, который включает запуск EF. При первом вызове переход от ~ 800 мс в родительском AppDomain до ~ 2700 мс в дочернем приложении AppDomain очень заметен. При последующих вызовах переход от ~ 1 мс до ~ 3 мс практически не заметен. Почему первый вызов (включая запуск EF) намного дороже внутри дочерних приложений AppDomains?
Ive обновил образец, чтобы сосредоточиться только на вызове FirstOrDefault()
, чтобы уменьшить шум. Некоторые тайминги для запуска в родительском AppDomain и запуске в 3 дочерних 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-решение. Сайт хранит файлы только в течение 30 дней. Не стесняйтесь предлагать лучший сайт для обмена файлами.