mfogliatto / mfogliatto/ReferenceCop
[Performance] ProjectPathProvider.GetRelativePath allocates Uri objects in hot loop
- 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
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