diff --git a/src/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceOutput.cs b/src/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceOutput.cs index 2212efb14e..099ff8afd4 100644 --- a/src/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceOutput.cs +++ b/src/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceOutput.cs @@ -50,7 +50,8 @@ private static void FormatEventTimeStat(StringBuilder builder, PerfTraceEvent e, AppendTime(builder, e.Duration.TotalSeconds / root.Duration.TotalSeconds, 0.2); } AppendTime(builder, e.Duration.TotalSeconds / parent?.Duration.TotalSeconds, 0.5); - builder.Append($"{e.Duration.ToString("ss\\.fff\\s").Blue()}]"); + builder.Append($"{(int)e.Duration.TotalSeconds}.{e.Duration.Milliseconds:000}s".Blue()); + builder.Append("]"); } private static void AppendTime(StringBuilder builder, double? percent, double treshold) diff --git a/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryExporter.cs b/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryExporter.cs index f5682ef29c..66dfcbea9a 100644 --- a/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryExporter.cs +++ b/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryExporter.cs @@ -25,6 +25,8 @@ public class LibraryExporter private readonly string _buildBasePath; private readonly string _solutionRootPath; + private List _cachedExports; + public LibraryExporter(ProjectDescription rootProject, LibraryManager manager, string configuration, @@ -84,62 +86,112 @@ public IEnumerable GetDependencies(LibraryType type) /// private IEnumerable ExportLibraries(Func condition) { - var seenMetadataReferences = new HashSet(); + // Project Exports cannot be cached because the binding redirect file + // for desktop apps is not added to runtimeassets of the project + // until after the project is built. + var cache = GetCacheWithRefreshedProjectExports(); - // Iterate over libraries in the library manager - foreach (var library in LibraryManager.GetLibraries()) + foreach (var export in cache) { - if (!condition(library)) + if (!condition(export.Library)) { continue; } + yield return export; + } + } - var compilationAssemblies = new List(); - var sourceReferences = new List(); - var analyzerReferences = new List(); - var libraryExport = GetExport(library); + private IEnumerable GetCacheWithRefreshedProjectExports() + { + if (_cachedExports == null) + { + _cachedExports = CalculateAllExports().ToList(); + } + + var seenMetadataReferences = new HashSet(); + var refreshedCache = new LibraryExport[_cachedExports.Count()]; - // We need to filter out source references from non-root libraries, - // so we rebuild the library export - foreach (var reference in libraryExport.CompilationAssemblies) + int index = 0; + foreach (var export in _cachedExports) + { + if (Equals(export.Library.Identity.Type, LibraryType.Project)) { - if (seenMetadataReferences.Add(reference.Name)) - { - compilationAssemblies.Add(reference); - } + refreshedCache[index++] = GenerateExportFromLibrary(seenMetadataReferences, export.Library); } - - // Source and analyzer references are not transitive - if (library.Parents.Contains(_rootProject)) + else { - sourceReferences.AddRange(libraryExport.SourceReferences); - analyzerReferences.AddRange(libraryExport.AnalyzerReferences); + refreshedCache[index++] = export; } - var builder = LibraryExportBuilder.Create(library); - if (_runtime != null && _runtimeFallbacks != null) + foreach (var reference in export.CompilationAssemblies) { - // For portable apps that are built with runtime trimming we replace RuntimeAssemblyGroups and NativeLibraryGroups - // with single default group that contains asset specific to runtime we are trimming for - // based on runtime fallback list - builder.WithRuntimeAssemblyGroups(TrimAssetGroups(libraryExport.RuntimeAssemblyGroups, _runtimeFallbacks)); - builder.WithNativeLibraryGroups(TrimAssetGroups(libraryExport.NativeLibraryGroups, _runtimeFallbacks)); + seenMetadataReferences.Add(reference.Name); } - else + } + + return refreshedCache; + } + + private IEnumerable CalculateAllExports() + { + var seenMetadataReferences = new HashSet(); + + // Iterate over libraries in the library manager + foreach (var library in LibraryManager.GetLibraries()) + { + yield return GenerateExportFromLibrary(seenMetadataReferences, library); + } + } + + private LibraryExport GenerateExportFromLibrary( + HashSet seenMetadataReferences, + LibraryDescription library) + { + var compilationAssemblies = new List(); + var sourceReferences = new List(); + var analyzerReferences = new List(); + var libraryExport = GetExport(library); + + // We need to filter out source references from non-root libraries, + // so we rebuild the library export + foreach (var reference in libraryExport.CompilationAssemblies) + { + if (seenMetadataReferences.Add(reference.Name)) { - builder.WithRuntimeAssemblyGroups(libraryExport.RuntimeAssemblyGroups); - builder.WithNativeLibraryGroups(libraryExport.NativeLibraryGroups); + compilationAssemblies.Add(reference); } + } - yield return builder - .WithCompilationAssemblies(compilationAssemblies) - .WithSourceReferences(sourceReferences) - .WithRuntimeAssets(libraryExport.RuntimeAssets) - .WithEmbedddedResources(libraryExport.EmbeddedResources) - .WithAnalyzerReference(analyzerReferences) - .WithResourceAssemblies(libraryExport.ResourceAssemblies) - .Build(); + // Source and analyzer references are not transitive + if (library.Parents.Contains(_rootProject)) + { + sourceReferences.AddRange(libraryExport.SourceReferences); + analyzerReferences.AddRange(libraryExport.AnalyzerReferences); + } + + var builder = LibraryExportBuilder.Create(library); + if (_runtime != null && _runtimeFallbacks != null) + { + // For portable apps that are built with runtime trimming we replace RuntimeAssemblyGroups and NativeLibraryGroups + // with single default group that contains asset specific to runtime we are trimming for + // based on runtime fallback list + builder.WithRuntimeAssemblyGroups(TrimAssetGroups(libraryExport.RuntimeAssemblyGroups, _runtimeFallbacks)); + builder.WithNativeLibraryGroups(TrimAssetGroups(libraryExport.NativeLibraryGroups, _runtimeFallbacks)); } + else + { + builder.WithRuntimeAssemblyGroups(libraryExport.RuntimeAssemblyGroups); + builder.WithNativeLibraryGroups(libraryExport.NativeLibraryGroups); + } + + return builder + .WithCompilationAssemblies(compilationAssemblies) + .WithSourceReferences(sourceReferences) + .WithRuntimeAssets(libraryExport.RuntimeAssets) + .WithEmbedddedResources(libraryExport.EmbeddedResources) + .WithAnalyzerReference(analyzerReferences) + .WithResourceAssemblies(libraryExport.ResourceAssemblies) + .Build(); } private IEnumerable TrimAssetGroups(IEnumerable runtimeAssemblyGroups, diff --git a/src/Microsoft.DotNet.ProjectModel/Files/PatternGroup.cs b/src/Microsoft.DotNet.ProjectModel/Files/PatternGroup.cs index 80c8e81577..b821be3485 100644 --- a/src/Microsoft.DotNet.ProjectModel/Files/PatternGroup.cs +++ b/src/Microsoft.DotNet.ProjectModel/Files/PatternGroup.cs @@ -8,10 +8,15 @@ using Microsoft.DotNet.ProjectModel.FileSystemGlobbing; using Newtonsoft.Json.Linq; +using FourStringTuple = System.Tuple; + namespace Microsoft.DotNet.ProjectModel.Files { public class PatternGroup { + private static readonly Dictionary> s_resolvedFilesCache = + new Dictionary>(); + private readonly List _excludeGroups = new List(); private readonly Matcher _matcher = new Matcher(); @@ -80,15 +85,47 @@ public PatternGroup ExcludeGroup(PatternGroup group) return this; } - public IEnumerable SearchFiles(string rootPath) + public IEnumerable SearchFiles(string rootDirectory) + { + var patternUnionKey = new FourStringTuple(rootDirectory, + (IncludePatterns.Any() ? string.Join(", ", IncludePatterns): ""), + (IncludeLiterals.Any() ? string.Join(", ", IncludeLiterals) : ""), + (ExcludePatterns.Any() ? string.Join(", ", ExcludePatterns) : "")); + + IEnumerable resolvedFiles; + lock (s_resolvedFilesCache) + { + if (s_resolvedFilesCache.TryGetValue(patternUnionKey, out resolvedFiles)) + { + return resolvedFiles; + } + } + + resolvedFiles = ResolveFilesFromPatterns(rootDirectory, IncludePatterns, IncludeLiterals, ExcludePatterns); + + lock (s_resolvedFilesCache) + { + s_resolvedFilesCache.Add(patternUnionKey, resolvedFiles); + } + + return resolvedFiles; + } + + private IEnumerable ResolveFilesFromPatterns( + string rootDirectory, + IEnumerable includePatterns, + IEnumerable includeLiterals, + IEnumerable excludePatterns) { + IEnumerable resolvedFiles; + // literal included files are added at the last, but the search happens early // so as to make the process fail early in case there is missing file. fail early // helps to avoid unnecessary globing for performance optimization var literalIncludedFiles = new List(); foreach (var literalRelativePath in IncludeLiterals) { - var fullPath = Path.GetFullPath(Path.Combine(rootPath, literalRelativePath)); + var fullPath = Path.GetFullPath(Path.Combine(rootDirectory, literalRelativePath)); if (!File.Exists(fullPath)) { @@ -100,7 +137,7 @@ public IEnumerable SearchFiles(string rootPath) } // globing files - var globbingResults = _matcher.GetResultsInFullPath(rootPath); + var globbingResults = _matcher.GetResultsInFullPath(rootDirectory); // if there is no results generated in globing, skip excluding other groups // for performance optimization. @@ -108,16 +145,25 @@ public IEnumerable SearchFiles(string rootPath) { foreach (var group in _excludeGroups) { - globbingResults = globbingResults.Except(group.SearchFiles(rootPath)); + globbingResults = globbingResults.Except(group.SearchFiles(rootDirectory)); } } - return globbingResults.Concat(literalIncludedFiles).Distinct(); + resolvedFiles = globbingResults.Concat(literalIncludedFiles).Distinct(); + return resolvedFiles; } public override string ToString() { return string.Format("Pattern group: Literals [{0}] Includes [{1}] Excludes [{2}]", string.Join(", ", IncludeLiterals), string.Join(", ", IncludePatterns), string.Join(", ", ExcludePatterns)); } + + public static void ClearCache() + { + if (s_resolvedFilesCache != null) + { + s_resolvedFilesCache.Clear(); + } + } } } diff --git a/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs b/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs index 14a6d17f9f..a6c8acf840 100644 --- a/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs +++ b/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs @@ -14,6 +14,9 @@ namespace Microsoft.DotNet.ProjectModel { public class ProjectContext { + private readonly Dictionary, LibraryExporter> _cachedExporters = + new Dictionary, LibraryExporter>(); + private string[] _runtimeFallbacks; public ProjectContextIdentity Identity { get; } @@ -71,18 +74,34 @@ internal ProjectContext( public LibraryExporter CreateExporter(string configuration, string buildBasePath = null) { + LibraryExporter exporter; + var libraryExporterCacheKey = Tuple.Create( + configuration ?? "", + buildBasePath ?? "", + RootDirectory); + + if (_cachedExporters.TryGetValue(libraryExporterCacheKey, out exporter)) + { + return exporter; + } + if (IsPortable && RuntimeIdentifier != null && _runtimeFallbacks == null) { var graph = RuntimeGraphCollector.Collect(LibraryManager.GetLibraries()); _runtimeFallbacks = graph.ExpandRuntime(RuntimeIdentifier).ToArray(); } - return new LibraryExporter(RootProject, + + exporter = new LibraryExporter(RootProject, LibraryManager, configuration, RuntimeIdentifier, _runtimeFallbacks, buildBasePath, RootDirectory); + + _cachedExporters[libraryExporterCacheKey] = exporter; + + return exporter; } /// diff --git a/src/Microsoft.DotNet.ProjectModel/ProjectModelPlatformExtensions.cs b/src/Microsoft.DotNet.ProjectModel/ProjectModelPlatformExtensions.cs index 5f6ea1b94e..dd407e1fee 100644 --- a/src/Microsoft.DotNet.ProjectModel/ProjectModelPlatformExtensions.cs +++ b/src/Microsoft.DotNet.ProjectModel/ProjectModelPlatformExtensions.cs @@ -30,39 +30,73 @@ private static void CollectDependencies(IDictionary expor var export = exports[dependency.Name]; if (export.Library.Identity.Version.Equals(dependency.VersionRange.MinVersion)) { - exclusionList.Add(export.Library.Identity.Name); - CollectDependencies(exports, export.Library.Dependencies, exclusionList); + if (exclusionList.Add(export.Library.Identity.Name)) + { + CollectDependencies(exports, export.Library.Dependencies, exclusionList); + } } } } public static HashSet GetTypeBuildExclusionList(this ProjectContext context, IDictionary exports) { - var acceptedExports = new HashSet(); + var rootProject = context.RootProject; + var buildExports = new HashSet(); + var nonBuildExports = new HashSet(); - // Accept the root project, obviously :) - acceptedExports.Add(context.RootProject.Identity.Name); + var nonBuildExportsToSearch = new Stack(); + var buildExportsToSearch = new Stack(); - // Walk all dependencies, tagging exports. But don't walk through Build dependencies. - CollectNonBuildDependencies(exports, context.RootProject.Dependencies, acceptedExports); + LibraryExport export; + string exportName; - // Whatever is left in exports was brought in ONLY by a build dependency - var exclusionList = new HashSet(exports.Keys); - exclusionList.ExceptWith(acceptedExports); - return exclusionList; - } + // Root project is non-build + nonBuildExportsToSearch.Push(rootProject.Identity.Name); + nonBuildExports.Add(rootProject.Identity.Name); - private static void CollectNonBuildDependencies(IDictionary exports, IEnumerable dependencies, HashSet acceptedExports) - { - foreach (var dependency in dependencies) + // Mark down all nonbuild exports and all of their dependencies + // Mark down build exports to come back to them later + while (nonBuildExportsToSearch.Count > 0) { - var export = exports[dependency.Name]; - if (!dependency.Type.Equals(LibraryDependencyType.Build)) + exportName = nonBuildExportsToSearch.Pop(); + export = exports[exportName]; + + foreach (var dependency in export.Library.Dependencies) + { + if (!dependency.Type.Equals(LibraryDependencyType.Build)) + { + if (!nonBuildExports.Contains(dependency.Name)) + { + nonBuildExportsToSearch.Push(dependency.Name); + nonBuildExports.Add(dependency.Name); + } + } + else + { + buildExportsToSearch.Push(dependency.Name); + } + } + } + + // Go through exports marked build and their dependencies + // For Exports not marked as non-build, mark them down as build + while (buildExportsToSearch.Count > 0) + { + exportName = buildExportsToSearch.Pop(); + export = exports[exportName]; + + buildExports.Add(exportName); + + foreach (var dependency in export.Library.Dependencies) { - acceptedExports.Add(export.Library.Identity.Name); - CollectNonBuildDependencies(exports, export.Library.Dependencies, acceptedExports); + if (!nonBuildExports.Contains(dependency.Name)) + { + buildExportsToSearch.Push(dependency.Name); + } } } + + return buildExports; } public static IEnumerable FilterExports(this IEnumerable exports, HashSet exclusionList) diff --git a/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectContextSnapshot.cs b/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectContextSnapshot.cs index 1307714d0e..4ce798fc6a 100644 --- a/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectContextSnapshot.cs +++ b/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectContextSnapshot.cs @@ -26,6 +26,9 @@ internal class ProjectContextSnapshot public static ProjectContextSnapshot Create(ProjectContext context, string configuration, IEnumerable previousSearchPaths) { + // Clear cached file glob results + PatternGroup.ClearCache(); + var snapshot = new ProjectContextSnapshot(); var allDependencyDiagnostics = new List(); @@ -78,6 +81,9 @@ public static ProjectContextSnapshot Create(ProjectContext context, string confi private static IEnumerable GetSourceFiles(ProjectContext context, string configuration) { + // Clear cached glob results + PatternGroup.ClearCache(); + var compilerOptions = context.ProjectFile.GetCompilerOptions(context.TargetFramework, configuration); if (compilerOptions.CompileInclude == null) diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/GivenAProjectContext.cs b/test/Microsoft.DotNet.ProjectModel.Tests/GivenAProjectContext.cs new file mode 100644 index 0000000000..77c15abdac --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/GivenAProjectContext.cs @@ -0,0 +1,72 @@ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Compilation; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; +using System.Linq; +using FluentAssertions; +using Microsoft.DotNet.Tools.Test.Utilities; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectModel.Tests +{ + public class GivenAProjectContext : TestBase + { + [Fact] + public void It_caches_library_exporter_when_configuration_and_buildBasePath_keep_value() + { + var projectContext = GetProjectContext(); + + var configurations = new string[] { "TestConfig", "TestConfig", "TestConfig2", "TestConfig2" }; + var buildBasePaths = new string[] { null, AppContext.BaseDirectory, null, AppContext.BaseDirectory }; + + for(int i=0; i(); + + for(int i=0; i