Cut CPU use and move heavy work off the UI thread
Redraws: - Merge bursts of session and bandmap events into one redraw (Coalesced). Closed Bandmap and Available windows now unsubscribe. - Grayline and Voice Keyer no longer subscribe to the session twice. - Available summary, Score table and Log rows redraw only when their contents change. Telnet lines are added in batches. - Bandmap view reuses its layout, shaped text and brushes. Grayline draws the coastlines as one cached shape. Threads: - Available, Bandmap colours, Check and Statistics do their judging and counting on the thread pool and draw the result on the UI thread. - ContestLog takes a lock in Judge and in each method that changes the log, so a background Judge cannot read the indexes mid-edit. Refresh rate: - Statistics refreshes every 30 seconds and when the QSO count changes. - Grayline refreshes at most every 15 seconds on session events. Publish with ReadyToRun to cut startup JIT time. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,12 @@ public sealed class ContestLog
|
||||
private readonly Dictionary<ScoreKey, ScoreTally> rows = [];
|
||||
private ScoreTally tally = new();
|
||||
|
||||
/// `Judge` is called from a background thread by the windows that judge
|
||||
/// every spot, while the UI thread adds and edits contacts. Both take this
|
||||
/// lock around the indexes. The lock can be entered again by the thread
|
||||
/// that holds it, because a rebuild judges each contact.
|
||||
private readonly Lock guard = new();
|
||||
|
||||
public ContestLog(
|
||||
Contest contest,
|
||||
StationInfo me,
|
||||
@@ -67,6 +73,14 @@ public sealed class ContestLog
|
||||
|
||||
/// What logging this contact right now would do, without logging it.
|
||||
public Verdict Judge(Qso candidate)
|
||||
{
|
||||
lock (guard)
|
||||
{
|
||||
return JudgeLocked(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
private Verdict JudgeLocked(Qso candidate)
|
||||
{
|
||||
if (!candidate.IsClaimed)
|
||||
{
|
||||
@@ -103,19 +117,25 @@ public sealed class ContestLog
|
||||
/// Adds the contact and returns it with points and multiplier flags filled in.
|
||||
public Qso Add(Qso qso)
|
||||
{
|
||||
Verdict verdict = Judge(qso);
|
||||
Qso scored = ApplyVerdict(qso, verdict);
|
||||
qsos.Add(scored);
|
||||
Index(scored, verdict);
|
||||
return scored;
|
||||
lock (guard)
|
||||
{
|
||||
Verdict verdict = Judge(qso);
|
||||
Qso scored = ApplyVerdict(qso, verdict);
|
||||
qsos.Add(scored);
|
||||
Index(scored, verdict);
|
||||
return scored;
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads contacts read back from storage. Points and multiplier flags in
|
||||
/// the stored rows are ignored and worked out again from the rules.
|
||||
public void Restore(IEnumerable<Qso> stored)
|
||||
{
|
||||
qsos.AddRange(stored);
|
||||
Rebuild();
|
||||
lock (guard)
|
||||
{
|
||||
qsos.AddRange(stored);
|
||||
Rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a contact that may be older than ones already logged, which is what
|
||||
@@ -123,41 +143,56 @@ public sealed class ContestLog
|
||||
/// again: the earlier of two contacts is the one that claims a multiplier.
|
||||
public void Insert(Qso qso)
|
||||
{
|
||||
qsos.Add(qso);
|
||||
Rebuild();
|
||||
lock (guard)
|
||||
{
|
||||
qsos.Add(qso);
|
||||
Rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
/// `Insert` for several contacts, scoring the log once instead of once per
|
||||
/// contact. A resync sends a few hundred in a burst.
|
||||
public void InsertRange(IEnumerable<Qso> arrived)
|
||||
{
|
||||
qsos.AddRange(arrived);
|
||||
Rebuild();
|
||||
lock (guard)
|
||||
{
|
||||
qsos.AddRange(arrived);
|
||||
Rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(string id)
|
||||
{
|
||||
qsos.RemoveAll(q => q.Id == id);
|
||||
Rebuild();
|
||||
lock (guard)
|
||||
{
|
||||
qsos.RemoveAll(q => q.Id == id);
|
||||
Rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
/// `Remove` for several contacts, scoring the log once.
|
||||
public void RemoveRange(IReadOnlyCollection<string> ids)
|
||||
{
|
||||
HashSet<string> gone = [.. ids];
|
||||
qsos.RemoveAll(q => gone.Contains(q.Id));
|
||||
Rebuild();
|
||||
lock (guard)
|
||||
{
|
||||
qsos.RemoveAll(q => gone.Contains(q.Id));
|
||||
Rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
public void Replace(Qso qso)
|
||||
{
|
||||
int at = qsos.FindIndex(q => q.Id == qso.Id);
|
||||
if (at < 0)
|
||||
lock (guard)
|
||||
{
|
||||
throw new InvalidOperationException($"no contact with id {qso.Id} in the log");
|
||||
int at = qsos.FindIndex(q => q.Id == qso.Id);
|
||||
if (at < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"no contact with id {qso.Id} in the log");
|
||||
}
|
||||
qsos[at] = qso;
|
||||
Rebuild();
|
||||
}
|
||||
qsos[at] = qso;
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
/// A station worked before, under the contest's dupe rule. A contest with
|
||||
@@ -284,7 +319,13 @@ public sealed class ContestLog
|
||||
/// rescores what it touches — but the stored points and multiplier flags
|
||||
/// are written from this, and a caller that is about to write them asks for
|
||||
/// a pass first.
|
||||
public void Rescore() => Rebuild();
|
||||
public void Rescore()
|
||||
{
|
||||
lock (guard)
|
||||
{
|
||||
Rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
/// Rescores the whole log. A removed or edited contact can hand its
|
||||
/// multiplier to a later one, which only a full pass gets right.
|
||||
|
||||
Reference in New Issue
Block a user