Commit resync writes in one transaction

Each insert, delete and rescore update was its own commit to disk, so a
resync of about a thousand QSOs froze the window thread. LogStore has a
new Batch() scope that commits its writes together. ContestSession uses
it in AddRangeFromNetwork, ClearFromNetwork and Rescore.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 11:01:05 +00:00
co-authored by Claude Sonnet 5.5
parent 36af4340cf
commit 826aa0ef97
4 changed files with 43 additions and 8 deletions
+9
View File
@@ -128,10 +128,13 @@ public sealed class ContestSession
&& q.TimestampUtc >= fromUtc && q.TimestampUtc >= fromUtc
&& q.TimestampUtc < fromUtc + length) && q.TimestampUtc < fromUtc + length)
.ToList(); .ToList();
using (store.Batch())
{
foreach (Qso qso in gone) foreach (Qso qso in gone)
{ {
store.Delete(qso.Id); store.Delete(qso.Id);
} }
}
if (gone.Count > 0) if (gone.Count > 0)
{ {
Log.RemoveRange([.. gone.Select(q => q.Id)]); Log.RemoveRange([.. gone.Select(q => q.Id)]);
@@ -152,11 +155,14 @@ public sealed class ContestSession
{ {
Log.Rescore(); Log.Rescore();
int written = 0; int written = 0;
using (store.Batch())
{
foreach (Qso qso in Log.Qsos.Where(q => q.TimestampUtc >= fromUtc)) foreach (Qso qso in Log.Qsos.Where(q => q.TimestampUtc >= fromUtc))
{ {
store.Update(qso); store.Update(qso);
written++; written++;
} }
}
Changed?.Invoke(this, EventArgs.Empty); Changed?.Invoke(this, EventArgs.Empty);
return written; return written;
} }
@@ -216,6 +222,8 @@ public sealed class ContestSession
{ {
HashSet<string> held = [.. Log.Qsos.Select(q => q.Id)]; HashSet<string> held = [.. Log.Qsos.Select(q => q.Id)];
List<Qso> stored = []; List<Qso> stored = [];
using (store.Batch())
{
foreach (Qso qso in arriving) foreach (Qso qso in arriving)
{ {
if (held.Add(qso.Id)) if (held.Add(qso.Id))
@@ -223,6 +231,7 @@ public sealed class ContestSession
stored.Add(store.Add(FromNetwork(qso))); stored.Add(store.Add(FromNetwork(qso)));
} }
} }
}
if (stored.Count == 0) if (stored.Count == 0)
{ {
return 0; return 0;
+5
View File
@@ -28,4 +28,9 @@ public interface LogStore : IDisposable
void Update(Qso qso); void Update(Qso qso);
void Delete(string id); 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();
} }
+12
View File
@@ -188,6 +188,18 @@ public sealed class SqliteLogStore : LogStore
public void Dispose() => connection.Dispose(); 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) private void Insert(Qso qso)
{ {
using SqliteCommand command = connection.CreateCommand(); using SqliteCommand command = connection.CreateCommand();
@@ -57,7 +57,16 @@ public sealed class FakeLogStore : LogStore
public void Delete(string id) => qsos.RemoveAll(q => q.Id == id); public void Delete(string id) => qsos.RemoveAll(q => q.Id == id);
public IDisposable Batch() => new NoBatch();
public void Dispose() public void Dispose()
{ {
} }
private sealed class NoBatch : IDisposable
{
public void Dispose()
{
}
}
} }