diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f1923c7..f2acca28 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,7 @@ - + @@ -19,7 +19,7 @@ - + @@ -27,7 +27,7 @@ - + diff --git a/src/Speckle.Sdk.Dependencies/Pools.cs b/src/Speckle.Sdk.Dependencies/Pools.cs index 5f3451fb..734b7432 100644 --- a/src/Speckle.Sdk.Dependencies/Pools.cs +++ b/src/Speckle.Sdk.Dependencies/Pools.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Text; using Microsoft.Extensions.ObjectPool; @@ -6,6 +6,8 @@ namespace Speckle.Sdk.Dependencies; public static class Pools { + public const int DefaultCapacity = 50; + public static Pool> ObjectDictionaries { get; } = new(new ObjectDictionaryPolicy()); private sealed class ObjectDictionaryPolicy : IPooledObjectPolicy> @@ -25,7 +27,7 @@ public static class Pools private sealed class ObjectDictionaryPolicy : IPooledObjectPolicy> where TKey : notnull { - public Dictionary Create() => new(50); + public Dictionary Create() => new(DefaultCapacity); public bool Return(Dictionary obj) { @@ -38,7 +40,7 @@ public static class Pools : IPooledObjectPolicy> where TKey : notnull { - public ConcurrentDictionary Create() => new(Environment.ProcessorCount, 50); + public ConcurrentDictionary Create() => new(Environment.ProcessorCount, DefaultCapacity); public bool Return(ConcurrentDictionary obj) { @@ -49,7 +51,7 @@ public static class Pools private sealed class ObjectListPolicy : IPooledObjectPolicy> { - public List Create() => new(50); + public List Create() => new(DefaultCapacity); public bool Return(List obj) { diff --git a/src/Speckle.Sdk.Dependencies/Serialization/Batch.cs b/src/Speckle.Sdk.Dependencies/Serialization/Batch.cs index 368cfd5c..fb6bb14c 100644 --- a/src/Speckle.Sdk.Dependencies/Serialization/Batch.cs +++ b/src/Speckle.Sdk.Dependencies/Serialization/Batch.cs @@ -1,10 +1,14 @@ -namespace Speckle.Sdk.Serialisation.V2.Send; +using System.Buffers; +using Speckle.Sdk.Dependencies; -public class Batch(int capacity) : IHasSize +namespace Speckle.Sdk.Serialisation.V2.Send; + +public sealed class Batch : IHasSize, IMemoryOwner where T : IHasSize { + private static readonly Pool> _pool = Pools.CreateListPool(); #pragma warning disable IDE0032 - private readonly List _items = new(capacity); + private readonly List _items = _pool.Get(); private int _batchSize; #pragma warning restore IDE0032 @@ -22,4 +26,8 @@ public class Batch(int capacity) : IHasSize public int Size => _batchSize; public List Items => _items; + + public void Dispose() => _pool.Return(_items); + + public Memory Memory => new(_items.ToArray()); } diff --git a/src/Speckle.Sdk.Dependencies/Serialization/ChannelExtensions.cs b/src/Speckle.Sdk.Dependencies/Serialization/ChannelExtensions.cs index a03a7337..e420a235 100644 --- a/src/Speckle.Sdk.Dependencies/Serialization/ChannelExtensions.cs +++ b/src/Speckle.Sdk.Dependencies/Serialization/ChannelExtensions.cs @@ -1,11 +1,12 @@ -using System.Threading.Channels; +using System.Buffers; +using System.Threading.Channels; using Open.ChannelExtensions; namespace Speckle.Sdk.Serialisation.V2.Send; public static class ChannelExtensions { - public static BatchingChannelReader> BatchBySize( + public static BatchingChannelReader> BatchBySize( this ChannelReader source, int batchSize, bool singleReader = false, diff --git a/src/Speckle.Sdk.Dependencies/Serialization/ChannelSaver.cs b/src/Speckle.Sdk.Dependencies/Serialization/ChannelSaver.cs index 97c00f10..99190998 100644 --- a/src/Speckle.Sdk.Dependencies/Serialization/ChannelSaver.cs +++ b/src/Speckle.Sdk.Dependencies/Serialization/ChannelSaver.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Threading.Channels; using Open.ChannelExtensions; using Speckle.Sdk.Serialisation.V2.Send; @@ -13,7 +14,7 @@ public abstract class ChannelSaver private const int MAX_PARALLELISM_HTTP = 4; private const int HTTP_CAPACITY = 500; private const int MAX_CACHE_WRITE_PARALLELISM = 4; - private const int MAX_CACHE_BATCH = 200; + private const int MAX_CACHE_BATCH = 500; private readonly Channel _checkCacheChannel = Channel.CreateBounded( new BoundedChannelOptions(SEND_CAPACITY) @@ -46,7 +47,13 @@ public abstract class ChannelSaver public ValueTask Save(T item, CancellationToken cancellationToken = default) => _checkCacheChannel.Writer.WriteAsync(item, cancellationToken); - public abstract Task> SendToServer(Batch batch, CancellationToken cancellationToken); + public async Task> SendToServer(IMemoryOwner batch, CancellationToken cancellationToken) + { + await SendToServer((Batch)batch, cancellationToken).ConfigureAwait(false); + return batch; + } + + public abstract Task SendToServer(Batch batch, CancellationToken cancellationToken); public Task Done() { diff --git a/src/Speckle.Sdk.Dependencies/Serialization/SizeBatchingChannelReader.cs b/src/Speckle.Sdk.Dependencies/Serialization/SizeBatchingChannelReader.cs index 0f66b49b..e979aba9 100644 --- a/src/Speckle.Sdk.Dependencies/Serialization/SizeBatchingChannelReader.cs +++ b/src/Speckle.Sdk.Dependencies/Serialization/SizeBatchingChannelReader.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Threading.Channels; using Open.ChannelExtensions; @@ -13,20 +14,20 @@ public class SizeBatchingChannelReader( int batchSize, bool singleReader, bool syncCont = false -) : BatchingChannelReader>(x => new(x), source, batchSize, singleReader, syncCont) +) : BatchingChannelReader>(x => new Batch(), source, batchSize, singleReader, syncCont) where T : IHasSize { - protected override Batch CreateBatch(int capacity) => new(capacity); + protected override IMemoryOwner CreateBatch(int capacity) => new Batch(); - protected override void TrimBatch(ref Batch batch, bool isVerifiedFull) + protected override void TrimBatch(ref IMemoryOwner batch, bool isVerifiedFull) { if (!isVerifiedFull) { - batch.TrimExcess(); + ((Batch)batch).TrimExcess(); } } - protected override void AddBatchItem(Batch batch, T item) => batch.Add(item); + protected override void AddBatchItem(IMemoryOwner batch, T item) => ((Batch)batch).Add(item); - protected override int GetBatchSize(Batch batch) => batch.Size; + protected override int GetBatchSize(IMemoryOwner batch) => ((Batch)batch).Size; } diff --git a/src/Speckle.Sdk.Dependencies/packages.lock.json b/src/Speckle.Sdk.Dependencies/packages.lock.json index 38bc5fac..123ae1af 100644 --- a/src/Speckle.Sdk.Dependencies/packages.lock.json +++ b/src/Speckle.Sdk.Dependencies/packages.lock.json @@ -19,9 +19,9 @@ }, "Microsoft.Extensions.ObjectPool": { "type": "Direct", - "requested": "[8.0.11, )", - "resolved": "8.0.11", - "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "UbsU/gYe4nv1DeqMXIVzDfNNek7Sk2kKuAOXL/Y+sLcAR0HwFUqzg1EPiU88jeHNe0g81aPvvHbvHarQr3r9IA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", @@ -44,13 +44,13 @@ }, "Open.ChannelExtensions": { "type": "Direct", - "requested": "[8.6.0, )", - "resolved": "8.6.0", - "contentHash": "g5axz417bA6FXifJaBlB0l62gV7dYmknXx0n8lT/LSA3+7isaGMsOjJp5J+H/yXDRe4r+KZrE+bzQcs4Ets2kA==", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "DP+l5S6G46wcuY4I4kNXE+RDOmJr0DKuMienOdt0mMBN9z7vmLSC8YQbqCyb9i9LNjXj1tgCx5LyitJiRr/v7g==", "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "8.0.0", - "System.Collections.Immutable": "8.0.0", - "System.Threading.Channels": "8.0.0" + "Microsoft.Bcl.AsyncInterfaces": "9.0.0", + "System.Collections.Immutable": "9.0.0", + "System.Threading.Channels": "9.0.0" } }, "Polly": { @@ -88,10 +88,11 @@ }, "System.Threading.Channels": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA==", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "hzACdIf1C+4Dqos5ijV404b94+LqfIC8nfS3mNpCDFWowb1N3PNfJPopneq32ahWlDeyaPZJqjBk76YFR69Rpg==", "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "9.0.0", "System.Threading.Tasks.Extensions": "4.5.4" } }, @@ -122,8 +123,8 @@ }, "System.Collections.Immutable": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==", + "resolved": "9.0.0", + "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==", "dependencies": { "System.Memory": "4.5.5", "System.Runtime.CompilerServices.Unsafe": "6.0.0" @@ -160,8 +161,8 @@ "Microsoft.Bcl.AsyncInterfaces": { "type": "CentralTransitive", "requested": "[5.0.0, )", - "resolved": "8.0.0", - "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==", + "resolved": "9.0.0", + "contentHash": "owmu2Cr3IQ8yQiBleBHlGk8dSQ12oaF2e7TpzwJKEl4m84kkZJjEY1n33L67Y3zM5jPOjmmbdHjbfiL0RqcMRQ==", "dependencies": { "System.Threading.Tasks.Extensions": "4.5.4" } @@ -185,9 +186,9 @@ }, "Microsoft.Extensions.ObjectPool": { "type": "Direct", - "requested": "[8.0.11, )", - "resolved": "8.0.11", - "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "UbsU/gYe4nv1DeqMXIVzDfNNek7Sk2kKuAOXL/Y+sLcAR0HwFUqzg1EPiU88jeHNe0g81aPvvHbvHarQr3r9IA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", @@ -201,9 +202,9 @@ }, "Open.ChannelExtensions": { "type": "Direct", - "requested": "[8.6.0, )", - "resolved": "8.6.0", - "contentHash": "g5axz417bA6FXifJaBlB0l62gV7dYmknXx0n8lT/LSA3+7isaGMsOjJp5J+H/yXDRe4r+KZrE+bzQcs4Ets2kA==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "DP+l5S6G46wcuY4I4kNXE+RDOmJr0DKuMienOdt0mMBN9z7vmLSC8YQbqCyb9i9LNjXj1tgCx5LyitJiRr/v7g==" }, "Polly": { "type": "Direct", @@ -240,9 +241,9 @@ }, "System.Threading.Channels": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA==" + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "hzACdIf1C+4Dqos5ijV404b94+LqfIC8nfS3mNpCDFWowb1N3PNfJPopneq32ahWlDeyaPZJqjBk76YFR69Rpg==" }, "ILRepack": { "type": "Transitive", diff --git a/src/Speckle.Sdk/Credentials/AccountManager.cs b/src/Speckle.Sdk/Credentials/AccountManager.cs index e8784ee1..2009ebbf 100644 --- a/src/Speckle.Sdk/Credentials/AccountManager.cs +++ b/src/Speckle.Sdk/Credentials/AccountManager.cs @@ -21,11 +21,13 @@ using Stream = System.IO.Stream; namespace Speckle.Sdk.Credentials; +public partial interface IAccountManager : IDisposable; + /// /// Manage accounts locally for desktop applications. /// [GenerateAutoInterface] -public class AccountManager( +public sealed class AccountManager( ISpeckleApplication application, ILogger logger, ISpeckleHttp speckleHttp, @@ -40,6 +42,13 @@ public class AccountManager( "AccountAddFlow" ); + [AutoInterfaceIgnore] + public void Dispose() + { + _accountStorage.Dispose(); + _accountAddLockStorage.Dispose(); + } + /// /// Gets the basic information about a server. /// @@ -321,7 +330,7 @@ public class AccountManager( { static bool IsInvalid(Account ac) => ac.userInfo == null || ac.serverInfo == null; - var sqlAccounts = _accountStorage.GetAllObjects().Select(x => JsonConvert.DeserializeObject(x)); + var sqlAccounts = _accountStorage.GetAllObjects().Select(x => JsonConvert.DeserializeObject(x.Json)); var localAccounts = GetLocalAccounts(); foreach (var acc in sqlAccounts) @@ -642,7 +651,7 @@ public class AccountManager( } // this uses the SQLite transport to store locks - var lockIds = _accountAddLockStorage.GetAllObjects().OrderByDescending(d => d).ToList(); + var lockIds = _accountAddLockStorage.GetAllObjects().Select(x => x.Id).OrderByDescending(d => d).ToList(); var now = DateTime.Now; foreach (var l in lockIds) { @@ -674,7 +683,7 @@ public class AccountManager( { s_isAddingAccount = false; // make sure all old locks are removed - foreach (var id in _accountAddLockStorage.GetAllObjects()) + foreach (var (id, _) in _accountAddLockStorage.GetAllObjects()) { _accountAddLockStorage.DeleteObject(id); } diff --git a/src/Speckle.Sdk/SQLite/CacheDbCommandPool.cs b/src/Speckle.Sdk/SQLite/CacheDbCommandPool.cs new file mode 100644 index 00000000..500d2230 --- /dev/null +++ b/src/Speckle.Sdk/SQLite/CacheDbCommandPool.cs @@ -0,0 +1,96 @@ +using System.Collections.Concurrent; +using Microsoft.Data.Sqlite; + +namespace Speckle.Sdk.SQLite; + +//inspired by https://github.com/neosmart/SqliteCache/blob/master/SqliteCache/DbCommandPool.cs +public sealed class CacheDbCommandPool : IDisposable +{ + private readonly ConcurrentBag[] _commands = new ConcurrentBag[CacheDbCommands.Count]; + private readonly ConcurrentBag _connections = new(); + private readonly string _connectionString; + + public CacheDbCommandPool(string connectionString, int concurrency) + { + _connectionString = connectionString; + for (int i = 0; i < _commands.Length; ++i) + { + _commands[i] = new ConcurrentBag(); + } + for (int i = 0; i < concurrency; ++i) + { + var connection = new SqliteConnection(_connectionString); + connection.Open(); + _connections.Add(connection); + } + } + + public void Use(CacheOperation type, Action handler) => + Use( + type, + cmd => + { + handler(cmd); + return true; + } + ); + + private T Use(Func handler) + { + if (!_connections.TryTake(out var db)) + { + db = new SqliteConnection(_connectionString); + db.Open(); + } + + try + { + return handler(db); + } + finally + { + _connections.Add(db); + } + } + + public T Use(CacheOperation type, Func handler) => + Use(conn => + { + var pool = _commands[(int)type]; + if (!pool.TryTake(out var command)) + { +#pragma warning disable CA2100 + command = new SqliteCommand(CacheDbCommands.Commands[(int)type], conn); +#pragma warning restore CA2100 + } + + try + { + command.Connection = conn; + return handler(command); + } + finally + { + command.Connection = null; + command.Parameters.Clear(); + pool.Add(command); + } + }); + + public void Dispose() + { + foreach (var pool in _commands) + { + while (pool.TryTake(out var cmd)) + { + cmd.Dispose(); + } + } + + foreach (var conn in _connections) + { + conn.Close(); + conn.Dispose(); + } + } +} diff --git a/src/Speckle.Sdk/SQLite/CacheDbCommands.cs b/src/Speckle.Sdk/SQLite/CacheDbCommands.cs new file mode 100644 index 00000000..33a28771 --- /dev/null +++ b/src/Speckle.Sdk/SQLite/CacheDbCommands.cs @@ -0,0 +1,35 @@ +namespace Speckle.Sdk.SQLite; + +public enum CacheOperation +{ + InsertOrIgnore, + InsertOrReplace, + Has, + Get, + Delete, + GetAll, + BulkInsertOrIgnore, +} + +public static class CacheDbCommands +{ + public static readonly string[] Commands; + public static readonly int Count = Enum.GetValues(typeof(CacheOperation)).Length; + +#pragma warning disable CA1810 + static CacheDbCommands() +#pragma warning restore CA1810 + { + Commands = new string[Count]; + + Commands[(int)CacheOperation.InsertOrIgnore] = + "INSERT OR IGNORE INTO objects(hash, content) VALUES(@hash, @content)"; + Commands[(int)CacheOperation.InsertOrReplace] = "REPLACE INTO objects(hash, content) VALUES(@hash, @content)"; + Commands[(int)CacheOperation.Has] = "SELECT 1 FROM objects WHERE hash = @hash LIMIT 1"; + Commands[(int)CacheOperation.Get] = "SELECT content FROM objects WHERE hash = @hash LIMIT 1"; + Commands[(int)CacheOperation.Delete] = "DELETE FROM objects WHERE hash = @hash"; + Commands[(int)CacheOperation.GetAll] = "SELECT hash, content FROM objects"; + + Commands[(int)CacheOperation.BulkInsertOrIgnore] = "INSERT OR IGNORE INTO objects (hash, content) VALUES "; + } +} diff --git a/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs b/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs index 8ffffddd..6a2983f4 100644 --- a/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs +++ b/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs @@ -1,19 +1,28 @@ +using System.Text; using Microsoft.Data.Sqlite; using Speckle.InterfaceGenerator; +using Speckle.Sdk.Dependencies; namespace Speckle.Sdk.SQLite; +public partial interface ISqLiteJsonCacheManager : IDisposable; + [GenerateAutoInterface] -public class SqLiteJsonCacheManager : ISqLiteJsonCacheManager +public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager { private readonly string _connectionString; + private readonly CacheDbCommandPool _pool; - public SqLiteJsonCacheManager(string rootPath) + public SqLiteJsonCacheManager(string connectionString, int concurrency) { - _connectionString = $"Data Source={rootPath};"; + _connectionString = connectionString; Initialize(); + _pool = new CacheDbCommandPool(_connectionString, concurrency); } + [AutoInterfaceIgnore] + public void Dispose() => _pool.Dispose(); + private void Initialize() { // NOTE: used for creating partioned object tables. @@ -57,100 +66,107 @@ public class SqLiteJsonCacheManager : ISqLiteJsonCacheManager using SqliteCommand cmd4 = new("PRAGMA page_size = 32768;", c); cmd4.ExecuteNonQuery(); + c.Close(); } - public IEnumerable GetAllObjects() - { - using var c = new SqliteConnection(_connectionString); - c.Open(); - using var command = new SqliteCommand("SELECT * FROM objects", c); + public IReadOnlyCollection<(string Id, string Json)> GetAllObjects() => + _pool.Use( + CacheOperation.GetAll, + command => + { + var list = new HashSet<(string, string)>(); + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + list.Add((reader.GetString(0), reader.GetString(1))); + } + return list; + } + ); - using var reader = command.ExecuteReader(); - while (reader.Read()) - { - yield return reader.GetString(1); - } - } + public void DeleteObject(string id) => + _pool.Use( + CacheOperation.Delete, + command => + { + command.Parameters.AddWithValue("@hash", id); + command.ExecuteNonQuery(); + } + ); - public void DeleteObject(string id) - { - using var c = new SqliteConnection(_connectionString); - c.Open(); - using var command = new SqliteCommand("DELETE FROM objects WHERE hash = @hash", c); - command.Parameters.AddWithValue("@hash", id); - command.ExecuteNonQuery(); - } - - public string? GetObject(string id) - { - using var c = new SqliteConnection(_connectionString); - c.Open(); - using var command = new SqliteCommand("SELECT * FROM objects WHERE hash = @hash LIMIT 1 ", c); - command.Parameters.AddWithValue("@hash", id); - using var reader = command.ExecuteReader(); - if (reader.Read()) - { - return reader.GetString(1); - } - - return null; // pass on the duty of null checks to consumers - } + public string? GetObject(string id) => + _pool.Use( + CacheOperation.Get, + command => + { + command.Parameters.AddWithValue("@hash", id); + return (string?)command.ExecuteScalar(); + } + ); //This does an insert or ignores if already exists - public void SaveObject(string id, string json) - { - using var c = new SqliteConnection(_connectionString); - c.Open(); - const string COMMAND_TEXT = "INSERT OR IGNORE INTO objects(hash, content) VALUES(@hash, @content)"; - - using var command = new SqliteCommand(COMMAND_TEXT, c); - command.Parameters.AddWithValue("@hash", id); - command.Parameters.AddWithValue("@content", json); - command.ExecuteNonQuery(); - } + public void SaveObject(string id, string json) => + _pool.Use( + CacheOperation.InsertOrIgnore, + command => + { + command.Parameters.AddWithValue("@hash", id); + command.Parameters.AddWithValue("@content", json); + command.ExecuteNonQuery(); + } + ); //This does an insert or replaces if already exists - public void UpdateObject(string id, string json) - { - using var c = new SqliteConnection(_connectionString); - c.Open(); - const string COMMAND_TEXT = "REPLACE INTO objects(hash, content) VALUES(@hash, @content)"; - using var command = new SqliteCommand(COMMAND_TEXT, c); - command.Parameters.AddWithValue("@hash", id); - command.Parameters.AddWithValue("@content", json); - command.ExecuteNonQuery(); - } + public void UpdateObject(string id, string json) => + _pool.Use( + CacheOperation.InsertOrReplace, + command => + { + command.Parameters.AddWithValue("@hash", id); + command.Parameters.AddWithValue("@content", json); + command.ExecuteNonQuery(); + } + ); - public void SaveObjects(IEnumerable<(string id, string json)> items) - { - using var c = new SqliteConnection(_connectionString); - c.Open(); - using var t = c.BeginTransaction(); - const string COMMAND_TEXT = "INSERT OR IGNORE INTO objects(hash, content) VALUES(@hash, @content)"; + public void SaveObjects(IEnumerable<(string id, string json)> items) => + _pool.Use( + CacheOperation.BulkInsertOrIgnore, + cmd => + { + CreateBulkInsert(cmd, items); + return cmd.ExecuteNonQuery(); + } + ); - using var command = new SqliteCommand(COMMAND_TEXT, c); - command.Transaction = t; - var idParam = command.Parameters.Add("@hash", SqliteType.Text); - var jsonParam = command.Parameters.Add("@content", SqliteType.Text); + private void CreateBulkInsert(SqliteCommand cmd, IEnumerable<(string id, string json)> items) + { + StringBuilder sb = Pools.StringBuilders.Get(); + sb.AppendLine(CacheDbCommands.Commands[(int)CacheOperation.BulkInsertOrIgnore]); + int i = 0; foreach (var (id, json) in items) { - idParam.Value = id; - jsonParam.Value = json; - command.ExecuteNonQuery(); + sb.Append($"(@key{i}, @value{i}),"); + cmd.Parameters.AddWithValue($"@key{i}", id); + cmd.Parameters.AddWithValue($"@value{i}", json); + i++; } - t.Commit(); + sb.Remove(sb.Length - 1, 1); + sb.Append(';'); +#pragma warning disable CA2100 + cmd.CommandText = sb.ToString(); +#pragma warning restore CA2100 + Pools.StringBuilders.Return(sb); } - public bool HasObject(string objectId) - { - using var c = new SqliteConnection(_connectionString); - c.Open(); - const string COMMAND_TEXT = "SELECT 1 FROM objects WHERE hash = @hash LIMIT 1 "; - using var command = new SqliteCommand(COMMAND_TEXT, c); - command.Parameters.AddWithValue("@hash", objectId); - - using var reader = command.ExecuteReader(); - bool rowFound = reader.Read(); - return rowFound; - } + public bool HasObject(string objectId) => + _pool.Use( + CacheOperation.Has, + command => + { + command.Parameters.AddWithValue("@hash", objectId); + using var reader = command.ExecuteReader(); + bool rowFound = reader.Read(); + return rowFound; + } + ); } diff --git a/src/Speckle.Sdk/SQLite/SqLiteJsonCacheManagerFactory.cs b/src/Speckle.Sdk/SQLite/SqLiteJsonCacheManagerFactory.cs index c73a28a7..94eb9339 100644 --- a/src/Speckle.Sdk/SQLite/SqLiteJsonCacheManagerFactory.cs +++ b/src/Speckle.Sdk/SQLite/SqLiteJsonCacheManagerFactory.cs @@ -7,10 +7,14 @@ namespace Speckle.Sdk.SQLite; [GenerateAutoInterface] public class SqLiteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory { - private ISqLiteJsonCacheManager Create(string path) => new SqLiteJsonCacheManager(path); + public const int INITIAL_CONCURRENCY = 4; + + private ISqLiteJsonCacheManager Create(string path, int concurrency) => + new SqLiteJsonCacheManager($"Data Source={path};", concurrency); public ISqLiteJsonCacheManager CreateForUser(string scope) => - Create(Path.Combine(SpecklePathProvider.UserApplicationDataPath(), "Speckle", $"{scope}.db")); + Create(Path.Combine(SpecklePathProvider.UserApplicationDataPath(), "Speckle", $"{scope}.db"), 1); - public ISqLiteJsonCacheManager CreateFromStream(string streamId) => Create(SqlitePaths.GetDBPath(streamId)); + public ISqLiteJsonCacheManager CreateFromStream(string streamId) => + Create(SqlitePaths.GetDBPath(streamId), INITIAL_CONCURRENCY); } diff --git a/src/Speckle.Sdk/Serialisation/V2/DummySendServerObjectManager.cs b/src/Speckle.Sdk/Serialisation/V2/DummySendServerObjectManager.cs index 827ed34a..26e50477 100644 --- a/src/Speckle.Sdk/Serialisation/V2/DummySendServerObjectManager.cs +++ b/src/Speckle.Sdk/Serialisation/V2/DummySendServerObjectManager.cs @@ -5,9 +5,11 @@ using Speckle.Sdk.Transports; namespace Speckle.Sdk.Serialisation.V2; -public class DummySqLiteJsonCacheManager : ISqLiteJsonCacheManager +public sealed class DummySqLiteJsonCacheManager : ISqLiteJsonCacheManager { - public IEnumerable GetAllObjects() => throw new NotImplementedException(); + public void Dispose() { } + + public IReadOnlyCollection<(string, string)> GetAllObjects() => throw new NotImplementedException(); public void DeleteObject(string id) => throw new NotImplementedException(); diff --git a/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs b/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs index 87aa22a3..5be948fd 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs @@ -13,6 +13,8 @@ public record DeserializeProcessOptions( bool SkipInvalidConverts = false ); +public partial interface IDeserializeProcess : IDisposable; + [GenerateAutoInterface] public sealed class DeserializeProcess( IProgress? progress, @@ -30,6 +32,9 @@ public sealed class DeserializeProcess( public IReadOnlyDictionary BaseCache => _baseCache; public long Total { get; private set; } + [AutoInterfaceIgnore] + public void Dispose() => objectLoader.Dispose(); + public async Task Deserialize(string rootId, CancellationToken cancellationToken) { var (rootJson, childrenIds) = await objectLoader diff --git a/src/Speckle.Sdk/Serialisation/V2/Receive/ObjectLoader.cs b/src/Speckle.Sdk/Serialisation/V2/Receive/ObjectLoader.cs index 7201c4d0..345190c5 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Receive/ObjectLoader.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Receive/ObjectLoader.cs @@ -9,6 +9,8 @@ using Speckle.Sdk.Transports; namespace Speckle.Sdk.Serialisation.V2.Receive; +public partial interface IObjectLoader : IDisposable; + [GenerateAutoInterface] public sealed class ObjectLoader( ISqLiteJsonCacheManager sqLiteJsonCacheManager, @@ -21,6 +23,9 @@ public sealed class ObjectLoader( private long _cached; private DeserializeProcessOptions _options = new(false); + [AutoInterfaceIgnore] + public void Dispose() => sqLiteJsonCacheManager.Dispose(); + public async Task<(string, IReadOnlyCollection)> GetAndCache( string rootId, DeserializeProcessOptions options, diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/BaseItem.cs b/src/Speckle.Sdk/Serialisation/V2/Send/BaseItem.cs new file mode 100644 index 00000000..38f2922f --- /dev/null +++ b/src/Speckle.Sdk/Serialisation/V2/Send/BaseItem.cs @@ -0,0 +1,19 @@ +using System.Text; + +namespace Speckle.Sdk.Serialisation.V2.Send; + +public readonly record struct BaseItem(Id Id, Json Json, bool NeedsStorage, Dictionary? Closures) : IHasSize +{ + public int Size { get; } = Encoding.UTF8.GetByteCount(Json.Value); + + public bool Equals(BaseItem? other) + { + if (other is null) + { + return false; + } + return string.Equals(Id.Value, other.Value.Id.Value, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() => Id.GetHashCode(); +} diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/PriorityScheduler.cs b/src/Speckle.Sdk/Serialisation/V2/Send/PriorityScheduler.cs index c38a3df3..46eacf4a 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Send/PriorityScheduler.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Send/PriorityScheduler.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; namespace Speckle.Sdk.Serialisation.V2.Send; diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs b/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs index 8f08173a..242fc5a0 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Text; using Speckle.InterfaceGenerator; using Speckle.Sdk.Common; using Speckle.Sdk.Dependencies; @@ -23,22 +22,6 @@ public readonly record struct SerializeProcessResults( IReadOnlyDictionary ConvertedReferences ); -public readonly record struct BaseItem(Id Id, Json Json, bool NeedsStorage, Closures? Closures) : IHasSize -{ - public int Size { get; } = Encoding.UTF8.GetByteCount(Json.Value); - - public bool Equals(BaseItem? other) - { - if (other is null) - { - return false; - } - return string.Equals(Id.Value, other.Value.Id.Value, StringComparison.OrdinalIgnoreCase); - } - - public override int GetHashCode() => Id.GetHashCode(); -} - public partial interface ISerializeProcess : IDisposable; [GenerateAutoInterface] @@ -77,6 +60,7 @@ public sealed class SerializeProcess( { _highest.Dispose(); _belowNormal.Dispose(); + sqLiteJsonCacheManager.Dispose(); } public async Task Serialize(Base root, CancellationToken cancellationToken) @@ -223,7 +207,7 @@ public sealed class SerializeProcess( return new BaseItem(id, json, true, closures); } - public override async Task> SendToServer(Batch batch, CancellationToken cancellationToken) + public override async Task SendToServer(Batch batch, CancellationToken cancellationToken) { if (!_options.SkipServer && batch.Items.Count != 0) { @@ -238,9 +222,7 @@ public sealed class SerializeProcess( Interlocked.Exchange(ref _uploaded, _uploaded + batch.Items.Count); } progress?.Report(new(ProgressEvent.UploadedObjects, _uploaded, null)); - return objectBatch; } - return batch.Items; } public override void SaveToCache(List batch) diff --git a/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs b/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs index 36cc08af..68615ec5 100644 --- a/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs +++ b/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs @@ -1,5 +1,3 @@ -using Speckle.Sdk.Helpers; -using Speckle.Sdk.Logging; using Speckle.Sdk.Serialisation.V2.Receive; using Speckle.Sdk.Serialisation.V2.Send; using Speckle.Sdk.SQLite; @@ -23,20 +21,14 @@ public interface ISerializeProcessFactory IProgress? progress, DeserializeProcessOptions? options = null ); - - public ISerializeProcess CreateSerializeProcess( - SerializeProcessOptions? options = null, - IProgress? progress = null - ); } public class SerializeProcessFactory( - ISpeckleHttp speckleHttp, - ISdkActivityFactory activityFactory, IBaseChildFinder baseChildFinder, IObjectSerializerFactory objectSerializerFactory, IObjectDeserializerFactory objectDeserializerFactory, - ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory + ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory, + IServerObjectManagerFactory serverObjectManagerFactory ) : ISerializeProcessFactory { public ISerializeProcess CreateSerializeProcess( @@ -48,24 +40,7 @@ public class SerializeProcessFactory( ) { var sqLiteJsonCacheManager = sqLiteJsonCacheManagerFactory.CreateFromStream(streamId); - var serverObjectManager = new ServerObjectManager(speckleHttp, activityFactory, url, streamId, authorizationToken); - return new SerializeProcess( - progress, - sqLiteJsonCacheManager, - serverObjectManager, - baseChildFinder, - objectSerializerFactory, - options - ); - } - - public ISerializeProcess CreateSerializeProcess( - SerializeProcessOptions? options = null, - IProgress? progress = null - ) - { - var sqLiteJsonCacheManager = new DummySqLiteJsonCacheManager(); - var serverObjectManager = new DummySendServerObjectManager(); + var serverObjectManager = serverObjectManagerFactory.Create(url, streamId, authorizationToken); return new SerializeProcess( progress, sqLiteJsonCacheManager, @@ -85,9 +60,12 @@ public class SerializeProcessFactory( ) { var sqLiteJsonCacheManager = sqLiteJsonCacheManagerFactory.CreateFromStream(streamId); - var serverObjectManager = new ServerObjectManager(speckleHttp, activityFactory, url, streamId, authorizationToken); + var serverObjectManager = serverObjectManagerFactory.Create(url, streamId, authorizationToken); +#pragma warning disable CA2000 + //owned by process, refactor later var objectLoader = new ObjectLoader(sqLiteJsonCacheManager, serverObjectManager, progress); +#pragma warning restore CA2000 return new DeserializeProcess(progress, objectLoader, objectDeserializerFactory, options); } } diff --git a/src/Speckle.Sdk/Serialisation/V2/ServerObjectManagerFactory.cs b/src/Speckle.Sdk/Serialisation/V2/ServerObjectManagerFactory.cs new file mode 100644 index 00000000..071a62b5 --- /dev/null +++ b/src/Speckle.Sdk/Serialisation/V2/ServerObjectManagerFactory.cs @@ -0,0 +1,13 @@ +using Speckle.InterfaceGenerator; +using Speckle.Sdk.Helpers; +using Speckle.Sdk.Logging; + +namespace Speckle.Sdk.Serialisation.V2; + +[GenerateAutoInterface] +public class ServerObjectManagerFactory(ISpeckleHttp speckleHttp, ISdkActivityFactory activityFactory) + : IServerObjectManagerFactory +{ + public IServerObjectManager Create(Uri url, string streamId, string? authorizationToken, int timeoutSeconds = 120) => + new ServerObjectManager(speckleHttp, activityFactory, url, streamId, authorizationToken, timeoutSeconds); +} diff --git a/tests/Speckle.Sdk.Serialization.Testing/Program.cs b/tests/Speckle.Sdk.Serialization.Testing/Program.cs index b5a324f4..77e81387 100644 --- a/tests/Speckle.Sdk.Serialization.Testing/Program.cs +++ b/tests/Speckle.Sdk.Serialization.Testing/Program.cs @@ -3,9 +3,7 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Speckle.Sdk; using Speckle.Sdk.Credentials; -using Speckle.Sdk.Helpers; using Speckle.Sdk.Host; -using Speckle.Sdk.Logging; using Speckle.Sdk.Models; using Speckle.Sdk.Serialisation.V2; using Speckle.Sdk.Serialisation.V2.Receive; @@ -43,12 +41,11 @@ var token = serviceProvider.GetRequiredService().GetDefaultAcco var progress = new Progress(true); var factory = new SerializeProcessFactory( - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService(), new BaseChildFinder(new BasePropertyGatherer()), new ObjectSerializerFactory(new BasePropertyGatherer()), new ObjectDeserializerFactory(), - serviceProvider.GetRequiredService() + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService() ); var process = factory.CreateDeserializeProcess(new Uri(url), streamId, token, progress, new(skipCacheReceive)); var @base = await process.Deserialize(rootId, default).ConfigureAwait(false); diff --git a/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs b/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs index 94a396e2..0448cb4a 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs @@ -529,7 +529,9 @@ public class DummyServerObjectManager : IServerObjectManager public class DummySendCacheManager(Dictionary objects) : ISqLiteJsonCacheManager { - public IEnumerable GetAllObjects() => throw new NotImplementedException(); + public void Dispose() { } + + public IReadOnlyCollection<(string, string)> GetAllObjects() => throw new NotImplementedException(); public void DeleteObject(string id) => throw new NotImplementedException(); diff --git a/tests/Speckle.Sdk.Serialization.Tests/DummySqLiteReceiveManager.cs b/tests/Speckle.Sdk.Serialization.Tests/DummySqLiteReceiveManager.cs index 42961d6b..797eacd9 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/DummySqLiteReceiveManager.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/DummySqLiteReceiveManager.cs @@ -4,7 +4,9 @@ namespace Speckle.Sdk.Serialization.Tests; public class DummySqLiteReceiveManager(Dictionary savedObjects) : ISqLiteJsonCacheManager { - public IEnumerable GetAllObjects() => throw new NotImplementedException(); + public void Dispose() { } + + public IReadOnlyCollection<(string, string)> GetAllObjects() => throw new NotImplementedException(); public void DeleteObject(string id) => throw new NotImplementedException(); diff --git a/tests/Speckle.Sdk.Serialization.Tests/DummySqLiteSendManager.cs b/tests/Speckle.Sdk.Serialization.Tests/DummySqLiteSendManager.cs index ce1dae8c..0b93af62 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/DummySqLiteSendManager.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/DummySqLiteSendManager.cs @@ -14,7 +14,9 @@ public class DummySqLiteSendManager : ISqLiteJsonCacheManager public bool HasObject(string objectId) => throw new NotImplementedException(); - public IEnumerable GetAllObjects() => throw new NotImplementedException(); + public IReadOnlyCollection<(string, string)> GetAllObjects() => throw new NotImplementedException(); public void DeleteObject(string id) => throw new NotImplementedException(); + + public void Dispose() { } } diff --git a/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs b/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs index aaf98d30..076730a2 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/SerializationTests.cs @@ -33,6 +33,8 @@ public class SerializationTests } public string? LoadId(string id) => null; + + public void Dispose() { } } private readonly Assembly _assembly = Assembly.GetExecutingAssembly(); @@ -103,6 +105,8 @@ public class SerializationTests } public string? LoadId(string id) => idToObject.GetValueOrDefault(id); + + public void Dispose() { } } [Test] @@ -154,7 +158,7 @@ public class SerializationTests var fullName = _assembly.GetManifestResourceNames().Single(x => x.EndsWith(fileName)); var json = await ReadJson(fullName); var closures = ReadAsObjects(json); - var process = new DeserializeProcess(null, new TestObjectLoader(closures), new ObjectDeserializerFactory()); + using var process = new DeserializeProcess(null, new TestObjectLoader(closures), new ObjectDeserializerFactory()); await process.Deserialize("3416d3fe01c9196115514c4a2f41617b", default); foreach (var (id, objJson) in closures) { @@ -251,7 +255,7 @@ public class SerializationTests new DummyReceiveServerObjectManager(closure), null ); - var process = new DeserializeProcess(null, o, new ObjectDeserializerFactory(), new(true)); + using var process = new DeserializeProcess(null, o, new ObjectDeserializerFactory(), new(true)); var root = await process.Deserialize(rootId, default); process.BaseCache.Count.ShouldBe(oldCount); process.Total.ShouldBe(oldCount); diff --git a/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs index 6ffc63ea..59fc7b33 100644 --- a/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs +++ b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs @@ -57,7 +57,7 @@ public class GeneralDeserializer : IDisposable null ); var o = new ObjectLoader(sqlite, serverObjects, null); - var process = new DeserializeProcess(null, o, new ObjectDeserializerFactory(), new(skipCache)); + using var process = new DeserializeProcess(null, o, new ObjectDeserializerFactory(), new(skipCache)); return await process.Deserialize(rootId, default).ConfigureAwait(false); } diff --git a/tests/Speckle.Sdk.Tests.Unit/SQLite/SQLiteJsonCacheManagerTests.cs b/tests/Speckle.Sdk.Tests.Unit/SQLite/SQLiteJsonCacheManagerTests.cs new file mode 100644 index 00000000..961729a5 --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Unit/SQLite/SQLiteJsonCacheManagerTests.cs @@ -0,0 +1,91 @@ +using Microsoft.Data.Sqlite; +using NUnit.Framework; +using Shouldly; +using Speckle.Sdk.Common; +using Speckle.Sdk.SQLite; + +namespace Speckle.Sdk.Tests.Unit.SQLite; + +[TestFixture] +public class SQLiteJsonCacheManagerTests +{ + private readonly string _basePath = $"{Guid.NewGuid()}.db"; + private string? _connectionString; + + [SetUp] + public void Setup() => _connectionString = $"Data Source={_basePath};"; + + [TearDown] + public void TearDown() + { + if (File.Exists(_basePath)) + { + SqliteConnection.ClearAllPools(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + File.Delete(_basePath); + } + } + + [Test] + public void TestGetAll() + { + var data = new List<(string id, string json)>() { ("id1", "1"), ("id2", "2") }; + using var manager = new SqLiteJsonCacheManager(_connectionString.NotNull(), 2); + manager.SaveObjects(data); + var items = manager.GetAllObjects(); + items.Count.ShouldBe(data.Count); + var i = items.ToDictionary(); + foreach (var (id, json) in data) + { + i.TryGetValue(id, out var j).ShouldBeTrue(); + j.ShouldBe(json); + } + } + + [Test] + public void TestGet() + { + var data = new List<(string id, string json)>() { ("id1", "1"), ("id2", "2") }; + using var manager = new SqLiteJsonCacheManager(_connectionString.NotNull(), 2); + foreach (var d in data) + { + manager.SaveObject(d.id, d.json); + } + foreach (var d in data) + { + manager.SaveObject(d.id, d.json); + } + var items = manager.GetAllObjects(); + items.Count.ShouldBe(data.Count); + + var id1 = data[0].id; + var json1 = manager.GetObject(id1); + json1.ShouldBe(data[0].json); + manager.HasObject(id1).ShouldBeTrue(); + + manager.UpdateObject(id1, "3"); + json1 = manager.GetObject(id1); + json1.ShouldBe("3"); + manager.HasObject(id1).ShouldBeTrue(); + + manager.DeleteObject(id1); + json1 = manager.GetObject(id1); + json1.ShouldBeNull(); + manager.HasObject(id1).ShouldBeFalse(); + + manager.UpdateObject(id1, "3"); + json1 = manager.GetObject(id1); + json1.ShouldBe("3"); + manager.HasObject(id1).ShouldBeTrue(); + + var id2 = data[1].id; + var json2 = manager.GetObject(id2); + json2.ShouldBe(data[1].json); + manager.HasObject(id2).ShouldBeTrue(); + manager.DeleteObject(id2); + json2 = manager.GetObject(id2); + json2.ShouldBeNull(); + manager.HasObject(id2).ShouldBeFalse(); + } +} diff --git a/tests/Speckle.Sdk.Tests.Unit/Serialisation/BatchTests.cs b/tests/Speckle.Sdk.Tests.Unit/Serialisation/BatchTests.cs index e994288d..a16b1937 100644 --- a/tests/Speckle.Sdk.Tests.Unit/Serialisation/BatchTests.cs +++ b/tests/Speckle.Sdk.Tests.Unit/Serialisation/BatchTests.cs @@ -1,5 +1,6 @@ -using NUnit.Framework; +using NUnit.Framework; using Shouldly; +using Speckle.Sdk.Dependencies; using Speckle.Sdk.Serialisation.V2.Send; namespace Speckle.Sdk.Tests.Unit.Serialisation; @@ -20,7 +21,7 @@ public class BatchTests [Test] public void TestBatchSize_Calc() { - var batch = new Batch(4); + using var batch = new Batch(); batch.Add(new BatchItem(1)); batch.Size.ShouldBe(1); batch.Add(new BatchItem(2)); @@ -30,12 +31,12 @@ public class BatchTests [Test] public void TestBatchSize_Trim() { - var batch = new Batch(4); + using var batch = new Batch(); batch.Add(new BatchItem(1)); batch.Add(new BatchItem(2)); batch.Size.ShouldBe(3); - batch.Items.Capacity.ShouldBe(4); + batch.Items.Capacity.ShouldBe(Pools.DefaultCapacity); batch.TrimExcess(); batch.Items.Capacity.ShouldBe(2);