From fb4bcd6dcafd1022934c5f3381f55a4c955e4fdc Mon Sep 17 00:00:00 2001 From: Michael Seibt Date: Wed, 17 Feb 2021 22:18:02 +0100 Subject: [PATCH] Improve RevisionDataGridView responsiveness (cherry picked from commit 4bbe805e6c3e06c37e8aa6a3bf9db6b4360a63d3) --- .../RevisionDataGridView.BackgroundUpdater.cs | 68 +++++ .../RevisionGrid/RevisionDataGridView.cs | 251 +++++++----------- 2 files changed, 164 insertions(+), 155 deletions(-) create mode 100644 GitUI/UserControls/RevisionGrid/RevisionDataGridView.BackgroundUpdater.cs diff --git a/GitUI/UserControls/RevisionGrid/RevisionDataGridView.BackgroundUpdater.cs b/GitUI/UserControls/RevisionGrid/RevisionDataGridView.BackgroundUpdater.cs new file mode 100644 index 00000000000..7de04aa852b --- /dev/null +++ b/GitUI/UserControls/RevisionGrid/RevisionDataGridView.BackgroundUpdater.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; + +namespace GitUI.UserControls.RevisionGrid +{ + public sealed partial class RevisionDataGridView + { + /// + /// Coordinates background update executions. Requested reruns only "queue up" upto one. + /// + private class BackgroundUpdater + { + private readonly Func _operation; + private readonly int _cooldownMilliseconds; + private readonly object _sync = new(); + + private volatile bool _executing; + private volatile bool _rerunRequested; + + public BackgroundUpdater(Func operation, int cooldownMilliseconds) + { + _operation = operation ?? throw new ArgumentNullException(nameof(operation)); + _cooldownMilliseconds = cooldownMilliseconds; + } + + public void ScheduleExcecution() + { + lock (_sync) + { + if (!_executing) + { + // if not running, start it + _executing = true; + Task.Run(WrappedOperationAsync); + } + else + { + // if currently running make sure it runs again + _rerunRequested = true; + } + } + } + + private async Task WrappedOperationAsync() + { + await _operation(); + + if (_rerunRequested) + { + await Task.Delay(_cooldownMilliseconds); + } + + lock (_sync) + { + if (_rerunRequested) + { + Task.Run(WrappedOperationAsync); + _rerunRequested = false; + } + else + { + _executing = false; + } + } + } + } + } +} diff --git a/GitUI/UserControls/RevisionGrid/RevisionDataGridView.cs b/GitUI/UserControls/RevisionGrid/RevisionDataGridView.cs index 9253ad0f62f..f8220b157d6 100644 --- a/GitUI/UserControls/RevisionGrid/RevisionDataGridView.cs +++ b/GitUI/UserControls/RevisionGrid/RevisionDataGridView.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Drawing; using System.Linq; -using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using GitCommands; @@ -15,7 +14,6 @@ using GitUI.UserControls.RevisionGrid.Columns; using GitUI.UserControls.RevisionGrid.Graph; using GitUIPluginInterfaces; -using Microsoft.VisualStudio.Threading; namespace GitUI.UserControls.RevisionGrid { @@ -28,39 +26,36 @@ public enum RevisionNodeFlags OnlyFirstParent = 4 } - public sealed class RevisionDataGridView : DataGridView + public sealed partial class RevisionDataGridView : DataGridView { private readonly SolidBrush _alternatingRowBackgroundBrush; private readonly SolidBrush _authoredHighlightBrush; + private readonly BackgroundUpdater _backgroundUpdater; + private readonly Stopwatch _lastRepaint = Stopwatch.StartNew(); internal RevisionGraph _revisionGraph = new(); private readonly List _columnProviders = new(); - private readonly CancellationTokenSequence _backgroundCancellationSequence; - private readonly AsyncQueue<(Func backgroundOperation, CancellationToken cancellationToken)> _backgroundQueue = - new(); - private CancellationToken _backgroundCancellationToken; - private JoinableTask? _backgroundProcessingTask; + private int _backgroundScrollTo; + private int _consecutiveScrollMessageCnt = 0; // Is used to detect if a forced repaint is needed. private int _rowHeight; // Height of elements in the cache. Is equal to the control's row height. private VisibleRowRange _visibleRowRange; - private readonly Font _normalFont; - private readonly Font _boldFont; - private readonly Font _monospaceFont; + private Font _normalFont; + private Font _boldFont; + private Font _monospaceFont; public bool UpdatingVisibleRows { get; private set; } +#pragma warning disable CS8618 // The analyzer does not get that the fonts _are_ assigned: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public RevisionDataGridView() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. { - _backgroundCancellationSequence = new CancellationTokenSequence(); - _backgroundCancellationToken = _backgroundCancellationSequence.Next(); - StartBackgroundProcessingTask(_backgroundCancellationToken); + InitFonts(); - _normalFont = AppSettings.Font; - _boldFont = new Font(AppSettings.Font, FontStyle.Bold); - _monospaceFont = AppSettings.MonospaceFont; + _backgroundUpdater = new BackgroundUpdater(UpdateVisibleRowRangeInternalAsync, 25); InitializeComponent(); DoubleBuffered = true; @@ -78,8 +73,9 @@ public RevisionDataGridView() provider.OnColumnWidthChanged(e); } }; - Scroll += delegate { UpdateVisibleRowRange(); }; - Resize += delegate { UpdateVisibleRowRange(); }; + + Scroll += (_, _) => UpdateVisibleRowRange(); + Resize += (_, _) => UpdateVisibleRowRange(); GotFocus += (_, _) => InvalidateSelectedRows(); LostFocus += (_, _) => InvalidateSelectedRows(); CellPainting += OnCellPainting; @@ -158,14 +154,6 @@ void InvalidateSelectedRows() protected override void Dispose(bool disposing) { - if (disposing) - { - // Make sure to mark the background queue as complete before disposing the cancellation token sequence. - _backgroundQueue.Complete(); - _backgroundCancellationSequence.Dispose(); - _backgroundProcessingTask?.Join(); - } - base.Dispose(disposing); } @@ -272,6 +260,8 @@ private Brush GetBackground(DataGridViewElementStates state, int rowIndex, GitRe private void OnCellPainting(object sender, DataGridViewCellPaintingEventArgs e) { + _lastRepaint.Restart(); + Debug.Assert(_rowHeight != 0, "_rowHeight != 0"); var revision = GetRevision(e.RowIndex); @@ -322,10 +312,6 @@ public void Clear() { _backgroundScrollTo = 0; - // Cancel all outstanding background operations, and provide a new cancellation token for future work. - var cancellationToken = _backgroundCancellationToken = _backgroundCancellationSequence.Next(); - _backgroundProcessingTask?.Join(); - // Set rowcount to 0 first, to ensure it is not possible to select or redraw, since we are about te delete the data SetRowCount(0); _revisionGraph.Clear(); @@ -340,8 +326,6 @@ public void Clear() // Redraw UpdateVisibleRowRange(); Invalidate(invalidateChildren: true); - - StartBackgroundProcessingTask(cancellationToken); } public void LoadingCompleted() @@ -357,16 +341,8 @@ public void LoadingCompleted() /// /// The hash to find. /// , if the given hash if found; otherwise . - /// is . public bool Contains(ObjectId objectId) => _revisionGraph.Contains(objectId); - private void StartBackgroundProcessingTask(CancellationToken cancellationToken) - { - // Start the background processing via JoinableTaskContext.Factory to avoid tracking the long-running - // operation in JoinPendingOperationsAsync. - _backgroundProcessingTask = ThreadHelper.JoinableTaskContext.Factory.RunAsync(() => RunBackgroundAsync(cancellationToken)); - } - public bool RowIsRelative(int rowIndex) { return _revisionGraph.IsRowRelative(rowIndex); @@ -451,7 +427,7 @@ private void SetRowCountAndSelectRowsIfReady(int rowCount) SelectRowsIfReady(rowCount); } - private async Task RunBackgroundAsync(CancellationToken cancellationToken) + private void UpdateVisibleRowRange() { if (LicenseManager.UsageMode == LicenseUsageMode.Designtime) { @@ -459,71 +435,10 @@ private async Task RunBackgroundAsync(CancellationToken cancellationToken) return; } - await TaskScheduler.Default; - - while (true) - { - if (cancellationToken.IsCancellationRequested) - { - // The background thread is requesting shutdown. Return immediately unless the work queue is marked - // as completed (meaning the background thread will not be restarted) and still contains work items. - if (!_backgroundQueue.IsCompleted || _backgroundQueue.IsEmpty) - { - return; - } - } - - try - { - CancellationToken timeoutToken = CancellationToken.None; - Func backgroundOperation; - CancellationToken backgroundOperationCancellation; - try - { - using CancellationTokenSource timeoutTokenSource = new(TimeSpan.FromMilliseconds(200)); - using var linkedCancellation = timeoutTokenSource.Token.CombineWith(cancellationToken); - timeoutToken = timeoutTokenSource.Token; - (backgroundOperation, backgroundOperationCancellation) = await _backgroundQueue.DequeueAsync(linkedCancellation.Token); - } - catch (OperationCanceledException) when (timeoutToken.IsCancellationRequested && !cancellationToken.IsCancellationRequested) - { - // No work was received from the queue within the timeout. - if (RowCount < _revisionGraph.Count) - { - this.InvokeAsync(() => { SetRowCountAndSelectRowsIfReady(_revisionGraph.Count); }).FileAndForget(); - } - - continue; - } - - if (backgroundOperationCancellation.IsCancellationRequested) - { - continue; - } - - try - { - await backgroundOperation(backgroundOperationCancellation); - } - catch (OperationCanceledException) when (backgroundOperationCancellation.IsCancellationRequested) - { - // Normal cancellation of background work - } - } - catch (OperationCanceledException) when (_backgroundQueue.IsCompleted && _backgroundQueue.IsEmpty) - { - // Normal completion of background work - return; - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - // Normal cancellation of background queue during clear - return; - } - } + _backgroundUpdater.ScheduleExcecution(); } - private void UpdateVisibleRowRange() + private async Task UpdateVisibleRowRangeInternalAsync() { var fromIndex = Math.Max(0, FirstDisplayedScrollingRowIndex); var visibleRowCount = _rowHeight > 0 ? (Height / _rowHeight) + 2 /*Add 2 for rounding*/ : 0; @@ -542,67 +457,49 @@ private void UpdateVisibleRowRange() if (_backgroundScrollTo != newBackgroundScrollTo) { _backgroundScrollTo = newBackgroundScrollTo; - _backgroundQueue.Enqueue((BackgroundUpdateAsync, _backgroundCancellationToken)); - } - this.InvokeAsync(NotifyProvidersVisibleRowRangeChanged).FileAndForget(); - } - } - } + if (AppSettings.ShowRevisionGridGraphColumn) + { + int scrollTo; + int curCount; - private Task BackgroundUpdateAsync(CancellationToken cancellationToken) - { - if (AppSettings.ShowRevisionGridGraphColumn) - { - int scrollTo; - int curCount; - do - { - if (cancellationToken.IsCancellationRequested) - { - return Task.CompletedTask; + do + { + scrollTo = newBackgroundScrollTo; + curCount = _revisionGraph.GetCachedCount(); + + await UpdateGraphAsync(fromIndex: curCount, toIndex: scrollTo); + } + while (curCount < scrollTo); + } + else + { + await UpdateGraphAsync(fromIndex: _revisionGraph.Count, toIndex: _revisionGraph.Count); + } } - scrollTo = _backgroundScrollTo; - curCount = _revisionGraph.GetCachedCount(); - UpdateGraph(curCount, scrollTo); + await this.InvokeAsync(NotifyProvidersVisibleRowRangeChanged); } - while (curCount < scrollTo); - } - else - { - UpdateGraph(_revisionGraph.Count, _revisionGraph.Count); } - this.InvokeAsync(NotifyProvidersVisibleRowRangeChanged).FileAndForget(); - return Task.CompletedTask; + return; - void UpdateGraph(int fromIndex, int toIndex) + async Task UpdateGraphAsync(int fromIndex, int toIndex) { - if (cancellationToken.IsCancellationRequested) - { - return; - } - // Cache the next item - _revisionGraph.CacheTo(toIndex, Math.Min(fromIndex + 1500, toIndex)); + _revisionGraph.CacheTo(currentRowIndex: toIndex, lastToCacheRowIndex: Math.Min(fromIndex + 1500, toIndex)); - var rowIndex = _revisionGraph.GetCachedCount(); + await UpdateRowCountAsync(); + } - this.InvokeAsync(UpdateRowCount, toIndex).FileAndForget(); - return; + async Task UpdateRowCountAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - void UpdateRowCount(int row) + int rowCount = _revisionGraph.Count; + if (RowCount < rowCount) { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - if (RowCount < _revisionGraph.Count) - { - SetRowCountAndSelectRowsIfReady(_revisionGraph.Count); - } + SetRowCountAndSelectRowsIfReady(rowCount); } } } @@ -617,9 +514,7 @@ private void NotifyProvidersVisibleRowRangeChanged() public override void Refresh() { - // TODO allow custom grid font - ////NormalFont = AppSettings.RevisionGridFont; - ////NormalFont = new Font(Settings.Font.Name, Settings.Font.Size + 2); // SystemFonts.DefaultFont.FontFamily, SystemFonts.DefaultFont.Size + 2); + InitFonts(); UpdateRowHeight(); UpdateVisibleRowRange(); @@ -708,6 +603,45 @@ protected override void OnKeyDown(KeyEventArgs e) } } + protected override void WndProc(ref Message m) + { + RepaintFilter(m); + base.WndProc(ref m); + } + + /// + /// Determines is a forced repaint is needed. + /// + /// + /// In situations where the mouse wheel is spinning fast (for example with free-spinning mouse wheels), + /// the message pump is flooded with WM_CTLCOLORSCROLLBAR messages and the DataGridView is not repainted. + /// This method injects a WM_PAINT message in such cases to make the GUI feel more responsive. + /// + private void RepaintFilter(Message m) + { + const int WM_CTLCOLORSCROLLBAR = 0x137; + const int WM_PAINT = 0xF; + + if (m.Msg != WM_CTLCOLORSCROLLBAR) + { + _consecutiveScrollMessageCnt = 0; + } + else + { + _consecutiveScrollMessageCnt++; + } + + if (_consecutiveScrollMessageCnt > 5 && _lastRepaint.ElapsedMilliseconds > 50) + { + // inject paint message + var mm = new Message() { HWnd = Handle, Msg = WM_PAINT }; + base.WndProc(ref mm); + + _consecutiveScrollMessageCnt = 0; + _lastRepaint.Restart(); + } + } + protected override void OnMouseDown(MouseEventArgs e) { var hit = HitTest(e.X, e.Y); @@ -751,6 +685,13 @@ protected override void OnMouseWheel(MouseEventArgs e) } } + private void InitFonts() + { + _normalFont = AppSettings.Font; + _boldFont = new Font(_normalFont, FontStyle.Bold); + _monospaceFont = AppSettings.MonospaceFont; + } + private static Color getHighlightedGrayTextColor(float degreeOfGrayness = 1f) => ColorHelper.GetHighlightGrayTextColor( backgroundColorName: KnownColor.Control,