From b5ca7dd297d3551c67ac5b4823549cee8cadf8e3 Mon Sep 17 00:00:00 2001 From: Matt Ward Date: Wed, 7 Aug 2019 16:45:58 +0100 Subject: [Core] Cache ProjectFile.Include to improve project save performance With an SDK style project containing 1500 C# files, adding a new C# class to the project would result in the saving taking about 20 seconds. The majority of this time was spent in the Project's SaveProjectItems as it tried to determine if an MSBuild remove item should be added for the new file. Caching the ProjectFile.Include takes the save time down to around 300 ms. Fixes VSTS #947103 - Class creation is very slow in projects with hundreds of classes --- .../MonoDevelop.Projects/ProjectFile.cs | 32 ++++++++++++++++++ .../MonoDevelop.Projects/DotNetCoreProjectTests.cs | 38 ++++++++++++++++++++++ .../MonoDevelop.Projects/ProjectTests.cs | 26 +++++++++++++++ 3 files changed, 96 insertions(+) (limited to 'main') diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/ProjectFile.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/ProjectFile.cs index b3f0717fdb..8c53b0504d 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/ProjectFile.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/ProjectFile.cs @@ -74,14 +74,22 @@ namespace MonoDevelop.Projects BuildAction = buildAction; } + string cachedInclude; + public override string Include { get { if (Project != null) { + if (cachedInclude != null) + return cachedInclude; + string path = MSBuildProjectService.ToMSBuildPath (Project.ItemDirectory, FilePath); if (path.Length > 0) { //directory paths must end with '/' if ((Subtype == Subtype.Directory) && path [path.Length - 1] != '\\') path = path + "\\"; + // Cache the include path to avoid recalculating MSBuildProjectService.ToMSBuildPath + // which can slow down saving SDK style projects that contain thousands of files. + cachedInclude = path; return path; } } @@ -186,6 +194,8 @@ namespace MonoDevelop.Projects if (IsLink && Link.FileName == oldPath.FileName) link = Path.Combine (Path.GetDirectoryName (link), filename.FileName); + cachedInclude = null; + // If a file that belongs to a project is being renamed, update the value of UnevaluatedInclude // since that is used when saving if (Project != null) @@ -474,15 +484,33 @@ namespace MonoDevelop.Projects } } + Project project; + protected override void OnProjectSet () { base.OnProjectSet (); + if (project != null) { + project.Modified -= OnProjectModified; + project = null; + } if (Project != null) { base.Include = Include; + project = Project; + project.Modified += OnProjectModified; VirtualPathChanged?.Invoke (this, new ProjectFileVirtualPathChangedEventArgs (this, FilePath.Null, ProjectVirtualPath)); } } + void OnProjectModified (object sender, SolutionItemModifiedEventArgs e) + { + foreach (var eventInfo in e) { + if (eventInfo.Hint == "FileName") { + cachedInclude = null; + return; + } + } + } + public override string ToString () { return "[ProjectFile: FileName=" + filename + "]"; @@ -503,6 +531,10 @@ namespace MonoDevelop.Projects public virtual void Dispose () { + if (project != null) { + project.Modified -= OnProjectModified; + project = null; + } } internal event EventHandler VirtualPathChanged; diff --git a/main/tests/MonoDevelop.Core.Tests/MonoDevelop.Projects/DotNetCoreProjectTests.cs b/main/tests/MonoDevelop.Core.Tests/MonoDevelop.Projects/DotNetCoreProjectTests.cs index 86a3aadfc2..0ba1113eb9 100644 --- a/main/tests/MonoDevelop.Core.Tests/MonoDevelop.Projects/DotNetCoreProjectTests.cs +++ b/main/tests/MonoDevelop.Core.Tests/MonoDevelop.Projects/DotNetCoreProjectTests.cs @@ -696,6 +696,44 @@ namespace MonoDevelop.Projects } } + [Test] + public async Task AddNewFileToProjectAndSave_ProjectHas1500CSharpFiles_SavingIsFast () + { + FilePath solFile = Util.GetSampleProject ("netstandard-sdk", "netstandard-sdk.sln"); + + // Create 1500 C# files. + var sourceDirectory = solFile.ParentDirectory.Combine ("Files"); + Directory.CreateDirectory (sourceDirectory); + + for (int i = 0; i < 1500; ++i) { + string fileName = sourceDirectory.Combine ($"Test{i}.cs"); + string code = "class Test" + i + "{}"; + File.WriteAllText (fileName, code); + } + + using (var solution = (Solution)await Services.ProjectService.ReadWorkspaceItem (Util.GetMonitor (), solFile)) { + var p = solution.GetAllProjects ().Single () as DotNetProject; + // Sanity check - ensure project and solution in same directory otherwise the generated .cs files + // will not be used. + Assert.AreEqual (solution.BaseDirectory, p.BaseDirectory); + + string fileName = p.BaseDirectory.Combine ("NewClass.cs"); + string code = "class NewClass {}"; + File.WriteAllText (fileName, code); + + p.AddFile (fileName, BuildAction.Compile); + + var timer = Stopwatch.StartNew (); + await p.SaveAsync (Util.GetMonitor ()); + timer.Stop (); + + // This was taking 20 seconds before. + // Takes about 300ms with ProjectFile.Include caching. Here we use 2 seconds + // in case the build server is slow. + Assert.That (timer.ElapsedMilliseconds, Is.LessThan (2000)); + } + } + static void RunMSBuildRestore (FilePath fileName) { CreateNuGetConfigFile (fileName.ParentDirectory); diff --git a/main/tests/MonoDevelop.Core.Tests/MonoDevelop.Projects/ProjectTests.cs b/main/tests/MonoDevelop.Core.Tests/MonoDevelop.Projects/ProjectTests.cs index e44b98f5c6..0913f550de 100644 --- a/main/tests/MonoDevelop.Core.Tests/MonoDevelop.Projects/ProjectTests.cs +++ b/main/tests/MonoDevelop.Core.Tests/MonoDevelop.Projects/ProjectTests.cs @@ -1226,6 +1226,32 @@ namespace MonoDevelop.Projects } } + /// + /// Ensure the cached value is updated when the Project or FileName changes. + /// + [Test] + public void ProjectFileIncludeCachingTests () + { + using (var project = Services.ProjectService.CreateDotNetProject ("C#")) { + FilePath directory = Util.CreateTmpDir ("ProjectFileIncludeCachingTests"); + project.FileName = directory.Combine ("Project.csproj"); + + var fileName = project.BaseDirectory.Combine ("Source", "Test.cs"); + var projectFile = new ProjectFile (fileName); + projectFile.Project = project; + + Assert.AreEqual (@"Source\Test.cs", projectFile.Include); + + projectFile.Name = projectFile.FilePath.ChangeName ("Changed"); + Assert.AreEqual (@"Source\Changed.cs", projectFile.Include); + Assert.AreEqual (@"Source\Changed.cs", projectFile.UnevaluatedInclude); + + // Change project's ItemDirectory. This will change the ProjectFile's Include. + project.FileName = project.BaseDirectory.Combine ("Source", "Project.csproj"); + Assert.AreEqual ("Changed.cs", projectFile.Include); + } + } + class TestGetReferencesProjectExtension : DotNetProjectExtension { protected internal override Task> OnGetReferences (ConfigurationSelector configuration, CancellationToken token) -- cgit v1.2.3