Tuesday, June 30, 2015

Three steps for fast entityframework 6.1 code-first startup performance

So you are using entity framework and frustrated about slow application startup? In this blog post I will show you three steps for massively improved first query performance (80% decrease), especially for code-first scenarios. With these steps, first query-time in our application dropped from ~4.5 seconds down to ~ 1 second!

1. Using a cached db model store

  • This has probabily the biggest effect on startup performance and is only necessary if you are using the code first model. Building and compiling large models using the Code First pipeline is extremly expensive in terms of start up time. This step will cache the code-first pipeline with its expensive o-c mapping generation and will store it in a xml file on the filesystem. The next time your application starts, EF will deserialize this cached mapping file which significantly reduces startup time.
    Unfortunately ef does not come with the cached db model store build in, so you need to clone and manually build my DbModelStore branch on github in order to use it. In this branch I integrated a patch provided byentityframework team-member emicolos which he kindly provided here. Once you have done that, you can enable the cached db model store with the following lines of code:
    
    public class MyContextConfiguration : DbConfiguration
    {
        public MyContextConfiguration()
        {
            string cachePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\YOUR_APP_NAME\EFCache\";
            MyDbModelStore cachedDbModelStore = new MyDbModelStore(cachePath);
            IDbDependencyResolver dependencyResolver = new SingletonDependencyResolver(cachedDbModelStore);
            AddDependencyResolver(dependencyResolver);
        }
    
        private class MyDbModelStore : DefaultDbModelStore
        {
            private static bool useCachedDbModelStore;
     
            // Note that you should only enable DbContextStore during normal run scenarios without migrations. Migrations are currently not supported and will crash.
            public static void Configure(bool useCachedDbModelStore)
            {
                MyContextConfiguration.useCachedDbModelStore = useCachedDbModelStore;
            }
     
            public MyContextConfiguration()
            {
                // CachedDbModel store wird derzeit nicht immer verwendet, da er z.b. bei Migrations derzeit noch nicht funktioniert (Exceptions im EF Code)
                if (useCachedDbModelStore)
                {
                    MyDbModelStore cachedDbModelStore = new MyDbModelStore(MyContext.EfCacheDirPath);
                    IDbDependencyResolver dependencyResolver = new SingletonDependencyResolver(cachedDbModelStore);
                    AddDependencyResolver(dependencyResolver);
                }
            }
    
            private class MyDbModelStore : DefaultDbModelStore
            {
                public MyDbModelStore(string location)
                    : base(location)
                {}
    
                public override DbCompiledModel TryLoad(Type contextType)
                {
                    string path = GetFilePath(contextType);
                    if(File.Exists(path))
                    {
                        DateTime lastWriteTime = File.GetLastWriteTimeUtc(path);
                        DateTime lastWriteTimeDomainAssembly = File.GetLastWriteTimeUtc(typeof(TypeInYourDomainAssembly).Assembly.Location);
                        if (lastWriteTimeDomainAssembly > lastWriteTime)
                        {
                            File.Delete(path);
                            Tracers.EntityFramework.TraceInformation("Cached db model obsolete. Re-creating cached db model edmx.");
                        }
                    }
                    else
                    {
                        Tracers.EntityFramework.TraceInformation("No cached db model found. Creating cached db model edmx.");
                    }
    
                    return base.TryLoad(contextType);
                }
            }
    }
    Using the cached db model store saves ~ 3.5 seconds on my I7 developer machine when using a model with about 80 entities.
    Important: Since the model store has to be invalidated and rebuild every time your model changes, this method will only make sense if your entities are located within an isolated assembly. Otherwise the cache will be invalidated every time you change a line in your code and so you wont be saving anything.
    Note that you should only enable DbContextStore during normal run scenarios without migrations. Migrations are currently not supported and will crash.

2. Generate pre-compiled views

  • Before Entity Framework can execute a query or save changes to a data source, it must generate a set of local query views to access the database. These views are part of the metadata which is cached per application domain. Depending on the model size (amount of entities, associations etc.) view generation can have a significant enhancement on startup performance (1-30 seconds and more) and so caching this step is a must in order to achieve good EF startup performance.
    There are different ways to generate and store pre-compiled views, however in my opionion the easiest and most flexible way is to use this very nice nuget package: Interactive Pregenerated Views for Entity Framework 6.
    Once you have downloaded and installed the libary, you just need to configure it in your db context:
    
    // Enable DbModelStore before creating your first DbContext.
    // Do not call this method when using migrations, as they are currently not supported.
    MyContextConfiguration.Configure(useCachedDbModelStore: true);
    
    using (var ctx = new MyContext())
    {
        InteractiveViews
            .SetViewCacheFactory(
                ctx, 
                new FileViewCacheFactory(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\YOUR_APP_NAME\EFCache\"));
    }
    Now you only pay the view generation penalty the first time you start your application. All subsequent startups will use the cached xml file and will result in significant reduced startup time.

3. Generate pre-compiled version of entityframework using n-gen to avoid jitting

  • Entity Framework does not come in the default installation of the .net Framework. Therefore, the EF assembly is not NGEN'd by default which means that EF code needs to be JITTED each time the application starts. Since EF is a really large and complex framework (EntityFramework assembly has over 5MB), and most of the code paths are needed even for simple scenarios, Jitting has a noticeable degradation on startup performance.
    Some rough benchmarks on my I7 developer machine and Core2 notebook showed a drop in startup time by about 1-2 seconds.
    On really slow machines the performance gains can even be bigger (E.x. we measured 3-4 seconds JIT time on a slow virtualized windows server instance).
    Running NGEN against EF is as simple as executing the following command within a root terminal session:
    %WINDIR%\Microsoft.NET\Framework\v4.0.30319\ngen install EntityFramework.dll
    For more information about Entity Framework 6 and NGEN I recommend this msdn article: http://msdn.microsoft.com/en-us/data/dn582034

No comments:

Post a Comment