perf(dbviewer): cache filteredHistory.Count once per export

The DB export loop called filteredHistory.Count twice per 5000-message
batch — once for the progress fraction, once for the status text.
filteredHistory is built lazily by Filter() and re-enumerates on every
.Count access, so on a 2M-message history each batch was paying for
two full passes through the IEnumerable. Materializing the count once
at the top reduces the export to a single O(N) traversal as intended.

The RunOnTick + delayTicks pacing is intentional (keeps SeString
encoding on the framework thread and rate-limits the export to avoid
laggy frames during long exports), so the rest of the loop stays put.
This commit is contained in:
2026-05-03 22:13:53 +02:00
parent 71f0b63079
commit 740c7cf1bb
+6 -2
View File
@@ -391,6 +391,10 @@ public class DbViewer : Window
await rangeMessageEnumerator.DisposeAsync(); await rangeMessageEnumerator.DisposeAsync();
var filteredHistory = Filter(messageHistory); var filteredHistory = Filter(messageHistory);
// Materialize Count once — re-enumerating the IEnumerable on
// every batch (twice per batch in the Notification update)
// turned the export into an O(N²) hot loop on large histories.
var totalCount = filteredHistory.Count;
var sb = new StringBuilder(); var sb = new StringBuilder();
await using var stream = new StreamWriter(Path.Join(InputPath, $"Chat2_{DateTime.Now:yyyy_dd_M__HH_mm_ss}.txt")); await using var stream = new StreamWriter(Path.Join(InputPath, $"Chat2_{DateTime.Now:yyyy_dd_M__HH_mm_ss}.txt"));
@@ -416,8 +420,8 @@ public class DbViewer : Window
} }
}, delayTicks: 5); }, delayTicks: 5);
Notification.Progress = (float)batch / filteredHistory.Count; Notification.Progress = (float)batch / totalCount;
Notification.Content = $"Exported {batch} of {filteredHistory.Count} messages"; Notification.Content = $"Exported {batch} of {totalCount} messages";
await stream.WriteAsync(sb.ToString()); await stream.WriteAsync(sb.ToString());
sb.Clear(); sb.Clear();
} }