Skip to content

Commit 27a0a6f

Browse files
benpeartdscho
authored andcommitted
fscache: teach fscache to use NtQueryDirectoryFile
Using FindFirstFileExW() requires the OS to allocate a 64K buffer for each directory and then free it when we call FindClose(). Update fscache to call the underlying kernel API NtQueryDirectoryFile so that we can do the buffer management ourselves. That allows us to allocate a single buffer for the lifetime of the cache and reuse it for each directory. This change improves performance of 'git status' by 18% in a repo with ~200K files and 30k folders. Documentation for NtQueryDirectoryFile can be found at: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntquerydirectoryfile https://docs.microsoft.com/en-us/windows/desktop/FileIO/file-attribute-constants https://docs.microsoft.com/en-us/windows/desktop/fileio/reparse-point-tags To determine if the specified directory is a symbolic link, inspect the FileAttributes member to see if the FILE_ATTRIBUTE_REPARSE_POINT flag is set. If so, EaSize will contain the reparse tag (this is a so far undocumented feature, but confirmed by the NTFS developers). To determine if the reparse point is a symbolic link (and not some other form of reparse point), test whether the tag value equals the value IO_REPARSE_TAG_SYMLINK. The NtQueryDirectoryFile() call works best (and on Windows 8.1 and earlier, it works *only*) with buffer sizes up to 64kB. Which is 32k wide characters, so let's use that as our buffer size. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent a4807b6 commit 27a0a6f

File tree

2 files changed

+224
-35
lines changed

2 files changed

+224
-35
lines changed

compat/win32/fscache.c

Lines changed: 93 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "fscache.h"
55
#include "config.h"
66
#include "../../mem-pool.h"
7+
#include "ntifs.h"
78

89
static volatile long initialized;
910
static DWORD dwTlsIndex;
@@ -23,6 +24,13 @@ struct fscache {
2324
unsigned int opendir_requests;
2425
unsigned int fscache_requests;
2526
unsigned int fscache_misses;
27+
/*
28+
* 32k wide characters translates to 64kB, which is the maximum that
29+
* Windows 8.1 and earlier can handle. On network drives, not only
30+
* the client's Windows version matters, but also the server's,
31+
* therefore we need to keep this to 64kB.
32+
*/
33+
WCHAR buffer[32 * 1024];
2634
};
2735
static struct trace_key trace_fscache = TRACE_KEY_INIT(FSCACHE);
2836

@@ -145,16 +153,30 @@ static void fsentry_release(struct fsentry *fse)
145153
InterlockedDecrement(&(fse->refcnt));
146154
}
147155

156+
static int xwcstoutfn(char *utf, int utflen, const wchar_t *wcs, int wcslen)
157+
{
158+
if (!wcs || !utf || utflen < 1) {
159+
errno = EINVAL;
160+
return -1;
161+
}
162+
utflen = WideCharToMultiByte(CP_UTF8, 0, wcs, wcslen, utf, utflen, NULL, NULL);
163+
if (utflen)
164+
return utflen;
165+
errno = ERANGE;
166+
return -1;
167+
}
168+
148169
/*
149-
* Allocate and initialize an fsentry from a WIN32_FIND_DATA structure.
170+
* Allocate and initialize an fsentry from a FILE_FULL_DIR_INFORMATION structure.
150171
*/
151172
static struct fsentry *fseentry_create_entry(struct fscache *cache, struct fsentry *list,
152-
const WIN32_FIND_DATAW *fdata)
173+
PFILE_FULL_DIR_INFORMATION fdata)
153174
{
154175
char buf[MAX_PATH * 3];
155176
int len;
156177
struct fsentry *fse;
157-
len = xwcstoutf(buf, fdata->cFileName, ARRAY_SIZE(buf));
178+
179+
len = xwcstoutfn(buf, ARRAY_SIZE(buf), fdata->FileName, fdata->FileNameLength / sizeof(wchar_t));
158180

159181
fse = fsentry_alloc(cache, list, buf, len);
160182

@@ -167,7 +189,8 @@ static struct fsentry *fseentry_create_entry(struct fscache *cache, struct fsent
167189
* Let's work around this by detecting that situation and
168190
* telling Git that these are *not* symbolic links.
169191
*/
170-
if (fdata->dwReserved0 == IO_REPARSE_TAG_SYMLINK &&
192+
if (fdata->FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT &&
193+
fdata->EaSize == IO_REPARSE_TAG_SYMLINK &&
171194
sizeof(buf) > (list ? list->len + 1 : 0) + fse->len + 1 &&
172195
is_inside_windows_container()) {
173196
size_t off = 0;
@@ -180,13 +203,13 @@ static struct fsentry *fseentry_create_entry(struct fscache *cache, struct fsent
180203
buf[off + fse->len] = '\0';
181204
}
182205

183-
fse->st_mode = file_attr_to_st_mode(fdata->dwFileAttributes,
184-
fdata->dwReserved0, buf);
206+
fse->st_mode = file_attr_to_st_mode(fdata->FileAttributes,
207+
fdata->EaSize, buf);
185208
fse->st_size = S_ISLNK(fse->st_mode) ? MAX_LONG_PATH :
186-
fdata->nFileSizeLow | (((off_t) fdata->nFileSizeHigh) << 32);
187-
filetime_to_timespec(&(fdata->ftLastAccessTime), &(fse->st_atim));
188-
filetime_to_timespec(&(fdata->ftLastWriteTime), &(fse->st_mtim));
189-
filetime_to_timespec(&(fdata->ftCreationTime), &(fse->st_ctim));
209+
fdata->EndOfFile.LowPart | (((off_t)fdata->EndOfFile.HighPart) << 32);
210+
filetime_to_timespec((FILETIME *)&(fdata->LastAccessTime), &(fse->st_atim));
211+
filetime_to_timespec((FILETIME *)&(fdata->LastWriteTime), &(fse->st_mtim));
212+
filetime_to_timespec((FILETIME *)&(fdata->CreationTime), &(fse->st_ctim));
190213

191214
return fse;
192215
}
@@ -199,8 +222,10 @@ static struct fsentry *fseentry_create_entry(struct fscache *cache, struct fsent
199222
static struct fsentry *fsentry_create_list(struct fscache *cache, const struct fsentry *dir,
200223
int *dir_not_found)
201224
{
202-
wchar_t pattern[MAX_LONG_PATH + 2]; /* + 2 for "\*" */
203-
WIN32_FIND_DATAW fdata;
225+
wchar_t pattern[MAX_LONG_PATH];
226+
NTSTATUS status;
227+
IO_STATUS_BLOCK iosb;
228+
PFILE_FULL_DIR_INFORMATION di;
204229
HANDLE h;
205230
int wlen;
206231
struct fsentry *list, **phead;
@@ -213,18 +238,18 @@ static struct fsentry *fsentry_create_list(struct fscache *cache, const struct f
213238
dir->len, MAX_PATH - 2, core_long_paths)) < 0)
214239
return NULL;
215240

216-
/*
217-
* append optional '\' and wildcard '*'. Note: we need to use '\' as
218-
* Windows doesn't translate '/' to '\' for "\\?\"-prefixed paths.
219-
*/
220-
if (wlen)
221-
pattern[wlen++] = '\\';
222-
pattern[wlen++] = '*';
223-
pattern[wlen] = 0;
224-
225-
/* open find handle */
226-
h = FindFirstFileExW(pattern, FindExInfoBasic, &fdata, FindExSearchNameMatch,
227-
NULL, FIND_FIRST_EX_LARGE_FETCH);
241+
/* handle CWD */
242+
if (!wlen) {
243+
wlen = GetCurrentDirectoryW(ARRAY_SIZE(pattern), pattern);
244+
if (!wlen || wlen >= ARRAY_SIZE(pattern)) {
245+
errno = wlen ? ENAMETOOLONG : err_win_to_posix(GetLastError());
246+
return NULL;
247+
}
248+
}
249+
250+
h = CreateFileW(pattern, FILE_LIST_DIRECTORY,
251+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
252+
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
228253
if (h == INVALID_HANDLE_VALUE) {
229254
err = GetLastError();
230255
*dir_not_found = 1; /* or empty directory */
@@ -240,22 +265,55 @@ static struct fsentry *fsentry_create_list(struct fscache *cache, const struct f
240265

241266
/* walk directory and build linked list of fsentry structures */
242267
phead = &list->next;
243-
do {
244-
*phead = fseentry_create_entry(cache, list, &fdata);
268+
status = NtQueryDirectoryFile(h, NULL, 0, 0, &iosb, cache->buffer,
269+
sizeof(cache->buffer), FileFullDirectoryInformation, FALSE, NULL, FALSE);
270+
if (!NT_SUCCESS(status)) {
271+
/*
272+
* NtQueryDirectoryFile returns STATUS_INVALID_PARAMETER when
273+
* asked to enumerate an invalid directory (ie it is a file
274+
* instead of a directory). Verify that is the actual cause
275+
* of the error.
276+
*/
277+
if (status == STATUS_INVALID_PARAMETER) {
278+
DWORD attributes = GetFileAttributesW(pattern);
279+
if (!(attributes & FILE_ATTRIBUTE_DIRECTORY))
280+
status = ERROR_DIRECTORY;
281+
}
282+
goto Error;
283+
}
284+
di = (PFILE_FULL_DIR_INFORMATION)(cache->buffer);
285+
for (;;) {
286+
287+
*phead = fseentry_create_entry(cache, list, di);
245288
phead = &(*phead)->next;
246-
} while (FindNextFileW(h, &fdata));
247289

248-
/* remember result of last FindNextFile, then close find handle */
249-
err = GetLastError();
250-
FindClose(h);
290+
/* If there is no offset in the entry, the buffer has been exhausted. */
291+
if (di->NextEntryOffset == 0) {
292+
status = NtQueryDirectoryFile(h, NULL, 0, 0, &iosb, cache->buffer,
293+
sizeof(cache->buffer), FileFullDirectoryInformation, FALSE, NULL, FALSE);
294+
if (!NT_SUCCESS(status)) {
295+
if (status == STATUS_NO_MORE_FILES)
296+
break;
297+
goto Error;
298+
}
299+
300+
di = (PFILE_FULL_DIR_INFORMATION)(cache->buffer);
301+
continue;
302+
}
303+
304+
/* Advance to the next entry. */
305+
di = (PFILE_FULL_DIR_INFORMATION)(((PUCHAR)di) + di->NextEntryOffset);
306+
}
251307

252-
/* return the list if we've got all the files */
253-
if (err == ERROR_NO_MORE_FILES)
254-
return list;
308+
CloseHandle(h);
309+
return list;
255310

256-
/* otherwise release the list and return error */
311+
Error:
312+
errno = (status == ERROR_DIRECTORY) ? ENOTDIR : err_win_to_posix(status);
313+
trace_printf_key(&trace_fscache, "fscache: error(%d) unable to query directory contents '%.*s'\n",
314+
errno, dir->len, dir->name);
315+
CloseHandle(h);
257316
fsentry_release(list);
258-
errno = err_win_to_posix(err);
259317
return NULL;
260318
}
261319

compat/win32/ntifs.h

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#ifndef _NTIFS_
2+
#define _NTIFS_
3+
4+
/*
5+
* Copy necessary structures and definitions out of the Windows DDK
6+
* to enable calling NtQueryDirectoryFile()
7+
*/
8+
9+
typedef _Return_type_success_(return >= 0) LONG NTSTATUS;
10+
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
11+
12+
typedef struct _UNICODE_STRING {
13+
USHORT Length;
14+
USHORT MaximumLength;
15+
#ifdef MIDL_PASS
16+
[size_is(MaximumLength / 2), length_is((Length) / 2)] USHORT * Buffer;
17+
#else // MIDL_PASS
18+
_Field_size_bytes_part_(MaximumLength, Length) PWCH Buffer;
19+
#endif // MIDL_PASS
20+
} UNICODE_STRING;
21+
typedef UNICODE_STRING *PUNICODE_STRING;
22+
typedef const UNICODE_STRING *PCUNICODE_STRING;
23+
24+
typedef enum _FILE_INFORMATION_CLASS {
25+
FileDirectoryInformation = 1,
26+
FileFullDirectoryInformation,
27+
FileBothDirectoryInformation,
28+
FileBasicInformation,
29+
FileStandardInformation,
30+
FileInternalInformation,
31+
FileEaInformation,
32+
FileAccessInformation,
33+
FileNameInformation,
34+
FileRenameInformation,
35+
FileLinkInformation,
36+
FileNamesInformation,
37+
FileDispositionInformation,
38+
FilePositionInformation,
39+
FileFullEaInformation,
40+
FileModeInformation,
41+
FileAlignmentInformation,
42+
FileAllInformation,
43+
FileAllocationInformation,
44+
FileEndOfFileInformation,
45+
FileAlternateNameInformation,
46+
FileStreamInformation,
47+
FilePipeInformation,
48+
FilePipeLocalInformation,
49+
FilePipeRemoteInformation,
50+
FileMailslotQueryInformation,
51+
FileMailslotSetInformation,
52+
FileCompressionInformation,
53+
FileObjectIdInformation,
54+
FileCompletionInformation,
55+
FileMoveClusterInformation,
56+
FileQuotaInformation,
57+
FileReparsePointInformation,
58+
FileNetworkOpenInformation,
59+
FileAttributeTagInformation,
60+
FileTrackingInformation,
61+
FileIdBothDirectoryInformation,
62+
FileIdFullDirectoryInformation,
63+
FileValidDataLengthInformation,
64+
FileShortNameInformation,
65+
FileIoCompletionNotificationInformation,
66+
FileIoStatusBlockRangeInformation,
67+
FileIoPriorityHintInformation,
68+
FileSfioReserveInformation,
69+
FileSfioVolumeInformation,
70+
FileHardLinkInformation,
71+
FileProcessIdsUsingFileInformation,
72+
FileNormalizedNameInformation,
73+
FileNetworkPhysicalNameInformation,
74+
FileIdGlobalTxDirectoryInformation,
75+
FileIsRemoteDeviceInformation,
76+
FileAttributeCacheInformation,
77+
FileNumaNodeInformation,
78+
FileStandardLinkInformation,
79+
FileRemoteProtocolInformation,
80+
FileMaximumInformation
81+
} FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS;
82+
83+
typedef struct _FILE_FULL_DIR_INFORMATION {
84+
ULONG NextEntryOffset;
85+
ULONG FileIndex;
86+
LARGE_INTEGER CreationTime;
87+
LARGE_INTEGER LastAccessTime;
88+
LARGE_INTEGER LastWriteTime;
89+
LARGE_INTEGER ChangeTime;
90+
LARGE_INTEGER EndOfFile;
91+
LARGE_INTEGER AllocationSize;
92+
ULONG FileAttributes;
93+
ULONG FileNameLength;
94+
ULONG EaSize;
95+
WCHAR FileName[1];
96+
} FILE_FULL_DIR_INFORMATION, *PFILE_FULL_DIR_INFORMATION;
97+
98+
typedef struct _IO_STATUS_BLOCK {
99+
union {
100+
NTSTATUS Status;
101+
PVOID Pointer;
102+
} DUMMYUNIONNAME;
103+
ULONG_PTR Information;
104+
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
105+
106+
typedef VOID
107+
(NTAPI *PIO_APC_ROUTINE)(
108+
IN PVOID ApcContext,
109+
IN PIO_STATUS_BLOCK IoStatusBlock,
110+
IN ULONG Reserved);
111+
112+
NTSYSCALLAPI
113+
NTSTATUS
114+
NTAPI
115+
NtQueryDirectoryFile(
116+
_In_ HANDLE FileHandle,
117+
_In_opt_ HANDLE Event,
118+
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
119+
_In_opt_ PVOID ApcContext,
120+
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
121+
_Out_writes_bytes_(Length) PVOID FileInformation,
122+
_In_ ULONG Length,
123+
_In_ FILE_INFORMATION_CLASS FileInformationClass,
124+
_In_ BOOLEAN ReturnSingleEntry,
125+
_In_opt_ PUNICODE_STRING FileName,
126+
_In_ BOOLEAN RestartScan
127+
);
128+
129+
#define STATUS_NO_MORE_FILES ((NTSTATUS)0x80000006L)
130+
131+
#endif

0 commit comments

Comments
 (0)