Skip to content

Commit

Permalink
Reduce allocations in MemoryFileSystem and ZipArchiveFileSystem
Browse files Browse the repository at this point in the history
  • Loading branch information
GerardSmit committed Dec 4, 2024
1 parent ba7197e commit 217a147
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 29 deletions.
2 changes: 1 addition & 1 deletion src/Zio.Tests/TestSearchPattern.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public void TestExpectedExceptions()
{
var searchPattern = "*";
var search = SearchPattern.Parse(ref path, ref searchPattern);
Assert.Throws<ArgumentNullException>(() => search.Match(null));
Assert.Throws<ArgumentNullException>(() => search.Match(((string)null)!));
}
}

Expand Down
44 changes: 44 additions & 0 deletions src/Zio.Tests/TestUPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,22 @@ public void TestGetDirectory(string path1, string expectedDir)
Assert.Equal(expectedDir, result);
}

[Theory]
[InlineData("", "")]
[InlineData("/", "")]
[InlineData("/a", "/")]
[InlineData("/a/b", "/a")]
[InlineData("/a/b/c.txt", "/a/b")]
[InlineData("a", "")]
[InlineData("../a", "..")]
[InlineData("../../a/b", "../../a")]
public void TestGetDirectoryAsSpan(string path1, string expectedDir)
{
var path = (UPath)path1;
var result = path.GetDirectoryAsSpan().ToString();
Assert.Equal(expectedDir, result);
}

[Theory]
[InlineData("", ".txt", "")]
[InlineData("/", ".txt", "/.txt")]
Expand Down Expand Up @@ -324,6 +340,34 @@ public void TestSplit()
Assert.Equal(new List<string>() { "a", "b", "c" }, ((UPath)"a/b/c").Split());
}

[Fact]
public void TestSplitSpan()
{
Assert.Equal(new List<string>(), ToList((UPath)""));
Assert.Equal(new List<string>(), ToList((UPath)"/"));
Assert.Equal(new List<string>() { "a" }, ToList((UPath)"/a"));
Assert.Equal(new List<string>() {"a", "b", "c"}, ToList((UPath) "/a/b/c"));
Assert.Equal(new List<string>() { "a" }, ToList((UPath)"a"));
Assert.Equal(new List<string>() { "a", "b" }, ToList((UPath)"a/b"));
Assert.Equal(new List<string>() { "a", "b", "c" }, ToList((UPath)"a/b/c"));
return;

List<string> ToList(UPath path)
{
var enumerator = path.SpanSplit();
var list = new List<string>(enumerator.Count);

foreach (var span in enumerator)
{
list.Add(span.ToString());
}

Assert.Equal(enumerator.Count, list.Count);

return list;
}
}


[Fact]
public void TestExpectedException()
Expand Down
2 changes: 1 addition & 1 deletion src/Zio.Tests/Zio.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net472</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net472</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>10</LangVersion>
</PropertyGroup>
Expand Down
16 changes: 10 additions & 6 deletions src/Zio/FileSystems/MemoryFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1304,7 +1304,7 @@ private NodeResult EnterFindNode(UPath path, FindNodeFlags flags, FileShare? sha
var isRequiringExclusiveLockForParent = (flags & (FindNodeFlags.CreatePathIfNotExist | FindNodeFlags.KeepParentNodeExclusive)) != 0;

var parentNode = _rootDirectory;
var names = path.Split();
var names = path.SpanSplit();

// Walking down the nodes in locking order:
// /a/b/c.txt
Expand All @@ -1324,21 +1324,25 @@ private NodeResult EnterFindNode(UPath path, FindNodeFlags flags, FileShare? sha
isParentLockTaken = true;
}

for (var i = 0; i < names.Count && parentNode != null; i++)
for (var i = 0; names.MoveNext() && parentNode != null; i++)
{
var name = names[i];
ReadOnlySpan<char> name = names.Current;
bool isLast = i + 1 == names.Count;

DirectoryNode? nextParent = null;
bool isNextParentLockTaken = false;
try
{
FileSystemNode? subNode;
if (!parentNode.Children.TryGetValue(name, out subNode))
#if HAS_ALTERNATEEQUALITYCOMPARER
if (!parentNode.Children.GetAlternateLookup<ReadOnlySpan<char>>().TryGetValue(name, out subNode))
#else
if (!parentNode.Children.TryGetValue(name.ToString(), out subNode))
#endif
{
if ((flags & FindNodeFlags.CreatePathIfNotExist) != 0)
{
subNode = new DirectoryNode(this, parentNode, name);
subNode = new DirectoryNode(this, parentNode, name.ToString());
}
}
else
Expand All @@ -1361,7 +1365,7 @@ private NodeResult EnterFindNode(UPath path, FindNodeFlags flags, FileShare? sha
flags &= ~(FindNodeFlags.KeepParentNodeExclusive | FindNodeFlags.KeepParentNodeShared);
}

result = new NodeResult(parentNode, subNode, name, flags);
result = new NodeResult(parentNode, subNode, name.ToString(), flags);

// The last subnode may be null but we still want to return a valid parent
// otherwise, lock the final node if necessary
Expand Down
37 changes: 23 additions & 14 deletions src/Zio/FileSystems/ZipArchiveFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,18 +196,18 @@ protected override void CopyFileImpl(UPath srcPath, UPath destPath, bool overwri

if (srcEntry == null)
{
if (!DirectoryExistsImpl(srcPath.GetDirectory()))
if (!DirectoryExistsImpl(srcPath.GetDirectoryAsSpan()))
{
throw new DirectoryNotFoundException(srcPath.GetDirectory().FullName);
}

throw FileSystemExceptionHelper.NewFileNotFoundException(srcPath);
}

var parentDirectory = destPath.GetDirectory();
var parentDirectory = destPath.GetDirectoryAsSpan();
if (!DirectoryExistsImpl(parentDirectory))
{
throw FileSystemExceptionHelper.NewDirectoryNotFoundException(parentDirectory);
throw FileSystemExceptionHelper.NewDirectoryNotFoundException(parentDirectory.ToString());
}

if (DirectoryExistsImpl(destPath))
Expand Down Expand Up @@ -261,12 +261,12 @@ protected override void CreateDirectoryImpl(UPath path)
throw FileSystemExceptionHelper.NewDestinationDirectoryExistException(path);
}

var parentPath = new UPath(GetParent(path.FullName));
if (parentPath != "")
var parentPath = GetParent(path.AsSpan());
if (!parentPath.IsEmpty)
{
if (!DirectoryExistsImpl(parentPath))
{
CreateDirectoryImpl(parentPath);
CreateDirectoryImpl(parentPath.ToString());
}
}

Expand Down Expand Up @@ -405,7 +405,12 @@ protected override void DeleteFileImpl(UPath path)
/// <inheritdoc />
protected override bool DirectoryExistsImpl(UPath path)
{
if (path.FullName is "/" or "\\" or "")
return DirectoryExistsImpl(path.FullName.AsSpan());
}

private bool DirectoryExistsImpl(ReadOnlySpan<char> path)
{
if (path is "/" or "\\" or "")
{
return true;
}
Expand All @@ -414,7 +419,11 @@ protected override bool DirectoryExistsImpl(UPath path)

try
{
return _entries.TryGetValue(path, out var entry) && entry.IsDirectory;
#if HAS_ALTERNATEEQUALITYCOMPARER
return _entries.GetAlternateLookup<ReadOnlySpan<char>>().TryGetValue(path, out var entry) && entry.IsDirectory;
#else
return _entries.TryGetValue(path.ToString(), out var entry) && entry.IsDirectory;
#endif
}
finally
{
Expand Down Expand Up @@ -651,7 +660,7 @@ protected override void MoveFileImpl(UPath srcPath, UPath destPath)
{
var srcEntry = GetEntry(srcPath) ?? throw FileSystemExceptionHelper.NewFileNotFoundException(srcPath);

if (!DirectoryExistsImpl(destPath.GetDirectory()))
if (!DirectoryExistsImpl(destPath.GetDirectoryAsSpan()))
{
throw FileSystemExceptionHelper.NewDirectoryNotFoundException(destPath.GetDirectory());
}
Expand Down Expand Up @@ -706,7 +715,7 @@ protected override Stream OpenFileImpl(UPath path, FileMode mode, FileAccess acc
}
else
{
if (!DirectoryExistsImpl(path.GetDirectory()))
if (!DirectoryExistsImpl(path.GetDirectoryAsSpan()))
{
throw FileSystemExceptionHelper.NewDirectoryNotFoundException(path.GetDirectory());
}
Expand Down Expand Up @@ -911,18 +920,18 @@ private ZipArchiveEntry CreateEntry(UPath path, bool isDirectory = false)

private static readonly char[] s_slashChars = { '/', '\\' };

private static string GetName(ZipArchiveEntry entry)
private static ReadOnlySpan<char> GetName(ZipArchiveEntry entry)
{
var name = entry.FullName.TrimEnd(s_slashChars);
var index = name.LastIndexOfAny(s_slashChars);
return name.Substring(index + 1);
return index == -1 ? name.AsSpan() : name.AsSpan(index + 1);
}

private static string GetParent(string path)
private static ReadOnlySpan<char> GetParent(ReadOnlySpan<char> path)
{
path = path.TrimEnd(s_slashChars);
var lastIndex = path.LastIndexOfAny(s_slashChars);
return lastIndex == -1 ? "" : path.Substring(0, lastIndex);
return lastIndex == -1 ? ReadOnlySpan<char>.Empty : path.Slice(0, lastIndex);
}

private FileSystemEventDispatcher<FileSystemWatcher>? TryGetDispatcher()
Expand Down
17 changes: 17 additions & 0 deletions src/Zio/SearchPattern.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.

using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

Expand Down Expand Up @@ -43,6 +44,22 @@ public bool Match(string name)
return _exactMatch != null ? _exactMatch == name : _regexMatch is null || _regexMatch.IsMatch(name);
}

/// <summary>
/// Tries to match the specified path with this instance.
/// </summary>
/// <param name="name">The path to match.</param>
/// <returns><c>true</c> if the path was matched, <c>false</c> otherwise.</returns>
public bool Match(ReadOnlySpan<char> name)
{
#if NET7_0_OR_GREATER
// if _execMatch is null and _regexMatch is null, we have a * match
return _exactMatch != null ? name.SequenceEqual(_exactMatch) : _regexMatch is null || _regexMatch.IsMatch(name);
#else
// Regex.Match(ReadOnlySpan<char>) is only available starting from .NET
return Match(name.ToString());
#endif
}

/// <summary>
/// Parses and normalize the specified path and <see cref="SearchPattern"/>.
/// </summary>
Expand Down
19 changes: 19 additions & 0 deletions src/Zio/UPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ public static explicit operator string(UPath path)
return path.FullName;
}

/// <summary>
/// Performs an explicit conversion from <see cref="UPath"/> to <see cref="ReadOnlySpan{Char}"/>.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>The result as a span of the conversion.</returns>
public static explicit operator ReadOnlySpan<char>(UPath path)
{
return path.FullName.AsSpan();
}

/// <summary>
/// Combines two paths into a new path.
/// </summary>
Expand Down Expand Up @@ -322,6 +332,15 @@ public override string ToString()
return FullName;
}

/// <summary>
/// Creates a new readonly span from this path.
/// </summary>
/// <returns>A new readonly span from this path.</returns>
public ReadOnlySpan<char> AsSpan()
{
return FullName.AsSpan();
}

/// <summary>
/// Tries to parse the specified string into a <see cref="UPath"/>
/// </summary>
Expand Down
41 changes: 41 additions & 0 deletions src/Zio/UPathComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.

using System.Diagnostics;

namespace Zio;

public class UPathComparer : IComparer<UPath>, IEqualityComparer<UPath>
#if HAS_ALTERNATEEQUALITYCOMPARER
, IAlternateEqualityComparer<ReadOnlySpan<char>, UPath>, IAlternateEqualityComparer<string, UPath>
#endif
{
public static readonly UPathComparer Ordinal = new(StringComparer.Ordinal);
public static readonly UPathComparer OrdinalIgnoreCase = new(StringComparer.OrdinalIgnoreCase);
Expand All @@ -16,6 +21,10 @@ public class UPathComparer : IComparer<UPath>, IEqualityComparer<UPath>
private UPathComparer(StringComparer comparer)
{
_comparer = comparer;

#if HAS_ALTERNATEEQUALITYCOMPARER
Debug.Assert(_comparer is IAlternateEqualityComparer<ReadOnlySpan<char>, string>);
#endif
}

public int Compare(UPath x, UPath y)
Expand All @@ -32,4 +41,36 @@ public int GetHashCode(UPath obj)
{
return _comparer.GetHashCode(obj.FullName);
}

#if HAS_ALTERNATEEQUALITYCOMPARER
bool IAlternateEqualityComparer<ReadOnlySpan<char>, UPath>.Equals(ReadOnlySpan<char> alternate, UPath other)
{
return ((IAlternateEqualityComparer<ReadOnlySpan<char>, string>)_comparer).Equals(alternate, other.FullName);
}

int IAlternateEqualityComparer<ReadOnlySpan<char>, UPath>.GetHashCode(ReadOnlySpan<char> alternate)
{
return ((IAlternateEqualityComparer<ReadOnlySpan<char>, string>)_comparer).GetHashCode(alternate);
}

UPath IAlternateEqualityComparer<ReadOnlySpan<char>, UPath>.Create(ReadOnlySpan<char> alternate)
{
return ((IAlternateEqualityComparer<ReadOnlySpan<char>, string>)_comparer).Create(alternate);
}

bool IAlternateEqualityComparer<string, UPath>.Equals(string alternate, UPath other)
{
return _comparer.Equals(alternate, other.FullName);
}

int IAlternateEqualityComparer<string, UPath>.GetHashCode(string alternate)
{
return _comparer.GetHashCode(alternate);
}

UPath IAlternateEqualityComparer<string, UPath>.Create(string alternate)
{
return alternate;
}
#endif
}
Loading

0 comments on commit 217a147

Please sign in to comment.