Browse Source

[genhttp] Improve database performance (#10340)

* Switch to context pooling and compiled models

* Fix tracking settings
Andreas Nägeli 2 weeks ago
parent
commit
0446810b68

+ 6 - 0
frameworks/CSharp/genhttp/Benchmarks/Benchmarks.csproj

@@ -35,6 +35,12 @@
         <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
 
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" PrivateAssets="all" />
+        
+    </ItemGroup>
+
+    <ItemGroup>
+      <Folder Include="Model\CompiledModel\" />
     </ItemGroup>
 
 </Project>

+ 9 - 0
frameworks/CSharp/genhttp/Benchmarks/Model/CompiledModel/DatabaseContextAssemblyAttributes.cs

@@ -0,0 +1,9 @@
+// <auto-generated />
+using Benchmarks;
+using Benchmarks.Model;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+#pragma warning disable 219, 612, 618
+#nullable disable
+
+[assembly: DbContextModel(typeof(DatabaseContext), typeof(DatabaseContextModel))]

+ 48 - 0
frameworks/CSharp/genhttp/Benchmarks/Model/CompiledModel/DatabaseContextModel.cs

@@ -0,0 +1,48 @@
+// <auto-generated />
+using Benchmarks.Model;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+
+#pragma warning disable 219, 612, 618
+#nullable disable
+
+namespace Benchmarks
+{
+    [DbContext(typeof(DatabaseContext))]
+    public partial class DatabaseContextModel : RuntimeModel
+    {
+        private static readonly bool _useOldBehavior31751 =
+            System.AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue31751", out var enabled31751) && enabled31751;
+
+        static DatabaseContextModel()
+        {
+            var model = new DatabaseContextModel();
+
+            if (_useOldBehavior31751)
+            {
+                model.Initialize();
+            }
+            else
+            {
+                var thread = new System.Threading.Thread(RunInitialization, 10 * 1024 * 1024);
+                thread.Start();
+                thread.Join();
+
+                void RunInitialization()
+                {
+                    model.Initialize();
+                }
+            }
+
+            model.Customize();
+            _instance = (DatabaseContextModel)model.FinalizeModel();
+        }
+
+        private static DatabaseContextModel _instance;
+        public static IModel Instance => _instance;
+
+        partial void Initialize();
+
+        partial void Customize();
+    }
+}

+ 32 - 0
frameworks/CSharp/genhttp/Benchmarks/Model/CompiledModel/DatabaseContextModelBuilder.cs

@@ -0,0 +1,32 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#pragma warning disable 219, 612, 618
+#nullable disable
+
+namespace Benchmarks
+{
+    public partial class DatabaseContextModel
+    {
+        private DatabaseContextModel()
+            : base(skipDetectChanges: false, modelId: new Guid("e6a922c5-5e25-4191-8617-6c6410b754cc"), entityTypeCount: 2)
+        {
+        }
+
+        partial void Initialize()
+        {
+            var fortune = FortuneEntityType.Create(this);
+            var world = WorldEntityType.Create(this);
+
+            FortuneEntityType.CreateAnnotations(fortune);
+            WorldEntityType.CreateAnnotations(world);
+
+            AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+            AddAnnotation("ProductVersion", "10.0.0");
+            AddAnnotation("Relational:MaxIdentifierLength", 63);
+        }
+    }
+}

+ 68 - 0
frameworks/CSharp/genhttp/Benchmarks/Model/CompiledModel/FortuneEntityType.cs

@@ -0,0 +1,68 @@
+// <auto-generated />
+using System;
+using System.Reflection;
+using Benchmarks.Model;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#pragma warning disable 219, 612, 618
+#nullable disable
+
+namespace Benchmarks
+{
+    [EntityFrameworkInternal]
+    public partial class FortuneEntityType
+    {
+        public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
+        {
+            var runtimeEntityType = model.AddEntityType(
+                "Benchmarks.Model.Fortune",
+                typeof(Fortune),
+                baseEntityType,
+                propertyCount: 2,
+                keyCount: 1);
+
+            var id = runtimeEntityType.AddProperty(
+                "Id",
+                typeof(int),
+                propertyInfo: typeof(Fortune).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+                fieldInfo: typeof(Fortune).GetField("<Id>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+                valueGenerated: ValueGenerated.OnAdd,
+                afterSaveBehavior: PropertySaveBehavior.Throw,
+                sentinel: 0);
+            id.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+            id.AddAnnotation("Relational:ColumnName", "id");
+
+            var message = runtimeEntityType.AddProperty(
+                "Message",
+                typeof(string),
+                propertyInfo: typeof(Fortune).GetProperty("Message", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+                fieldInfo: typeof(Fortune).GetField("<Message>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+                nullable: true,
+                maxLength: 2048);
+            message.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
+            message.AddAnnotation("Relational:ColumnName", "message");
+
+            var key = runtimeEntityType.AddKey(
+                new[] { id });
+            runtimeEntityType.SetPrimaryKey(key);
+
+            return runtimeEntityType;
+        }
+
+        public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
+        {
+            runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
+            runtimeEntityType.AddAnnotation("Relational:Schema", null);
+            runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
+            runtimeEntityType.AddAnnotation("Relational:TableName", "fortune");
+            runtimeEntityType.AddAnnotation("Relational:ViewName", null);
+            runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
+
+            Customize(runtimeEntityType);
+        }
+
+        static partial void Customize(RuntimeEntityType runtimeEntityType);
+    }
+}

+ 67 - 0
frameworks/CSharp/genhttp/Benchmarks/Model/CompiledModel/WorldEntityType.cs

@@ -0,0 +1,67 @@
+// <auto-generated />
+using System;
+using System.Reflection;
+using Benchmarks.Model;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#pragma warning disable 219, 612, 618
+#nullable disable
+
+namespace Benchmarks
+{
+    [EntityFrameworkInternal]
+    public partial class WorldEntityType
+    {
+        public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
+        {
+            var runtimeEntityType = model.AddEntityType(
+                "Benchmarks.Model.World",
+                typeof(World),
+                baseEntityType,
+                propertyCount: 2,
+                keyCount: 1);
+
+            var id = runtimeEntityType.AddProperty(
+                "Id",
+                typeof(int),
+                propertyInfo: typeof(World).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+                fieldInfo: typeof(World).GetField("<Id>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+                valueGenerated: ValueGenerated.OnAdd,
+                afterSaveBehavior: PropertySaveBehavior.Throw,
+                sentinel: 0);
+            id.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+            id.AddAnnotation("Relational:ColumnName", "id");
+
+            var randomNumber = runtimeEntityType.AddProperty(
+                "RandomNumber",
+                typeof(int),
+                propertyInfo: typeof(World).GetProperty("RandomNumber", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+                fieldInfo: typeof(World).GetField("<RandomNumber>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+                sentinel: 0);
+            randomNumber.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
+            randomNumber.AddAnnotation("Relational:ColumnName", "randomnumber");
+
+            var key = runtimeEntityType.AddKey(
+                new[] { id });
+            runtimeEntityType.SetPrimaryKey(key);
+
+            return runtimeEntityType;
+        }
+
+        public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
+        {
+            runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
+            runtimeEntityType.AddAnnotation("Relational:Schema", null);
+            runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
+            runtimeEntityType.AddAnnotation("Relational:TableName", "world");
+            runtimeEntityType.AddAnnotation("Relational:ViewName", null);
+            runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
+
+            Customize(runtimeEntityType);
+        }
+
+        static partial void Customize(RuntimeEntityType runtimeEntityType);
+    }
+}

+ 15 - 0
frameworks/CSharp/genhttp/Benchmarks/Model/Database.cs

@@ -0,0 +1,15 @@
+namespace Benchmarks.Model;
+
+public static class Database
+{
+    public static readonly DatabaseContextPool<DatabaseContext> NoTrackingPool;
+
+    public static readonly DatabaseContextPool<DatabaseContext> TrackingPool;
+
+    static Database()
+    {
+        NoTrackingPool = new DatabaseContextPool<DatabaseContext>(factory: DatabaseContext.CreateNoTracking, maxSize: 512);
+        TrackingPool = new DatabaseContextPool<DatabaseContext>(factory: DatabaseContext.CreateTracking, maxSize: 512);
+    }
+
+}

+ 23 - 18
frameworks/CSharp/genhttp/Benchmarks/Model/DatabaseContext.cs

@@ -1,43 +1,48 @@
 using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
 
 namespace Benchmarks.Model;
 
 public sealed class DatabaseContext : DbContext
 {
-    private static DbContextOptions<DatabaseContext> _options;
+    private static readonly Lazy<DbContextOptions<DatabaseContext>> TrackingOptions = new(() => CreateOptions(true), LazyThreadSafetyMode.ExecutionAndPublication);
 
-    private static DbContextOptions<DatabaseContext> _noTrackingOptions;
+    private static readonly Lazy<DbContextOptions<DatabaseContext>> NoTrackingOptions = new(() => CreateOptions(false), LazyThreadSafetyMode.ExecutionAndPublication);
 
-    #region Factory
+    public static DatabaseContext CreateTracking() => new(TrackingOptions.Value, true);
 
-    public static DatabaseContext Create() => new(_options ??= GetOptions(true));
+    public static DatabaseContext CreateNoTracking() => new(NoTrackingOptions.Value, false);
 
-    public static DatabaseContext CreateNoTracking() => new(_noTrackingOptions ??= GetOptions(false));
-
-    private static DbContextOptions<DatabaseContext> GetOptions(bool tracking)
+    private static DbContextOptions<DatabaseContext> CreateOptions(bool tracking)
     {
-        var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>();
+        var services = new ServiceCollection();
+
+        services.AddEntityFrameworkNpgsql();
+
+        var provider = services.BuildServiceProvider();
 
-        optionsBuilder.UseNpgsql("Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;SSL Mode=Disable;Maximum Pool Size=512;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4");
+        var builder = new DbContextOptionsBuilder<DatabaseContext>();
+
+        builder.UseInternalServiceProvider(provider)
+               .UseNpgsql("Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;SSL Mode=Disable;Maximum Pool Size=512;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4;Multiplexing=true")
+               .EnableThreadSafetyChecks(false)
+               .UseModel(DatabaseContextModel.Instance);
 
         if (!tracking)
         {
-            optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
+            builder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
         }
 
-        return optionsBuilder.Options;
+        return builder.Options;
     }
 
-    private DatabaseContext(DbContextOptions options) : base(options) { }
-
-    #endregion
-
-    #region Entities
+    internal DatabaseContext(DbContextOptions<DatabaseContext> options, bool tracking = false) : base(options)
+    {
+        ChangeTracker.AutoDetectChangesEnabled = tracking;
+    }
 
     public DbSet<World> World { get; set; }
 
     public DbSet<Fortune> Fortune { get; set; }
 
-    #endregion
-
 }

+ 18 - 0
frameworks/CSharp/genhttp/Benchmarks/Model/DatabaseContextFactory.cs

@@ -0,0 +1,18 @@
+namespace Benchmarks.Model;
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+
+public class DatabaseContextFactory : IDesignTimeDbContextFactory<DatabaseContext>
+{
+
+    public DatabaseContext CreateDbContext(string[] args)
+    {
+        var options = new DbContextOptionsBuilder<DatabaseContext>()
+                      .UseNpgsql("Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;SSL Mode=Disable")
+                      .Options;
+
+        return new DatabaseContext(options);
+    }
+
+}

+ 46 - 0
frameworks/CSharp/genhttp/Benchmarks/Model/DatabaseContextPool.cs

@@ -0,0 +1,46 @@
+namespace Benchmarks.Model;
+
+using System.Collections.Concurrent;
+
+using Microsoft.EntityFrameworkCore;
+
+public sealed class DatabaseContextPool<TContext> where TContext : DbContext
+{
+    private readonly ConcurrentBag<TContext> _pool = new();
+
+    private readonly Func<TContext> _factory;
+
+    private readonly int _maxSize;
+
+    public DatabaseContextPool(Func<TContext> factory, int maxSize)
+    {
+        _factory = factory;
+        _maxSize = maxSize;
+    }
+
+    public TContext Rent()
+    {
+        if (_pool.TryTake(out var ctx))
+        {
+            ctx.ChangeTracker.Clear();
+            return ctx;
+        }
+
+        return _factory();
+    }
+
+    public void Return(TContext context)
+    {
+        if (_pool.Count >= _maxSize)
+        {
+            context.Dispose();
+            return;
+        }
+
+
+        context.ChangeTracker.Clear();
+
+        _pool.Add(context);
+    }
+
+}

+ 21 - 14
frameworks/CSharp/genhttp/Benchmarks/Tests/CacheResource.cs

@@ -37,29 +37,36 @@ public sealed class CacheResource
 
         var result = new List<World>(count);
 
-        await using var context = DatabaseContext.CreateNoTracking();
+        var context = Database.NoTrackingPool.Rent();
 
-        for (var i = 0; i < count; i++)
+        try
         {
-            var id = Random.Next(1, 10001);
+            for (var i = 0; i < count; i++)
+            {
+                var id = Random.Next(1, 10001);
 
-            var key = CacheKeys[id];
+                var key = CacheKeys[id];
 
-            var data = Cache.Get<World>(key);
+                var data = Cache.Get<World>(key);
 
-            if (data != null)
-            {
-                result.Add(data);
-            }
-            else
-            {
-                var resolved = await context.World.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false);
+                if (data != null)
+                {
+                    result.Add(data);
+                }
+                else
+                {
+                    var resolved = await context.World.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false);
 
-                Cache.Set(key, resolved);
+                    Cache.Set(key, resolved);
 
-                result.Add(resolved);
+                    result.Add(resolved);
+                }
             }
         }
+        finally
+        {
+            Database.NoTrackingPool.Return(context);
+        }
 
         return result;
     }

+ 9 - 2
frameworks/CSharp/genhttp/Benchmarks/Tests/DbResource.cs

@@ -13,9 +13,16 @@ public sealed class DbResource
     {
         var id = Random.Next(1, 10001);
 
-        await using var context = DatabaseContext.CreateNoTracking();
+        var context = Database.NoTrackingPool.Rent();
 
-        return await context.World.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false);
+        try
+        {
+            return await context.World.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false);
+        }
+        finally
+        {
+            Database.NoTrackingPool.Return(context);
+        }
     }
 
 }

+ 28 - 21
frameworks/CSharp/genhttp/Benchmarks/Tests/FortuneHandler.cs

@@ -48,36 +48,43 @@ public class FortuneHandler : IHandler
 
     private static async ValueTask<List<Value>> GetFortunes()
     {
-        await using var context = DatabaseContext.CreateNoTracking();
+        var context = Database.NoTrackingPool.Rent();
 
-        var fortunes = await context.Fortune.ToListAsync().ConfigureAwait(false);
+        try
+        {
+            var fortunes = await context.Fortune.ToListAsync().ConfigureAwait(false);
 
-        var result = new List<Value>(fortunes.Count + 1);
+            var result = new List<Value>(fortunes.Count + 1);
+
+            foreach (var fortune in fortunes)
+            {
+                result.Add(Value.FromDictionary(new Dictionary<Value, Value>()
+                {
+                    ["id"] = fortune.Id,
+                    ["message"] = HttpUtility.HtmlEncode(fortune.Message)
+                }));
+            }
 
-        foreach (var fortune in fortunes)
-        {
             result.Add(Value.FromDictionary(new Dictionary<Value, Value>()
             {
-                ["id"] = fortune.Id,
-                ["message"] = HttpUtility.HtmlEncode(fortune.Message)
+                ["id"] = 0,
+                ["message"] = "Additional fortune added at request time."
             }));
-        }
-
-        result.Add(Value.FromDictionary(new Dictionary<Value, Value>()
-        {
-            ["id"] = 0,
-            ["message"] = "Additional fortune added at request time."
-        }));
 
-        result.Sort((one, two) =>
-        {
-            var firstMessage = one.Fields["message"].AsString;
-            var secondMessage = two.Fields["message"].AsString;
+            result.Sort((one, two) =>
+            {
+                var firstMessage = one.Fields["message"].AsString;
+                var secondMessage = two.Fields["message"].AsString;
 
-            return string.Compare(firstMessage, secondMessage, StringComparison.Ordinal);
-        });
+                return string.Compare(firstMessage, secondMessage, StringComparison.Ordinal);
+            });
 
-        return result;
+            return result;
+        }
+        finally
+        {
+            Database.NoTrackingPool.Return(context);
+        }
     }
 
     #endregion

+ 12 - 5
frameworks/CSharp/genhttp/Benchmarks/Tests/QueryResource.cs

@@ -29,16 +29,23 @@ public sealed class QueryResource
 
         var result = new List<World>(count);
 
-        using var context = DatabaseContext.CreateNoTracking();
+        var context = Database.NoTrackingPool.Rent();
 
-        for (var _ = 0; _ < count; _++)
+        try
         {
-            var id = Random.Next(1, 10001);
+            for (var _ = 0; _ < count; _++)
+            {
+                var id = Random.Next(1, 10001);
 
-            result.Add(await context.World.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false));
+                result.Add(await context.World.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false));
+            }
+        }
+        finally
+        {
+            Database.NoTrackingPool.Return(context);
         }
 
         return result;
     }
 
-}
+}

+ 8 - 2
frameworks/CSharp/genhttp/Benchmarks/Tests/UpdateResource.cs

@@ -31,7 +31,9 @@ public sealed class UpdateResource
 
         var ids = Enumerable.Range(1, 10000).Select(x => Random.Next(1, 10001)).Distinct().Take(count).ToArray();
 
-        using (var context = DatabaseContext.Create())
+        var context = Database.TrackingPool.Rent();
+
+        try
         {
             foreach (var id in ids)
             {
@@ -58,7 +60,11 @@ public sealed class UpdateResource
                 await context.SaveChangesAsync();
             }
         }
+        finally
+        {
+            Database.TrackingPool.Return(context);
+        }
 
         return result;
     }
-}
+}