Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve latest snapshot query #12

Merged
merged 20 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e781c80
introduce performance regression tests
hahn-kev Aug 23, 2024
b73e9a5
fix filtering issue with current snapshots query
hahn-kev Aug 26, 2024
64c1baf
use in memory db
hahn-kev Aug 26, 2024
8317068
ensure the harmony table names are consistent now that we have a hard…
hahn-kev Aug 26, 2024
89857f8
make it easy to trace performance test and persist the database to disk
hahn-kev Aug 29, 2024
4baa679
implement some performance improvements, tweak tests to be more reali…
hahn-kev Aug 29, 2024
812eeeb
disable task wait warnings in benchmark code
hahn-kev Aug 29, 2024
2690f71
DataModelPerformanceTests doesn't need to inherit from DataModelTestBase
hahn-kev Aug 29, 2024
fd59105
re-enable some tests that were disabled because they were slow
hahn-kev Aug 29, 2024
93a14ff
remove benchmarkdotnet test adapter, we want to run the benchmarks ma…
hahn-kev Sep 2, 2024
fedce8a
add test to ensure changes are round tripped through the db properly
hahn-kev Sep 2, 2024
af44a4e
Fix bug where changes outside the model were saved in snapshots, cove…
hahn-kev Sep 2, 2024
de6236b
add HybridDateTimeTests.cs
hahn-kev Sep 2, 2024
819cf2a
Define interface to describe Changes return value, should help APIs i…
hahn-kev Sep 4, 2024
5508102
add a performance category to our performance tests
hahn-kev Sep 4, 2024
59a1700
introduce test to ensure that changes can still be written after an a…
hahn-kev Sep 12, 2024
0063f05
fix bug where you couldn't submit updates to an existing entity after…
hahn-kev Sep 20, 2024
74e5b2e
add node check to prove that it's null before
hahn-kev Oct 7, 2024
39204db
make it more obvious that the ignore data params are the same in the …
hahn-kev Oct 7, 2024
a767219
instead of looking up all snapshot ids when there's more than 10 comm…
hahn-kev Oct 7, 2024
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
43 changes: 29 additions & 14 deletions src/SIL.Harmony.Core/QueryHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,49 @@ public static async Task<SyncState> GetSyncState(this IQueryable<CommitBase> com
return new SyncState(dict);
}

public static async Task<ChangesResult<TCommit>> GetChanges<TCommit, TChange>(this IQueryable<TCommit> commits, SyncState remoteState) where TCommit : CommitBase<TChange>
public static async Task<ChangesResult<TCommit>> GetChanges<TCommit, TChange>(this IQueryable<TCommit> commits,
SyncState remoteState) where TCommit : CommitBase<TChange>
{
var newHistory = new List<TCommit>();
var localSyncState = await commits.GetSyncState();
foreach (var (clientId, localTimestamp) in localSyncState.ClientHeads)
var localState = await commits.AsNoTracking().GetSyncState();
return new ChangesResult<TCommit>(
await GetMissingCommits<TCommit, TChange>(commits, localState, remoteState).ToArrayAsync(),
localState);
}

public static async IAsyncEnumerable<TCommit> GetMissingCommits<TCommit, TChange>(
this IQueryable<TCommit> commits,
SyncState localState,
SyncState remoteState) where TCommit : CommitBase<TChange>
{
commits = commits.AsNoTracking();
foreach (var (clientId, localTimestamp) in localState.ClientHeads)
{
//client is new to the other history
if (!remoteState.ClientHeads.TryGetValue(clientId, out var otherTimestamp))
{
//todo slow, it would be better if we could query on client id and get latest changes per client
newHistory.AddRange(await commits.Include(c => c.ChangeEntities).DefaultOrder()
.Where(c => c.ClientId == clientId)
.ToArrayAsync());
await foreach (var commit in commits.Include(c => c.ChangeEntities).DefaultOrder()
.Where(c => c.ClientId == clientId)
.AsAsyncEnumerable())
{
yield return commit;
}
}
//client has newer history than the other history
else if (localTimestamp > otherTimestamp)
{
var otherDt = DateTimeOffset.FromUnixTimeMilliseconds(otherTimestamp);
//todo even slower because we need to filter out changes that are already in the other history
newHistory.AddRange((await commits.Include(c => c.ChangeEntities).DefaultOrder()
.Where(c => c.ClientId == clientId && c.HybridDateTime.DateTime > otherDt)
.ToArrayAsync())
//fixes an issue where the query would include commits that are already in the other history
.Where(c => c.DateTime.ToUnixTimeMilliseconds() > otherTimestamp));
await foreach (var commit in commits.Include(c => c.ChangeEntities)
.DefaultOrder()
.Where(c => c.ClientId == clientId && c.HybridDateTime.DateTime > otherDt)
.AsAsyncEnumerable())
{
if (commit.DateTime.ToUnixTimeMilliseconds() > otherTimestamp)
yield return commit;
}
}
}

return new(newHistory.ToArray(), localSyncState);
}

public static IQueryable<T> DefaultOrder<T>(this IQueryable<T> queryable) where T: CommitBase
Expand Down
9 changes: 7 additions & 2 deletions src/SIL.Harmony.Core/SyncState.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
namespace SIL.Harmony.Core;

public record SyncState(Dictionary<Guid, long> ClientHeads);

public record ChangesResult<TCommit>(TCommit[] MissingFromClient, SyncState ServerSyncState) where TCommit : CommitBase
public interface IChangesResult
{
IEnumerable<CommitBase> MissingFromClient { get; }
SyncState ServerSyncState { get; }
}
public record ChangesResult<TCommit>(TCommit[] MissingFromClient, SyncState ServerSyncState): IChangesResult where TCommit : CommitBase
{
IEnumerable<CommitBase> IChangesResult.MissingFromClient => MissingFromClient;
public static ChangesResult<TCommit> Empty => new([], new SyncState([]));
}
15 changes: 13 additions & 2 deletions src/SIL.Harmony.Sample/CrdtSampleKernel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Data.Common;
using System.Diagnostics;
using SIL.Harmony.Changes;
using SIL.Harmony.Linq2db;
using SIL.Harmony.Sample.Changes;
Expand All @@ -11,11 +12,21 @@ namespace SIL.Harmony.Sample;
public static class CrdtSampleKernel
{
public static IServiceCollection AddCrdtDataSample(this IServiceCollection services, string dbPath)
{
return services.AddCrdtDataSample(builder => builder.UseSqlite($"Data Source={dbPath}"));
}
public static IServiceCollection AddCrdtDataSample(this IServiceCollection services, DbConnection connection)
{
return services.AddCrdtDataSample(builder => builder.UseSqlite(connection, true));
}

public static IServiceCollection AddCrdtDataSample(this IServiceCollection services,
Action<DbContextOptionsBuilder> optionsBuilder)
{
services.AddDbContext<SampleDbContext>((provider, builder) =>
{
builder.UseLinqToDbCrdt(provider);
builder.UseSqlite($"Data Source={dbPath}");
optionsBuilder(builder);
builder.EnableDetailedErrors();
builder.EnableSensitiveDataLogging();
#if DEBUG
Expand Down
77 changes: 77 additions & 0 deletions src/SIL.Harmony.Tests/Core/HybridDateTimeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using SIL.Harmony.Core;

namespace SIL.Harmony.Tests.Core;

public class HybridDateTimeTests
{
[Fact]
public void Equals_TrueWhenTheSame()
{
var dateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), 0);
var otherDateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), 0);

(dateTime == otherDateTime).Should().BeTrue();
}

[Fact]
public void Equals_FalseWhenDifferentDateTime()
{
var dateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), 0);
var otherDateTime = new HybridDateTime(new DateTimeOffset(2001, 1, 1, 0, 0, 0, TimeSpan.Zero), 0);

(dateTime != otherDateTime).Should().BeTrue();
}

[Fact]
public void Equals_FalseWhenDifferentCounter()
{
var dateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), 0);
var otherDateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), 1);

dateTime.Should().NotBe(otherDateTime);
}

[Fact]
public void Constructor_ThrowsArgumentOutOfRangeExceptionWhenCounterIsNegative()
{
Action action = () => new HybridDateTime(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), -1);
action.Should().Throw<ArgumentOutOfRangeException>();
}

[Fact]
public void CompareTo_ReturnsOneWhenOtherIsNull()
{
var dateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), 0);
dateTime.CompareTo(null).Should().Be(1);
}

[Fact]
public void CompareTo_ReturnsNegativeOneWhenThisIsLessThanOther()
{
var dateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), 0);
var otherDateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 2, 0, 0, 0, TimeSpan.Zero), 0);

var result = dateTime.CompareTo(otherDateTime);
result.Should().BeLessThan(0);
}

[Fact]
public void CompareTo_ReturnsZeroWhenThisIsEqualToOther()
{
var dateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), 0);
var otherDateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), 0);

var result = dateTime.CompareTo(otherDateTime);
result.Should().Be(0);
}

[Fact]
public void CompareTo_ReturnsOneWhenThisIsGreaterThanOther()
{
var dateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 2, 0, 0, 0, TimeSpan.Zero), 0);
var otherDateTime = new HybridDateTime(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), 0);

var result = dateTime.CompareTo(otherDateTime);
result.Should().Be(1);
}
}
Loading