Skip to content

Commit d870cfd

Browse files
fix: add entity changes clear for EFCore batch inserts
1 parent ea573a8 commit d870cfd

12 files changed

Lines changed: 167 additions & 88 deletions

File tree

benchmark/BenchmarkRunner/Benchmarks/MysqlReadBenchmark.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class MysqlReadBenchmark
1717
private readonly string _connectionString = Config.GetMysqlConnectionString();
1818
private QuerySql _sqlcImpl = null!;
1919
private const int CustomerCount = 500;
20-
private const int QueriesToRun = 1000;
20+
private const int QueriesToRun = 500;
2121

2222
[Params(100, 1000)]
2323
public int Limit { get; set; }

benchmark/BenchmarkRunner/Benchmarks/PostgresqlReadBenchmark.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class PostgresqlReadBenchmark
1717
private readonly string _connectionString = Config.GetPostgresConnectionString();
1818
private QuerySql _sqlcImpl = null!;
1919
private const int CustomerCount = 500;
20-
private const int QueriesToRun = 1000;
20+
private const int QueriesToRun = 500;
2121

2222
[Params(100, 1000)]
2323
public int Limit { get; set; }

benchmark/BenchmarkRunner/MysqlRunner.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,34 @@ public class MysqlRunner(string connectionString, ILogger<MysqlRunner> logger)
99
private readonly string _connectionString = connectionString;
1010
private readonly ILogger<MysqlRunner> _logger = logger;
1111
public string ConnectionString => _connectionString;
12+
private readonly Stopwatch _stopwatch = new();
1213

13-
public Task RunAsync()
14+
public Task RunReadsAsync()
1415
{
15-
var path = Path.Combine("benchmark", "BenchmarkDotNet.Artifacts", "results", "mysql");
16+
var path = Path.Combine("benchmark", "BenchmarkDotNet.Artifacts", "results", "mysql", "reads");
1617
var config = DefaultConfig.Instance.WithArtifactsPath(path);
1718

1819
_logger.LogInformation("Running MySQL Reads benchmarks...");
19-
var stopwatch = Stopwatch.StartNew();
20+
_stopwatch.Restart();
2021
BenchmarkDotNet.Running.BenchmarkRunner.Run<MysqlReadBenchmark>(config);
21-
stopwatch.Stop();
22-
var readTime = stopwatch.Elapsed;
22+
_stopwatch.Stop();
23+
var readTime = _stopwatch.Elapsed;
24+
25+
_logger.LogInformation("MySQL Reads benchmarks completed in {ElapsedTime}", Helpers.FormatElapsedTime(readTime));
26+
return Task.CompletedTask;
27+
}
28+
29+
public Task RunWritesAsync()
30+
{
31+
var path = Path.Combine("benchmark", "BenchmarkDotNet.Artifacts", "results", "mysql", "writes");
32+
var config = DefaultConfig.Instance.WithArtifactsPath(path);
2333

2434
_logger.LogInformation("Running MySQL Writes benchmarks...");
25-
stopwatch.Restart();
35+
_stopwatch.Restart();
2636
BenchmarkDotNet.Running.BenchmarkRunner.Run<MysqlWriteBenchmark>(config);
27-
stopwatch.Stop();
28-
var writeTime = stopwatch.Elapsed;
37+
_stopwatch.Stop();
38+
var writeTime = _stopwatch.Elapsed;
2939

30-
_logger.LogInformation("MySQL Reads benchmarks completed in {ElapsedTime}", Helpers.FormatElapsedTime(readTime));
3140
_logger.LogInformation("MySQL Writes benchmarks completed in {ElapsedTime}", Helpers.FormatElapsedTime(writeTime));
3241
return Task.CompletedTask;
3342
}

benchmark/BenchmarkRunner/PostgresqlRunner.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,34 @@ public class PostgresqlRunner(string connectionString, ILogger<PostgresqlRunner>
99
private readonly string _connectionString = connectionString;
1010
private readonly ILogger<PostgresqlRunner> _logger = logger;
1111
public string ConnectionString => _connectionString;
12+
private readonly Stopwatch _stopwatch = new();
1213

13-
public Task RunAsync()
14+
public Task RunReadsAsync()
1415
{
15-
var path = Path.Combine("benchmark", "BenchmarkDotNet.Artifacts", "results", "postgresql");
16+
var path = Path.Combine("benchmark", "BenchmarkDotNet.Artifacts", "postgresql", "reads");
1617
var config = DefaultConfig.Instance.WithArtifactsPath(path);
1718

1819
_logger.LogInformation("Running PostgreSQL Reads benchmarks...");
19-
var stopwatch = Stopwatch.StartNew();
20+
_stopwatch.Restart();
2021
BenchmarkDotNet.Running.BenchmarkRunner.Run<PostgresqlReadBenchmark>(config);
21-
stopwatch.Stop();
22-
var readTime = stopwatch.Elapsed;
22+
_stopwatch.Stop();
23+
var readTime = _stopwatch.Elapsed;
24+
25+
_logger.LogInformation("PostgreSQL Reads benchmarks completed in {ElapsedTime}", Helpers.FormatElapsedTime(readTime));
26+
return Task.CompletedTask;
27+
}
28+
29+
public Task RunWritesAsync()
30+
{
31+
var path = Path.Combine("benchmark", "BenchmarkDotNet.Artifacts", "postgresql", "writes");
32+
var config = DefaultConfig.Instance.WithArtifactsPath(path);
2333

2434
_logger.LogInformation("Running PostgreSQL Writes benchmarks...");
25-
stopwatch.Restart();
35+
_stopwatch.Restart();
2636
BenchmarkDotNet.Running.BenchmarkRunner.Run<PostgresqlWriteBenchmark>(config);
27-
stopwatch.Stop();
28-
var writeTime = stopwatch.Elapsed;
37+
_stopwatch.Stop();
38+
var writeTime = _stopwatch.Elapsed;
2939

30-
_logger.LogInformation("PostgreSQL Reads benchmarks completed in {ElapsedTime}", Helpers.FormatElapsedTime(readTime));
3140
_logger.LogInformation("PostgreSQL Writes benchmarks completed in {ElapsedTime}", Helpers.FormatElapsedTime(writeTime));
3241
return Task.CompletedTask;
3342
}

benchmark/BenchmarkRunner/Program.cs

Lines changed: 77 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,59 +4,95 @@
44

55
public class Program
66
{
7+
private static readonly HashSet<string> _databases = ["mysql", "postgresql", "sqlite"];
8+
private static readonly HashSet<string> _types = ["reads", "writes"];
9+
710
public static async Task Main(string[] args)
811
{
9-
using var loggerFactory = LoggerFactory.Create(builder =>
10-
{
11-
builder
12-
.AddConsole()
13-
.SetMinimumLevel(LogLevel.Information);
14-
});
12+
var rootCommand = new RootCommand("Run benchmarks");
13+
14+
var databaseOption = GetDatabaseOption();
15+
rootCommand.AddOption(databaseOption);
16+
17+
var typeOption = GetTypeOption();
18+
rootCommand.AddOption(typeOption);
1519

16-
var rootCommand = new RootCommand("Run database benchmarks");
17-
var databaseOption = new Option<string>(
20+
rootCommand.SetHandler(CommandHandler, databaseOption, typeOption);
21+
await rootCommand.InvokeAsync(args);
22+
}
23+
24+
private static Option<string> GetDatabaseOption()
25+
{
26+
var option = new Option<string>(
1827
"--database",
1928
"Database to benchmark (mysql, postgresql, or sqlite)")
2029
{
2130
IsRequired = true
2231
};
23-
databaseOption.AddValidator(result =>
32+
option.AddValidator(result =>
2433
{
25-
var value = result.GetValueForOption(databaseOption);
26-
if (value != null && value != "mysql" && value != "postgresql" && value != "sqlite")
27-
{
28-
result.ErrorMessage = $"Invalid database: {value}. Must be one of: mysql, postgresql, sqlite";
29-
}
34+
var value = result.GetValueForOption(option);
35+
if (value != null && !_databases.Contains(value))
36+
result.ErrorMessage = $"Invalid database: {value}. Must be one of: {string.Join(", ", _databases)}";
3037
});
31-
rootCommand.AddOption(databaseOption);
38+
return option;
39+
}
3240

33-
rootCommand.SetHandler(async (database) =>
41+
private static Option<string> GetTypeOption()
42+
{
43+
var option = new Option<string>(
44+
"--type",
45+
$"Type of benchmark to run ({string.Join(", ", _types)})")
3446
{
35-
switch (database)
36-
{
37-
case "mysql":
38-
var mysqlRunner = new MysqlRunner(
39-
Config.GetMysqlConnectionString(),
40-
loggerFactory.CreateLogger<MysqlRunner>());
41-
await mysqlRunner.RunAsync();
42-
break;
43-
case "postgresql":
44-
var postgresqlRunner = new PostgresqlRunner(
45-
Config.GetPostgresConnectionString(),
46-
loggerFactory.CreateLogger<PostgresqlRunner>());
47-
await postgresqlRunner.RunAsync();
48-
break;
49-
case "sqlite":
50-
var sqliteRunner = new SqliteRunner(
51-
Config.GetSqliteConnectionString(),
52-
loggerFactory.CreateLogger<SqliteRunner>());
53-
await sqliteRunner.RunAsync();
54-
break;
55-
default:
56-
throw new ArgumentException($"Invalid database: {database}");
57-
}
58-
}, databaseOption);
47+
IsRequired = true
48+
};
49+
option.AddValidator(result =>
50+
{
51+
var value = result.GetValueForOption(option);
52+
if (value != null && !_types.Contains(value))
53+
result.ErrorMessage = $"Invalid type: {value}. Must be one of: {string.Join(", ", _types)}";
54+
});
55+
return option;
56+
}
57+
private static async Task CommandHandler(string database, string type)
58+
{
59+
using var loggerFactory = LoggerFactory.Create(builder =>
60+
builder
61+
.AddConsole()
62+
.SetMinimumLevel(LogLevel.Information));
5963

60-
await rootCommand.InvokeAsync(args);
64+
var mysqlRunner = new MysqlRunner(
65+
Config.GetMysqlConnectionString(),
66+
loggerFactory.CreateLogger<MysqlRunner>());
67+
var postgresqlRunner = new PostgresqlRunner(
68+
Config.GetPostgresConnectionString(),
69+
loggerFactory.CreateLogger<PostgresqlRunner>());
70+
var sqliteRunner = new SqliteRunner(
71+
Config.GetSqliteConnectionString(),
72+
loggerFactory.CreateLogger<SqliteRunner>());
73+
74+
switch (database, type)
75+
{
76+
case ("mysql", "read"):
77+
await mysqlRunner.RunReadsAsync();
78+
break;
79+
case ("mysql", "write"):
80+
await mysqlRunner.RunWritesAsync();
81+
break;
82+
case ("postgresql", "read"):
83+
await postgresqlRunner.RunReadsAsync();
84+
break;
85+
case ("postgresql", "write"):
86+
await postgresqlRunner.RunWritesAsync();
87+
break;
88+
case ("sqlite", "read"):
89+
await sqliteRunner.RunReadsAsync();
90+
break;
91+
case ("sqlite", "write"):
92+
await sqliteRunner.RunWritesAsync();
93+
break;
94+
default:
95+
throw new ArgumentException($"Invalid database - {database}, type - {type}");
96+
}
6197
}
6298
}

benchmark/BenchmarkRunner/SqliteRunner.cs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,35 @@ public class SqliteRunner(string connectionString, ILogger<SqliteRunner> logger)
88
{
99
private readonly string _connectionString = connectionString;
1010
private readonly ILogger<SqliteRunner> _logger = logger;
11-
1211
public string ConnectionString => _connectionString;
12+
private readonly Stopwatch _stopwatch = new();
1313

14-
public Task RunAsync()
14+
public Task RunReadsAsync()
1515
{
16-
var path = Path.Combine("benchmark", "BenchmarkDotNet.Artifacts", "results", "sqlite");
16+
var path = Path.Combine("benchmark", "BenchmarkDotNet.Artifacts", "sqlite", "reads");
1717
var config = DefaultConfig.Instance.WithArtifactsPath(path);
1818

1919
_logger.LogInformation("Running SQLite Reads benchmarks...");
20-
var stopwatch = Stopwatch.StartNew();
20+
_stopwatch.Restart();
2121
BenchmarkDotNet.Running.BenchmarkRunner.Run<SqliteReadBenchmark>(config);
22-
stopwatch.Stop();
23-
var readTime = stopwatch.Elapsed;
22+
_stopwatch.Stop();
23+
var readTime = _stopwatch.Elapsed;
24+
25+
_logger.LogInformation("SQLite Reads benchmarks completed in {ElapsedTime}", Helpers.FormatElapsedTime(readTime));
26+
return Task.CompletedTask;
27+
}
28+
29+
public Task RunWritesAsync()
30+
{
31+
var path = Path.Combine("benchmark", "BenchmarkDotNet.Artifacts", "sqlite", "writes");
32+
var config = DefaultConfig.Instance.WithArtifactsPath(path);
2433

2534
_logger.LogInformation("Running SQLite Writes benchmarks...");
26-
stopwatch.Restart();
35+
_stopwatch.Restart();
2736
BenchmarkDotNet.Running.BenchmarkRunner.Run<SqliteWriteBenchmark>(config);
28-
stopwatch.Stop();
29-
var writeTime = stopwatch.Elapsed; ;
37+
_stopwatch.Stop();
38+
var writeTime = _stopwatch.Elapsed; ;
3039

31-
_logger.LogInformation("SQLite Reads benchmarks completed in {ElapsedTime}", Helpers.FormatElapsedTime(readTime));
3240
_logger.LogInformation("SQLite Writes benchmarks completed in {ElapsedTime}", Helpers.FormatElapsedTime(writeTime));
3341
return Task.CompletedTask;
3442
}

benchmark/MysqlEFCoreImpl/Queries.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public async Task AddProducts(List<AddProductsArgs> args)
9595

9696
await _dbContext.Products.AddRangeAsync(products);
9797
await _dbContext.SaveChangesAsync();
98+
_dbContext.ChangeTracker.Clear();
9899
}
99100

100101
public record AddOrdersArgs(int CustomerId, string OrderState, decimal TotalAmount);
@@ -114,6 +115,7 @@ public async Task AddOrders(List<AddOrdersArgs> args)
114115

115116
await _dbContext.Orders.AddRangeAsync(orders);
116117
await _dbContext.SaveChangesAsync();
118+
_dbContext.ChangeTracker.Clear();
117119
}
118120

119121
public record AddOrderItemsArgs(long OrderId, int ProductId, int Quantity, decimal UnitPrice);
@@ -133,6 +135,6 @@ public async Task AddOrderItems(List<AddOrderItemsArgs> args)
133135

134136
await _dbContext.OrderItems.AddRangeAsync(orderItems);
135137
await _dbContext.SaveChangesAsync();
136-
_dbContext.ChangeTracker.Clear(); // Clear tracking to prevent conflicts in batch operations
138+
_dbContext.ChangeTracker.Clear();
137139
}
138140
}

benchmark/PostgresqlEFCoreImpl/Queries.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public async Task AddProducts(List<AddProductsArgs> args)
7878

7979
await _dbContext.Products.AddRangeAsync(products);
8080
await _dbContext.SaveChangesAsync();
81+
_dbContext.ChangeTracker.Clear();
8182
}
8283

8384
public record AddOrdersArgs(int CustomerId, string OrderState, decimal TotalAmount);
@@ -98,6 +99,7 @@ public async Task AddOrders(List<AddOrdersArgs> args)
9899

99100
await _dbContext.Orders.AddRangeAsync(orders);
100101
await _dbContext.SaveChangesAsync();
102+
_dbContext.ChangeTracker.Clear();
101103
}
102104

103105
public record AddOrderItemsArgs(Guid OrderId, int ProductId, int Quantity, decimal UnitPrice);
@@ -118,6 +120,6 @@ public async Task AddOrderItems(List<AddOrderItemsArgs> args)
118120

119121
await _dbContext.OrderItems.AddRangeAsync(orderItems);
120122
await _dbContext.SaveChangesAsync();
121-
_dbContext.ChangeTracker.Clear(); // Clear tracking to prevent conflicts in batch operations
123+
_dbContext.ChangeTracker.Clear();
122124
}
123125
}

benchmark/SqliteEFCoreImpl/Queries.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public async Task AddProducts(List<AddProductsArgs> args)
9595

9696
await _dbContext.Products.AddRangeAsync(products);
9797
await _dbContext.SaveChangesAsync();
98+
_dbContext.ChangeTracker.Clear();
9899
}
99100

100101
public record AddOrdersArgs(int CustomerId, string OrderState, decimal TotalAmount);
@@ -114,6 +115,7 @@ public async Task AddOrders(List<AddOrdersArgs> args)
114115

115116
await _dbContext.Orders.AddRangeAsync(orders);
116117
await _dbContext.SaveChangesAsync();
118+
_dbContext.ChangeTracker.Clear();
117119
}
118120

119121
public record AddOrderItemsArgs(int OrderId, int ProductId, int Quantity, decimal UnitPrice);
@@ -133,5 +135,6 @@ public async Task AddOrderItems(List<AddOrderItemsArgs> args)
133135

134136
await _dbContext.OrderItems.AddRangeAsync(orderItems);
135137
await _dbContext.SaveChangesAsync();
138+
_dbContext.ChangeTracker.Clear();
136139
}
137140
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
3+
set -ex
4+
5+
destroy() { docker-compose down --volumes; }
6+
trap destroy EXIT
7+
docker-compose up --build --detach --force-recreate --remove-orphans --wait
8+
9+
dotnet build ./benchmark/BenchmarkRunner/BenchmarkRunner.csproj -c Release
10+
11+
./benchmark/scripts/run_single_benchmark.sh mysql reads
12+
./benchmark/scripts/run_single_benchmark.sh mysql writes
13+
./benchmark/scripts/run_single_benchmark.sh postgresql reads
14+
./benchmark/scripts/run_single_benchmark.sh postgresql writes
15+
./benchmark/scripts/run_single_benchmark.sh sqlite reads
16+
./benchmark/scripts/run_single_benchmark.sh sqlite writes

0 commit comments

Comments
 (0)