diff --git a/src/Nonemm.Session/ContestSession.cs b/src/Nonemm.Session/ContestSession.cs index 822a67f..fa444b8 100644 --- a/src/Nonemm.Session/ContestSession.cs +++ b/src/Nonemm.Session/ContestSession.cs @@ -128,9 +128,12 @@ public sealed class ContestSession && q.TimestampUtc >= fromUtc && q.TimestampUtc < fromUtc + length) .ToList(); - foreach (Qso qso in gone) + using (store.Batch()) { - store.Delete(qso.Id); + foreach (Qso qso in gone) + { + store.Delete(qso.Id); + } } if (gone.Count > 0) { @@ -152,10 +155,13 @@ public sealed class ContestSession { Log.Rescore(); int written = 0; - foreach (Qso qso in Log.Qsos.Where(q => q.TimestampUtc >= fromUtc)) + using (store.Batch()) { - store.Update(qso); - written++; + foreach (Qso qso in Log.Qsos.Where(q => q.TimestampUtc >= fromUtc)) + { + store.Update(qso); + written++; + } } Changed?.Invoke(this, EventArgs.Empty); return written; @@ -216,11 +222,14 @@ public sealed class ContestSession { HashSet held = [.. Log.Qsos.Select(q => q.Id)]; List stored = []; - foreach (Qso qso in arriving) + using (store.Batch()) { - if (held.Add(qso.Id)) + foreach (Qso qso in arriving) { - stored.Add(store.Add(FromNetwork(qso))); + if (held.Add(qso.Id)) + { + stored.Add(store.Add(FromNetwork(qso))); + } } } if (stored.Count == 0) diff --git a/src/Nonemm.Storage/LogStore.cs b/src/Nonemm.Storage/LogStore.cs index 6238c29..489b751 100644 --- a/src/Nonemm.Storage/LogStore.cs +++ b/src/Nonemm.Storage/LogStore.cs @@ -28,4 +28,9 @@ public interface LogStore : IDisposable void Update(Qso qso); void Delete(string id); + + /// Writes made until the returned scope is disposed are committed + /// together. A write on its own is a commit to disk, which makes a burst + /// of a thousand contacts take seconds. + IDisposable Batch(); } diff --git a/src/Nonemm.Storage/SqliteLogStore.cs b/src/Nonemm.Storage/SqliteLogStore.cs index 7bb8aef..e23b581 100644 --- a/src/Nonemm.Storage/SqliteLogStore.cs +++ b/src/Nonemm.Storage/SqliteLogStore.cs @@ -188,6 +188,18 @@ public sealed class SqliteLogStore : LogStore public void Dispose() => connection.Dispose(); + public IDisposable Batch() => new CommitOnDispose(connection.BeginTransaction()); + + /// Disposing a `SqliteTransaction` rolls it back, so this commits first. + private sealed class CommitOnDispose(SqliteTransaction transaction) : IDisposable + { + public void Dispose() + { + transaction.Commit(); + transaction.Dispose(); + } + } + private void Insert(Qso qso) { using SqliteCommand command = connection.CreateCommand(); diff --git a/tests/Nonemm.Session.Tests/FakeLogStore.cs b/tests/Nonemm.Session.Tests/FakeLogStore.cs index 6670fb3..618b76d 100644 --- a/tests/Nonemm.Session.Tests/FakeLogStore.cs +++ b/tests/Nonemm.Session.Tests/FakeLogStore.cs @@ -57,7 +57,16 @@ public sealed class FakeLogStore : LogStore public void Delete(string id) => qsos.RemoveAll(q => q.Id == id); + public IDisposable Batch() => new NoBatch(); + public void Dispose() { } + + private sealed class NoBatch : IDisposable + { + public void Dispose() + { + } + } }