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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,24 @@ await foreach (var entry in engine.GetRecurrencesAsync("tenant1", "user123/calen
| MongoDB | [RecurringThings.MongoDB](src/RecurringThings.MongoDB/README.md) |
| PostgreSQL | [RecurringThings.PostgreSQL](src/RecurringThings.PostgreSQL/README.md) |

## Benchmarking

Run benchmarks locally against MongoDB and/or PostgreSQL:

```bash
# Set connection strings (PowerShell)
$env:MONGODB_CONNECTION_STRING = "mongodb://localhost:27017"
$env:POSTGRES_CONNECTION_STRING = "Host=localhost;Database=postgres;Username=postgres;Password=password"

# Run all benchmarks
dotnet run -c Release --project benchmarks/RecurringThings.Benchmarks

# Run specific benchmark class
dotnet run -c Release --project benchmarks/RecurringThings.Benchmarks -- --filter *QueryBenchmarks*
```

Results are generated in `./BenchmarkResults/` including HTML reports and PNG charts.

## License

Apache 2.0 - see [LICENSE](LICENSE)
128 changes: 128 additions & 0 deletions benchmarks/RecurringThings.Benchmarks/Benchmarks/CreateBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
namespace RecurringThings.Benchmarks.Benchmarks;

using BenchmarkDotNet.Attributes;
using RecurringThings.Benchmarks.Infrastructure;
using RecurringThings.Engine;
using RecurringThings.Models;

/// <summary>
/// Benchmarks for create operations.
/// </summary>
[Config(typeof(BenchmarkConfig))]
public class CreateBenchmarks
{
private IRecurrenceEngine _engine = null!;
private List<CalendarEntry> _createdEntries = null!;
private int _createIndex;

/// <summary>
/// Gets or sets the database provider to benchmark.
/// </summary>
[Params(BenchmarkProvider.MongoDB, BenchmarkProvider.PostgreSQL)]
public BenchmarkProvider Provider { get; set; }

private const string TimeZone = "America/New_York";

/// <summary>
/// Setup that runs once before all benchmark iterations for each parameter combination.
/// </summary>
[GlobalSetup]
public void GlobalSetup()
{
Console.WriteLine();
Console.WriteLine($"[CreateBenchmarks] GlobalSetup: Provider={Provider}");

_engine = ProviderFactory.CreateEngine(Provider);
_createdEntries = new List<CalendarEntry>();
_createIndex = 0;

Console.WriteLine($"[CreateBenchmarks] Setup complete. Ready to benchmark.");
}

/// <summary>
/// Benchmarks creating a recurrence pattern.
/// </summary>
[Benchmark(Description = "Create recurrence pattern")]
public async Task<CalendarEntry> CreateRecurrenceAsync()
{
var index = Interlocked.Increment(ref _createIndex);
var startTime = DateTime.UtcNow.Date.AddHours(9);
var rrule = $"FREQ=DAILY;UNTIL={DateTime.UtcNow.AddMonths(6):yyyyMMdd}T235959Z";

var entry = await _engine.CreateRecurrenceAsync(
DataSeeder.Organization,
DataSeeder.ResourcePath,
$"benchmark-type-{index}",
startTime,
TimeSpan.FromHours(1),
rrule,
TimeZone);

lock (_createdEntries)
{
_createdEntries.Add(entry);
}

return entry;
}

/// <summary>
/// Benchmarks creating a standalone occurrence.
/// </summary>
[Benchmark(Description = "Create standalone occurrence")]
public async Task<CalendarEntry> CreateOccurrenceAsync()
{
var index = Interlocked.Increment(ref _createIndex);
var startTime = DateTime.UtcNow.Date.AddDays(index).AddHours(10);

var entry = await _engine.CreateOccurrenceAsync(
DataSeeder.Organization,
DataSeeder.ResourcePath,
$"appointment-{index}",
startTime,
TimeSpan.FromMinutes(45),
TimeZone);

lock (_createdEntries)
{
_createdEntries.Add(entry);
}

return entry;
}

/// <summary>
/// Cleanup that runs once after all benchmark iterations for each parameter combination.
/// </summary>
[GlobalCleanup]
public async Task GlobalCleanup()
{
Console.WriteLine();
Console.WriteLine($"[CreateBenchmarks] GlobalCleanup: Deleting {_createdEntries.Count} entries...");

var deleted = 0;
foreach (var entry in _createdEntries)
{
try
{
if (entry.RecurrenceId.HasValue && entry.EntryType == CalendarEntryType.Recurrence)
{
await _engine.DeleteRecurrenceAsync(
DataSeeder.Organization, DataSeeder.ResourcePath, entry.RecurrenceId.Value);
deleted++;
}
else if (entry.OccurrenceId.HasValue)
{
await _engine.DeleteOccurrenceAsync(entry);
deleted++;
}
}
catch
{
// Ignore cleanup errors
}
}

Console.WriteLine($"[CreateBenchmarks] Cleanup complete: {deleted} entries deleted.");
}
}
135 changes: 135 additions & 0 deletions benchmarks/RecurringThings.Benchmarks/Benchmarks/DeleteBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
namespace RecurringThings.Benchmarks.Benchmarks;

using BenchmarkDotNet.Attributes;
using RecurringThings.Benchmarks.Infrastructure;
using RecurringThings.Engine;
using RecurringThings.Models;

/// <summary>
/// Benchmarks for delete operations.
/// </summary>
[Config(typeof(BenchmarkConfig))]
public class DeleteBenchmarks
{
private IRecurrenceEngine _engine = null!;
private Queue<CalendarEntry> _occurrencesToDelete = null!;
private Queue<Guid> _recurrencesToDelete = null!;

/// <summary>
/// Gets or sets the database provider to benchmark.
/// </summary>
[Params(BenchmarkProvider.MongoDB, BenchmarkProvider.PostgreSQL)]
public BenchmarkProvider Provider { get; set; }

private const string TimeZone = "America/New_York";

/// <summary>
/// Setup that runs once before all benchmark iterations for each parameter combination.
/// </summary>
[GlobalSetup]
public void GlobalSetup()
{
Console.WriteLine();
Console.WriteLine($"[DeleteBenchmarks] GlobalSetup: Provider={Provider}");
_engine = ProviderFactory.CreateEngine(Provider);
Console.WriteLine($"[DeleteBenchmarks] Setup complete.");
}

/// <summary>
/// Setup that runs before each benchmark iteration.
/// Pre-creates entries to be deleted during the benchmark.
/// </summary>
[IterationSetup]
public void IterationSetup()
{
_occurrencesToDelete = new Queue<CalendarEntry>();
_recurrencesToDelete = new Queue<Guid>();

// Pre-create entries to delete during benchmark iteration
for (int i = 0; i < 10; i++)
{
var occurrence = _engine.CreateOccurrenceAsync(
DataSeeder.Organization, DataSeeder.ResourcePath, "delete-test",
DateTime.UtcNow.AddDays(i), TimeSpan.FromHours(1), TimeZone)
.GetAwaiter().GetResult();
_occurrencesToDelete.Enqueue(occurrence);

var rrule = $"FREQ=DAILY;UNTIL={DateTime.UtcNow.AddMonths(1):yyyyMMdd}T235959Z";
var recurrence = _engine.CreateRecurrenceAsync(
DataSeeder.Organization, DataSeeder.ResourcePath, "delete-test",
DateTime.UtcNow.AddDays(i), TimeSpan.FromHours(1), rrule, TimeZone)
.GetAwaiter().GetResult();
_recurrencesToDelete.Enqueue(recurrence.RecurrenceId!.Value);
}
}

/// <summary>
/// Benchmarks deleting a standalone occurrence.
/// </summary>
[Benchmark(Description = "Delete standalone occurrence")]
public async Task DeleteOccurrenceAsync()
{
if (_occurrencesToDelete.TryDequeue(out var entry))
{
await _engine.DeleteOccurrenceAsync(entry);
}
}

/// <summary>
/// Benchmarks deleting a recurrence with cascade delete.
/// </summary>
[Benchmark(Description = "Delete recurrence (cascade)")]
public async Task DeleteRecurrenceAsync()
{
if (_recurrencesToDelete.TryDequeue(out var recurrenceId))
{
await _engine.DeleteRecurrenceAsync(DataSeeder.Organization, DataSeeder.ResourcePath, recurrenceId);
}
}

/// <summary>
/// Cleanup that runs after each benchmark iteration.
/// Removes any entries that weren't deleted during the benchmark.
/// </summary>
[IterationCleanup]
public void IterationCleanup()
{
// Clean up any entries that weren't deleted during the benchmark
while (_occurrencesToDelete.TryDequeue(out var entry))
{
try
{
_engine.DeleteOccurrenceAsync(entry).GetAwaiter().GetResult();
}
catch
{
// Ignore cleanup errors
}
}

while (_recurrencesToDelete.TryDequeue(out var id))
{
try
{
_engine.DeleteRecurrenceAsync(DataSeeder.Organization, DataSeeder.ResourcePath, id)
.GetAwaiter().GetResult();
}
catch
{
// Ignore cleanup errors
}
}
}

/// <summary>
/// Cleanup that runs once after all benchmark iterations for each parameter combination.
/// </summary>
[GlobalCleanup]
public async Task GlobalCleanup()
{
Console.WriteLine();
Console.WriteLine($"[DeleteBenchmarks] GlobalCleanup: Ensuring all test data is removed...");
await DataSeeder.CleanupAllAsync(_engine);
Console.WriteLine($"[DeleteBenchmarks] Cleanup complete.");
}
}
Loading