Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance optimization of FileSystem.CreateDirectory on windows #62860

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7199fef
Usage of end-expression
deeprobin Dec 15, 2021
e8ce9ab
Usage of target-typed new
deeprobin Dec 15, 2021
9c7606d
Code readbility improvements
deeprobin Dec 15, 2021
5c54b2c
Usage of implicit-operator string to ReadOnlySpan<char> conversion
deeprobin Dec 15, 2021
b52b72e
re-use ReadOnlySpan<char>
deeprobin Dec 15, 2021
3810cd7
Usage of logical pattern
deeprobin Dec 15, 2021
4d4f223
Further code readability improvements
deeprobin Dec 15, 2021
11590fa
Remove unnessecary `add`
deeprobin Dec 15, 2021
6986b2c
Reduce 2 FillAttributeInfo calls into one call
deeprobin Dec 15, 2021
fd5af6c
Remove unnessecary duplicate logic
deeprobin Dec 15, 2021
2b59aac
Remove impossible check
deeprobin Dec 17, 2021
4a843a3
Further span improvements
deeprobin Dec 21, 2021
948e395
Span Length improvements
deeprobin Dec 21, 2021
322c102
Undo fs-call duplication
deeprobin Dec 21, 2021
1be01c5
Eliminate duplicate Kernel32 calls
deeprobin Dec 21, 2021
6e5d9b5
Bugfix
deeprobin Dec 21, 2021
ea72436
Merge branch 'main' into issue-61954
deeprobin Dec 28, 2021
b5e4384
Reduce win32-calls
deeprobin Dec 28, 2021
687faca
Reset work to produce better results
deeprobin Dec 28, 2021
33bd739
Add PathExists method
deeprobin Dec 28, 2021
69903c4
Add removalIndex local
deeprobin Dec 28, 2021
5956664
Usage of only one ReadOnlySpan<char>
deeprobin Dec 28, 2021
d4854aa
Usage of ReadOnlySpan<char>.Slice instead of string.Substring
deeprobin Dec 28, 2021
bb80a81
Remove unnessecary path check & remove unnessecary stack allocation b…
deeprobin Dec 28, 2021
f37b440
List preallocation
deeprobin Dec 28, 2021
e103c01
Style changes
deeprobin Dec 28, 2021
0945f16
Style changes and only one Span usage
deeprobin Dec 28, 2021
199468d
Remove unnessecary GetAttributeData method
deeprobin Dec 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ public static bool DirectoryExists(string? fullPath)
return DirectoryExists(fullPath, out int lastError);
}

private static Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA GetAttributeData(string? path, out int lastError)
{
Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default;
lastError = FillAttributeInfo(path, ref data, true);

return data;
}

private static bool DirectoryExists(string? path, out int lastError)
{
Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.IO;
using System.Text;

namespace System.IO
{
Expand All @@ -26,39 +22,41 @@ public static unsafe void CreateDirectory(string fullPath, byte[]? securityDescr
return;
}

List<string> stackDir = new List<string>();
List<string> stackDir = new();

// Attempt to figure out which directories don't exist, and only
// create the ones we need. Note that FileExists may fail due
// to Win32 ACL's preventing us from seeing a directory, and this
// isn't threadsafe.

bool somepathexists = false;
int length = fullPath.Length;
bool somePathExists = false;

ReadOnlySpan<char> fullPathSpan = fullPath.AsSpan();
int length = fullPathSpan.Length;

// We need to trim the trailing slash or the code will try to create 2 directories of the same name.
if (length >= 2 && PathInternal.EndsInDirectorySeparator(fullPath.AsSpan()))
if (length >= 2 && PathInternal.EndsInDirectorySeparator(fullPathSpan))
{
length--;
}

int lengthRoot = PathInternal.GetRootLength(fullPath.AsSpan());
int lengthRoot = PathInternal.GetRootLength(fullPathSpan);

if (length > lengthRoot)
{
// Special case root (fullpath = X:\\)
int i = length - 1;
while (i >= lengthRoot && !somepathexists)
while (i >= lengthRoot && !somePathExists)
{
string dir = fullPath.Substring(0, i + 1);
string dir = fullPath[..length];

if (!DirectoryExists(dir)) // Create only the ones missing
{
stackDir.Add(dir);
}
else
{
somepathexists = true;
somePathExists = true;
}

while (i > lengthRoot && !PathInternal.IsDirectorySeparator(fullPath[i]))
Expand All @@ -77,49 +75,64 @@ public static unsafe void CreateDirectory(string fullPath, byte[]? securityDescr

fixed (byte* pSecurityDescriptor = securityDescriptor)
{
Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = new Interop.Kernel32.SECURITY_ATTRIBUTES
Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = new()
{
nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES),
lpSecurityDescriptor = (IntPtr)pSecurityDescriptor
};

while (stackDir.Count > 0)
{
string name = stackDir[stackDir.Count - 1];
string name = stackDir[^1];
stackDir.RemoveAt(stackDir.Count - 1);

r = Interop.Kernel32.CreateDirectory(name, ref secAttrs);
if (!r && (firstError == 0))
if (r || firstError != 0)
{
int currentError = Marshal.GetLastWin32Error();
// While we tried to avoid creating directories that don't
// exist above, there are at least two cases that will
// cause us to see ERROR_ALREADY_EXISTS here. FileExists
// can fail because we didn't have permission to the
// directory. Secondly, another thread or process could
// create the directory between the time we check and the
// time we try using the directory. Thirdly, it could
// fail because the target does exist, but is a file.
if (currentError != Interop.Errors.ERROR_ALREADY_EXISTS)
{
firstError = currentError;
}
else
continue;
}

int currentError = Marshal.GetLastWin32Error();
// While we tried to avoid creating directories that don't
// exist above, there are at least two cases that will
// cause us to see ERROR_ALREADY_EXISTS here. FileExists
// can fail because we didn't have permission to the
// directory. Secondly, another thread or process could
// create the directory between the time we check and the
// time we try using the directory. Thirdly, it could
// fail because the target does exist, but is a file.
if (currentError != Interop.Errors.ERROR_ALREADY_EXISTS)
{
firstError = currentError;
}
else
{
Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = GetAttributeData(name, out currentError);
int errorCode = FillAttributeInfo(fullPath, ref data, true);

bool fileExists = errorCode == 0 &&
data.dwFileAttributes != -1 &&
(data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) == 0;
bool directoryExists = errorCode == 0 &&
data.dwFileAttributes != -1 &&
(data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0;

// If there's a file in this directory's place, or if we have ERROR_ACCESS_DENIED when checking if the directory already exists throw.
if (!fileExists && (directoryExists ||
currentError != Interop.Errors.ERROR_ACCESS_DENIED))
{
// If there's a file in this directory's place, or if we have ERROR_ACCESS_DENIED when checking if the directory already exists throw.
if (FileExists(name) || (!DirectoryExists(name, out currentError) && currentError == Interop.Errors.ERROR_ACCESS_DENIED))
{
firstError = currentError;
errorString = name;
}
continue;
}

firstError = currentError;
errorString = name;
}
}
}

// We need this check to mask OS differences
// Handle CreateDirectory("X:\\") when X: doesn't exist. Similarly for n/w paths.
if ((count == 0) && !somepathexists)
if (count == 0 && !somePathExists)
{
string? root = Path.GetPathRoot(fullPath);

Expand All @@ -133,7 +146,7 @@ public static unsafe void CreateDirectory(string fullPath, byte[]? securityDescr

// Only throw an exception if creating the exact directory we
// wanted failed to work correctly.
if (!r && (firstError != 0))
if (!r && firstError != 0)
{
throw Win32Marshal.GetExceptionForWin32Error(firstError, errorString);
}
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/Common/src/System/IO/PathInternal.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ internal static bool IsPartiallyQualified(ReadOnlySpan<char> path)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsDirectorySeparator(char c)
{
return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
return c is DirectorySeparatorChar or AltDirectorySeparatorChar;
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/Common/src/System/IO/PathInternal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ internal static ReadOnlySpan<char> TrimEndingDirectorySeparator(ReadOnlySpan<cha
/// Returns true if the path ends in a directory separator.
/// </summary>
internal static bool EndsInDirectorySeparator(ReadOnlySpan<char> path) =>
path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]);
path.Length > 0 && IsDirectorySeparator(path[^1]);

internal static string GetLinkTargetFullPath(string path, string pathToTarget)
=> IsPartiallyQualified(pathToTarget.AsSpan()) ?
Expand Down