Fast file enumeration for Windows#25426
Conversation
| /// Returns basic information (timestamps and attributes). | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Thunks to NtQueryInformationFile & FileBasicInformation. |
There was a problem hiding this comment.
& needs to be escaped in xml as & (applies throughout these xml doc comments)
|
|
||
| namespace System.IO | ||
| { | ||
| internal partial class FindEnumerable<T> : IEnumerable<T> |
There was a problem hiding this comment.
Any reason this couldn't implement both IEnumerable<T> and IEnumerator<T>, and return itself from GetEnumerator in the common case to avoid the enumerator allocation, like Win32FileSystemEnumerableIterator<TSource> before this change?
There was a problem hiding this comment.
I was thinking of being friendly to Linq queries being run multiple times. I'll think it through some more.
There was a problem hiding this comment.
It should still support being able to call GetEnumerator multiple times... in such cases, you'd create a new instance with initial state.
There was a problem hiding this comment.
It should still support being able to call GetEnumerator multiple times... in such cases, you'd create a new instance with initial state.
Not sure I follow. How would that save an allocation?
There was a problem hiding this comment.
Take a look at Iterator<TSource> (which is the base class of Win32FileSystemEnumerableIterator<TSource>) :
Iterator<TSource> implements both IEnumerable<T> and IEnumerator<T>.
Here's the GetEnumerator implementation:
corefx/src/System.IO.FileSystem/src/System/IO/Iterator.cs
Lines 43 to 54 in db1793c
The common case when using foreach or a LINQ query is to get the enumerable and then immediately call GetEnumerator to get the enumerator to start enumerating through the enumerable (e.g. foreach (string file in Directory.EnumerateFiles(...)) { ... }). If GetEnumerator hasn't already been called and it's the same thread, it can simply return itself. Otherwise, a new instance is created.
This is also how iterators work in C# when you use yield return. Similar iterator code is emitted by the compiler to use the same instance for both the enumerable and enumerator, only allocating a new instance if GetEnumerator if necessary.
There was a problem hiding this comment.
Fair enough- I'll look at utilizing Iterator.
There was a problem hiding this comment.
Whether or not Iterator is used as a base class, the same approach of reusing the enumerable object as the enumerator object for at least the initial enumeration should definitely be done.
There was a problem hiding this comment.
Making the change now.
| int* priorStack = stackalloc int[16]; | ||
| int* currentStack = stackalloc int[16]; | ||
| Span<int> priorMatches = new Span<int>(priorStack, 16); | ||
| Span<int> currentMatches = new Span<int>(currentStack, 16); |
There was a problem hiding this comment.
Any reason these can't use the recently added C# stackalloc/span support, e.g.
Span<int> priorMatches = stackalloc int[16];
Span<int> currentMatches = stackalloc int[16];at which point, it looks like this method would no longer need to be unsafe.
There was a problem hiding this comment.
Wasn't aware of it. Tried switching and it wouldn't let me do the swap between the two anymore (at the end of the method).
There was a problem hiding this comment.
Tried switching and it wouldn't let me do the swap between the two anymore (at the end of the method).
@JeremyKuhne, what error do you get?
@VSadov?
There was a problem hiding this comment.
"P:\repos\corefx\src\System.IO.FileSystem\src\System.IO.FileSystem.csproj" (default target) (1) ->
(CoreCompile target) ->
System\IO\DosMatcher.cs(298,24): error CS8352: Cannot use local 'priorMatches' in this context because it may expose referenced variables outside of their declaration scope [P:\repos
\corefx\src\System.IO.FileSystem\src\System.IO.FileSystem.csproj]
|
|
||
| internal partial class Interop | ||
| { | ||
| public struct BOOLEAN |
There was a problem hiding this comment.
I realize this is nested within an internal type, so it will never actually be public, but I'm curious: should these nested types be public or internal? The visibility of Interop types in this PR are inconsistent. Is there an existing convention?
There was a problem hiding this comment.
Internal is better- I'll fix it
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| internal static bool EqualsOrdinal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, bool ignoreCase = false) | ||
| { | ||
| return first.Length != second.Length ? false : CompareOrdinal(first, second, ignoreCase) == 0; |
There was a problem hiding this comment.
return first.Length == second.Length && CompareOrdinal(first, second, ignoreCase) == 0;| /// </remarks> | ||
| public unsafe static bool MatchPattern(string expression, ReadOnlySpan<char> name, bool ignoreCase = true) | ||
| { | ||
| string debug = new string(name); |
There was a problem hiding this comment.
Was this intended to remain?
| private string _originalUserPath; | ||
| private bool _recursive; | ||
| private FindTransform<T> _transform; | ||
| private FindPredicate _predicate; |
There was a problem hiding this comment.
Can any of these fields be readonly?
| && DosMatcher.MatchPattern(expression, findData.FileName, ignoreCase: true); | ||
| } | ||
|
|
||
| return new FindEnumerable<string>(directory, recursive, FindTransforms.AsUserFullPath, predicate); |
There was a problem hiding this comment.
Passing FindTransforms.AsUserFullPath and predicate like this is going to result in two delegate allocations each time UserFiles is called. Same for the methods below. It'd be nice if these delegate instances were lazily allocated and cached/reused.
You could attempt to address this by writing it as:
FindTransform<string> transform = (ref RawFindData findData) =>
{
ReadOnlySpan<char> subdirectory = findData.Directory.AsReadOnlySpan().Slice(findData.OriginalDirectory.Length);
return PathHelpers.CombineNoChecks(findData.OriginalUserDirectory, subdirectory, findData.FileName);
};
FindPredicate predicate = (ref RawFindData findData) =>
{
return FindPredicates.NotDotOrDotDot(ref findData)
&& !FindPredicates.IsDirectory(ref findData)
&& DosMatcher.MatchPattern(expression, findData.FileName, ignoreCase: true);
};
return new FindEnumerable<string>(directory, recursive, transform, predicate);The C# compiler will lazily allocate and cache/reuse the transform delegate, but predicate is problematic as written above as it captures expression resulting in a closure allocation.
To avoid that, you'd have to change the FindPredicate signature to also take a string expression parameter, and then pass the expression to the FindEnumerable constructor so it can pass it along whenever it calls predicate, e.g.:
internal delegate bool FindPredicate(string expression, ref RawFindData findData); FindPredicate predicate = (string expr, ref RawFindData findData) =>
{
return FindPredicates.NotDotOrDotDot(ref findData)
&& !FindPredicates.IsDirectory(ref findData)
&& DosMatcher.MatchPattern(expr, findData.FileName, ignoreCase: true);
};
return new FindEnumerable<string>(directory, expression, recursive, transform, predicate); _predicate(_expression, ref findData);There was a problem hiding this comment.
I can do the first part for sure. Redefining the predicate delegate is problematic I think. Not all predicates need the extra parameter and future ones will need different arguments (think all files modified after some DateTime).
There was a problem hiding this comment.
Not all predicates need the extra parameter and future ones will need different arguments
Can FindPredicate take a generic TState that can be used to pass any extra info needed?
Alternatively, instead of using delegates for the predicate and transform you could have abstract/virtual Transform/Predicate methods on the enumerable class, and have sealed subclasses for each type of enumeration.
| Interop.Kernel32.CloseHandle(queue.Dequeue().Handle); | ||
| } | ||
|
|
||
| Interop.Kernel32.CloseHandle(_directoryHandle); |
There was a problem hiding this comment.
Is it OK to call CloseHandle multiple times if Dispose is called multiple times?
There was a problem hiding this comment.
I'll move it up to the buffer interlock to be safe about it, thanks!
| if (second.Length == 0) | ||
| return first; | ||
|
|
||
| string result = CombineNoChecksInternal(first.AsReadOnlySpan(), second); |
There was a problem hiding this comment.
string result is unnecessary. (multiple places)
There was a problem hiding this comment.
Thanks- missed taking this out. Debugging through the AsReadOnlySpan() kept randomly terminating the debugger (under the latest public release of VS 2017) so I temporarily put this in. @stephentoub, are you aware of any issues like this with debugging Span? I'm not sure where to begin looking for more details when the debugger terminates unexpectedly...
|
|
||
| internal partial class Interop | ||
| { | ||
| internal struct BOOLEAN |
There was a problem hiding this comment.
I would think that
internal enum BOOLEAN : byte
{
FALSE = 0,
TRUE = 1,
}
should be sufficient given the limited use. Similar how Interop.BOOL.cs is done.
There was a problem hiding this comment.
The definition I have deals with non 0/1 values can be used just like a bool. What do we gain by making it an enum other than a simpler definition?
There was a problem hiding this comment.
You will get a simpler definition that was my point.
There was a problem hiding this comment.
Do you plan to address this? We are inconsistent today - both Interop.BOOL.cs and Interop.BOOLEAN.cs should use the same style.
There was a problem hiding this comment.
Do you plan to address this? We are inconsistent today - both Interop.BOOL.cs and Interop.BOOLEAN.cs should use the same style.
As both you and Stephen like the enum version I'll switch it to that format.
|
|
||
| int totalLength = first.Length + second.Length + (hasSeparator ? 0 : 1); | ||
| string fullPath = new string('\0', totalLength); | ||
| fixed (char* f = fullPath) |
There was a problem hiding this comment.
Sight...
I am wondering whether the new string stringCreate<TState>(int length, TState state, System.Buffers.SpanAction<char, TState> action) API is good since you cannot really use it here.
cc @stephentoub
There was a problem hiding this comment.
since you cannot really use it here
Because you can't pass spans in generics? We're going to have that problem in a bunch of places and is something we should try to solve more generally, e.g. a "ref struct" constraint and then an overload that accepts a delegate that takes such a constrained arg (and maybe have a built-in value tuple constrained to ref structs).
You can use the overload here, though, it just means pinning the input spans instead of the destination string, e.g.
using (char* firstPtr = &first.DangerousGetPinnableReference(), secondPtr = &second.DangerousGetPinnableReference())
{
return string.Create(totalLength, ((IntPtr)firstPtr, first.Length, (IntPtr)secondPtr, second.Length, hasSeparator), (dst, state) =>
{
new ReadOnlySpan<char>((char*)state.Item1, state.Item2).CopyTo(dst);
if (!state.Item5) dst[state.Item2] = Path.DirectorySeparatorChar;
new ReadOnlyspan<char>((char*)state.Item3, state.Item4).CopyTo(dst.Slice(state.Item2 + (state.Item5 ? 0 : 1));
});
}etc. And tuple arg names could be used to avoid those ItemXs.
There was a problem hiding this comment.
I'll try using string.Create
| { | ||
| internal static partial class CharSpanExtensions | ||
| { | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] |
There was a problem hiding this comment.
Instead of adding AggressiveInlining inlining that leads to codebloat, you should avoid the need for marshaling in CompareStringOrdinal (use char* and BOOL in the signature) and do the marshaling yourself (using fixed, etc.). It will be smaller and faster code that way.
There was a problem hiding this comment.
I'm not sure I follow exactly. Do fully blittable arguments (when they're simple types) make that much of a difference in imports? We've been encouraging using ref char in other places to avoid needing to use fixed... @stephentoub I recall you suggesting ref char for Span- I might be remembering incorrectly. I want to make sure we're clear on guidance here.
The results and error handling aren't exactly straight forward so I think having a wrapper method for usage is appropriate. (I also think this is a method we'd want to ultimately have public). I'm fine with removing the AggressiveInlining if the advantage is trivial.
There was a problem hiding this comment.
I've encouraged using ref instead of pointers to keep the code simpler and minimize unsafe pointer usage that we could mess up. If specific cases benefit from using pointers instead for perf, that's fine.
| /// Equivalent to <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/hh447298.aspx">FILE_FULL_DIR_INFO</a> structure. | ||
| /// </summary> | ||
| [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] | ||
| public unsafe struct FILE_FULL_DIR_INFORMATION |
There was a problem hiding this comment.
If it's equivalent to FILE_FULL_DIR_INFO, shouldn't it be called FILE_FULL_DIR_INFO rather than FILE_FULL_DIR_INFORMATION?
There was a problem hiding this comment.
The same struct is defined twice in Windows under those two names. Given that I had to pick one I went with the "real" one.
There was a problem hiding this comment.
Then shouldn't the comment match? If there are two equally valid names, it's fine to pick one, but we should pick one ;)
There was a problem hiding this comment.
It does- the summary calls out the actual name we used first. I also list the second definition as it isn't inherently obvious and we use it in both Win32 and NT API imports.
| @@ -0,0 +1 @@ | |||
| advapi32.dll!LsaNtStatusToWinError No newline at end of file | |||
There was a problem hiding this comment.
It's ok that this is being exempted?
There was a problem hiding this comment.
Not sure- I called it out in my PR comments. I'm following up. If I have to I'll add logic to fall back to GetFileInformationByHandleEx when it isn't available. Win7 doesn't have the support we need in said API and it is slightly less efficient so we'd prefer to use the NtQuery API instead. It is sort of strange that they might not expose the ability to convert NTSTATUS to windows errors, given that they expose NTSTATUS. I'm trying to confirm that this API doesn't exist or if there is another entry point that gets to the same underlying API.
There was a problem hiding this comment.
This API is legalized for UWP (and it's not a new API) so it should be fine to use it on Nano, etc: our OneCore baseline just isn't updated to include it. I've reached out to understand exactly. This exclusion is fine.
There was a problem hiding this comment.
Thanks for looking into it!
| return false; | ||
|
|
||
| if (ignoreCase == true) | ||
| return first.SequenceEqual(second); |
There was a problem hiding this comment.
I don't understand the logic here. Shouldn't this be if (!ignoreCase)?
There was a problem hiding this comment.
also ==true and ==false are ugly in general.
| { | ||
| internal static partial class CharSpanExtensions | ||
| { | ||
| internal static unsafe bool EqualsOrdinal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, bool ignoreCase = false) |
There was a problem hiding this comment.
@KrzysztofCwalina, @ahsonkhan, another case where we're having to write this logic in multiple places, e.g. here and https://github.com/dotnet/coreclr/blob/master/src/mscorlib/shared/System/StringSpanHelpers.cs#L40. Relates to https://github.com/dotnet/corefx/issues/21395.
There was a problem hiding this comment.
Yeah, it's totally in the plan to add such APIs. We just need to find some cycles to do it.
There was a problem hiding this comment.
Thanks. Just wanted to highlight the proliferation :)
| { | ||
| if (value.Length == 0) | ||
| return true; | ||
| else if (span.Length == 0 || value.Length > span.Length) |
There was a problem hiding this comment.
Do we need the first part of the condition here? Couldn't it just be:
else if (value.Length > span.Length)?
|
|
||
| // Want to throw the original argument name | ||
| OriginalPath = originalPath ?? throw new ArgumentNullException("path"); | ||
| FullPath = isNormalized ? fullPath ?? originalPath : Path.GetFullPath(fullPath ?? originalPath); |
There was a problem hiding this comment.
How about storing fullPath ?? originalPath into a local? It's used three times in the above three lines.
| { | ||
| for (int i = 0; i < length; i++) | ||
| { | ||
| switch (c[i]) |
There was a problem hiding this comment.
Why are we using unsafe code here? I would expect the JIT would do a very good job with:
foreach (char c in expression)or
for (int i = 0; i < expression.Length; i++)
{
char c = expression[i];
...
}There was a problem hiding this comment.
Fair enough- I'll trust the JIT :)
|
|
||
| if (expression.IndexOfAny(s_wildcardChars, startIndex: 1) == -1) | ||
| { | ||
| // Handle the special case of a single starting *, which essentially means "ends with" |
There was a problem hiding this comment.
Nit: extra space between single and starting
| _directory = Path.GetFullPath(directory); | ||
| _recursive = recursive; | ||
| _predicate = predicate ?? throw new ArgumentNullException(nameof(predicate)); | ||
| _transform = transform ?? throw new ArgumentNullException(nameof(transform)); |
There was a problem hiding this comment.
I don't understand... if these aren't allowed to be null, why are they optional arguments?
There was a problem hiding this comment.
Whoops- artifact of earlier iteration. Fixing.
| if (buffer != null) | ||
| ArrayPool<byte>.Shared.Return(buffer); | ||
|
|
||
| var queue = Interlocked.Exchange(ref _pending, null); |
There was a problem hiding this comment.
Nit: var should be replaced by the actual type
There was a problem hiding this comment.
Here I can't afaik. It doesn't allow me to use .Handle without redefining.
There was a problem hiding this comment.
Can you elaborate? What code did you write and what error did you get? You should be able to just write:
Queue<(IntPtr Handle, string Path)> queue = Interlocked.Exchange(ref _pending, null);That doesn't work?
There was a problem hiding this comment.
That does, you just have to redefine the names again. With var you don't.
There was a problem hiding this comment.
With var you don't.
But with var as I'm reading the code I have no idea what this thing is.
| } | ||
|
|
||
| protected void Dispose(bool disposing) | ||
| { |
There was a problem hiding this comment.
Where is the GCHandle freed?
| { | ||
| return FindPredicates.NotDotOrDotDot(ref findData) | ||
| && !FindPredicates.IsDirectory(ref findData) | ||
| && DosMatcher.MatchPattern(expression, findData.FileName, ignoreCase: true); |
There was a problem hiding this comment.
This results in a closure allocation to capture expression.
| { | ||
| return FindPredicates.NotDotOrDotDot(ref findData) | ||
| && FindPredicates.IsDirectory(ref findData) | ||
| && DosMatcher.MatchPattern(expression, findData.FileName, ignoreCase: true); |
There was a problem hiding this comment.
This results in a closure allocation to capture expression.
| bool predicate(ref RawFindData findData) | ||
| { | ||
| return FindPredicates.NotDotOrDotDot(ref findData) | ||
| && DosMatcher.MatchPattern(expression, findData.FileName, ignoreCase: true); |
There was a problem hiding this comment.
This results in a closure allocation to capture expression.
| { | ||
| return FindPredicates.NotDotOrDotDot(ref findData) | ||
| && !FindPredicates.IsDirectory(ref findData) | ||
| && DosMatcher.MatchPattern(expression, findData.FileName, ignoreCase: true); |
There was a problem hiding this comment.
Same; I'll stop commenting on these.
| { | ||
| FileAttributes attributes = findData.Attributes; | ||
| return attributes != (FileAttributes)(-1) | ||
| && (findData.Attributes & FileAttributes.Directory) != 0; |
There was a problem hiding this comment.
findData.Attributes was just stored into a local... the local should be used instead.
|
|
||
| // Historically we always treated "." as "*" | ||
| if (string.IsNullOrEmpty(expression) | ||
| || expression == "." || expression == "*.*") |
There was a problem hiding this comment.
Nit: pull this up to the previous line
|
|
||
| namespace System.IO | ||
| { | ||
| internal static partial class FindPredicates |
There was a problem hiding this comment.
Nit: To me personally, calling this FindPredicates made me though that it would have members that were somehow directly related via the type system to FindPredicate, but that's not the case.
There was a problem hiding this comment.
Open to suggestions here.
| return tempSearchPattern; | ||
| // In situations where this method is invoked, "<DriveLetter>:" should be special-cased | ||
| // to instead go to the current directory. | ||
| return path != null && path.Length == 2 && path[1] == ':'; |
There was a problem hiding this comment.
Do we need to check what path[0] is?
There was a problem hiding this comment.
This check I'm not thrilled about in general. Not sure what the original intent was. Adding the check wouldn't do much other than make !: not "work" when it does now. I'll open an issue to track this weird behavior.
| internal static string TrimEndingDirectorySeparator(string path) => | ||
| EndsInDirectorySeparator(path) ? | ||
| path.Substring(0, path.Length - 1) : | ||
| path; |
There was a problem hiding this comment.
Could this work in terms of spans to avoid the substring? Or the caller really needs a string?
There was a problem hiding this comment.
It can when we add the Span overloads to Path.
|
|
||
| public override IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string fullPath, string searchPattern, SearchOption searchOption, SearchTarget searchTarget) | ||
| { | ||
| FindEnumerableFactory.NormalizeInputs(ref fullPath, ref searchPattern); |
There was a problem hiding this comment.
This would appear to change what exception gets thrown if both searchTarget wasn't a known enum value and if searchPattern was bad. Previously we would throw for the searchTarget, no we'll throw for the searchPattern. We may not care about that ordering, but wanted to call it out, and if no tests failed for it, we might have a tiny test hole to consider patching.
There was a problem hiding this comment.
I'm not seeing it. I removed asserts, I believe this is still checked at all of the entry points.
There was a problem hiding this comment.
I'm not seeing it
Let's assume you had both an invalid searchPattern and an invalid searchExpression. Previously, you'd enter the method, it would jump to the default in the switch, and it would throw an expression for the searchPattern. Now, the first thing it does is call NormalizeInputs, which will first check searchExpression, and if it's invalid, it'll throw an exception for searchExpression. I'm pointing out that we're throwing a different error now with this change. Whether that matters, eh, up to you.
There was a problem hiding this comment.
Derp- I was looking at SearchOption, sorry. In this case I think I prefer the order change and don't think it is risky.
| [Fact] | ||
| [PlatformSpecific(TestPlatforms.Windows)] // Search pattern with double dots throws ArgumentException | ||
| public void WindowsSearchPatternWithDoubleDots() | ||
| [SkipOnTargetFramework(~TargetFrameworkMonikers.NetFramework)] |
There was a problem hiding this comment.
If we're skipping on everything but .NET Framework, we probably don't need the PlatformSpecific attribute, too.
| [PlatformSpecific(TestPlatforms.Windows)] // Search pattern with double dots throws ArgumentException | ||
| public void WindowsSearchPatternWithDoubleDots() | ||
| [SkipOnTargetFramework(~TargetFrameworkMonikers.NetFramework)] | ||
| public void WindowsSearchPatternWithDoubleDots_Desktop() |
There was a problem hiding this comment.
Will we consider porting these changes back to netfx? What would we do there... quirk it?
There was a problem hiding this comment.
Yes, we'll quirk. And this will only work in full trust.
| } | ||
|
|
||
| [Fact] | ||
| [PlatformSpecific(TestPlatforms.Windows)] // Search pattern with double dots throws ArgumentException |
There was a problem hiding this comment.
So we throw for patterns with double dots, but only on .NET Core on Windows, not on .NET Framework and not on .NET Core on Unix?
There was a problem hiding this comment.
Thanks. I'll remove it from Unix as well, forgot.
| [Fact] | ||
| [PlatformSpecific(TestPlatforms.Windows)] // Windows-invalid search patterns throw | ||
| public void WindowsSearchPatternInvalid() | ||
| [SkipOnTargetFramework(~TargetFrameworkMonikers.NetFramework)] |
There was a problem hiding this comment.
Is it that we've made something which didn't use to work and threw an exception now instead work? So we still throw on netfx and .NET Core on Unix but not on Windows? Will this be made to work on Unix as well for consistency, and then presumably ported to netfx for consistency there as well?
There was a problem hiding this comment.
Yes, I missed removing this from Unix. We'll port to NetFX in full trust- this was a limited trust based block.
|
|
||
| namespace System.IO.Tests | ||
| { | ||
| public class Regressions : FileSystemTest |
There was a problem hiding this comment.
I don't think we need to call these out as regression tests. In general we add lots of tests to fill gaps where we previously had test gaps, and how we discover such gaps really doesn't matter. In a year's time it won't really matter whether we added this test because we spotted it as a regression or not. I would just add this to whatever existing test class is most closely aligned to what's being tested, e.g. to our test class for enumerators.
| _enumerator = Directory.EnumerateFiles(directory).GetEnumerator(); | ||
| Race(_enumerator); | ||
| } | ||
| source.Cancel(); |
There was a problem hiding this comment.
This isn't actually stopping the tasks if they've already started running, so this test is leaking two tasks that run until the end of the process. Passing a token to the Task ctor only cancels the task if the task hasn't yet started executing when the cancellation request arrives; if it's already started executing, there's nothing to cancel.
Add length setter for ValueStringBuilder. Remove Unix .. check for enumerable.
| _enumerator = Directory.EnumerateFiles(directory).GetEnumerator(); | ||
| Enumerate(_enumerator); | ||
| } | ||
| source.Cancel(); |
There was a problem hiding this comment.
You're not waiting on the tasks you scheduled here, so you'll never know if any exceptions occurred in those tasks.
| if (x != null) | ||
| Enumerate(x); | ||
| } while (!token.IsCancellationRequested); | ||
| token.ThrowIfCancellationRequested(); |
There was a problem hiding this comment.
This ThrowIfCancellationRequested isn't needed.
| Enumerate(_enumerator); | ||
| } | ||
| source.Cancel(); | ||
| } |
There was a problem hiding this comment.
It would be better to do:
Task t1 = Task.Run(Work);
Task t2 = Task.Run(Work);
...
source.Cancel();
Task.WaitAll(t1, t2);There was a problem hiding this comment.
Just realized this as well. Thanks :)
| for (int i = 0; i < 1000; i++) | ||
| { | ||
| _enumerator = Directory.EnumerateFiles(directory).GetEnumerator(); | ||
| Enumerate(_enumerator); |
There was a problem hiding this comment.
If this throws, the source will never be canceled, and the tasks you've scheduled will loop infinitely until the process ends.
| { | ||
| throw; | ||
| } | ||
| catch (Exception) |
There was a problem hiding this comment.
This is fine. Another approach would be:
catch (Exception e) when (!(e is IOException)) { }There was a problem hiding this comment.
I'll update it as I'm updating VSB.
| { | ||
| Grow(value - _chars.Length); | ||
| } | ||
| _pos = value; |
There was a problem hiding this comment.
This looks potentially buggy. Growing the builder might end up getting an array from the pool, which might contain garbage data, but we're updating the position to indicate that it's valid.
There was a problem hiding this comment.
StringBuilder appends nulls, I'll do that.
…es. Add tests for it.
|
@dotnet-bot test Windows x64 Debug Build |
* Fast file enumeration for Windows Implements an optimized, low allocation replacement for file enumeration on Windows. In preliminary tests this improves performance at least two fold, particularly for recursive searches. Removes aggressive filter validation. Copies and cleans up filename matching code from FileSystemWatcher. Add length setter for ValueStringBuilder. Remove Unix .. check for enumerable. Commit migrated from dotnet/corefx@eb0d438
Implements an optimized, low allocation replacement for file enumeration on Windows. In preliminary tests this improves performance at least two fold, particularly for recursive searches.
Removes agressive filter validation.
Copies and cleans up filename matching code from FileSystemWatcher.
Other notes: