fix(ui): offload DiffAlignment.Build off the UI thread above a line threshold
DiffTextView.ReloadFile ran DiffAlignment.Build synchronously for both the constructor and the File-property path, so a large diff (or several at once in Planning mode's per-file ItemsControl) could freeze the UI thread. Diffs at or under 500 raw lines still build inline (no placeholder frame); larger ones build via Task.Run and a generation counter discards the result if File changed again before the build finished.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
@@ -60,6 +61,24 @@ public partial class DiffTextView : UserControl
|
||||
|
||||
private bool _renderersInstalled;
|
||||
|
||||
// Above this many raw diff lines, DiffAlignment.Build moves to a background thread instead
|
||||
// of running inline in ReloadFile. Build is O(lines) plus one word-diff LCS per 1:1 changed
|
||||
// pair, and each LCS is up to MaxWordDiffTokens² (400² = 160k int cells, ~640KB) — a diff
|
||||
// this size can plausibly stack up enough such pairs to blow a 16ms frame, and Planning mode
|
||||
// instantiates one DiffTextView per changed file in an ItemsControl, so several can hit this
|
||||
// at once. A line-count threshold was chosen over chunked/incremental alignment because Build
|
||||
// is already pure and side-effect-free (safe to run on Task.Run as-is), whereas incremental
|
||||
// construction would need to fragment its LCS/gap-detection state across calls for no benefit
|
||||
// to the common case. Below the threshold, staying synchronous avoids a placeholder frame
|
||||
// (there is nothing to show until the first build completes) for the overwhelming majority of
|
||||
// diffs, which finish well under a frame either way.
|
||||
private const int AsyncBuildLineThreshold = 500;
|
||||
|
||||
// Bumped by every ReloadFile call; a background build only applies its result if this is
|
||||
// still the generation that started it, so a fast File-to-File switch during a slow build
|
||||
// can't overwrite the newer file's rows with a stale AlignedDiff.
|
||||
private int _buildGeneration;
|
||||
|
||||
public DiffTextView()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -97,9 +116,29 @@ public partial class DiffTextView : UserControl
|
||||
|
||||
/// Recomputes the alignment (word-diff LCS) for the current <see cref="File"/> and then
|
||||
/// refreshes everything downstream of it. Only ever needed when the file actually changes.
|
||||
/// Small diffs build inline; large ones move to a background thread (see
|
||||
/// <see cref="AsyncBuildLineThreshold"/>) and keep showing the previous file's rows until
|
||||
/// the new build lands, rather than flashing an empty frame.
|
||||
private void ReloadFile()
|
||||
{
|
||||
_aligned = DiffAlignment.Build(File?.Lines);
|
||||
var lines = File?.Lines;
|
||||
var generation = ++_buildGeneration;
|
||||
|
||||
if (lines is null || lines.Count <= AsyncBuildLineThreshold)
|
||||
{
|
||||
_aligned = DiffAlignment.Build(lines);
|
||||
RefreshLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
_ = BuildAsync(lines, generation);
|
||||
}
|
||||
|
||||
private async Task BuildAsync(IReadOnlyList<DiffLineViewModel> lines, int generation)
|
||||
{
|
||||
var aligned = await Task.Run(() => DiffAlignment.Build(lines));
|
||||
if (generation != _buildGeneration) return; // superseded by a newer File in the meantime.
|
||||
_aligned = aligned;
|
||||
RefreshLayout();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user