From 5bc64f6321b547794d0bf4a31ae4e942e9eba888 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 19:11:56 +0000 Subject: [PATCH 1/2] enhance: generate commit graph off the UI thread `CommitGraph.Generate` for up to `MaxHistoryCommits` (20000 by default) commits used to run on the UI thread after every history refresh (and twice when graph highlighting depends on selection), and again on every selection change when highlighting selected commits only. - `Repository.RefreshCommits` now builds the graph in its background task and publishes commits and graph together via `Histories.SetCommits`, so the list and the graph always describe the same commits - Selection-dependent regeneration runs in the background; stale results are dropped and the previous graph stays visible until the new one is ready - `CommitGraph.Generate` no longer sets `Commit.IsHighlightedInGraph` (an observable property bound by visible rows); it is applied from the generated dots on the UI thread instead Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KCxH1zwDXLd9kfAAkcf6PZ --- src/Models/CommitGraph.cs | 1 - src/ViewModels/Histories.cs | 71 +++++++++++++++++++++++++++--------- src/ViewModels/Repository.cs | 7 +++- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 916a79b1bb..27f1ac43bd 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -174,7 +174,6 @@ public static CommitGraph Generate(List commits, bool firstParentOnlyEna break; } } - commit.IsHighlightedInGraph = isHighlighted; // If no path found, create new curve for branch head // Otherwise, create new curve for new merged commit diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index a22f810cdb..8b1ff427b6 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; @@ -110,12 +111,6 @@ public bool IsCommitTimeColumnVisible public List Commits { get => _commits; - set - { - GenerateGraph(value); - if (SetProperty(ref _commits, value)) - PostCommitsChanged(); - } } public Models.CommitGraph Graph @@ -132,7 +127,7 @@ public Models.CommitGraphHighlighting GraphHighlighting if (_repo.UIStates.GraphHighlighting != value) { _repo.UIStates.GraphHighlighting = value; - GenerateGraph(_commits); + RefreshGraph(); } } } @@ -254,6 +249,32 @@ public void ToggleShowFlag(Models.HistoryShowFlags flag) _repo.RefreshCommits(); } + public void SetCommits(List commits, Models.CommitGraph graph) + { + if (_cancellationRefreshGraph is { IsCancellationRequested: false }) + _cancellationRefreshGraph.Cancel(); + + ApplyGraph(commits, graph); + + if (SetProperty(ref _commits, commits, nameof(Commits))) + PostCommitsChanged(); + } + + public Models.CommitGraph GenerateGraph(List commits) + { + var firstParentOnly = _repo.UIStates.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly); + var highlighting = _repo.UIStates.GraphHighlighting; + var extraHeads = new HashSet(); + + if (highlighting >= Models.CommitGraphHighlighting.SelectedCommitsOnly) + { + foreach (var c in _selectedCommits) + extraHeads.Add(c.SHA); + } + + return Models.CommitGraph.Generate(commits, firstParentOnly, highlighting, extraHeads); + } + public Models.BisectState UpdateBisectInfo() { var test = Path.Combine(_repo.GitDir, "BISECT_START"); @@ -564,22 +585,37 @@ private void PostSelectedCommitsChanged() } if (_repo.UIStates.GraphHighlighting >= Models.CommitGraphHighlighting.SelectedCommitsOnly) - GenerateGraph(_commits); + RefreshGraph(); } - private void GenerateGraph(List commits) + private void RefreshGraph() { - var firstParentOnly = _repo.UIStates.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly); - var highlighting = _repo.UIStates.GraphHighlighting; - var extraHeads = new HashSet(); + if (_cancellationRefreshGraph is { IsCancellationRequested: false }) + _cancellationRefreshGraph.Cancel(); - if (highlighting >= Models.CommitGraphHighlighting.SelectedCommitsOnly) + _cancellationRefreshGraph = new CancellationTokenSource(); + var token = _cancellationRefreshGraph.Token; + var commits = _commits; + + Task.Run(() => { - foreach (var c in _selectedCommits) - extraHeads.Add(c.SHA); - } + var graph = GenerateGraph(commits); + Dispatcher.UIThread.Post(() => + { + if (!token.IsCancellationRequested) + ApplyGraph(commits, graph); + }); + }, token); + } + + private void ApplyGraph(List commits, Models.CommitGraph graph) + { + // There is exactly one dot for each commit (in the same order). Update `IsHighlightedInGraph` here + // so that the property-changed notifications of displayed commits are raised on the UI thread. + for (var i = 0; i < commits.Count; i++) + commits[i].IsHighlightedInGraph = graph.Dots[i].IsHighlighted; - Graph = Models.CommitGraph.Generate(commits, firstParentOnly, highlighting, extraHeads); + Graph = graph; } private Repository _repo = null; @@ -589,6 +625,7 @@ private void GenerateGraph(List commits) private bool _isLoading = true; private List _commits = []; private Models.CommitGraph _graph = null; + private CancellationTokenSource _cancellationRefreshGraph = null; private List _selectedCommits = []; private Models.Bisect _bisect = null; private object _detailContext = Models.Null.Instance; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 4d3b5541c9..6f1d04c51b 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1292,6 +1292,11 @@ public void RefreshCommits() } } + if (token.IsCancellationRequested) + return; + + var graph = _histories.GenerateGraph(commits); + Dispatcher.UIThread.Invoke(() => { if (token.IsCancellationRequested) @@ -1300,7 +1305,7 @@ public void RefreshCommits() if (_histories != null) { _histories.IsLoading = false; - _histories.Commits = commits; + _histories.SetCommits(commits, graph); BisectState = _histories.UpdateBisectInfo(); if (!string.IsNullOrEmpty(_navigateToCommitDelayed)) From e3aa5b0c471ec1674babcd89d078d3902e6bf9fb Mon Sep 17 00:00:00 2001 From: Bert Verhelst Date: Mon, 9 Nov 2026 08:36:00 +0100 Subject: [PATCH 2/2] fix: do not read selected commits from background threads when generating the graph - Collect the selected commits used as extra graph heads on the UI thread before starting background work - Regenerate the graph if the selection changed while a history refresh was running - Guard against `_histories` being null in `Repository.RefreshCommits` Co-Authored-By: Claude Opus 5.5 --- src/ViewModels/Histories.cs | 29 ++++++++++++++++++++++------- src/ViewModels/Repository.cs | 17 +++++++++++++---- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 8b1ff427b6..b74e98784a 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -249,7 +249,7 @@ public void ToggleShowFlag(Models.HistoryShowFlags flag) _repo.RefreshCommits(); } - public void SetCommits(List commits, Models.CommitGraph graph) + public void SetCommits(List commits, Models.CommitGraph graph, HashSet graphExtraHeads) { if (_cancellationRefreshGraph is { IsCancellationRequested: false }) _cancellationRefreshGraph.Cancel(); @@ -258,20 +258,34 @@ public void SetCommits(List commits, Models.CommitGraph graph) if (SetProperty(ref _commits, commits, nameof(Commits))) PostCommitsChanged(); + + // The selection may have changed while the graph was generated in background. + if (!graphExtraHeads.SetEquals(CollectGraphExtraHeads())) + RefreshGraph(); } - public Models.CommitGraph GenerateGraph(List commits) + /// + /// Must be called on the UI thread: `_selectedCommits` is only modified there. + /// + public HashSet CollectGraphExtraHeads() { - var firstParentOnly = _repo.UIStates.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly); - var highlighting = _repo.UIStates.GraphHighlighting; var extraHeads = new HashSet(); - - if (highlighting >= Models.CommitGraphHighlighting.SelectedCommitsOnly) + if (_repo.UIStates.GraphHighlighting >= Models.CommitGraphHighlighting.SelectedCommitsOnly) { foreach (var c in _selectedCommits) extraHeads.Add(c.SHA); } + return extraHeads; + } + + /// + /// Thread-safe as long as `extraHeads` is not shared with the UI thread. + /// + public Models.CommitGraph GenerateGraph(List commits, HashSet extraHeads) + { + var firstParentOnly = _repo.UIStates.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly); + var highlighting = _repo.UIStates.GraphHighlighting; return Models.CommitGraph.Generate(commits, firstParentOnly, highlighting, extraHeads); } @@ -596,10 +610,11 @@ private void RefreshGraph() _cancellationRefreshGraph = new CancellationTokenSource(); var token = _cancellationRefreshGraph.Token; var commits = _commits; + var extraHeads = CollectGraphExtraHeads(); Task.Run(() => { - var graph = GenerateGraph(commits); + var graph = GenerateGraph(commits, extraHeads); Dispatcher.UIThread.Post(() => { if (!token.IsCancellationRequested) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 6f1d04c51b..2b8c79525c 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1255,7 +1255,15 @@ public void RefreshCommits() Task.Run(async () => { - await Dispatcher.UIThread.InvokeAsync(() => _histories.IsLoading = true); + var graphExtraHeads = new HashSet(); + await Dispatcher.UIThread.InvokeAsync(() => + { + if (_histories == null) + return; + + _histories.IsLoading = true; + graphExtraHeads = _histories.CollectGraphExtraHeads(); + }); var builder = new StringBuilder(); builder @@ -1292,10 +1300,11 @@ public void RefreshCommits() } } - if (token.IsCancellationRequested) + var histories = _histories; + if (token.IsCancellationRequested || histories == null) return; - var graph = _histories.GenerateGraph(commits); + var graph = histories.GenerateGraph(commits, graphExtraHeads); Dispatcher.UIThread.Invoke(() => { @@ -1305,7 +1314,7 @@ public void RefreshCommits() if (_histories != null) { _histories.IsLoading = false; - _histories.SetCommits(commits, graph); + _histories.SetCommits(commits, graph, graphExtraHeads); BisectState = _histories.UpdateBisectInfo(); if (!string.IsNullOrEmpty(_navigateToCommitDelayed))