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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
@@ -60,6 +61,24 @@ public partial class DiffTextView : UserControl
|
|||||||
|
|
||||||
private bool _renderersInstalled;
|
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()
|
public DiffTextView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -97,9 +116,29 @@ public partial class DiffTextView : UserControl
|
|||||||
|
|
||||||
/// Recomputes the alignment (word-diff LCS) for the current <see cref="File"/> and then
|
/// 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.
|
/// 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()
|
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();
|
RefreshLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user