Skip to content

Commit

Permalink
Limit how often GitStatus polls for changes
Browse files Browse the repository at this point in the history
Reported in gitextensions#4069
GE can in some situations  use a lot of CPU and disk load, as it runs "git status --untracked-files" at changes in the filesystem
If a status command is not done before next update is detected, the next start immediately after. "git status" can therefore run continuously.
(This is more visible with certain viruses like Symantec and McAfee.)

This PR limits the checks in two ways:
 * First run is started after 1000ms + timer (fires every 500ms so adds on average 250ms) instead of 500ms +timer, to collect more changes (probably not essential)
 * Next command is started minimum 3000ms after the previous command. If 3*time to run the status update is longer than 3000ms, the interval time is dynamically increased.
So normal one shot changes are detected about the same as now, but continuous changes are not updating so frequently, "git status" should not run continuously.

A few other changes like that the "next run" time could be overwritten by a time in the future and that retrieved "git status" data were not used if more file system changes were detected.

If "git status" requires more than 5s to run, the behavior is not improved. The counter in the Commit button should not be used then. Configurable time could improve (like longest of 5s and twice the time to run git status), configurable time is not going to be used.

There are situations where the "ignored files detection" is insufficient. See gitextensions#4256 for a discussion.
  • Loading branch information
gerhardol committed Jan 31, 2018
1 parent e6cf41f commit 369b048
Showing 1 changed file with 44 additions and 38 deletions.
82 changes: 44 additions & 38 deletions GitUI/CommandsDialogs/BrowseDialog/GitStatusMonitor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using GitCommands;
Expand All @@ -12,7 +12,12 @@ public partial class GitStatusMonitor : IDisposable
/// We often change several files at once.
/// Wait a second so they're all changed before we try to get the status.
/// </summary>
private const int UpdateDelay = 500;
private const int UpdateDelay = 1000;

/// <summary>
/// Minimum interval between subsequent updates
/// </summary>
private static readonly int MinUpdateInterval = 3000;

/// <summary>
/// Update every 5min, just to make sure something didn't slip through the cracks.
Expand All @@ -28,7 +33,10 @@ public partial class GitStatusMonitor : IDisposable
private bool _ignoredFilesAreStale;
private string _gitPath;
private string _submodulesPath;
//Timestamps to schedule status updates, limit the update interval dynamically
private int _nextUpdateTime;
private int _prevUpdateTime;
private int _currUpdateInterval = MinUpdateInterval;
private GitStatusMonitorState _currentStatus;
private HashSet<string> _ignoredFiles = new HashSet<string>();

Expand All @@ -46,6 +54,7 @@ public partial class GitStatusMonitor : IDisposable

public GitStatusMonitor()
{
_prevUpdateTime = 0;
InitializeComponent();

ignoredFilesTimer.Interval = MaxUpdatePeriod;
Expand Down Expand Up @@ -117,7 +126,7 @@ private GitStatusMonitorState CurrentStatus
_workTreeWatcher.EnableRaisingEvents = true;
_gitDirWatcher.EnableRaisingEvents = !_gitDirWatcher.Path.StartsWith(_workTreeWatcher.Path);
_globalIgnoreWatcher.EnableRaisingEvents = !string.IsNullOrWhiteSpace(_globalIgnoreWatcher.Path);
ScheduleDeferredUpdate();
ScheduleNext(UpdateDelay);
}
break;

Expand All @@ -132,7 +141,6 @@ private GitStatusMonitorState CurrentStatus

private IGitUICommandsSource UICommandsSource { get; set; }


public void Init(IGitUICommandsSource commandsSource)
{
if (commandsSource == null)
Expand All @@ -144,7 +152,6 @@ public void Init(IGitUICommandsSource commandsSource)
commandsSource_activate(commandsSource);
}


protected void OnGitStatusMonitorStateChanged(GitStatusMonitorStateEventArgs e)
{
GitStatusMonitorStateChanged?.Invoke(this, e);
Expand All @@ -155,13 +162,12 @@ protected void OnGitWorkingDirectoryStatusChanged(GitWorkingDirectoryStatusEvent
GitWorkingDirectoryStatusChanged?.Invoke(this, e);
}


private void GlobalIgnoreChanged(object sender, FileSystemEventArgs e)
{
if (e.FullPath == _globalIgnoreFilePath)
{
_ignoredFilesAreStale = true;
ScheduleDeferredUpdate();
ScheduleNext(UpdateDelay);
}
}

Expand Down Expand Up @@ -211,6 +217,8 @@ private void StartWatchingChanges(string workTreePath, string gitDirPath)
}
_gitPath = Path.GetDirectoryName(gitDirPath);
_submodulesPath = Path.Combine(_gitPath, "modules");
_currUpdateInterval = MinUpdateInterval;
_prevUpdateTime = 0;
UpdateIgnoredFiles(true);
ignoredFilesTimer.Stop();
ignoredFilesTimer.Start();
Expand Down Expand Up @@ -274,13 +282,14 @@ private void Update()

_commandIsRunning = true;
_statusIsUpToDate = true;
_prevUpdateTime = Environment.TickCount;
if (_ignoredFilesAreStale)
{
UpdateIgnoredFiles(false);
}
AsyncLoader.DoAsync(RunStatusCommand, UpdatedStatusReceived, OnUpdateStatusError);
// Always update every 5 min, even if we don't know anything changed
ScheduleNextJustInCaseUpdate();
// Schedule update every 5 min, even if we don't know that anything changed
ScheduleNext(MaxUpdatePeriod);
}
}

Expand All @@ -298,41 +307,32 @@ private void OnUpdateStatusError(AsyncErrorEventArgs e)

private void UpdatedStatusReceived(string updatedStatus)
{
//Adjust the interval between updates. (This does not affect an update already scheculed).
_currUpdateInterval = Math.Max(MinUpdateInterval, 3 * (Environment.TickCount - _prevUpdateTime));
_commandIsRunning = false;

if (CurrentStatus != GitStatusMonitorState.Running)
return;

if (_statusIsUpToDate)
var allChangedFiles = GitCommandHelpers.GetAllChangedFilesFromString(Module, updatedStatus);
OnGitWorkingDirectoryStatusChanged(new GitWorkingDirectoryStatusEventArgs(allChangedFiles));
if (!_statusIsUpToDate)
{
var allChangedFiles = GitCommandHelpers.GetAllChangedFilesFromString(Module, updatedStatus);
OnGitWorkingDirectoryStatusChanged(new GitWorkingDirectoryStatusEventArgs(allChangedFiles));
//Still not up-to-date, but present what received, GetAllChangedFilesCmd() is the heavy command
ScheduleNext(UpdateDelay);
}
else
{
UpdateImmediately();
}
}

private void ScheduleNextJustInCaseUpdate()
{
_nextUpdateTime = Environment.TickCount + MaxUpdatePeriod;
}

private void ScheduleDeferredUpdate()
private void ScheduleNext(int delay)
{
_nextUpdateTime = Environment.TickCount + UpdateDelay;
}

private void ScheduleImmediateUpdate()
{
_nextUpdateTime = Environment.TickCount;
}

private void UpdateImmediately()
{
ScheduleImmediateUpdate();
Update();
var next = Environment.TickCount + delay;
if(_nextUpdateTime > Environment.TickCount)
{
//A time is already set, use closest
next = Math.Min(_nextUpdateTime, next);
}
//Enforce a minimal time between updates, to not update too frequently
_nextUpdateTime = Math.Max(next, _prevUpdateTime + _currUpdateInterval);
}

private void commandsSource_GitUICommandsChanged(object sender, GitUICommandsChangedEventArgs e)
Expand Down Expand Up @@ -380,8 +380,8 @@ private void GitDirChanged(object sender, FileSystemEventArgs e)
if (e.FullPath.StartsWith(_submodulesPath) && (Directory.Exists(e.FullPath)))
return;

ScheduleDeferredUpdate();
}
ScheduleNext(UpdateDelay);
}

private void GitUICommands_PreCheckout(object sender, GitUIBaseEventArgs e)
{
Expand Down Expand Up @@ -411,11 +411,17 @@ private void timerRefresh_Tick(object sender, EventArgs e)
// Called for instance at buffer overflow
private void WorkTreeWatcherError(object sender, ErrorEventArgs e)
{
ScheduleDeferredUpdate();
ScheduleNext(UpdateDelay);
}

private void WorkTreeChanged(object sender, FileSystemEventArgs e)
{
//Update already scheduled?
if (_nextUpdateTime < Environment.TickCount + UpdateDelay)
return;

//TODO Smarter detection of ignored files created since the files were created
//Parse .gitignore?
var fileName = e.FullPath.Substring(_workTreeWatcher.Path.Length).ToPosixPath();
if (_ignoredFiles.Contains(fileName))
return;
Expand All @@ -434,7 +440,7 @@ private void WorkTreeChanged(object sender, FileSystemEventArgs e)
if (e.FullPath.EndsWith("\\.git\\index.lock"))
return;

ScheduleDeferredUpdate();
ScheduleNext(UpdateDelay);
}
}
}

0 comments on commit 369b048

Please sign in to comment.