dotnet / dotnet/efcore

Inefficient Type.GetProperties usage in when compiling/running a query against SqlServer

Open
#32,649 5 comments 2 reactions 1 assignee Claimed by @AndriySvyryd View on GitHub
area-model-building area-perf area-query customer-reported
Dominant language
C#
Stars
14.8k
Forks
3.4k
PR merge metrics
PR metrics pending

Description

We've noticed that heap dumps collected from prod workloads contain a lot of `PropertyInfo[]` objects, which seem to contain properties of our entities and projected DTOs. Take the following code as an example:

```c#
var connStr = Environment.GetEnvironmentVariable("SQL_CONN_STRING");
var builder = new DbContextOptionsBuilder().UseSqlServer(connStr);
using var context = new SampleDbContext(builder.Options);

for (int i = 0; i < 50000; i++) {
var user = context.Users.Where(u => u.Id == 0).Select(u => new SampleUserDto { Id = u.Id }).FirstOrDefault();
}
```

It seems like a very simple query that fetches a non existing `SampleUser` row from the `Users` table and projects onto the the `SampleUserDto` object. I collected a full heap dump while this code was running using `dotnet dump collect --type Full`. Here is what `dumpheap -stat` shows:

```
...
7fb54fa8cb20 83,485 2,863,216 System.Type[]
7fb552cc5fc0 11,919 2,955,912 Microsoft.Data.SqlClient.SqlDataReader
7fb55076dc78 47,683 3,051,712 System.Collections.Generic.HashSet
7fb54f2589e0 95,671 3,518,252 System.Int32[]
7fb552cc4be8 11,919 3,528,024 Microsoft.Data.SqlClient.SqlCommand
7fb550632f80 37,263 3,875,352 System.Reflection.RuntimeMethodInfo
7fb550634728 71,564 6,870,144 System.RuntimeMethodInfoStub
7fb550736fb0 35,759 29,749,872 System.Reflection.PropertyInfo[]
```

As you can see, there are a lot of `PropertyInfo[]` objects in heap. I ran `dumpheap -mt 7fb550736fb0` to dump all. The list is large (35K objects) but almost of them have 832 items, which means they contain `PropertyInfo`s of the same type:

```
7f7543b6b398 7fb550736fb0 832
7f7543b6bbb8 7fb550736fb0 832
7f7543b6bf18 7fb550736fb0 832
7f7543b6dce0 7fb550736fb0 832
7f7543b6e500 7fb550736fb0 832
7f7543b6e860 7fb550736fb0 832
7f7543b70628 7fb550736fb0 832
```

Then I selected a few randomly and inspected their contents a bit:

```
> dumparray 7f7543b6b398
[0] 00007f753cc2a348
[1] 00007f753cc2a438
[2] 00007f753cc2a590
[3] 00007f753cc2a6e8
...

# do to dump the PropertyInfo at index 0
> do 00007f753cc2a348
...
Name: System.Reflection.RuntimePropertyInfo
...
00007fb54f25d7c8 4001538 8 System.String 0 instance 00007f753cc2a418 m_name
...

# Dump the name of this PropertyInfo
> do 00007f753cc2a418
Name: System.String
String: Id

# # Dump the name of the second PropertyInfo
do 00007f753cc2a570
Name: System.String
String: Test1
```

`Id` and `Test1` are properties in `SampleUser` and `SampleUserDto`. I inspected tens of those PropertyInfo objects. They all pointed to `typeof(SampleUser).GetProperties()` or `typeof(SampleUserDto).GetProperties()`. I understand reflection is neccessary but type info doesn't change at runtime so it should be pretty straight-forward to cache these.

Maybe I am doing something that is causing EFCore not cache these expression trees but I tried compiled queries as well without any luck (still lots of `PropertyInfo[]` allocations).

I uploaded a test project that demostrates this: [EFTest.tar.gz](https://github.com/dotnet/efcore/files/13720262/EFTest.tar.gz). All you need is a SQL Server database with a table called `Users` having an int `Id` field. You don't need the other `TestX` properties I added to the entity and the dto object. I added a lot of properties to make `PropertyInfo` arrays larger (to emphasize the problem). You need to set an env var called `SQL_CONN_STRING` to your test SQL Server instance. The problem might reproduce on Sqlite too but I haven't checked.

Our real `User` entity has more than 150 properties mostly due to navigational properties (foreign keys from other tables) so this problem is actually pretty visible on prod. There are lot of other reflection-related objects but `PropertyInfo[]` is one of the biggest problems and is probably easier to optimize/cache.

Let me know if I am doing something wrong that's causing EFCore to not cache these info objects.

EF Core version: 8.0.0
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 8.0
Operating system: Linux/Debian
IDE: dotnet CLI

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.