fix(ui): stop the reconcile tick on native close, index rows in the tick
Two follow-ups to the Phase 3 reconcile tick: - LogVisualizer and WorktreesOverview only stopped their timer in the Close command, but ShowLogVisualizerAsync/ShowWorktreesOverviewAsync had no dlg.Closed fallback (unlike the MergeHelper picker). An Alt+F4 or owner close left a 4s timer polling the worker / re-running LoadAsync on an orphaned VM, accumulating one per open. Extracted StopReconcileTick on both VMs and wired it from Closed. - The tick scanned Items linearly per id; at the 500-row cap that is ~250k string comparisons every few seconds. Index the rows once instead.
This commit is contained in:
@@ -1298,6 +1298,10 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
if (ReconcileTickTestBarrier is { } barrier) await barrier();
|
if (ReconcileTickTestBarrier is { } barrier) await barrier();
|
||||||
|
|
||||||
var byId = entities.ToDictionary(e => e.Id);
|
var byId = entities.ToDictionary(e => e.Id);
|
||||||
|
// Index the rows once instead of scanning Items per id — at the 500-row cap a linear
|
||||||
|
// scan per id is 250k comparisons every few seconds, for nothing.
|
||||||
|
var rowById = new Dictionary<string, TaskRowViewModel>(Items.Count);
|
||||||
|
foreach (var r in Items) rowById[r.Id] = r;
|
||||||
foreach (var id in ids)
|
foreach (var id in ids)
|
||||||
{
|
{
|
||||||
// Superseded by a fresher delta refresh or a later tick that landed while this one
|
// Superseded by a fresher delta refresh or a later tick that landed while this one
|
||||||
@@ -1305,8 +1309,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
if (!_deltaSeq.TryGetValue(id, out var current) || current != seqByTaskId[id]) continue;
|
if (!_deltaSeq.TryGetValue(id, out var current) || current != seqByTaskId[id]) continue;
|
||||||
if (!byId.TryGetValue(id, out var entity)) continue; // deleted; the delta path removes rows, not the tick
|
if (!byId.TryGetValue(id, out var entity)) continue; // deleted; the delta path removes rows, not the tick
|
||||||
|
|
||||||
var row = Items.FirstOrDefault(r => r.Id == id);
|
if (rowById.TryGetValue(id, out var row)) row.UpdateFromEntity(entity);
|
||||||
row?.UpdateFromEntity(entity);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,10 +75,18 @@ public sealed partial class LogVisualizerViewModel : ViewModelBase
|
|||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void Close()
|
private void Close()
|
||||||
|
{
|
||||||
|
StopReconcileTick();
|
||||||
|
CloseAction?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stops the reconcile tick. Also called from the dialog's native-close fallback
|
||||||
|
/// (WindowDialogService), which bypasses <see cref="CloseCommand"/> — otherwise an Alt+F4
|
||||||
|
/// leaves the timer polling the worker on an orphaned VM. Idempotent.</summary>
|
||||||
|
internal void StopReconcileTick()
|
||||||
{
|
{
|
||||||
_reconcileTimer.Stop();
|
_reconcileTimer.Stop();
|
||||||
_reconcileTimer.Dispose();
|
_reconcileTimer.Dispose();
|
||||||
CloseAction?.Invoke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanCopyLast() => Rows.Count > 0;
|
private bool CanCopyLast() => Rows.Count > 0;
|
||||||
|
|||||||
@@ -192,10 +192,18 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
|||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void Close()
|
private void Close()
|
||||||
|
{
|
||||||
|
StopReconcileTick();
|
||||||
|
CloseAction?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stops the reconcile tick. Also called from the dialog's native-close fallback
|
||||||
|
/// (WindowDialogService), which bypasses <see cref="CloseCommand"/> — otherwise an Alt+F4
|
||||||
|
/// leaves the timer re-running LoadAsync (git + DB) on an orphaned VM. Idempotent.</summary>
|
||||||
|
internal void StopReconcileTick()
|
||||||
{
|
{
|
||||||
_reconcileTimer.Stop();
|
_reconcileTimer.Stop();
|
||||||
_reconcileTimer.Dispose();
|
_reconcileTimer.Dispose();
|
||||||
CloseAction?.Invoke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
{
|
{
|
||||||
var dlg = new WorktreesOverviewModalView { DataContext = vm };
|
var dlg = new WorktreesOverviewModalView { DataContext = vm };
|
||||||
vm.CloseAction = () => dlg.Close();
|
vm.CloseAction = () => dlg.Close();
|
||||||
|
dlg.Closed += (_, _) => vm.StopReconcileTick(); // native close bypasses CloseCommand
|
||||||
vm.JumpToTaskAction = (listId, taskId) =>
|
vm.JumpToTaskAction = (listId, taskId) =>
|
||||||
{
|
{
|
||||||
if (Shell is { } s) _ = JumpToTaskHelper.SelectAsync(s, listId, taskId);
|
if (Shell is { } s) _ = JumpToTaskHelper.SelectAsync(s, listId, taskId);
|
||||||
@@ -161,6 +162,7 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
{
|
{
|
||||||
var dlg = new LogVisualizerView { DataContext = vm };
|
var dlg = new LogVisualizerView { DataContext = vm };
|
||||||
vm.CloseAction = () => dlg.Close();
|
vm.CloseAction = () => dlg.Close();
|
||||||
|
dlg.Closed += (_, _) => vm.StopReconcileTick(); // native close bypasses CloseCommand
|
||||||
await dlg.ShowDialog(_owner);
|
await dlg.ShowDialog(_owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user