Perf of different tracking behaviors

Open
#23,558 6 comments 9 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
25/100
Issue type
Refactor
Clarity
Needs clarification
Activity status
Stale
Tech stack
csharp

Research direction

Start with the benchmark code included in the issue, especially QueryTrackingBehavior, the Tracking, NoTracking, and NoTrackingWithIdentityResolution methods, and the Include variants. Run the benchmark to reproduce the allocation and CPU results, then investigate the listed identity-resolution and shadow-property concerns; done means a measured improvement supported by updated benchmark results.

Written by the indexing model from the issue text.

Description

area-benchmarks area-perf area-query customer-reported
public class Program { public static void Main(string[] args) { BenchmarkRunner.Run(); } }
[MemoryDiagnoser]
public class QueryTrackingBehavior
{
    private BloggingContext _context;

    [Params(1000)]
    public int NumBlogs { get; set; }

    [Params(100)]
    public int NumPostsPerBlog { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        Console.WriteLine("Setting up database...");
        using var context = new BloggingContext();
        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();
        context.SeedData(NumBlogs, NumPostsPerBlog);
        Console.WriteLine("Setup complete.");
    }

    [IterationSetup]
    public void CreateContext()
    {
        _context = new BloggingContext();
        _context.ChangeTracker.QueryTrackingBehavior = Microsoft.EntityFrameworkCore.QueryTrackingBehavior.NoTrackingWithIdentityResolution;
    }

    [IterationCleanup]
    public void DisposeContext()
    {
        _context.Dispose();
    }

    [Benchmark]
    public void Tracking()
    {
        foreach (var item in _context.Posts.AsTracking()/*.Include(p => p.Blog)*/)
        {
        }
    }

    [Benchmark]
    public void NoTracking()
    {
        foreach (var item in _context.Posts.AsNoTracking()/*.Include(p => p.Blog)*/)
        {
        }
    }

    [Benchmark]
    public void NoTrackingWithIdentityResolution()
    {
        foreach (var item in _context.Posts/*.Include(p => p.Blog)*/)
        {
        }
    }

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(
                @"Server=SLDW;Database=test;Trusted_Connection=True;;Connect Timeout=60;ConnectRetryCount=0");
            // @"Server=(localdb)\mssqllocaldb;Database=Blogging;Integrated Security=True");
        }

        public void SeedData(int numBlogs, int numPostsPerBlog)
        {
            using var context = new BloggingContext();
            context.AddRange(
                Enumerable.Range(0, numBlogs).Select(_ => new Blog
                {
                    Posts = Enumerable.Range(0, numPostsPerBlog).Select(_ => new Post()).ToList()
                }));
            context.SaveChanges();
        }
    }

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
        public int Rating { get; set; }
        public List<Post> Posts { get; set; }
    }

    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
}

Results without Include

Method NumBlogs NumPostsPerBlog Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
Tracking 1000 100 320.86 ms 6.290 ms 6.178 ms 16000.0000 7000.0000 - 110.76 MB
NoTracking 1000 100 51.55 ms 0.997 ms 1.493 ms 5000.0000 - - 25.95 MB
NoTrackingWithIdentityResolution 1000 100 305.46 ms 3.989 ms 3.114 ms 16000.0000 7000.0000 - 110.73 MB

Results with Include

Method NumBlogs NumPostsPerBlog Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
Tracking 1000 100 583.1 ms 11.64 ms 21.29 ms 24000.0000 10000.0000 1000.0000 154.54 MB
NoTracking 1000 100 149.0 ms 2.00 ms 1.87 ms 20000.0000 - - 93.86 MB
NoTrackingWithIdentityResolution 1000 100 703.3 ms 11.23 ms 9.95 ms 33000.0000 10000.0000 1000.0000 210.26 MB

The AsTracking operator has no effect in the result how it is configured.

  • Allocations/CPU is higher in identity resolution compared to tracking when doing include.
  • Tracking would have higher allocation without duplicate but in Include scenario there should be a lot of duplicate blogs so allocations should be higher than tracking and other.

Few ideas

  • Identity resolution sets IsLoaded flag for navigation differently than tracking query. That could be reason for very bad perf. (may be for allocation too)
  • There are GC everywhere but what is getting garbage collected? At least for tracking kind of scenario, we are not getting any reference out of scope.

Improvements for NoTrackingWithIdentityResolution

  • Stop passing shadow property values from query side. They cannot be accessed anyway.
  • Have a different implementation of statemanager for identity resolution which optimizes in it. We don't need
    • any snapshoting
    • Everything could be just InternalClrEntityEntry
    • No state data needs to be set

Came out discussion with @roji

Dominant language
C#
Stars
14.8k
Forks
3.4k
Avg merge
2d 5h
Merged PRs (30d)
134

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/efcore

All issues in dotnet/efcore

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.