-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix to #35239 - EF9: SaveChanges() is significantly slower in .NET9 vs. .NET8 when using .ToJson() Mapping vs. PostgreSQL Legacy POCO mapping #35326
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
|
41306eb
to
56e51cf
Compare
@maumar for reference, could you include the benchmark results before and after this change, as well as the benchmark code itself? |
934f69c
to
da82547
Compare
|
a949c83
to
c8d3f3f
Compare
perf numbers with warmup, so that comparers are/should be compiled: no warmup: Will convert it to proper BDN and post code and more accurate numbers. But the improvement is significant. |
src/EFCore.Cosmos/ChangeTracking/Internal/StringDictionaryComparer.cs
Outdated
Show resolved
Hide resolved
BDN numbers: 8.0.11
9.0
9.0.2 (with fix)
benchmark code: // See https://aka.ms/new-console-template for more information
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
using Microsoft.EntityFrameworkCore;
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, DefaultConfig.Instance);
public class SaveChangesBenchmark
{
private MyContext _ctx = new MyContext();
[GlobalSetup]
public virtual async Task Initialize()
{
var ctx = new MyContext();
await ctx.Database.EnsureDeletedAsync();
await ctx.Database.EnsureCreatedAsync();
var seedData = ReturnSeedData();
ctx.SampleEntities.AddRange(seedData);
ctx.SaveChanges();
}
[IterationSetup]
public void Setup()
{
_ctx = new MyContext();
_ctx.ChangeTracker.Clear();
foreach (var testId in Enumerable.Range(1, 200))
{
var src = _ctx.SampleEntities.Where(a => a.TestId == testId).FirstOrDefault();
}
}
[Benchmark]
public async Task SaveChangesTest()
{
await _ctx.SaveChangesAsync();
}
private static List<SampleEntity> ReturnSeedData()
{
var list = new List<SampleEntity>();
for (int i = 0; i < 200; i++)
{
var s = new SampleEntity
{
TestId = i,
RockId = Guid.NewGuid(),
SccId = Guid.NewGuid(),
AnotherId = Guid.NewGuid(),
IsPushed = false,
SeId = Guid.NewGuid(),
SampleId = Guid.NewGuid(),
Jsons = []
};
for (int j = 0; j < 300; j++)
{
var studentGradingNeed = new SampleJson
{
ComponentGroupId = Guid.NewGuid(),
ComponentId = Guid.NewGuid(),
MonthId = Guid.NewGuid(),
IsGroupLevel = false,
MeasureId = Guid.NewGuid(),
OrderedComponentTrackingId = Guid.NewGuid(),
Result = new ResultJson
{
CreatedBy = Guid.NewGuid(),
CreatedByName = "Test",
CreatedDate = DateTime.UtcNow,
LastModifiedBy = Guid.NewGuid(),
LastModifiedByName = "Test",
LastModifiedDate = DateTime.UtcNow,
MarkIdValue = Guid.NewGuid(),
NumericValue = 44,
CommentIds = new List<Guid> { Guid.NewGuid() },
TextValue = "FF"
}
};
s.Jsons.Add(studentGradingNeed);
}
list.Add(s);
}
return list;
}
}
public class MyContext : DbContext
{
public DbSet<SampleEntity> SampleEntities { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=ReproSaveChangesBDN;Trusted_Connection=True;MultipleActiveResultSets=true");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<SampleEntity>().OwnsMany(c => c.Jsons, d =>
{
d.ToJson();
d.OwnsOne(e => e.Result);
});
}
}
public class SampleEntity
{
public Guid Id { get; set; }
public Guid SampleId { get; set; }
public Guid AnotherId { get; set; }
public int TestId { get; set; }
public Guid RockId { get; set; }
public Guid SccId { get; set; }
public Guid SeId { get; set; }
public List<SampleJson> Jsons { get; set; } = [];
public bool IsPushed { get; set; }
}
public record SampleJson
{
public Guid TrackingId { get; set; }
public Guid ComponentGroupId { get; set; }
public Guid ComponentId { get; set; }
public Guid? OrderedComponentTrackingId { get; set; }
public Guid? MonthId { get; set; }
public Guid? MeasureId { get; set; }
public bool IsGroupLevel { get; set; }
public ResultJson? Result { get; set; }
}
public record ResultJson
{
public string? TextValue { get; set; }
public decimal? NumericValue { get; set; }
public Guid? MarkIdValue { get; set; }
public List<Guid> CommentIds { get; set; } = [];
public Guid CreatedBy { get; set; }
public string CreatedByName { get; set; } = null!;
public DateTime CreatedDate { get; set; }
public Guid LastModifiedBy { get; set; }
public string LastModifiedByName { get; set; } = null!;
public DateTime LastModifiedDate { get; set; }
}
public interface ISampleDbContext
{
DbSet<SampleEntity> SampleEntities { get; set; }
void SetConnectionString(string connectionString);
} |
/// doing so can result in application failures when updating to a new Entity Framework Core release. | ||
/// </remarks> | ||
public class ConvertingValueComparer<TTo, TFrom> : ValueComparer<TTo>, IInfrastructure<ValueComparer> | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the constraint as conversions happen both ways
in case of object[] { 1, 2, 3 }
target is target is object and source is int
in case of nested lists (List<List> target is List and source is object (because element comparer is ListOfReferenceTypesComparer
which is typed as ValueComparer
src/EFCore.Cosmos/ChangeTracking/Internal/StringDictionaryComparer.cs
Outdated
Show resolved
Hide resolved
338fd9d
to
14e0dc6
Compare
0a3ffd3
to
24796f0
Compare
test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs
Outdated
Show resolved
Hide resolved
4b0f773
to
9448534
Compare
…s. .NET8 when using .ToJson() Mapping vs. PostgreSQL Legacy POCO mapping Problem was that as part of AOT refactoring we changed way that we build comparers. Specifically, comparers of collections - ListOfValueTypesComparer, ListOfNullableValueTypesComparer and ListOfReferenceTypesComparer. Before those list comparer Compare, Hashcode and Snapshot methods would take as argument element comparer, which was responsible for comparing elements. We need to be able to express these in code for AOT but we are not able to generate constant of type ValueComparer (or ValueComparer) that was needed. As a solution, each comparer now stores expression describing how it can be constructed, so we use that instead (as we are perfectly capable to expressing that in code form). Problem is that now every time compare, snapshot or hashcode method is called for array type, we construct new ValueComparer for the element type. As a result in the reported case we would generate 1000s of comparers which all have to be compiled and that causes huge overhead. Fix is to pass relevant func from the element comparer to the outer comparer. We only passed the element comparer object to the outer Compare/Hashcode/Snapshot function to call that relevant func. This way we avoid constructing redundant comparers. In order to do that safely we need to make sure that type of the element comparer and the type on the list comparer are compatible (so that when func from element comparer is passed to the list comparer Equals/Hashcode/Snapshot method the resulting expression is valid. We do that by introducing a comparer that converts from one type to another, so that they are always aligned. Also removed ConstructorExpression from the ValueComparer (it was marked as experimental) as it is no longer used and was the underlying source of the bug. Fixes #35239
Problem was that as part of AOT refactoring we changed way that we build comparers. Specifically, comparers of collections - ListOfValueTypesComparer, ListOfNullableValueTypesComparer and ListOfReferenceTypesComparer. Before those list comparer Compare, Hashcode and Snapshot methods would take as argument element comparer, which was responsible for comparing elements. We need to be able to express these in code for AOT but we are not able to generate constant of type ValueComparer (or ValueComparer) that was needed. As a solution, each comparer now stores expression describing how it can be constructed, so we use that instead (as we are perfectly capable to expressing that in code form). Problem is that now every time compare, snapshot or hashcode method is called for array type, we construct new ValueComparer for the element type. As a result in the reported case we would generate 1000s of comparers which all have to be compiled and that causes huge overhead.
Fix is to pass relevant func from the element comparer to the outer comparer. We only passed the element comparer object to the outer Compare/Hashcode/Snapshot function to call that relevant func. This way we avoid constructing redundant comparers.
Fixes #35239