-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Fix performance degradation in converters because the instance was not reused #36895
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
Conversation
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
AndriySvyryd
left a comment
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.
Do you plan on porting this to GA?
d4cabc0 to
7821ac5
Compare
|
Yes: #36898. |
roji
left a comment
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.
As discussed offline, we can add a DEBUG check to ensure that the shaper tree never contains a NewExpression for a ValueConverter - this would be the regression test for this. This shouldn't be too hard - we already had some validation in https://github.com/roji/efcore/blob/eb5f3905d3141dfb74dd67daed828285e483f166/src/EFCore/Query/LiftableConstantProcessor.cs#L263 (even though that's currently disabled).
See also #33517 which is referenced from that commented-out code and which is relevant to everything that's going on here (i.e. why value converters are problematic).
7821ac5 to
d55fd8f
Compare
|
The |
|
Are you sure that's the case? That in itself would mean that JSON doesn't work with precompiled queries, no? I thought when you dump the final shaper (at the point where we also do liftable constant inlining/lifting), you can always recursively see all the shapers of all structural types in the tree, otherwise I'm a bit confused about how it's all working. |
As far as I can tell.
🤷♂️
Sadly for you, I'm not an expert in this area. |
|
Well, I did a quick minimal test program that queries something that has a JSON complex type in it (full source below): _ = await context.Blogs.ToListAsync();I put a breakpoint right before the liftable constant processor, and dumped the shaper with ExpressionPrinter.Print. The full dump is below, but the relevant part with the innerShaper is the following: ShaperProcessingExpressionVisitor.IncludeJsonEntityReference<Blog, BlogDetails>(
queryContext: queryContext,
keyPropertyValues: null,
jsonReaderData: jsonReader,
structuralType: instance1,
innerShaper: (queryContext, namelessParameter{3}, namelessParameter{4}) =>
{
BlogDetails entityShaperMaterializer;
return entityShaperMaterializer =
{
MaterializationContext materializationContext2;
IComplexType entityType2;
BlogDetails instance2;
...So there's a call to ShaperProcessingExpressionVisitor.IncludeJsonEntityReference (which is a regular .NET method, not code-generated or anything), and the innerShaper is passed in as an argument, but it's present in the shaper tree as an expression (the parameter type is I think that means that the liftable constant processor recurses inside the innerShaper just like any other expression tree node - let me know if I'm missing something. Full codeawait using var context = new BlogContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
_ = await context.Blogs.ToListAsync();
public class BlogContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer("Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0;Encrypt=false")
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().ComplexProperty(b => b.Details, b => b.ToJson());
}
}
public class Blog
{
public int Id { get; set; }
public BlogDetails Details { get; set; }
}
public class BlogDetails
{
public string Foo { get; set; }
public int Bar { get; set; }
}Full shaper dumpqueryContext => SingleQueryingEnumerable.Create<Blog>(
relationalQueryContext: (RelationalQueryContext)queryContext,
relationalCommandResolver: parameters => [LIFTABLE Constant: RelationalCommandCache.QueryExpression(
Projection Mapping:
EmptyProjectionMember -> Dictionary<IPropertyBase, int> { [Property: Blog.Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd, 0], [ComplexProperty: Blog.Details (BlogDetails) Required, 1] }
SELECT b.Id, b.Details ->
FROM Blogs AS b) | Resolver: c => new RelationalCommandCache(
c.Dependencies.MemoryCache,
c.RelationalDependencies.QuerySqlGeneratorFactory,
c.RelationalDependencies.RelationalParameterBasedSqlProcessorFactory,
Projection Mapping:
EmptyProjectionMember -> Dictionary<IPropertyBase, int> { [Property: Blog.Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd, 0], [ComplexProperty: Blog.Details (BlogDetails) Required, 1] }
SELECT b.Id, b.Details ->
FROM Blogs AS b,
False,
MultipleParameters
)].GetRelationalCommandTemplate(parameters),
readerColumns: null,
shaper: (queryContext, dataReader, resultContext, resultCoordinator) =>
{
Blog entity;
Stream jsonStream;
JsonReaderData jsonReader;
Utf8JsonReaderManager jsonReaderManager;
jsonStream = dataReader.IsDBNull(1) ? default(MemoryStream) : SqlServerStructuralJsonTypeMapping.CreateUtf8Stream(dataReader.GetString(1));
jsonReader = jsonStream == default(MemoryStream) ? default(JsonReaderData) : new JsonReaderData(jsonStream);
jsonReader != default(JsonReaderData) ?
{
jsonReaderManager = new Utf8JsonReaderManager(
jsonReader,
queryContext.QueryLogger
);
jsonReaderManager.MoveNext();
jsonReaderManager.CaptureState();
} : default(void);
entity =
{
MaterializationContext materializationContext1;
IEntityType entityType1;
Blog instance1;
InternalEntityEntry entry1;
bool hasNullKey1;
materializationContext1 = new MaterializationContext(
[LIFTABLE Constant: ValueBuffer | Resolver: _ => (object)ValueBuffer.Empty],
queryContext.Context
);
instance1 = default(Blog);
entry1 = queryContext.TryGetEntry(
key: [LIFTABLE Constant: Key: Blog.Id PK | Resolver: c => c.Dependencies.Model.FindEntityType("Blog").FindPrimaryKey()],
keyValues: new object[]{ (object)dataReader.GetInt32(0) },
throwOnNullKey: True,
hasNullKey: hasNullKey1);
!(hasNullKey1) ? entry1 != default(InternalEntityEntry) ?
{
entityType1 = entry1.EntityType;
return instance1 = (Blog)entry1.Entity;
} :
{
ISnapshot shadowSnapshot1;
shadowSnapshot1 = [LIFTABLE Constant: Snapshot | Resolver: _ => Snapshot.Empty];
entityType1 = [LIFTABLE Constant: EntityType: Blog | Resolver: namelessParameter{0} => namelessParameter{0}.Dependencies.Model.FindEntityType("Blog")];
instance1 = switch (entityType1)
{
case [LIFTABLE Constant: EntityType: Blog | Resolver: namelessParameter{1} => namelessParameter{1}.Dependencies.Model.FindEntityType("Blog")]:
{
return
{
Blog instance;
instance = new Blog();
instance.<Id>k__BackingField = dataReader.GetInt32(0);
(instance is IInjectableService) ? ((IInjectableService)instance).Injected(
context: materializationContext1.Context,
entity: instance,
queryTrackingBehavior: TrackAll,
structuralType: [LIFTABLE Constant: EntityType: Blog | Resolver: namelessParameter{2} => namelessParameter{2}.Dependencies.Model.FindEntityType("Blog")]) : default(void);
return instance;
}}
default:
default(Blog)
}
;
ShaperProcessingExpressionVisitor.IncludeJsonEntityReference<Blog, BlogDetails>(
queryContext: queryContext,
keyPropertyValues: null,
jsonReaderData: jsonReader,
structuralType: instance1,
innerShaper: (queryContext, namelessParameter{3}, namelessParameter{4}) =>
{
BlogDetails entityShaperMaterializer;
return entityShaperMaterializer =
{
MaterializationContext materializationContext2;
IComplexType entityType2;
BlogDetails instance2;
materializationContext2 = new MaterializationContext(
[LIFTABLE Constant: ValueBuffer | Resolver: _ => (object)ValueBuffer.Empty],
queryContext.Context
);
instance2 = default(BlogDetails);
{
ISnapshot shadowSnapshot2;
shadowSnapshot2 = [LIFTABLE Constant: Snapshot | Resolver: _ => Snapshot.Empty];
entityType2 = [LIFTABLE Constant: ComplexType: Blog.Details#BlogDetails (BlogDetails) | Resolver: namelessParameter{5} => namelessParameter{5}.Dependencies.Model.FindEntityType("Blog").FindComplexProperty("Details").ComplexType];
instance2 =
{
Utf8JsonReaderManager jsonReaderManager;
JsonTokenType tokenType;
BlogDetails instance;
int namelessParameter{6};
string namelessParameter{7};
jsonReaderManager = new Utf8JsonReaderManager(
namelessParameter{4},
queryContext.QueryLogger
);
tokenType = jsonReaderManager.CurrentReader.TokenType;
Loop(Break: done Continue: )
{
{
tokenType = jsonReaderManager.MoveNext();
switch (tokenType)
{
case PropertyName:
jsonReaderManager.CurrentReader.ValueTextEquals((ReadOnlySpan<byte>)Encoding.UTF8.GetBytes("Bar")
.AsSpan()) ?
{
jsonReaderManager.MoveNext();
namelessParameter{6} = (int)[LIFTABLE Constant: JsonInt32ReaderWriter | Resolver: c => c.Dependencies.Model.FindEntityType("Blog").FindComplexProperty("Details").ComplexType.FindProperty("Bar").GetJsonValueReaderWriter() ?? c.Dependencies.Model.FindEntityType("Blog").FindComplexProperty("Details").ComplexType.FindProperty("Bar").GetTypeMapping().JsonValueReaderWriter].FromJsonTyped(
manager: jsonReaderManager,
existingObject: default(object));
} : jsonReaderManager.CurrentReader.ValueTextEquals((ReadOnlySpan<byte>)Encoding.UTF8.GetBytes("Foo")
.AsSpan()) ?
{
jsonReaderManager.MoveNext();
namelessParameter{7} = (string)[LIFTABLE Constant: JsonStringReaderWriter | Resolver: c => c.Dependencies.Model.FindEntityType("Blog").FindComplexProperty("Details").ComplexType.FindProperty("Foo").GetJsonValueReaderWriter() ?? c.Dependencies.Model.FindEntityType("Blog").FindComplexProperty("Details").ComplexType.FindProperty("Foo").GetTypeMapping().JsonValueReaderWriter].FromJsonTyped(
manager: jsonReaderManager,
existingObject: default(object));
} : default(void)
case EndObject:
Goto(break done)
default:
{
jsonReaderManager.Skip();
}
}
}}
jsonReaderManager.CaptureState();
instance = new BlogDetails();
instance.<Bar>k__BackingField = namelessParameter{6};
instance.<Foo>k__BackingField = namelessParameter{7};
return instance;
};
return instance2;
}return instance2;
};
},
fixup: (namelessParameter{8}, namelessParameter{9}) =>
{
return namelessParameter{8}.<Details>k__BackingField = namelessParameter{9};
},
performFixup: True);
entry1 = entityType1 == default(IEntityType) ? default(InternalEntityEntry) : queryContext.StartTracking(
entityType: entityType1,
entity: instance1,
snapshot: shadowSnapshot1);
return instance1;
} : default(void);
return instance1;
};
return entity;
},
contextType: BlogContext,
standAloneStateManager: False,
detailedErrorsEnabled: False,
threadSafetyChecksEnabled: True) |
|
I did override The Did you have something else in mind? |
|
OK, let's not spend more time on this. |
Fixes #36856.