diff --git a/NexusMods.EventSourcing.sln b/NexusMods.EventSourcing.sln index bd5bd40d..d8bf9e6e 100644 --- a/NexusMods.EventSourcing.sln +++ b/NexusMods.EventSourcing.sln @@ -40,6 +40,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.Fas EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.RocksDB", "src\NexusMods.EventSourcing.RocksDB\NexusMods.EventSourcing.RocksDB.csproj", "{B8D0772A-FAD1-49AD-A85B-B2B6C5B14420}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.LMDB", "src\NexusMods.EventSourcing.LMDB\NexusMods.EventSourcing.LMDB.csproj", "{4D9C9933-B3F9-4713-B3C7-A7C5E3D47E14}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,6 +55,7 @@ Global {96977D99-BF4B-4952-B594-6E44CCD826B9} = {72AFE85F-8C12-436A-894E-638ED2C92A76} {AFB69777-0A6A-4C61-A621-32C894673002} = {0377EBE6-F147-4233-86AD-32C821B9567E} {B8D0772A-FAD1-49AD-A85B-B2B6C5B14420} = {0377EBE6-F147-4233-86AD-32C821B9567E} + {4D9C9933-B3F9-4713-B3C7-A7C5E3D47E14} = {0377EBE6-F147-4233-86AD-32C821B9567E} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A92DED3D-BC67-4E04-9A06-9A1B302B3070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -83,5 +86,9 @@ Global {B8D0772A-FAD1-49AD-A85B-B2B6C5B14420}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8D0772A-FAD1-49AD-A85B-B2B6C5B14420}.Release|Any CPU.ActiveCfg = Release|Any CPU {B8D0772A-FAD1-49AD-A85B-B2B6C5B14420}.Release|Any CPU.Build.0 = Release|Any CPU + {4D9C9933-B3F9-4713-B3C7-A7C5E3D47E14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D9C9933-B3F9-4713-B3C7-A7C5E3D47E14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D9C9933-B3F9-4713-B3C7-A7C5E3D47E14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D9C9933-B3F9-4713-B3C7-A7C5E3D47E14}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/benchmarks/NexusMods.EventSourcing.Benchmarks/EventStoreBenchmarks.cs b/benchmarks/NexusMods.EventSourcing.Benchmarks/EventStoreBenchmarks.cs new file mode 100644 index 00000000..7ae06fed --- /dev/null +++ b/benchmarks/NexusMods.EventSourcing.Benchmarks/EventStoreBenchmarks.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.Logging; +using NexusMods.EventSourcing.Abstractions; +using NexusMods.EventSourcing.RocksDB; +using NexusMods.EventSourcing.TestModel; +using NexusMods.EventSourcing.TestModel.Events; +using NexusMods.EventSourcing.TestModel.Model; + +namespace NexusMods.EventSourcing.Benchmarks; + +[MemoryDiagnoser] +public class EventStoreBenchmarks : ABenchmark +{ + private readonly EntityId[] _ids; + private readonly RenameLoadout[] _events; + + public EventStoreBenchmarks() + { + _ids = Enumerable.Range(0, 100) + .Select(_ => EntityId.NewId()) + .ToArray(); + + _events = _ids + .Select(id => new RenameLoadout(id, "Loadout")) + .ToArray(); + + } + + + [Params(typeof(InMemoryEventStore), + //typeof(FasterKVEventStore), + typeof(RocksDBEventStore))] + public Type EventStoreType { get; set; } = null!; + + [GlobalSetup] + public void Setup() + { + MakeStore(EventStoreType); + + foreach (var evEvent in _events) + { + EventStore.Add(evEvent); + } + } + + [Benchmark] + public void AddEvent() + { + var rndEvent = _events[Random.Shared.Next(0, _events.Length)]; + EventStore.Add(rndEvent); + } + + + [Benchmark] + public void ReadEvents() + { + var ingester = new EventCounter(); + EventStore.EventsForEntity(_ids[Random.Shared.Next(0, _ids.Length)].Value, ingester); + } + + private struct EventCounter : IEventIngester + { + public int Count { get; private set; } + + public void Ingest(IEvent @event) + { + Count++; + } + } + + +} diff --git a/benchmarks/NexusMods.EventSourcing.Benchmarks/Program.cs b/benchmarks/NexusMods.EventSourcing.Benchmarks/Program.cs index f758d712..8442854c 100644 --- a/benchmarks/NexusMods.EventSourcing.Benchmarks/Program.cs +++ b/benchmarks/NexusMods.EventSourcing.Benchmarks/Program.cs @@ -20,5 +20,5 @@ readBenchmarks.LoadAllEntities(); Console.WriteLine("LoadAllEntities done"); #else -BenchmarkRunner.Run(); +BenchmarkRunner.Run(); #endif diff --git a/src/NexusMods.EventSourcing.LMDB/LMDBEventStore.cs b/src/NexusMods.EventSourcing.LMDB/LMDBEventStore.cs new file mode 100644 index 00000000..cac8e50f --- /dev/null +++ b/src/NexusMods.EventSourcing.LMDB/LMDBEventStore.cs @@ -0,0 +1,99 @@ +using System; +using System.Buffers.Binary; +using LightningDB; +using NexusMods.EventSourcing.Abstractions; + +namespace NexusMods.EventSourcing.LMDB; + +public class LMDBEventStore : IEventStore + where TSerializer : IEventSerializer +{ + private readonly TSerializer _serializer; + private readonly Settings _settings; + private readonly LightningEnvironment _env; + private TransactionId _tx; + + public LMDBEventStore(TSerializer serializer, Settings settings) + { + _serializer = serializer; + _settings = settings; + + _env = new LightningEnvironment(_settings.StorageLocation.ToString()); + _env.MapSize = 1024L * 1024L; // 1 TiB + _env.Open(); + _tx = TransactionId.From(0); + } + + public TransactionId Add(T eventValue) where T : IEvent + { + using var tx = _env.BeginTransaction(); + + lock (this) + { + _tx = _tx.Next(); + + { + using var db = tx.OpenDatabase("events"); + Span keySpan = stackalloc byte[8]; + BinaryPrimitives.WriteUInt64BigEndian(keySpan, _tx.Value); + var serialized = _serializer.Serialize(eventValue); + tx.Put(db, keySpan, serialized); + } + + { + using var db = tx.OpenDatabase("entityIndex"); + var ingester = new ModifiedEntitiesIngester(); + eventValue.Apply(ingester); + Span keySpan = stackalloc byte[24]; + BinaryPrimitives.WriteUInt64BigEndian(keySpan[16..], _tx.Value); + foreach (var entityId in ingester.Entities) + { + entityId.Value.TryWriteBytes(keySpan); + tx.Put(db, keySpan, keySpan); + } + } + tx.Commit(); + return _tx; + } + } + + public void EventsForEntity(EntityId entityId, TIngester ingester) where TIngester : IEventIngester + { + using var tx = _env.BeginTransaction(TransactionBeginFlags.ReadOnly); + using var dbEIdx = tx.OpenDatabase("entityIndex"); + using var dbEvents = tx.OpenDatabase("events"); + + using var cursor = tx.CreateCursor(dbEIdx); + + Span startKey = stackalloc byte[24]; + entityId.Value.TryWriteBytes(startKey); + Span endKey = stackalloc byte[24]; + entityId.Value.TryWriteBytes(endKey); + BinaryPrimitives.WriteUInt64BigEndian(endKey[16..], ulong.MaxValue); + + cursor.SetRange(startKey); + + while (true) + { + var (result, key, _) = cursor.GetCurrent(); + if (result != MDBResultCode.Success) + { + break; + } + + var id = EntityId.From(new Guid(key.AsSpan()[..16])); + if (id != entityId) + { + break; + } + + var eventData = tx.Get(dbEvents, key.AsSpan()[16..]); + if (eventData.resultCode != MDBResultCode.Success) + { + break; + } + var evt = _serializer.Deserialize(eventData.value.AsSpan()); + ingester.Ingest(evt); + } + } +} diff --git a/src/NexusMods.EventSourcing.LMDB/NexusMods.EventSourcing.LMDB.csproj b/src/NexusMods.EventSourcing.LMDB/NexusMods.EventSourcing.LMDB.csproj new file mode 100644 index 00000000..74946061 --- /dev/null +++ b/src/NexusMods.EventSourcing.LMDB/NexusMods.EventSourcing.LMDB.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/src/NexusMods.EventSourcing.LMDB/Settings.cs b/src/NexusMods.EventSourcing.LMDB/Settings.cs new file mode 100644 index 00000000..2c7f402c --- /dev/null +++ b/src/NexusMods.EventSourcing.LMDB/Settings.cs @@ -0,0 +1,8 @@ +using NexusMods.Paths; + +namespace NexusMods.EventSourcing.LMDB; + +public class Settings +{ + public AbsolutePath StorageLocation { get; set; } = default!; +} diff --git a/src/NexusMods.EventSourcing.RocksDB/RocksDBEventStore.cs b/src/NexusMods.EventSourcing.RocksDB/RocksDBEventStore.cs index 58936e99..05881486 100644 --- a/src/NexusMods.EventSourcing.RocksDB/RocksDBEventStore.cs +++ b/src/NexusMods.EventSourcing.RocksDB/RocksDBEventStore.cs @@ -40,29 +40,29 @@ public RocksDBEventStore(TSerializer serializer, Settings settings) public TransactionId Add(T eventValue) where T : IEvent { lock (this) - { - _tx = _tx.Next(); + { + _tx = _tx.Next(); - { - Span keySpan = stackalloc byte[8]; - BinaryPrimitives.WriteUInt64BigEndian(keySpan, _tx.Value); - var serialized = _serializer.Serialize(eventValue); - _db.Put(keySpan, serialized, _eventsColumn); - } + { + Span keySpan = stackalloc byte[8]; + BinaryPrimitives.WriteUInt64BigEndian(keySpan, _tx.Value); + var serialized = _serializer.Serialize(eventValue); + _db.Put(keySpan, serialized, _eventsColumn); + } - { - var ingester = new ModifiedEntitiesIngester(); - eventValue.Apply(ingester); - Span keySpan = stackalloc byte[24]; - BinaryPrimitives.WriteUInt64BigEndian(keySpan[16..], _tx.Value); - foreach (var entityId in ingester.Entities) - { - entityId.Value.TryWriteBytes(keySpan); - _db.Put(keySpan, keySpan, _entityIndexColumn); - } - } - return _tx; - } + { + var ingester = new ModifiedEntitiesIngester(); + eventValue.Apply(ingester); + Span keySpan = stackalloc byte[24]; + BinaryPrimitives.WriteUInt64BigEndian(keySpan[16..], _tx.Value); + foreach (var entityId in ingester.Entities) + { + entityId.Value.TryWriteBytes(keySpan); + _db.Put(keySpan, keySpan, _entityIndexColumn); + } + } + return _tx; + } } public void EventsForEntity(EntityId entityId, TIngester ingester) where TIngester : IEventIngester diff --git a/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs b/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs index bf194de8..e4957ad9 100644 --- a/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs +++ b/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs @@ -67,6 +67,11 @@ public IAccumulator GetAccumulator(EntityId ownerId, TAttrib return loadedValues[attributeDefinition]; } + public void EmptyCaches() + { + throw new NotImplementedException(); + } + private readonly struct Ingester(EntityId id) : IEventIngester, IEventContext { public readonly Dictionary Values = new(); diff --git a/tests/NexusMods.EventSourcing.Tests/EventStoreTests/LMDBEventStoreTests.cs b/tests/NexusMods.EventSourcing.Tests/EventStoreTests/LMDBEventStoreTests.cs new file mode 100644 index 00000000..d80d6a41 --- /dev/null +++ b/tests/NexusMods.EventSourcing.Tests/EventStoreTests/LMDBEventStoreTests.cs @@ -0,0 +1,12 @@ +using NexusMods.EventSourcing.LMDB; +using NexusMods.Paths; +using Settings = NexusMods.EventSourcing.RocksDB.Settings; + +namespace NexusMods.EventSourcing.Tests.EventStoreTests; + +public class LMDBEventStoreTests(EventSerializer serializer) : AEventStoreTest>( + new LMDBEventStore(serializer, new LMDB.Settings + { + StorageLocation = FileSystem.Shared.GetKnownPath(KnownPath.EntryDirectory) + .Combine("FasterKV.EventStore" + Guid.NewGuid()) + })); diff --git a/tests/NexusMods.EventSourcing.Tests/NexusMods.EventSourcing.Tests.csproj b/tests/NexusMods.EventSourcing.Tests/NexusMods.EventSourcing.Tests.csproj index a59ca656..437365ab 100644 --- a/tests/NexusMods.EventSourcing.Tests/NexusMods.EventSourcing.Tests.csproj +++ b/tests/NexusMods.EventSourcing.Tests/NexusMods.EventSourcing.Tests.csproj @@ -7,6 +7,7 @@ +