From 47742270cbf8bc51864658484a7fdc378f6f53a9 Mon Sep 17 00:00:00 2001 From: therzok Date: Tue, 18 Jun 2019 03:22:42 +0300 Subject: Initial work towards optimizing FileService handlers Fixes VSTS #853896 - [Feedback] Visual Studio freezes when i run mono Fixes VSTS #902435 - Investigate performance issue with FileSystemWatcher --- main/src/core/MonoDevelop.Core/CoreExtensions.cs | 77 ++++++++++++++ .../MonoDevelop.Core/FileService.cs | 12 ++- .../DefaultMSBuildEngine.cs | 5 +- .../MSBuildProjectService.cs | 4 +- .../MonoDevelop.Projects/Project.cs | 112 +++++++++++---------- .../MonoDevelop.Projects/ProjectFileCollection.cs | 5 + 6 files changed, 156 insertions(+), 59 deletions(-) (limited to 'main/src') diff --git a/main/src/core/MonoDevelop.Core/CoreExtensions.cs b/main/src/core/MonoDevelop.Core/CoreExtensions.cs index 3d90766d6b..0818ad2c72 100644 --- a/main/src/core/MonoDevelop.Core/CoreExtensions.cs +++ b/main/src/core/MonoDevelop.Core/CoreExtensions.cs @@ -26,6 +26,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using MonoDevelop.Core; @@ -296,6 +297,82 @@ namespace System } } } + + internal static void TimeInvoke (this EventHandler events, object sender, T args) + { + var sw = new Diagnostics.Stopwatch (); + foreach (var ev in events.GetInvocationList ()) { + try { + sw.Restart (); + ((EventHandler)ev) (sender, args); + sw.Stop (); + + lock (timings) { + if (!timings.TryGetValue (sender, out var timingPair)) { + timings [sender] = timingPair = new Dictionary (); + } + + if (!timingPair.TryGetValue (ev.Method, out var previousTime)) + previousTime = TimeSpan.Zero; + + timingPair [ev.Method] = previousTime.Add (sw.Elapsed); + } + } catch (Exception ex) { + LoggingService.LogInternalError (ex); + } + } + } + + static readonly Dictionary> timings = new Dictionary> (); + + internal static void TimingsReport () + { + foreach (var kvp in timings) { + var source = kvp.Key; + var values = kvp.Value; + + Console.WriteLine ("Source {0}", source); + foreach (var timeReport in values.OrderByDescending (x => x.Value)) { + Console.WriteLine ("{0} - {1} {2}", timeReport.Value.ToString (), timeReport.Key.DeclaringType, timeReport.Key); + } + } + } + + internal static void TimeInvoke (this EventHandler events, object sender, EventArgs args) + { + var sw = new Diagnostics.Stopwatch (); + foreach (var ev in events.GetInvocationList ()) { + try { + sw.Restart (); + ((EventHandler)ev) (sender, args); + sw.Stop (); + + lock (timings) { + if (!timings.TryGetValue (sender, out var timingPair)) { + timings [sender] = timingPair = new Dictionary (MethodInfoEqualityComparer.Instance); + } + + if (!timingPair.TryGetValue (ev.Method, out var previousTime)) + previousTime = TimeSpan.Zero; + + timingPair [ev.Method] = previousTime.Add (sw.Elapsed); + } + } catch (Exception ex) { + LoggingService.LogInternalError (ex); + } + } + } + + sealed class MethodInfoEqualityComparer : IEqualityComparer + { + public static MethodInfoEqualityComparer Instance = new MethodInfoEqualityComparer (); + public bool Equals (MethodInfo x, MethodInfo y) + => x.Name == y.Name + && x.DeclaringType == y.DeclaringType; + + public int GetHashCode (MethodInfo obj) + => obj.Name.GetHashCode () ^ obj.DeclaringType.GetHashCode (); + } } } diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Core/FileService.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Core/FileService.cs index 81221d827b..cab986eacc 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Core/FileService.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Core/FileService.cs @@ -909,6 +909,8 @@ namespace MonoDevelop.Core class EventQueue { + static readonly EventQueue q = new EventQueue (); + static void RaiseSync (FileService.EventDataKind kind, FileEventArgs args) { var handler = FileService.GetHandler (kind); @@ -918,14 +920,14 @@ namespace MonoDevelop.Core // Ugly, but it saves us the problem of having to deal with generic event handlers without covariance. if (args is FileCopyEventArgs copyArgs) { if (handler is EventHandler copyHandler) { - copyHandler.Invoke (null, copyArgs); + copyHandler?.TimeInvoke (q, copyArgs); return; } throw new InvalidOperationException (); } if (handler is EventHandler fileHandler) - fileHandler.Invoke (null, args); + fileHandler?.TimeInvoke (q, args); else throw new InvalidOperationException (); } @@ -1345,17 +1347,17 @@ namespace MonoDevelop.Core internal void OnFileCreated (FileEventArgs args) { - FileCreated?.Invoke (this, Clone (args)); + FileCreated?.TimeInvoke (this, Clone (args)); } internal void OnFileRemoved (FileEventArgs args) { - FileRemoved?.Invoke (this, Clone (args)); + FileRemoved?.TimeInvoke (this, Clone (args)); } internal void OnFileRenamed (FileCopyEventArgs args) { - FileRenamed?.Invoke (this, Clone (args)); + FileRenamed?.TimeInvoke (this, Clone (args)); } static T Clone (T args) where T : FileEventArgs, new() diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.MSBuild/DefaultMSBuildEngine.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.MSBuild/DefaultMSBuildEngine.cs index 0cc36a1f0e..49f790ef94 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.MSBuild/DefaultMSBuildEngine.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.MSBuild/DefaultMSBuildEngine.cs @@ -1435,7 +1435,10 @@ namespace MonoDevelop.Projects.MSBuild { var pi = (ProjectInfo)projectInstance; string filePath = MSBuildProjectService.FromMSBuildPath (pi.Project.BaseDirectory, include); - foreach (var g in pi.GlobIncludes.Where (g => g.Condition)) { + foreach (var g in pi.GlobIncludes) { + if (!g.Condition) + continue; + if (g.ExcludeRegex != null) { if (g.ExcludeRegex.IsMatch (include)) continue; diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.MSBuild/MSBuildProjectService.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.MSBuild/MSBuildProjectService.cs index d2874912c7..703bbcf97d 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.MSBuild/MSBuildProjectService.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.MSBuild/MSBuildProjectService.cs @@ -843,7 +843,7 @@ namespace MonoDevelop.Projects.MSBuild { int i = str.IndexOfAny (specialCharacters); if (i != -1) { - var sb = new System.Text.StringBuilder (); + var sb = StringBuilderCache.Allocate (); int start = 0; while (i != -1) { sb.Append (str, start, i - start); @@ -855,7 +855,7 @@ namespace MonoDevelop.Projects.MSBuild } if (start < str.Length) sb.Append (str, start, str.Length - start); - return sb.ToString (); + return StringBuilderCache.ReturnAndFree (sb); } return str; } diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Project.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Project.cs index 7caab35662..a7f140539b 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Project.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Project.cs @@ -48,6 +48,7 @@ using Microsoft.CodeAnalysis; using MonoDevelop.Core.Collections; using ICSharpCode.Decompiler.TypeSystem.Implementation; using System.Runtime.CompilerServices; +using Microsoft.Extensions.ObjectPool; namespace MonoDevelop.Projects { @@ -2406,19 +2407,23 @@ namespace MonoDevelop.Projects internal virtual void OnFileChanged (object source, FileEventArgs e) { + var args = new ProjectFileEventArgs (); + foreach (FileEventInfo fi in e) { ProjectFile file = GetProjectFile (fi.FileName); if (file != null) { SetFastBuildCheckDirty (); - try { - NotifyFileChangedInProject (file); - } catch { - // Workaround Mono bug. The watcher seems to - // stop watching if an exception is thrown in - // the event handler - } + args.Add (new ProjectFileEventInfo (this, file)); } } + + try { + OnFileChangedInProject (args); + } catch { + // Workaround Mono bug. The watcher seems to + // stop watching if an exception is thrown in + // the event handler + } } protected override IEnumerable OnGetItemFiles (bool includeReferencedFiles) @@ -4488,17 +4493,17 @@ namespace MonoDevelop.Projects void OnFileRenamed (object sender, FileCopyEventArgs e) { foreach (FileEventInfo info in e) { - OnFileRenamed (info.SourceFile, info.TargetFile); + OnFileRenamed (info.SourceFile, info.TargetFile, info.IsDirectory); } } - void OnFileRenamed (FilePath sourceFile, FilePath targetFile) + void OnFileRenamed (FilePath sourceFile, FilePath targetFile, bool isDirectory) { if (Runtime.IsMainThread) return; try { - if (Directory.Exists (targetFile)) { + if (isDirectory) { OnDirectoryRenamedExternally (sourceFile, targetFile); return; } @@ -4508,7 +4513,7 @@ namespace MonoDevelop.Projects Runtime.RunInMainThread (() => { OnFileCreatedExternally (targetFile); - OnFileDeletedExternally (sourceFile); + OnFileDeletedExternally (sourceFile, isDirectory); }); } @@ -4518,25 +4523,30 @@ namespace MonoDevelop.Projects return; foreach (FileEventInfo info in e) { - OnFileCreated (info.FileName); + OnFileCreated (info.FileName, info.IsDirectory); } } - void OnFileCreated (FilePath filePath) + void OnFileCreated (FilePath filePath, bool isDirectory) { if (Runtime.IsMainThread) return; try { - if (Directory.Exists (filePath)) + if (isDirectory) return; - if (filePath.FileName == ".DS_Store") - return; + var fileName = ((string)filePath).AsSpan (); + fileName = fileName.Slice (fileName.LastIndexOf (Path.DirectorySeparatorChar) + 1); - // Ignore temporary files created when saving a file in the editor. - if (filePath.FileName.StartsWith (".#", StringComparison.OrdinalIgnoreCase)) - return; + if (fileName[0] == '.') { + // Ignore temporary files created when saving a file in the editor. + if (fileName [1] == '#') + return; + + if (fileName.SequenceEqual (".DS_Store".AsSpan ())) + return; + } OnFileCreatedExternally (filePath); } catch (Exception ex) { @@ -4551,7 +4561,7 @@ namespace MonoDevelop.Projects Runtime.RunInMainThread (() => { foreach (FileEventInfo info in e) { - OnFileDeletedExternally (info.FileName); + OnFileDeletedExternally (info.FileName, info.IsDirectory); } }); } @@ -4594,36 +4604,33 @@ namespace MonoDevelop.Projects void OnDirectoryMovedOutOfProject (FilePath oldDirectory) { // Directory moved outside project directory. Remove files from project. - var projectFilesInDirectory = Files.GetFilesInPath (oldDirectory); - if (!projectFilesInDirectory.Any ()) - return; - Runtime.RunInMainThread (() => { - foreach (ProjectFile file in projectFilesInDirectory) - Files.Remove (file); + Files.RemoveFilesInPath (oldDirectory); }); } + static readonly ObjectPool> projectItemListPool = ObjectPool.Create> (); + void OnFileCreatedExternally (FilePath fileName) { if (sourceProject == null) { - // sometimes this method is called after disposing this class. + // sometimes this method is called after disposing this class. // (i.e. when quitting MD or creating a new project.) LoggingService.LogWarning ("File created externally not processed. {0}", fileName); return; } - // Check file is inside the project directory. The file globs would exclude the file anyway - // if the relative path starts with "..\" but checking here avoids checking the file globs. - if (!fileName.IsChildPathOf (BaseDirectory)) - return; - - if (Files.Any (file => file.FilePath == fileName)) { + if (Files.GetFile (fileName) != null) { // File exists in project. This can happen if the file was added // in the IDE and not externally. return; } + // Check file is inside the project directory. The file globs would exclude the file anyway + // if the relative path starts with "..\" but checking here avoids checking the file globs. + if (!fileName.IsChildPathOf (BaseDirectory)) + return; + string include = MSBuildProjectService.ToMSBuildPath (ItemDirectory, fileName); var globItems = sourceProject.FindGlobItemsIncludingFile (include); if (globItems == null) { @@ -4633,34 +4640,37 @@ namespace MonoDevelop.Projects } if (!UseAdvancedGlobSupport) - globItems = globItems.Where (it => it.Metadata.GetProperties ().Count () == 0); + globItems = globItems.Where (it => !it.Metadata.GetProperties ().Any ()); + var list = projectItemListPool.Get (); foreach (var it in globItems) { var eit = CreateFakeEvaluatedItem (sourceProject, it, include, null); var pi = CreateProjectItem (eit); pi.Read (this, eit); - if (Runtime.IsMainThread) { - Items.Add (pi); - } else { - Runtime.RunInMainThread (() => { - // Double check the file has not been added on the UI thread by the IDE. - if (!Files.Any (file => file.FilePath == fileName)) { - Items.Add (pi); - } - }).Ignore (); - } + + list.Add (pi); } + + Runtime.RunInMainThread (() => { + // Double check the file has not been added on the UI thread by the IDE. + list.RemoveAll (x => Files.GetFile (fileName) != null); + Items.AddRange (list); + projectItemListPool.Return (list); + }).Ignore (); } - void OnFileDeletedExternally (string fileName) + void OnFileDeletedExternally (string fileName, bool isDirectory) { - if (File.Exists (fileName) || Directory.Exists (fileName)) { - // File has not been deleted. The delete event could have been due to - // the file being saved. Saving with TextFileUtility will result in - // FileService.SystemRename being called to move a temporary file - // to the file being saved which deletes and then creates the file. + // File has not been deleted. The delete event could have been due to + // the file being saved. Saving with TextFileUtility will result in + // FileService.SystemRename being called to move a temporary file + // to the file being saved which deletes and then creates the file. + bool notDeleted = isDirectory + ? Directory.Exists (fileName) + : File.Exists (fileName); + + if (notDeleted) return; - } Files.Remove (fileName); } diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/ProjectFileCollection.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/ProjectFileCollection.cs index e70d1e46ef..2e0e1d39b4 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/ProjectFileCollection.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/ProjectFileCollection.cs @@ -251,6 +251,11 @@ namespace MonoDevelop.Projects } return list.ToArray (); } + + internal void RemoveFilesInPath (FilePath path) + { + SetItems (this.Where (file => !file.FilePath.IsChildPathOf (path))); + } public void Remove (string fileName) { -- cgit v1.2.3