Skip to content

[Performance] ProjectPathProvider.GetRelativePath allocates Uri objects in hot loop #67

Description

@mfogliatto

Description

ProjectPathProvider.GetRelativePath() creates two Uri objects and performs URI escaping/unescaping on every call. This method is called inside the inner loop of ProjectPathViolationDetector.GetViolationsFrom() (line 48) — once per reference × matching rule combination.

Affected Files

  • src/ReferenceCop/Providers/ProjectPathProvider.csGetRelativePath(), lines 35-36 (new Uri() allocations)
  • src/ReferenceCop/Detectors/ProjectPathViolationDetector.cs — line 48 (inner loop call site)

Impact

  • Allocation pressure: Two Uri allocations + Path.GetFullPath + Uri.UnescapeDataString per inner loop iteration. For 20 rules × 50 references, that's up to 2,000 Uri objects per project build.
  • Redundant computation: The repositoryRoot URI (line 35) is the same every call but recomputed each time. The projectFilePath URI for the same reference is also recomputed if multiple rules match.
  • GC pressure: Uri is a relatively heavy object (parses/normalizes the URI string internally), and these short-lived allocations add GC pressure during build.

Suggested Optimization

  1. Cache the repository root URI as a field computed once in the constructor:
public class ProjectPathProvider : IProjectPathProvider
{
    private readonly Uri repositoryRootUri;
    private readonly string repositoryRoot;

    public ProjectPathProvider(string repositoryRoot)
    {
        this.repositoryRoot = repositoryRoot ?? throw new ArgumentNullException(nameof(repositoryRoot));
        var normalized = Path.GetFullPath(repositoryRoot)
            .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
            + Path.DirectorySeparatorChar;
        this.repositoryRootUri = new Uri(normalized);
    }

    public string GetRelativePath(string projectFilePath)
    {
        var fullPath = Path.GetFullPath(projectFilePath);
        var projectUri = new Uri(fullPath);
        var relativePath = Uri.UnescapeDataString(
            this.repositoryRootUri.MakeRelativeUri(projectUri).ToString());
        return relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
    }
}
  1. Consider using Path.GetRelativePath (available since .NET Standard 2.1 / .NET Core 2.0), which avoids Uri altogether:
public string GetRelativePath(string projectFilePath)
{
    return Path.GetRelativePath(this.repositoryRoot, projectFilePath);
}

This eliminates all Uri allocations and is the idiomatic .NET approach for relative path computation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance optimization

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions