mfogliatto / mfogliatto/ReferenceCop

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

Open
#67 0 comments 0 reactions 0 assignees View on GitHub
performance
Dominant language
C#
Stars
1
Forks
2
PR merge metrics
No merged PRs in 30d

Description

## 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.cs` — `GetRelativePath()`, 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:

```csharp
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);
}
}
```

2. **Consider using `Path.GetRelativePath`** (available since .NET Standard 2.1 / .NET Core 2.0), which avoids `Uri` altogether:

```csharp
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.

Contributor guide

Open the contributing guide

Research direction

Read GetRelativePath() in src/ReferenceCop/Providers/ProjectPathProvider.cs and its inner-loop call at line 48 of src/ReferenceCop/Detectors/ProjectPathViolationDetector.cs. Compare caching the repository URI with Path.GetRelativePath, then verify relative-path behavior is preserved while repeated Uri allocations are eliminated.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
build-system, performance
Issue type
Refactor
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.