chore(claude-do): merge [B2] DiffAlignment.Build — begründete Grenze gegen unbegrenz

ClaudeDo-Task: ee8117fa-283a-4470-a823-ce57f52a40be
This commit is contained in:
Mika Kuns
2026-08-12 14:11:41 +02:00
@@ -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();
}