From 369b04822ca86fe1cd2717184dcd4cef186c6f30 Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Sat, 6 Jan 2018 13:46:27 +0100 Subject: [PATCH] Limit how often GitStatus polls for changes Reported in #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 #4256 for a discussion. --- .../BrowseDialog/GitStatusMonitor.cs | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/GitUI/CommandsDialogs/BrowseDialog/GitStatusMonitor.cs b/GitUI/CommandsDialogs/BrowseDialog/GitStatusMonitor.cs index 40a76f360cd..a42e81f1ae2 100644 --- a/GitUI/CommandsDialogs/BrowseDialog/GitStatusMonitor.cs +++ b/GitUI/CommandsDialogs/BrowseDialog/GitStatusMonitor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using GitCommands; @@ -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. /// - private const int UpdateDelay = 500; + private const int UpdateDelay = 1000; + + /// + /// Minimum interval between subsequent updates + /// + private static readonly int MinUpdateInterval = 3000; /// /// Update every 5min, just to make sure something didn't slip through the cracks. @@ -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 _ignoredFiles = new HashSet(); @@ -46,6 +54,7 @@ public partial class GitStatusMonitor : IDisposable public GitStatusMonitor() { + _prevUpdateTime = 0; InitializeComponent(); ignoredFilesTimer.Interval = MaxUpdatePeriod; @@ -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; @@ -132,7 +141,6 @@ private GitStatusMonitorState CurrentStatus private IGitUICommandsSource UICommandsSource { get; set; } - public void Init(IGitUICommandsSource commandsSource) { if (commandsSource == null) @@ -144,7 +152,6 @@ public void Init(IGitUICommandsSource commandsSource) commandsSource_activate(commandsSource); } - protected void OnGitStatusMonitorStateChanged(GitStatusMonitorStateEventArgs e) { GitStatusMonitorStateChanged?.Invoke(this, e); @@ -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); } } @@ -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(); @@ -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); } } @@ -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) @@ -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) { @@ -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; @@ -434,7 +440,7 @@ private void WorkTreeChanged(object sender, FileSystemEventArgs e) if (e.FullPath.EndsWith("\\.git\\index.lock")) return; - ScheduleDeferredUpdate(); + ScheduleNext(UpdateDelay); } } } \ No newline at end of file