Skip to content

Commit

Permalink
Merge pull request #9 from Nexus-Mods/datoms-rocksdb-rewrite
Browse files Browse the repository at this point in the history
Datoms rocksdb rewrite
  • Loading branch information
halgari authored Feb 20, 2024
2 parents c7fdca6 + 257dc3d commit b5167cb
Show file tree
Hide file tree
Showing 65 changed files with 1,488 additions and 429 deletions.
7 changes: 0 additions & 7 deletions NexusMods.EventSourcing.sln
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.Tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.DatomStore.Tests", "tests\NexusMods.EventSourcing.DatomStore.Tests\NexusMods.EventSourcing.DatomStore.Tests.csproj", "{81CCE07D-818D-4153-8486-5D2A860C4D9D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.SourceGenerator", "src\NexusMods.EventSourcing.SourceGenerator\NexusMods.EventSourcing.SourceGenerator.csproj", "{F2A6F6B9-5D36-4416-BDD8-C7D30EE3ED4A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.Tests", "tests\NexusMods.EventSourcing.Tests\NexusMods.EventSourcing.Tests.csproj", "{07E2C578-8644-474D-8F07-B25CFEB28408}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.Benchmarks", "benchmarks\NexusMods.EventSourcing.Benchmarks\NexusMods.EventSourcing.Benchmarks.csproj", "{930B3AB7-56EA-48D6-B603-24D79C7DD00A}"
Expand All @@ -52,7 +50,6 @@ Global
{F2C1FB09-D01D-4E8B-B6BE-B548AB00187B} = {0377EBE6-F147-4233-86AD-32C821B9567E}
{EC1570A4-18B9-4A76-84FF-275BAA76A357} = {6ED01F9D-5E12-4EB2-9601-64A2ADC719DE}
{81CCE07D-818D-4153-8486-5D2A860C4D9D} = {6ED01F9D-5E12-4EB2-9601-64A2ADC719DE}
{F2A6F6B9-5D36-4416-BDD8-C7D30EE3ED4A} = {0377EBE6-F147-4233-86AD-32C821B9567E}
{07E2C578-8644-474D-8F07-B25CFEB28408} = {6ED01F9D-5E12-4EB2-9601-64A2ADC719DE}
{930B3AB7-56EA-48D6-B603-24D79C7DD00A} = {72AFE85F-8C12-436A-894E-638ED2C92A76}
EndGlobalSection
Expand All @@ -77,10 +74,6 @@ Global
{81CCE07D-818D-4153-8486-5D2A860C4D9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81CCE07D-818D-4153-8486-5D2A860C4D9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81CCE07D-818D-4153-8486-5D2A860C4D9D}.Release|Any CPU.Build.0 = Release|Any CPU
{F2A6F6B9-5D36-4416-BDD8-C7D30EE3ED4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2A6F6B9-5D36-4416-BDD8-C7D30EE3ED4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2A6F6B9-5D36-4416-BDD8-C7D30EE3ED4A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2A6F6B9-5D36-4416-BDD8-C7D30EE3ED4A}.Release|Any CPU.Build.0 = Release|Any CPU
{07E2C578-8644-474D-8F07-B25CFEB28408}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07E2C578-8644-474D-8F07-B25CFEB28408}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07E2C578-8644-474D-8F07-B25CFEB28408}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down
3 changes: 1 addition & 2 deletions benchmarks/NexusMods.EventSourcing.Benchmarks/AppHost.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NexusMods.EventSourcing.Benchmarks.Model;
using NexusMods.EventSourcing.DatomStore;
using NexusMods.Paths;

Expand All @@ -16,7 +15,7 @@ public static IServiceProvider Create()
{
services.AddDatomStore()
.AddEventSourcing()
.AddFileModel()
.AddTestModel()
.AddSingleton(new DatomStoreSettings()
{
Path = FileSystem.Shared.GetKnownPath(KnownPath.TempDirectory).Combine(Guid.NewGuid() + ".rocksdb")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,89 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;
using NexusMods.EventSourcing.Abstractions;
using NexusMods.EventSourcing.Benchmarks.Model;
using NexusMods.EventSourcing.TestModel.Model;

namespace NexusMods.EventSourcing.Benchmarks.Benchmarks;

[MemoryDiagnoser]
public class ReadTests
{
private readonly IConnection _connection;
private readonly List<EntityId> _entityIds;
private List<EntityId> _entityIdsAscending = null!;
private List<EntityId> _entityIdsDescending = null!;
private List<EntityId> _entityIdsRandom = null!;

public ReadTests()
{
var services = AppHost.Create();

_connection = services.GetRequiredService<IConnection>();
_entityIds = new List<EntityId>();
}

public const int MaxCount = 10000;

[GlobalSetup]
public void Setup()
{
var tx = _connection.BeginTransaction();
_entityIds.Clear();
for (var i = 0; i < Count; i++)
var entityIds = new List<EntityId>();
for (var i = 0; i < MaxCount; i++)
{
var id = Ids.MakeId(Ids.Partition.Entity, (ulong)i);
File.Hash.Assert(tx.TempId(), (ulong)i, tx);
File.Path.Assert(tx.TempId(), $"C:\\test_{i}.txt", tx);
File.Index.Assert(tx.TempId(), (ulong)i, tx);
_entityIds.Add(EntityId.From(id));
var file = new File(tx)
{
Hash = (ulong)i,
Path = $"C:\\test_{i}.txt",
Index = (ulong)i
};
entityIds.Add(file.Id);
}
tx.Commit();
var result = tx.Commit();

entityIds = entityIds.Select(e => result[e]).ToList();
_entityIdsAscending = entityIds.OrderBy(id => id.Value).ToList();
_entityIdsDescending = entityIds.OrderByDescending(id => id.Value).ToList();

var idArray = entityIds.ToArray();
Random.Shared.Shuffle(idArray);
_entityIdsRandom = idArray.ToList();
}


[Params(1, 1000, MaxCount)]
public int Count { get; set; } = MaxCount;

public enum SortOrder
{
Ascending,
Descending,
Random
}


[Params(1, 10, 100, 1000)]
public int Count { get; set; } = 1000;
//[Params(SortOrder.Ascending, SortOrder.Descending, SortOrder.Random)]
public SortOrder Order { get; set; } = SortOrder.Descending;

public List<EntityId> Ids => Order switch
{
SortOrder.Ascending => _entityIdsAscending,
SortOrder.Descending => _entityIdsDescending,
SortOrder.Random => _entityIdsRandom,
_ => throw new ArgumentOutOfRangeException()
};

[Benchmark]
public int ReadFiles()
public ulong ReadFiles()
{
var db = _connection.Db;
var read = db.Get<Model.FileReadModel>(_entityIds).ToList();
return read.Count;
ulong sum = 0;
foreach (var itm in db.Get<File>(Ids.Take(Count)))
{
sum += itm.Index;
}
return (ulong)sum;
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;
using NexusMods.EventSourcing.Abstractions;
using NexusMods.EventSourcing.Benchmarks.Model;

namespace NexusMods.EventSourcing.Benchmarks.Benchmarks;

Expand All @@ -23,6 +22,7 @@ public WriteTests()
[Benchmark]
public void AddFiles()
{
/*
var tx = _connection.BeginTransaction();
for (var i = 0; i < Count; i++)
{
Expand All @@ -32,6 +32,7 @@ public void AddFiles()
File.Index.Assert(tx.TempId(), (ulong)i, tx);
}
tx.Commit();
*/
}

}
14 changes: 0 additions & 14 deletions benchmarks/NexusMods.EventSourcing.Benchmarks/Model/File.cs

This file was deleted.

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

<ItemGroup>
<ProjectReference Include="..\..\src\NexusMods.EventSourcing.Abstractions\NexusMods.EventSourcing.Abstractions.csproj" />
<ProjectReference Include="..\..\src\NexusMods.EventSourcing.SourceGenerator\NexusMods.EventSourcing.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
<ProjectReference Include="..\..\src\NexusMods.EventSourcing\NexusMods.EventSourcing.csproj" />
<ProjectReference Include="..\..\tests\NexusMods.EventSourcing.TestModel\NexusMods.EventSourcing.TestModel.csproj" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion benchmarks/NexusMods.EventSourcing.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#if DEBUG

var benchmark = new ReadTests();
benchmark.Count = 1;
benchmark.Count = 1000;

var sw = Stopwatch.StartNew();
benchmark.Setup();
Expand Down
159 changes: 159 additions & 0 deletions docs/AttributeDefinitions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
hide:
- toc
---


## Attribute Definitions
The datoms stored in the store require typed attributes with unique name and serializer tagging. Storing this information in
a way that provides typed access to attribues is strangely difficult. There are several ways to tackle this issue, which we will talk about here.

### Symbolic Names
The simplest approach is to do what Datomic does and use symbolic names for attributes. For example a `:loadout/name` attribute
would be registered as a string type:

```csharp

System.RegisterAttribute<string>("loadout/name");
var nameSymbol = Symbol.Intern("loadout/name");

tx.Add(eid, nameSymbol, "My Loadout");
```

The problem with this approach is there is nothing stopping someone from using the wrong type for an attribute. The error happens
at runtime instead of at compile time. In addition loading values from the store may result in boxing unless care is taken.

This approach does allow for the use of fairly dynamic queries however.

```csharp
var query = from e in db.Entities
where e[Loadout_Name] == "My Loadout"
select e.Pull(Loadout_Name, Loadout_Version);

// What is the type of query result?
var results = query.ToList();
```

!!!info
Dapper uses this approach, as does Datomic, but it assumes a dynamic query result. This is not a bad thing, but it does
reduce the ability to use the type system to catch errors.

If we want to predefine a query model, we end up with something even more complex

```csharp

interface QueryResult
{
string Name { get; }
int Version { get; }
}


var results = from e in db.Entities<QueryResult>()
where e.Name == "My Loadout"
select e;
````

The question here is how we map the symbolic names to the result type. We can use attributes, but attributes must have
constant values as arguments, so we can't symbolc names and must use strings or some sort of `nameof` expression.

```csharp
interface QueryResult
{
[From("loadout/name")]
string Name { get; }
[From(nameof(NexusMods.Model.Loadout_Version))]
int Version { get; }
}
```

Since `nameof` only names the specific type, we have to give it a fully qualified name. This is also suboptimal.

### Attributes as Types

Another approach would be to use the type system to define the attributes. This has the advantage of being able to use the types
to provide strict type checking, and we can use attribues to provide the symbolic names.

```csharp
namespace Loadout {
public class Name : Attribute<Name, string>();
public class Version : Attribute<Version, int>();
}

public class QueryResult {
[From<Name>]
public string Name { get; }
[From<File.Version>]
public int Version { get; }
}
```

Unfortunately in this approach we have to make sure to use the correct type for the getter in the read model. There's nothing
stopping us from accidentally defining `Name` as an `int` for example. This would not result in a compile time error. If we pre-register
all our read models (like `QueryResult`) we can use reflection to check the types and at least we get a startup time error.

Another problem with this approach is that C#'s inference system is not good at resolving complex constraints, for example:

```csharp

// This requires us to know at usage time that Name is a string attribute, and to make sure that the type is correct.
tx.Add<Name, string>("foo");

// What we can do, is put the `.Bar` method in a static extension method
tx.Add<Name>("foo");

public static class AttributeExtensions
{
public static void Add<T>(this Transaction tx, T attribute, string value)
where T : Attribute<T, string>
{
tx.Add(attribute, value);
}

public static void Add<T>(this Transaction tx, T attribute, float value)
where T : Attribute<T, float>
{
tx.Add(attribute, value);
}
}
```

This works quite well, and we only need to perform the operation once per attribute value type. After we do this we can easily define
models that use the attributes.

```csharp

record ReadModel
{
[From<Name>]
public string Name { get; }
[From<Version>]
public int Version { get; }
}

record WriteModel
{
[From<Name>]
public required string Name { get; init;}
[From<Version>]
public required int Version { get; init;}
}

/// Define a model that responds to new transactions and fires INotifyPropertyChanged events
public class ActiveModel : ActiveModel
{
[From<Name>]
public string Name { get; set; }
[From<Version>]
public int Version { get; set; }
}

/// Perform an ad-hoc query
/// Find files with the path of "c:\temp\foo.txt" and look up the name and version of the mods that contain them.
var results = from f in db.Where<File.Name>(@"c:\temp\foo.txt")
from m in db.Where<File.Mod>(f.EntityId)
select db.Pull<ReadModel>(m.EntityId);
```


Loading

0 comments on commit b5167cb

Please sign in to comment.