Skip to content
This repository has been archived by the owner on Apr 20, 2023. It is now read-only.

Performance Fixes #3942

Closed
wants to merge 16 commits into from
3 changes: 2 additions & 1 deletion src/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 15 additions & 4 deletions src/Microsoft.DotNet.ProjectModel/Compilation/LibraryExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class LibraryExporter
private readonly string _buildBasePath;
private readonly string _solutionRootPath;

private IEnumerable<LibraryExport> _cachedExports;

public LibraryExporter(ProjectDescription rootProject,
LibraryManager manager,
string configuration,
Expand Down Expand Up @@ -84,16 +86,25 @@ public IEnumerable<LibraryExport> GetDependencies(LibraryType type)
/// </summary>
private IEnumerable<LibraryExport> ExportLibraries(Func<LibraryDescription, bool> condition)
{
var seenMetadataReferences = new HashSet<string>();
IEnumerable<LibraryExport> exports = _cachedExports ?? (_cachedExports = CalculateAllExports().ToArray());

This comment was marked as spam.

This comment was marked as spam.


// Iterate over libraries in the library manager
foreach (var library in LibraryManager.GetLibraries())
foreach (var export in exports)
{
if (!condition(library))
if (!condition(export.Library))
{
continue;
}
yield return export;
}
}

private IEnumerable<LibraryExport> CalculateAllExports()
{
var seenMetadataReferences = new HashSet<string>();

// Iterate over libraries in the library manager
foreach (var library in LibraryManager.GetLibraries())
{
var compilationAssemblies = new List<LibraryAsset>();
var sourceReferences = new List<LibraryAsset>();
var analyzerReferences = new List<AnalyzerReference>();
Expand Down
45 changes: 40 additions & 5 deletions src/Microsoft.DotNet.ProjectModel/Files/PatternGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ namespace Microsoft.DotNet.ProjectModel.Files
{
public class PatternGroup
{
private static readonly Dictionary<string, IEnumerable<string>> s_resolvedFilesCache = new Dictionary<string, IEnumerable<string>>();

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.


private readonly List<PatternGroup> _excludeGroups = new List<PatternGroup>();
private readonly Matcher _matcher = new Matcher();

Expand Down Expand Up @@ -80,15 +82,47 @@ public PatternGroup ExcludeGroup(PatternGroup group)
return this;
}

public IEnumerable<string> SearchFiles(string rootPath)
public IEnumerable<string> SearchFiles(string rootDirectory)
{
var patternUnionKey = rootDirectory
+ (IncludePatterns.Any() ? " ++ " + string.Join(", ", IncludePatterns): "")
+ (IncludeLiterals.Any() ? " + " + string.Join(", ", IncludeLiterals) : "")
+ (ExcludePatterns.Any() ? " -- " + string.Join(", ", ExcludePatterns) : "");

IEnumerable<string> 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<string> ResolveFilesFromPatterns(
string rootDirectory,
IEnumerable<string> includePatterns,
IEnumerable<string> includeLiterals,
IEnumerable<string> excludePatterns)
{
IEnumerable<string> 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<string>();
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))
{
Expand All @@ -100,19 +134,20 @@ public IEnumerable<string> 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.
if (globbingResults.Any())
{
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()
Expand Down
17 changes: 16 additions & 1 deletion src/Microsoft.DotNet.ProjectModel/ProjectContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace Microsoft.DotNet.ProjectModel
{
public class ProjectContext
{
private readonly Dictionary<string, LibraryExporter> _cachedExporters = new Dictionary<string, LibraryExporter>();

private string[] _runtimeFallbacks;

public ProjectContextIdentity Identity { get; }
Expand Down Expand Up @@ -71,18 +73,31 @@ internal ProjectContext(

public LibraryExporter CreateExporter(string configuration, string buildBasePath = null)
{
LibraryExporter exporter;
var libraryExporterCacheKey = "+ " + (configuration ?? "") + " - " + (buildBasePath ?? "");

This comment was marked as spam.

This comment was marked as spam.


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;
}

/// <summary>
Expand Down
70 changes: 52 additions & 18 deletions src/Microsoft.DotNet.ProjectModel/ProjectModelPlatformExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using Microsoft.DotNet.ProjectModel.Compilation;
using Microsoft.DotNet.ProjectModel.Graph;
using System;

This comment was marked as spam.


namespace Microsoft.DotNet.ProjectModel
{
Expand All @@ -28,7 +29,8 @@ private static void CollectDependencies(IDictionary<string, LibraryExport> expor
foreach (var dependency in dependencies)
{
var export = exports[dependency.Name];
if (export.Library.Identity.Version.Equals(dependency.VersionRange.MinVersion))
if (export.Library.Identity.Version.Equals(dependency.VersionRange.MinVersion)
&& !exclusionList.Contains(dependency.Name))

This comment was marked as spam.

This comment was marked as spam.

{
exclusionList.Add(export.Library.Identity.Name);
CollectDependencies(exports, export.Library.Dependencies, exclusionList);
Expand All @@ -38,31 +40,63 @@ private static void CollectDependencies(IDictionary<string, LibraryExport> expor

public static HashSet<string> GetTypeBuildExclusionList(this ProjectContext context, IDictionary<string, LibraryExport> exports)
{
var acceptedExports = new HashSet<string>();
var rootProject = context.RootProject;
var buildExports = new HashSet<string>();
var nonBuildExports = new HashSet<string>();

// Accept the root project, obviously :)
acceptedExports.Add(context.RootProject.Identity.Name);
var nonBuildExportsToSearch = new Stack<string>();
var buildExportsToSearch = new Stack<string>();

// 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<string>(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<string, LibraryExport> exports, IEnumerable<LibraryRange> dependencies, HashSet<string> 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<LibraryExport> FilterExports(this IEnumerable<LibraryExport> exports, HashSet<string> exclusionList)
Expand Down
72 changes: 72 additions & 0 deletions test/Microsoft.DotNet.ProjectModel.Tests/GivenAProjectContext.cs
Original file line number Diff line number Diff line change
@@ -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<configurations.Length; ++i)
{
var configuration = configurations[i];
var buildBasePath = buildBasePaths[i];

var exporter1 = projectContext.CreateExporter(configuration, buildBasePath);
var exporter2 = projectContext.CreateExporter(configuration, buildBasePath);

ReferenceEquals(exporter1, exporter2).Should().BeTrue();
}
}

[Fact]
public void It_does_not_cache_library_exporter_when_configuration_or_buildBasePath_changes()
{
var projectContext = GetProjectContext();

var configurations = new string[] { "TestConfig", "TestConfig", "TestConfig2", "TestConfig2" };
var buildBasePaths = new string[] { null, AppContext.BaseDirectory, null, AppContext.BaseDirectory };
var previousExporters = new List<LibraryExporter>();

for(int i=0; i<configurations.Length; ++i)
{
var configuration = configurations[i];
var buildBasePath = buildBasePaths[i];
var exporter = projectContext.CreateExporter(configuration, buildBasePath);

foreach (var previousExporter in previousExporters)
{
ReferenceEquals(exporter, previousExporter).Should().BeFalse();
}

previousExporters.Add(exporter);
}
}

private ProjectContext GetProjectContext()
{
var testInstance = TestAssetsManager.CreateTestInstance("TestAppSimple")
.WithLockFiles();

return ProjectContext.Create(testInstance.TestRoot, FrameworkConstants.CommonFrameworks.NetCoreApp10);
}
}
}
3 changes: 2 additions & 1 deletion test/Microsoft.DotNet.ProjectModel.Tests/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"target": "project"
},
"xunit": "2.1.0",
"dotnet-test-xunit": "1.0.0-rc2-192208-24"
"dotnet-test-xunit": "1.0.0-rc2-192208-24",
"NuGet.Frameworks": "3.5.0-beta2-1484"
},
"frameworks": {
"netcoreapp1.0": {
Expand Down