Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
<PackageReference Include="Testcontainers.PostgreSql" Version="4.7.0" />
<PackageReference Include="Z.EntityFramework.Extensions.EFCore" Version="9.103.9.3" />
</ItemGroup>

<ItemGroup>
Expand Down
54 changes: 2 additions & 52 deletions CmdScale.EntityFrameworkCore.TimescaleDB.Benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,9 @@ This project uses **BenchmarkDotNet** to measure the performance of high-through
## Prerequisites

- .NET 8 SDK or later
- Docker and Docker Compose
- Docker

---

## Step 1: Start a Clean Database

The benchmarks require a running TimescaleDB instance. A `docker-compose.yml` file is provided in the project root to simplify this process.

### 🔄 Stop and Reset (if needed)

To ensure you start with a clean slate, run this command to stop any running containers and permanently delete all existing data.

```bash
docker compose down -v
```

### ▶️ Start the Database

Launch a new TimescaleDB instance in the background.

```bash
docker compose up -d
```

### ✅ Verify Connection String

Ensure the connection string in `BulkCopyToAsyncBenchmarks.cs` matches the settings in your `docker-compose.yml` file (the default should work).

---

## Step 2: Apply Migrations

Next, create the necessary tables in the new database using EF Core migrations.

### ➕ Add a Migration

Create a new migration. Run this from the root of the solution.

```bash
dotnet ef migrations add <YourMigrationName> --project CmdScale.EntityFrameworkCore.TimescaleDB.Example.DataAccess --startup-project CmdScale.EntityFrameworkCore.TimescaleDB.Example
```

### ⬆️ Update the Database

Apply the migrations to create the hypertable schema.

```bash
dotnet ef database update --project CmdScale.EntityFrameworkCore.TimescaleDB.Example.DataAccess --startup-project CmdScale.EntityFrameworkCore.TimescaleDB.Example
```

---

## Step 3: Run the Benchmarks
## Run the Benchmarks

Once the database is set up, you can run the performance tests.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using BenchmarkDotNet.Attributes;
using CmdScale.EntityFrameworkCore.TimescaleDB.Example.DataAccess;
using Microsoft.EntityFrameworkCore;
using Testcontainers.PostgreSql;

namespace CmdScale.EntityFrameworkCore.TimescaleDB.Benchmarks
{
public abstract class WriteRecordsBenchmarkBase<T> where T : class
{
public int NumberOfRecords;
public int MaxBatchSize;
public int NumberOfWorkers;

private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder()
.WithImage("timescale/timescaledb:latest-pg17")
.WithDatabase("benchmark_tests_db")
.WithUsername("test_user")
.WithPassword("test_password")
.Build();

protected string ConnectionString = "";
protected readonly List<T> Trades = [];
protected TimescaleContext? Context;

[GlobalSetup]
public async Task Setup()
{
await _dbContainer.StartAsync();
ConnectionString = _dbContainer.GetConnectionString();

DbContextOptionsBuilder<TimescaleContext> optionsBuilder = new();
optionsBuilder.UseNpgsql(ConnectionString).UseTimescaleDb();
Context = new TimescaleContext(optionsBuilder.Options);

await Context.Database.MigrateAsync();
Console.WriteLine("Migration applied successfully.");
}

[GlobalCleanup]
public async Task GlobalCleanup()
{
await _dbContainer.DisposeAsync();
}

[IterationSetup]
public void IterationSetup()
{
Trades.Clear();
var random = new Random();
string[] tickers = ["AAPL", "GOOGL", "MSFT", "TSLA", "AMZN"];
var baseTimestamp = DateTime.UtcNow.AddMinutes(-30);

for (int i = 0; i < NumberOfRecords; i++)
{
var trade = CreateTradeInstance(i, baseTimestamp, tickers[random.Next(tickers.Length)], random);
Trades.Add(trade);
}

// Truncate the table before each iteration for a clean slate
string tableName = GetTableName();
string sql = $"TRUNCATE TABLE \"{tableName}\"";
Context!.Database.ExecuteSqlRaw(sql);
}

protected abstract T CreateTradeInstance(int index, DateTime baseTimestamp, string ticker, Random random);
protected abstract string GetTableName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using BenchmarkDotNet.Attributes;
using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions;
using CmdScale.EntityFrameworkCore.TimescaleDB.Example.DataAccess.Models;
using Npgsql;
using NpgsqlTypes;

namespace CmdScale.EntityFrameworkCore.TimescaleDB.Benchmarks
{
[Config(typeof(InProcessConfig))]
[MemoryDiagnoser]
[ThreadingDiagnoser]
public class WriteRecordsKeylessBenchmarks : WriteRecordsBenchmarkBase<Trade>
{
[Params(100_000, 500_000)]
public new int NumberOfRecords;

[Params(25_000, 50_000, 100_000)]
public new int MaxBatchSize;

[Params(8)]
public new int NumberOfWorkers;

private readonly List<Task> tasks = [];
private int totalRecords;
private int workerChunkSize;

protected override Trade CreateTradeInstance(int index, DateTime baseTimestamp, string ticker, Random random)
{
return new Trade
{
Timestamp = baseTimestamp.AddMicroseconds(index),
Ticker = ticker,
Price = (decimal)(100 + random.NextDouble() * 400),
Size = random.Next(1, 100)
};
}

protected override string GetTableName() => "Trades";

[Benchmark]
public async Task BulkCopyAsync()
{
TimescaleCopyConfig<Trade> config = new TimescaleCopyConfig<Trade>()
.ToTable(GetTableName())
.WithWorkers(NumberOfWorkers)
.WithBatchSize(MaxBatchSize);

await Trades.BulkCopyAsync(ConnectionString, config);
}

[Benchmark]
public async Task HardcodedBulkCopyAsync()
{
totalRecords = Trades.Count;
workerChunkSize = (int)Math.Ceiling((double)totalRecords / NumberOfWorkers);

for (int i = 0; i < NumberOfWorkers; i++)
{
int startIndex = i * workerChunkSize;
int currentWorkerDataSize = Math.Min(workerChunkSize, totalRecords - startIndex);

if (currentWorkerDataSize <= 0)
{
break;
}

List<Trade> workerData = [.. Trades.Skip(startIndex).Take(currentWorkerDataSize)];
tasks.Add(Task.Run(async () =>
{
// Open new connection to DB
using NpgsqlConnection connection = new(ConnectionString);
await connection.OpenAsync();

// Command to copy data in a binary format from a client-application
string copyCommand = "COPY \"Trades\" (\"Time\", \"Value\", \"SegmentId\", \"SignalId\") FROM STDIN (FORMAT BINARY)";

for (int j = 0; j < workerData.Count; j += MaxBatchSize)
{
List<Trade> currentBatch = [.. workerData.Skip(j).Take(MaxBatchSize)];

// Start a binary import stream
await using NpgsqlBinaryImporter writer = connection.BeginBinaryImport(copyCommand);
foreach (Trade item in currentBatch)
{
// IMPORTANT: Columns must be inserted in the exact order
await writer.WriteAsync(item.Timestamp, NpgsqlDbType.TimestampTz);
await writer.WriteAsync(item.Ticker, NpgsqlDbType.Text);
await writer.WriteAsync(item.Price, NpgsqlDbType.Numeric);
await writer.WriteAsync(item.Size, NpgsqlDbType.Integer);
await writer.WriteAsync(item.Exchange, NpgsqlDbType.Text);
}

await writer.CompleteAsync();
}
}));
}

await Task.WhenAll(tasks);
}

[Benchmark]
public async Task BatchedBulkInsertOptimizedAsync()
{
foreach (Trade[] chunk in Trades.Chunk(MaxBatchSize))
{
await Context!.BulkInsertOptimizedAsync(chunk.ToList());
}
}
}
}
Loading
Loading