efcore: Keeping a DbContext open overtime eventually slows down SaveChanges()

As part of trying to re-engineer my application to combat GC collections, I’ve come across what I consider to be an EFCore performance degradation.

My scenario is a pretty much insert-only application, where I completely disable change tracking and set AutoDetectChangesEnabled = false.

Here’s the code I’m using the in-memory database to prove my point…

When I run the benchmark program I’ve written (dotnet run -c release fast) while constantly creating and disposing the DbContext objects I get a very consistent time for SaveChanges() which is around 4-6ms per 1000 entities saves.

When I run the benchmark in the “slow” mode (dotnet run -c release slow) where a single DbContext is preallocated and used throughout the test, the time for SaveChanges() gradually increases over a few hundred cycles and reaches tens of millieseconds within a few hundred cycles… The expectation is that the reuse of the same DbContext instance would be better for performance, but in fact it causes a pretty severe degradation. It would seem as though there’s some sort of resource leakage even after SaveChanges() is called.

Steps to reproduce

Here’s the small test I’ve come up with:

class Program
{
    static void Main(string[] args)
    {
        if (args[0] == "fast")
            Fast(args);
        if (args[0] == "slow")
            Slow(args);
    }

    static void Slow(string[] args)
    {
        using (var ctx = new BlogContext())
        {
            ctx.Database.EnsureDeleted();
            ctx.Database.EnsureCreated();
            ctx.ChangeTracker.QueryTrackingBehavior    = QueryTrackingBehavior.NoTracking;
            ctx.ChangeTracker.AutoDetectChangesEnabled = false;

            var sw = Stopwatch.StartNew();
            var cycle = 1;
            while (true)
            {
                PumpIntoDB(ctx, sw, cycle++);
            }
        }
    }

    static void Fast(string[] args)
    {
        using (var ctx = new BlogContext())
        {
            ctx.Database.EnsureDeleted();
            ctx.Database.EnsureCreated();
        }

        var sw = Stopwatch.StartNew();
        var cycle = 1;
        while (true)
        {
            using (var ctx = new BlogContext())
            {
                ctx.ChangeTracker.AutoDetectChangesEnabled = false;
                ctx.ChangeTracker.QueryTrackingBehavior    = QueryTrackingBehavior.NoTracking;

                PumpIntoDB(ctx, sw, cycle++);
            }
        }
    }

    static void PumpIntoDB(BlogContext ctx, Stopwatch sw, int cycle)
    {
        foreach (var i in Enumerable.Range(0, 1000))
            ctx.Blogs.Add(new Blog() {Author = $"dans{i}"});

        sw.Restart();
        ctx.SaveChanges();
        var saveChangesAdd = sw.ElapsedMilliseconds;
        Console.WriteLine($"Cycle {cycle:D4}\tSave:{saveChangesAdd}ms");
    }
}

public class Blog
{
    public int Id { get; set; }
    public string Author { get; set; }
}

public class BlogContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        //optionsBuilder.UseNpgsql("Host=localhost;Username=test;Password=test;Database=leakymcleakage");
        optionsBuilder.UseInMemoryDatabase("leakymcleakage");
    }

    public DbSet<Blog> Blogs { get; set; }
}

Further technical details

EFCore version: <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.1.0-preview2-final" />

Operating system: ubuntu 18.04 IDE: console

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 1
  • Comments: 20 (17 by maintainers)

Most upvoted comments

I am in no way recommending using IDbContextPoolable.ResetState for this :trollface:

@divega thanks for the explanation. I think I understand better.

My expectation was that if one turns off change tracking on the context (ctx.ChangeTracker.AutoDetectChangesEnabled = false; ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;) there would be no need for the DbContext to remember anything about entities which were inserted. However, I understand that the above two properties allow one to turn off certain aspects of change tracking, but without turning the feature off as a whole.

I know that at least some users (such as @damageboy) really aren’t interested in the change tracking part of EF, and prefer to manage that part themselves, but it would seem as though that isn’t 100% possible at this point. As you already provide knobs for disabling some parts of change tracking, it may be worth thinking about allowing users to go all the way and disable it altogether.