mirror of
https://github.com/git-for-windows/git.git
synced 2026-02-03 18:59:59 -06:00
Merge remote-tracking branch 'benpeart/fscache-per-thread-gfw'
This brings substantial wins in performance because the FSCache is now per-thread, being merged to the primary thread only at the end, so we do not have to lock (except while merging). Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
This commit is contained in:
commit
16e58542e5
@ -493,7 +493,7 @@ int cmd_add(int argc,
|
||||
die_in_unpopulated_submodule(repo->index, prefix);
|
||||
die_path_inside_submodule(repo->index, &pathspec);
|
||||
|
||||
enable_fscache(1);
|
||||
enable_fscache(0);
|
||||
/* We do not really re-read the index but update the up-to-date flags */
|
||||
preload_index(repo->index, &pathspec, 0);
|
||||
|
||||
|
||||
@ -415,7 +415,7 @@ static int checkout_worktree(const struct checkout_opts *opts,
|
||||
if (pc_workers > 1)
|
||||
init_parallel_checkout();
|
||||
|
||||
enable_fscache(1);
|
||||
enable_fscache(the_repository->index->cache_nr);
|
||||
for (pos = 0; pos < the_repository->index->cache_nr; pos++) {
|
||||
struct cache_entry *ce = the_repository->index->cache[pos];
|
||||
if (ce->ce_flags & CE_MATCHED) {
|
||||
@ -441,7 +441,7 @@ static int checkout_worktree(const struct checkout_opts *opts,
|
||||
errs |= run_parallel_checkout(&state, pc_workers, pc_threshold,
|
||||
NULL, NULL);
|
||||
mem_pool_discard(&ce_mem_pool, should_validate_cache_entries());
|
||||
enable_fscache(0);
|
||||
disable_fscache();
|
||||
remove_marked_cache_entries(the_repository->index, 1);
|
||||
remove_scheduled_dirs();
|
||||
errs |= finish_delayed_checkout(&state, opts->show_progress);
|
||||
|
||||
@ -1623,7 +1623,7 @@ struct repository *repo UNUSED)
|
||||
PATHSPEC_PREFER_FULL,
|
||||
prefix, argv);
|
||||
|
||||
enable_fscache(1);
|
||||
enable_fscache(0);
|
||||
if (status_format != STATUS_FORMAT_PORCELAIN &&
|
||||
status_format != STATUS_FORMAT_PORCELAIN_V2)
|
||||
progress_flag = REFRESH_PROGRESS;
|
||||
@ -1664,7 +1664,7 @@ struct repository *repo UNUSED)
|
||||
wt_status_print(&s);
|
||||
wt_status_collect_free_buffers(&s);
|
||||
|
||||
enable_fscache(0);
|
||||
disable_fscache();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
#include "symlinks.h"
|
||||
#include "trace2.h"
|
||||
#include "win32.h"
|
||||
#include "win32/fscache.h"
|
||||
#include "win32/lazyload.h"
|
||||
#include "wrapper.h"
|
||||
#include "write-or-die.h"
|
||||
@ -4051,6 +4052,9 @@ int wmain(int argc, const wchar_t **wargv)
|
||||
InitializeCriticalSection(&pinfo_cs);
|
||||
InitializeCriticalSection(&phantom_symlinks_cs);
|
||||
|
||||
/* initialize critical section for fscache */
|
||||
InitializeCriticalSection(&fscache_cs);
|
||||
|
||||
/* set up default file mode and file modes for stdin/out/err */
|
||||
_fmode = _O_BINARY;
|
||||
_setmode(_fileno(stdin), _O_BINARY);
|
||||
|
||||
@ -6,15 +6,35 @@
|
||||
#include "../../abspath.h"
|
||||
#include "../../trace.h"
|
||||
#include "config.h"
|
||||
#include "../../mem-pool.h"
|
||||
#include "ntifs.h"
|
||||
|
||||
static int initialized;
|
||||
static volatile long enabled;
|
||||
static struct hashmap map;
|
||||
static CRITICAL_SECTION mutex;
|
||||
static unsigned int lstat_requests;
|
||||
static unsigned int opendir_requests;
|
||||
static unsigned int fscache_requests;
|
||||
static unsigned int fscache_misses;
|
||||
static volatile long initialized;
|
||||
static DWORD dwTlsIndex;
|
||||
CRITICAL_SECTION fscache_cs;
|
||||
|
||||
/*
|
||||
* Store one fscache per thread to avoid thread contention and locking.
|
||||
* This is ok because multi-threaded access is 1) uncommon and 2) always
|
||||
* splitting up the cache entries across multiple threads so there isn't
|
||||
* any overlap between threads anyway.
|
||||
*/
|
||||
struct fscache {
|
||||
volatile long enabled;
|
||||
struct hashmap map;
|
||||
struct mem_pool mem_pool;
|
||||
unsigned int lstat_requests;
|
||||
unsigned int opendir_requests;
|
||||
unsigned int fscache_requests;
|
||||
unsigned int fscache_misses;
|
||||
/*
|
||||
* 32k wide characters translates to 64kB, which is the maximum that
|
||||
* Windows 8.1 and earlier can handle. On network drives, not only
|
||||
* the client's Windows version matters, but also the server's,
|
||||
* therefore we need to keep this to 64kB.
|
||||
*/
|
||||
WCHAR buffer[32 * 1024];
|
||||
};
|
||||
static struct trace_key trace_fscache = TRACE_KEY_INIT(FSCACHE);
|
||||
|
||||
/*
|
||||
@ -34,8 +54,6 @@ struct fsentry {
|
||||
union {
|
||||
/* Reference count of the directory listing. */
|
||||
volatile long refcnt;
|
||||
/* Handle to wait on the loading thread. */
|
||||
HANDLE hwait;
|
||||
struct {
|
||||
/* More stat members (only used for file entries). */
|
||||
off64_t st_size;
|
||||
@ -121,11 +139,12 @@ static void fsentry_init(struct fsentry *fse, struct fsentry *list,
|
||||
/*
|
||||
* Allocate an fsentry structure on the heap.
|
||||
*/
|
||||
static struct fsentry *fsentry_alloc(struct fsentry *list, const char *name,
|
||||
static struct fsentry *fsentry_alloc(struct fscache *cache, struct fsentry *list, const char *name,
|
||||
size_t len)
|
||||
{
|
||||
/* overallocate fsentry and copy the name to the end */
|
||||
struct fsentry *fse = xmalloc(sizeof(struct fsentry) + len + 1);
|
||||
struct fsentry *fse =
|
||||
mem_pool_alloc(&cache->mem_pool, sizeof(*fse) + len + 1);
|
||||
/* init the rest of the structure */
|
||||
fsentry_init(fse, list, name, len);
|
||||
fse->next = NULL;
|
||||
@ -145,45 +164,57 @@ inline static void fsentry_addref(struct fsentry *fse)
|
||||
}
|
||||
|
||||
/*
|
||||
* Release the reference to an fsentry, frees the memory if its the last ref.
|
||||
* Release the reference to an fsentry.
|
||||
*/
|
||||
static void fsentry_release(struct fsentry *fse)
|
||||
{
|
||||
if (fse->list)
|
||||
fse = fse->list;
|
||||
|
||||
if (InterlockedDecrement(&(fse->u.refcnt)))
|
||||
return;
|
||||
InterlockedDecrement(&(fse->u.refcnt));
|
||||
}
|
||||
|
||||
while (fse) {
|
||||
struct fsentry *next = fse->next;
|
||||
free(fse);
|
||||
fse = next;
|
||||
static int xwcstoutfn(char *utf, int utflen, const wchar_t *wcs, int wcslen)
|
||||
{
|
||||
if (!wcs || !utf || utflen < 1) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
utflen = WideCharToMultiByte(CP_UTF8, 0, wcs, wcslen, utf, utflen, NULL, NULL);
|
||||
if (utflen)
|
||||
return utflen;
|
||||
errno = ERANGE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Allocate and initialize an fsentry from a WIN32_FIND_DATA structure.
|
||||
* Allocate and initialize an fsentry from a FILE_FULL_DIR_INFORMATION structure.
|
||||
*/
|
||||
static struct fsentry *fseentry_create_entry(struct fsentry *list,
|
||||
const WIN32_FIND_DATAW *fdata)
|
||||
static struct fsentry *fseentry_create_entry(struct fscache *cache,
|
||||
struct fsentry *list,
|
||||
PFILE_FULL_DIR_INFORMATION fdata)
|
||||
{
|
||||
char buf[MAX_PATH * 3];
|
||||
int len;
|
||||
struct fsentry *fse;
|
||||
len = xwcstoutf(buf, fdata->cFileName, ARRAY_SIZE(buf));
|
||||
|
||||
fse = fsentry_alloc(list, buf, len);
|
||||
len = xwcstoutfn(buf, ARRAY_SIZE(buf), fdata->FileName, fdata->FileNameLength / sizeof(wchar_t));
|
||||
|
||||
fse = fsentry_alloc(cache, list, buf, len);
|
||||
|
||||
fse->st_mode = file_attr_to_st_mode(fdata->FileAttributes,
|
||||
fdata->EaSize);
|
||||
fse->dirent.d_type = S_ISREG(fse->st_mode) ? DT_REG :
|
||||
S_ISDIR(fse->st_mode) ? DT_DIR : DT_LNK;
|
||||
fse->u.s.st_size = (((off64_t) (fdata->nFileSizeHigh)) << 32)
|
||||
| fdata->nFileSizeLow;
|
||||
filetime_to_timespec(&(fdata->ftLastAccessTime), &(fse->u.s.st_atim));
|
||||
filetime_to_timespec(&(fdata->ftLastWriteTime), &(fse->u.s.st_mtim));
|
||||
filetime_to_timespec(&(fdata->ftCreationTime), &(fse->u.s.st_ctim));
|
||||
fse->u.s.st_size = S_ISLNK(fse->st_mode) ? MAX_PATH :
|
||||
fdata->EndOfFile.LowPart |
|
||||
(((off_t)fdata->EndOfFile.HighPart) << 32);
|
||||
filetime_to_timespec((FILETIME *)&(fdata->LastAccessTime),
|
||||
&(fse->u.s.st_atim));
|
||||
filetime_to_timespec((FILETIME *)&(fdata->LastWriteTime),
|
||||
&(fse->u.s.st_mtim));
|
||||
filetime_to_timespec((FILETIME *)&(fdata->CreationTime),
|
||||
&(fse->u.s.st_ctim));
|
||||
|
||||
return fse;
|
||||
}
|
||||
@ -193,11 +224,13 @@ static struct fsentry *fseentry_create_entry(struct fsentry *list,
|
||||
* Dir should not contain trailing '/'. Use an empty string for the current
|
||||
* directory (not "."!).
|
||||
*/
|
||||
static struct fsentry *fsentry_create_list(const struct fsentry *dir,
|
||||
static struct fsentry *fsentry_create_list(struct fscache *cache, const struct fsentry *dir,
|
||||
int *dir_not_found)
|
||||
{
|
||||
wchar_t pattern[MAX_PATH + 2]; /* + 2 for '/' '*' */
|
||||
WIN32_FIND_DATAW fdata;
|
||||
wchar_t pattern[MAX_PATH];
|
||||
NTSTATUS status;
|
||||
IO_STATUS_BLOCK iosb;
|
||||
PFILE_FULL_DIR_INFORMATION di;
|
||||
HANDLE h;
|
||||
int wlen;
|
||||
struct fsentry *list, **phead;
|
||||
@ -213,15 +246,18 @@ static struct fsentry *fsentry_create_list(const struct fsentry *dir,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* append optional '/' and wildcard '*' */
|
||||
if (wlen)
|
||||
pattern[wlen++] = '/';
|
||||
pattern[wlen++] = '*';
|
||||
pattern[wlen] = 0;
|
||||
/* handle CWD */
|
||||
if (!wlen) {
|
||||
wlen = GetCurrentDirectoryW(ARRAY_SIZE(pattern), pattern);
|
||||
if (!wlen || wlen >= (ssize_t)ARRAY_SIZE(pattern)) {
|
||||
errno = wlen ? ENAMETOOLONG : err_win_to_posix(GetLastError());
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* open find handle */
|
||||
h = FindFirstFileExW(pattern, FindExInfoBasic, &fdata, FindExSearchNameMatch,
|
||||
NULL, FIND_FIRST_EX_LARGE_FETCH);
|
||||
h = CreateFileW(pattern, FILE_LIST_DIRECTORY,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||
if (h == INVALID_HANDLE_VALUE) {
|
||||
err = GetLastError();
|
||||
*dir_not_found = 1; /* or empty directory */
|
||||
@ -232,114 +268,126 @@ static struct fsentry *fsentry_create_list(const struct fsentry *dir,
|
||||
}
|
||||
|
||||
/* allocate object to hold directory listing */
|
||||
list = fsentry_alloc(NULL, dir->dirent.d_name, dir->len);
|
||||
list = fsentry_alloc(cache, NULL, dir->dirent.d_name, dir->len);
|
||||
list->st_mode = S_IFDIR;
|
||||
list->dirent.d_type = DT_DIR;
|
||||
|
||||
/* walk directory and build linked list of fsentry structures */
|
||||
phead = &list->next;
|
||||
do {
|
||||
*phead = fseentry_create_entry(list, &fdata);
|
||||
status = NtQueryDirectoryFile(h, NULL, 0, 0, &iosb, cache->buffer,
|
||||
sizeof(cache->buffer), FileFullDirectoryInformation, FALSE, NULL, FALSE);
|
||||
if (!NT_SUCCESS(status)) {
|
||||
/*
|
||||
* NtQueryDirectoryFile returns STATUS_INVALID_PARAMETER when
|
||||
* asked to enumerate an invalid directory (ie it is a file
|
||||
* instead of a directory). Verify that is the actual cause
|
||||
* of the error.
|
||||
*/
|
||||
if (status == (NTSTATUS)STATUS_INVALID_PARAMETER) {
|
||||
DWORD attributes = GetFileAttributesW(pattern);
|
||||
if (!(attributes & FILE_ATTRIBUTE_DIRECTORY))
|
||||
status = ERROR_DIRECTORY;
|
||||
}
|
||||
goto Error;
|
||||
}
|
||||
di = (PFILE_FULL_DIR_INFORMATION)(cache->buffer);
|
||||
for (;;) {
|
||||
|
||||
*phead = fseentry_create_entry(cache, list, di);
|
||||
phead = &(*phead)->next;
|
||||
} while (FindNextFileW(h, &fdata));
|
||||
|
||||
/* remember result of last FindNextFile, then close find handle */
|
||||
err = GetLastError();
|
||||
FindClose(h);
|
||||
/* If there is no offset in the entry, the buffer has been exhausted. */
|
||||
if (di->NextEntryOffset == 0) {
|
||||
status = NtQueryDirectoryFile(h, NULL, 0, 0, &iosb, cache->buffer,
|
||||
sizeof(cache->buffer), FileFullDirectoryInformation, FALSE, NULL, FALSE);
|
||||
if (!NT_SUCCESS(status)) {
|
||||
if (status == STATUS_NO_MORE_FILES)
|
||||
break;
|
||||
goto Error;
|
||||
}
|
||||
|
||||
/* return the list if we've got all the files */
|
||||
if (err == ERROR_NO_MORE_FILES)
|
||||
return list;
|
||||
di = (PFILE_FULL_DIR_INFORMATION)(cache->buffer);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* otherwise free the list and return error */
|
||||
/* Advance to the next entry. */
|
||||
di = (PFILE_FULL_DIR_INFORMATION)(((PUCHAR)di) + di->NextEntryOffset);
|
||||
}
|
||||
|
||||
CloseHandle(h);
|
||||
return list;
|
||||
|
||||
Error:
|
||||
trace_printf_key(&trace_fscache,
|
||||
"fscache: status(%ld) unable to query directory "
|
||||
"contents '%s'\n", status, dir->dirent.d_name);
|
||||
CloseHandle(h);
|
||||
fsentry_release(list);
|
||||
errno = err_win_to_posix(err);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Adds a directory listing to the cache.
|
||||
*/
|
||||
static void fscache_add(struct fsentry *fse)
|
||||
static void fscache_add(struct fscache *cache, struct fsentry *fse)
|
||||
{
|
||||
if (fse->list)
|
||||
fse = fse->list;
|
||||
|
||||
for (; fse; fse = fse->next)
|
||||
hashmap_add(&map, &fse->ent);
|
||||
hashmap_add(&cache->map, &fse->ent);
|
||||
}
|
||||
|
||||
/*
|
||||
* Clears the cache.
|
||||
*/
|
||||
static void fscache_clear(void)
|
||||
static void fscache_clear(struct fscache *cache)
|
||||
{
|
||||
hashmap_clear_and_free(&map, struct fsentry, ent);
|
||||
hashmap_init(&map, (hashmap_cmp_fn)fsentry_cmp, NULL, 0);
|
||||
lstat_requests = opendir_requests = 0;
|
||||
fscache_misses = fscache_requests = 0;
|
||||
mem_pool_discard(&cache->mem_pool, 0);
|
||||
mem_pool_init(&cache->mem_pool, 0);
|
||||
hashmap_clear(&cache->map);
|
||||
hashmap_init(&cache->map, (hashmap_cmp_fn)fsentry_cmp, NULL, 0);
|
||||
cache->lstat_requests = cache->opendir_requests = 0;
|
||||
cache->fscache_misses = cache->fscache_requests = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks if the cache is enabled for the given path.
|
||||
*/
|
||||
int fscache_enabled(const char *path)
|
||||
static int do_fscache_enabled(struct fscache *cache, const char *path)
|
||||
{
|
||||
return enabled > 0 && !is_absolute_path(path);
|
||||
return cache->enabled > 0 && !is_absolute_path(path);
|
||||
}
|
||||
|
||||
/*
|
||||
* Looks up a cache entry, waits if its being loaded by another thread.
|
||||
* The mutex must be owned by the calling thread.
|
||||
*/
|
||||
static struct fsentry *fscache_get_wait(struct fsentry *key)
|
||||
int fscache_enabled(const char *path)
|
||||
{
|
||||
struct fsentry *fse = hashmap_get_entry(&map, key, ent, NULL);
|
||||
struct fscache *cache = fscache_getcache();
|
||||
|
||||
/* return if its a 'real' entry (future entries have refcnt == 0) */
|
||||
if (!fse || fse->list || fse->u.refcnt)
|
||||
return fse;
|
||||
|
||||
/* create an event and link our key to the future entry */
|
||||
key->u.hwait = CreateEvent(NULL, TRUE, FALSE, NULL);
|
||||
key->next = fse->next;
|
||||
fse->next = key;
|
||||
|
||||
/* wait for the loading thread to signal us */
|
||||
LeaveCriticalSection(&mutex);
|
||||
WaitForSingleObject(key->u.hwait, INFINITE);
|
||||
CloseHandle(key->u.hwait);
|
||||
EnterCriticalSection(&mutex);
|
||||
|
||||
/* repeat cache lookup */
|
||||
return hashmap_get_entry(&map, key, ent, NULL);
|
||||
return cache ? do_fscache_enabled(cache, path) : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Looks up or creates a cache entry for the specified key.
|
||||
*/
|
||||
static struct fsentry *fscache_get(struct fsentry *key)
|
||||
static struct fsentry *fscache_get(struct fscache *cache, struct fsentry *key)
|
||||
{
|
||||
struct fsentry *fse, *future, *waiter;
|
||||
struct fsentry *fse;
|
||||
int dir_not_found;
|
||||
|
||||
EnterCriticalSection(&mutex);
|
||||
fscache_requests++;
|
||||
cache->fscache_requests++;
|
||||
/* check if entry is in cache */
|
||||
fse = fscache_get_wait(key);
|
||||
fse = hashmap_get_entry(&cache->map, key, ent, NULL);
|
||||
if (fse) {
|
||||
if (fse->st_mode)
|
||||
fsentry_addref(fse);
|
||||
else
|
||||
fse = NULL; /* non-existing directory */
|
||||
LeaveCriticalSection(&mutex);
|
||||
return fse;
|
||||
}
|
||||
/* if looking for a file, check if directory listing is in cache */
|
||||
if (!fse && key->list) {
|
||||
fse = fscache_get_wait(key->list);
|
||||
fse = hashmap_get_entry(&cache->map, key->list, ent, NULL);
|
||||
if (fse) {
|
||||
LeaveCriticalSection(&mutex);
|
||||
/*
|
||||
* dir entry without file entry, or dir does not
|
||||
* exist -> file doesn't exist
|
||||
@ -349,25 +397,8 @@ static struct fsentry *fscache_get(struct fsentry *key)
|
||||
}
|
||||
}
|
||||
|
||||
/* add future entry to indicate that we're loading it */
|
||||
future = key->list ? key->list : key;
|
||||
future->next = NULL;
|
||||
future->u.refcnt = 0;
|
||||
hashmap_add(&map, &future->ent);
|
||||
|
||||
/* create the directory listing (outside mutex!) */
|
||||
LeaveCriticalSection(&mutex);
|
||||
fse = fsentry_create_list(future, &dir_not_found);
|
||||
EnterCriticalSection(&mutex);
|
||||
|
||||
/* remove future entry and signal waiting threads */
|
||||
hashmap_remove(&map, &future->ent, NULL);
|
||||
waiter = future->next;
|
||||
while (waiter) {
|
||||
HANDLE h = waiter->u.hwait;
|
||||
waiter = waiter->next;
|
||||
SetEvent(h);
|
||||
}
|
||||
/* create the directory listing */
|
||||
fse = fsentry_create_list(cache, key->list ? key->list : key, &dir_not_found);
|
||||
|
||||
/* leave on error (errno set by fsentry_create_list) */
|
||||
if (!fse) {
|
||||
@ -377,23 +408,22 @@ static struct fsentry *fscache_get(struct fsentry *key)
|
||||
* empty, which for all practical matters is the same
|
||||
* thing as far as fscache is concerned).
|
||||
*/
|
||||
fse = fsentry_alloc(key->list->list,
|
||||
fse = fsentry_alloc(cache, key->list->list,
|
||||
key->list->dirent.d_name,
|
||||
key->list->len);
|
||||
fse->st_mode = 0;
|
||||
hashmap_add(&map, &fse->ent);
|
||||
hashmap_add(&cache->map, &fse->ent);
|
||||
}
|
||||
LeaveCriticalSection(&mutex);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* add directory listing to the cache */
|
||||
fscache_misses++;
|
||||
fscache_add(fse);
|
||||
cache->fscache_misses++;
|
||||
fscache_add(cache, fse);
|
||||
|
||||
/* lookup file entry if requested (fse already points to directory) */
|
||||
if (key->list)
|
||||
fse = hashmap_get_entry(&map, key, ent, NULL);
|
||||
fse = hashmap_get_entry(&cache->map, key, ent, NULL);
|
||||
|
||||
if (fse && !fse->st_mode)
|
||||
fse = NULL; /* non-existing directory */
|
||||
@ -404,55 +434,109 @@ static struct fsentry *fscache_get(struct fsentry *key)
|
||||
else
|
||||
errno = ENOENT;
|
||||
|
||||
LeaveCriticalSection(&mutex);
|
||||
return fse;
|
||||
}
|
||||
|
||||
/*
|
||||
* Enables or disables the cache. Note that the cache is read-only, changes to
|
||||
* Enables the cache. Note that the cache is read-only, changes to
|
||||
* the working directory are NOT reflected in the cache while enabled.
|
||||
*/
|
||||
int fscache_enable(int enable)
|
||||
int fscache_enable(size_t initial_size)
|
||||
{
|
||||
int result;
|
||||
int fscache;
|
||||
struct fscache *cache;
|
||||
int result = 0;
|
||||
|
||||
/* allow the cache to be disabled entirely */
|
||||
fscache = git_env_bool("GIT_TEST_FSCACHE", -1);
|
||||
if (fscache != -1)
|
||||
core_fscache = fscache;
|
||||
if (!core_fscache)
|
||||
return 0;
|
||||
|
||||
/*
|
||||
* refcount the global fscache initialization so that the
|
||||
* opendir and lstat function pointers are redirected if
|
||||
* any threads are using the fscache.
|
||||
*/
|
||||
EnterCriticalSection(&fscache_cs);
|
||||
if (!initialized) {
|
||||
int fscache = git_env_bool("GIT_TEST_FSCACHE", -1);
|
||||
if (!dwTlsIndex) {
|
||||
dwTlsIndex = TlsAlloc();
|
||||
if (dwTlsIndex == TLS_OUT_OF_INDEXES) {
|
||||
LeaveCriticalSection(&fscache_cs);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* allow the cache to be disabled entirely */
|
||||
if (fscache != -1)
|
||||
core_fscache = fscache;
|
||||
if (!core_fscache)
|
||||
return 0;
|
||||
|
||||
InitializeCriticalSection(&mutex);
|
||||
lstat_requests = opendir_requests = 0;
|
||||
fscache_misses = fscache_requests = 0;
|
||||
hashmap_init(&map, (hashmap_cmp_fn) fsentry_cmp, NULL, 0);
|
||||
initialized = 1;
|
||||
}
|
||||
|
||||
result = enable ? InterlockedIncrement(&enabled)
|
||||
: InterlockedDecrement(&enabled);
|
||||
|
||||
if (enable && result == 1) {
|
||||
/* redirect opendir and lstat to the fscache implementations */
|
||||
opendir = fscache_opendir;
|
||||
lstat = fscache_lstat;
|
||||
} else if (!enable && !result) {
|
||||
}
|
||||
initialized++;
|
||||
LeaveCriticalSection(&fscache_cs);
|
||||
|
||||
/* refcount the thread specific initialization */
|
||||
cache = fscache_getcache();
|
||||
if (cache) {
|
||||
cache->enabled++;
|
||||
} else {
|
||||
cache = (struct fscache *)xcalloc(1, sizeof(*cache));
|
||||
cache->enabled = 1;
|
||||
/*
|
||||
* avoid having to rehash by leaving room for the parent dirs.
|
||||
* '4' was determined empirically by testing several repos
|
||||
*/
|
||||
hashmap_init(&cache->map, (hashmap_cmp_fn)fsentry_cmp, NULL, initial_size * 4);
|
||||
mem_pool_init(&cache->mem_pool, 0);
|
||||
if (!TlsSetValue(dwTlsIndex, cache))
|
||||
BUG("TlsSetValue error");
|
||||
}
|
||||
|
||||
trace_printf_key(&trace_fscache, "fscache: enable\n");
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Disables the cache.
|
||||
*/
|
||||
void fscache_disable(void)
|
||||
{
|
||||
struct fscache *cache;
|
||||
|
||||
if (!core_fscache)
|
||||
return;
|
||||
|
||||
/* update the thread specific fscache initialization */
|
||||
cache = fscache_getcache();
|
||||
if (!cache)
|
||||
BUG("fscache_disable() called on a thread where fscache has not been initialized");
|
||||
if (!cache->enabled)
|
||||
BUG("fscache_disable() called on an fscache that is already disabled");
|
||||
cache->enabled--;
|
||||
if (!cache->enabled) {
|
||||
TlsSetValue(dwTlsIndex, NULL);
|
||||
trace_printf_key(&trace_fscache, "fscache_disable: lstat %u, opendir %u, "
|
||||
"total requests/misses %u/%u\n",
|
||||
cache->lstat_requests, cache->opendir_requests,
|
||||
cache->fscache_requests, cache->fscache_misses);
|
||||
mem_pool_discard(&cache->mem_pool, 0);
|
||||
hashmap_clear(&cache->map);
|
||||
free(cache);
|
||||
}
|
||||
|
||||
/* update the global fscache initialization */
|
||||
EnterCriticalSection(&fscache_cs);
|
||||
initialized--;
|
||||
if (!initialized) {
|
||||
/* reset opendir and lstat to the original implementations */
|
||||
opendir = dirent_opendir;
|
||||
lstat = mingw_lstat;
|
||||
EnterCriticalSection(&mutex);
|
||||
trace_printf_key(&trace_fscache, "fscache: lstat %u, opendir %u, "
|
||||
"total requests/misses %u/%u\n",
|
||||
lstat_requests, opendir_requests,
|
||||
fscache_requests, fscache_misses);
|
||||
fscache_clear();
|
||||
LeaveCriticalSection(&mutex);
|
||||
}
|
||||
trace_printf_key(&trace_fscache, "fscache: enable(%d)\n", enable);
|
||||
return result;
|
||||
LeaveCriticalSection(&fscache_cs);
|
||||
|
||||
trace_printf_key(&trace_fscache, "fscache: disable\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -460,10 +544,10 @@ int fscache_enable(int enable)
|
||||
*/
|
||||
void fscache_flush(void)
|
||||
{
|
||||
if (enabled) {
|
||||
EnterCriticalSection(&mutex);
|
||||
fscache_clear();
|
||||
LeaveCriticalSection(&mutex);
|
||||
struct fscache *cache = fscache_getcache();
|
||||
|
||||
if (cache && cache->enabled) {
|
||||
fscache_clear(cache);
|
||||
}
|
||||
}
|
||||
|
||||
@ -481,11 +565,12 @@ int fscache_lstat(const char *filename, struct stat *st)
|
||||
struct heap_fsentry key[2];
|
||||
#pragma GCC diagnostic pop
|
||||
struct fsentry *fse;
|
||||
struct fscache *cache = fscache_getcache();
|
||||
|
||||
if (!fscache_enabled(filename))
|
||||
if (!cache || !do_fscache_enabled(cache, filename))
|
||||
return mingw_lstat(filename, st);
|
||||
|
||||
lstat_requests++;
|
||||
cache->lstat_requests++;
|
||||
/* split filename into path + name */
|
||||
len = strlen(filename);
|
||||
if (len && is_dir_sep(filename[len - 1]))
|
||||
@ -498,7 +583,7 @@ int fscache_lstat(const char *filename, struct stat *st)
|
||||
/* lookup entry for path + name in cache */
|
||||
fsentry_init(&key[0].u.ent, NULL, filename, dirlen);
|
||||
fsentry_init(&key[1].u.ent, &key[0].u.ent, filename + base, len - base);
|
||||
fse = fscache_get(&key[1].u.ent);
|
||||
fse = fscache_get(cache, &key[1].u.ent);
|
||||
if (!fse) {
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
@ -575,11 +660,12 @@ DIR *fscache_opendir(const char *dirname)
|
||||
struct fsentry *list;
|
||||
fscache_DIR *dir;
|
||||
int len;
|
||||
struct fscache *cache = fscache_getcache();
|
||||
|
||||
if (!fscache_enabled(dirname))
|
||||
if (!cache || !do_fscache_enabled(cache, dirname))
|
||||
return dirent_opendir(dirname);
|
||||
|
||||
opendir_requests++;
|
||||
cache->opendir_requests++;
|
||||
/* prepare name (strip trailing '/', replace '.') */
|
||||
len = strlen(dirname);
|
||||
if ((len == 1 && dirname[0] == '.') ||
|
||||
@ -588,7 +674,7 @@ DIR *fscache_opendir(const char *dirname)
|
||||
|
||||
/* get directory listing from cache */
|
||||
fsentry_init(&key.u.ent, NULL, dirname, len);
|
||||
list = fscache_get(&key.u.ent);
|
||||
list = fscache_get(cache, &key.u.ent);
|
||||
if (!list)
|
||||
return NULL;
|
||||
|
||||
@ -599,3 +685,55 @@ DIR *fscache_opendir(const char *dirname)
|
||||
dir->pfsentry = list;
|
||||
return (DIR*) dir;
|
||||
}
|
||||
|
||||
struct fscache *fscache_getcache(void)
|
||||
{
|
||||
return (struct fscache *)TlsGetValue(dwTlsIndex);
|
||||
}
|
||||
|
||||
void fscache_merge(struct fscache *dest)
|
||||
{
|
||||
struct hashmap_iter iter;
|
||||
struct hashmap_entry *e;
|
||||
struct fscache *cache = fscache_getcache();
|
||||
|
||||
/*
|
||||
* Only do the merge if fscache was enabled and we have a dest
|
||||
* cache to merge into.
|
||||
*/
|
||||
if (!dest) {
|
||||
fscache_enable(0);
|
||||
return;
|
||||
}
|
||||
if (!cache)
|
||||
BUG("fscache_merge() called on a thread where fscache has not been initialized");
|
||||
|
||||
TlsSetValue(dwTlsIndex, NULL);
|
||||
trace_printf_key(&trace_fscache, "fscache_merge: lstat %u, opendir %u, "
|
||||
"total requests/misses %u/%u\n",
|
||||
cache->lstat_requests, cache->opendir_requests,
|
||||
cache->fscache_requests, cache->fscache_misses);
|
||||
|
||||
/*
|
||||
* This is only safe because the primary thread we're merging into
|
||||
* isn't being used so the critical section only needs to prevent
|
||||
* the the child threads from stomping on each other.
|
||||
*/
|
||||
EnterCriticalSection(&fscache_cs);
|
||||
|
||||
hashmap_iter_init(&cache->map, &iter);
|
||||
while ((e = hashmap_iter_next(&iter)))
|
||||
hashmap_add(&dest->map, e);
|
||||
|
||||
mem_pool_combine(&dest->mem_pool, &cache->mem_pool);
|
||||
|
||||
dest->lstat_requests += cache->lstat_requests;
|
||||
dest->opendir_requests += cache->opendir_requests;
|
||||
dest->fscache_requests += cache->fscache_requests;
|
||||
dest->fscache_misses += cache->fscache_misses;
|
||||
initialized--;
|
||||
LeaveCriticalSection(&fscache_cs);
|
||||
|
||||
free(cache);
|
||||
|
||||
}
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
#ifndef FSCACHE_H
|
||||
#define FSCACHE_H
|
||||
|
||||
int fscache_enable(int enable);
|
||||
#define enable_fscache(x) fscache_enable(x)
|
||||
/*
|
||||
* The fscache is thread specific. enable_fscache() must be called
|
||||
* for each thread where caching is desired.
|
||||
*/
|
||||
|
||||
extern CRITICAL_SECTION fscache_cs;
|
||||
|
||||
int fscache_enable(size_t initial_size);
|
||||
#define enable_fscache(initial_size) fscache_enable(initial_size)
|
||||
|
||||
void fscache_disable(void);
|
||||
#define disable_fscache() fscache_disable()
|
||||
|
||||
int fscache_enabled(const char *path);
|
||||
#define is_fscache_enabled(path) fscache_enabled(path)
|
||||
@ -13,4 +23,13 @@ void fscache_flush(void);
|
||||
DIR *fscache_opendir(const char *dir);
|
||||
int fscache_lstat(const char *file_name, struct stat *buf);
|
||||
|
||||
/* opaque fscache structure */
|
||||
struct fscache;
|
||||
|
||||
struct fscache *fscache_getcache(void);
|
||||
#define getcache_fscache() fscache_getcache()
|
||||
|
||||
void fscache_merge(struct fscache *dest);
|
||||
#define merge_fscache(dest) fscache_merge(dest)
|
||||
|
||||
#endif
|
||||
|
||||
131
compat/win32/ntifs.h
Normal file
131
compat/win32/ntifs.h
Normal file
@ -0,0 +1,131 @@
|
||||
#ifndef _NTIFS_
|
||||
#define _NTIFS_
|
||||
|
||||
/*
|
||||
* Copy necessary structures and definitions out of the Windows DDK
|
||||
* to enable calling NtQueryDirectoryFile()
|
||||
*/
|
||||
|
||||
typedef _Return_type_success_(return >= 0) LONG NTSTATUS;
|
||||
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
|
||||
|
||||
#if !defined(_NTSECAPI_) && !defined(_WINTERNL_) && \
|
||||
!defined(__UNICODE_STRING_DEFINED)
|
||||
#define __UNICODE_STRING_DEFINED
|
||||
typedef struct _UNICODE_STRING {
|
||||
USHORT Length;
|
||||
USHORT MaximumLength;
|
||||
PWSTR Buffer;
|
||||
} UNICODE_STRING;
|
||||
typedef UNICODE_STRING *PUNICODE_STRING;
|
||||
typedef const UNICODE_STRING *PCUNICODE_STRING;
|
||||
#endif /* !_NTSECAPI_ && !_WINTERNL_ && !__UNICODE_STRING_DEFINED */
|
||||
|
||||
typedef enum _FILE_INFORMATION_CLASS {
|
||||
FileDirectoryInformation = 1,
|
||||
FileFullDirectoryInformation,
|
||||
FileBothDirectoryInformation,
|
||||
FileBasicInformation,
|
||||
FileStandardInformation,
|
||||
FileInternalInformation,
|
||||
FileEaInformation,
|
||||
FileAccessInformation,
|
||||
FileNameInformation,
|
||||
FileRenameInformation,
|
||||
FileLinkInformation,
|
||||
FileNamesInformation,
|
||||
FileDispositionInformation,
|
||||
FilePositionInformation,
|
||||
FileFullEaInformation,
|
||||
FileModeInformation,
|
||||
FileAlignmentInformation,
|
||||
FileAllInformation,
|
||||
FileAllocationInformation,
|
||||
FileEndOfFileInformation,
|
||||
FileAlternateNameInformation,
|
||||
FileStreamInformation,
|
||||
FilePipeInformation,
|
||||
FilePipeLocalInformation,
|
||||
FilePipeRemoteInformation,
|
||||
FileMailslotQueryInformation,
|
||||
FileMailslotSetInformation,
|
||||
FileCompressionInformation,
|
||||
FileObjectIdInformation,
|
||||
FileCompletionInformation,
|
||||
FileMoveClusterInformation,
|
||||
FileQuotaInformation,
|
||||
FileReparsePointInformation,
|
||||
FileNetworkOpenInformation,
|
||||
FileAttributeTagInformation,
|
||||
FileTrackingInformation,
|
||||
FileIdBothDirectoryInformation,
|
||||
FileIdFullDirectoryInformation,
|
||||
FileValidDataLengthInformation,
|
||||
FileShortNameInformation,
|
||||
FileIoCompletionNotificationInformation,
|
||||
FileIoStatusBlockRangeInformation,
|
||||
FileIoPriorityHintInformation,
|
||||
FileSfioReserveInformation,
|
||||
FileSfioVolumeInformation,
|
||||
FileHardLinkInformation,
|
||||
FileProcessIdsUsingFileInformation,
|
||||
FileNormalizedNameInformation,
|
||||
FileNetworkPhysicalNameInformation,
|
||||
FileIdGlobalTxDirectoryInformation,
|
||||
FileIsRemoteDeviceInformation,
|
||||
FileAttributeCacheInformation,
|
||||
FileNumaNodeInformation,
|
||||
FileStandardLinkInformation,
|
||||
FileRemoteProtocolInformation,
|
||||
FileMaximumInformation
|
||||
} FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS;
|
||||
|
||||
typedef struct _FILE_FULL_DIR_INFORMATION {
|
||||
ULONG NextEntryOffset;
|
||||
ULONG FileIndex;
|
||||
LARGE_INTEGER CreationTime;
|
||||
LARGE_INTEGER LastAccessTime;
|
||||
LARGE_INTEGER LastWriteTime;
|
||||
LARGE_INTEGER ChangeTime;
|
||||
LARGE_INTEGER EndOfFile;
|
||||
LARGE_INTEGER AllocationSize;
|
||||
ULONG FileAttributes;
|
||||
ULONG FileNameLength;
|
||||
ULONG EaSize;
|
||||
WCHAR FileName[1];
|
||||
} FILE_FULL_DIR_INFORMATION, *PFILE_FULL_DIR_INFORMATION;
|
||||
|
||||
typedef struct _IO_STATUS_BLOCK {
|
||||
union {
|
||||
NTSTATUS Status;
|
||||
PVOID Pointer;
|
||||
} u;
|
||||
ULONG_PTR Information;
|
||||
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
|
||||
|
||||
typedef VOID
|
||||
(NTAPI *PIO_APC_ROUTINE)(
|
||||
IN PVOID ApcContext,
|
||||
IN PIO_STATUS_BLOCK IoStatusBlock,
|
||||
IN ULONG Reserved);
|
||||
|
||||
NTSYSCALLAPI
|
||||
NTSTATUS
|
||||
NTAPI
|
||||
NtQueryDirectoryFile(
|
||||
_In_ HANDLE FileHandle,
|
||||
_In_opt_ HANDLE Event,
|
||||
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
|
||||
_In_opt_ PVOID ApcContext,
|
||||
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
|
||||
_Out_writes_bytes_(Length) PVOID FileInformation,
|
||||
_In_ ULONG Length,
|
||||
_In_ FILE_INFORMATION_CLASS FileInformationClass,
|
||||
_In_ BOOLEAN ReturnSingleEntry,
|
||||
_In_opt_ PUNICODE_STRING FileName,
|
||||
_In_ BOOLEAN RestartScan
|
||||
);
|
||||
|
||||
#define STATUS_NO_MORE_FILES ((NTSTATUS)0x80000006L)
|
||||
|
||||
#endif
|
||||
@ -760,7 +760,7 @@ static void mark_complete_and_common_ref(struct fetch_negotiator *negotiator,
|
||||
save_commit_buffer = 0;
|
||||
|
||||
trace2_region_enter("fetch-pack", "parse_remote_refs_and_find_cutoff", NULL);
|
||||
enable_fscache(1);
|
||||
enable_fscache(0);
|
||||
for (ref = *refs; ref; ref = ref->next) {
|
||||
struct commit *commit;
|
||||
|
||||
@ -785,7 +785,7 @@ static void mark_complete_and_common_ref(struct fetch_negotiator *negotiator,
|
||||
if (!cutoff || cutoff < commit->date)
|
||||
cutoff = commit->date;
|
||||
}
|
||||
enable_fscache(0);
|
||||
disable_fscache();
|
||||
trace2_region_leave("fetch-pack", "parse_remote_refs_and_find_cutoff", NULL);
|
||||
|
||||
/*
|
||||
|
||||
@ -1063,10 +1063,18 @@ static inline int is_missing_file_error(int errno_)
|
||||
* data or even file content without the need to synchronize with the file
|
||||
* system.
|
||||
*/
|
||||
|
||||
/* opaque fscache structure */
|
||||
struct fscache;
|
||||
|
||||
#ifndef enable_fscache
|
||||
#define enable_fscache(x) /* noop */
|
||||
#endif
|
||||
|
||||
#ifndef disable_fscache
|
||||
#define disable_fscache() /* noop */
|
||||
#endif
|
||||
|
||||
#ifndef is_fscache_enabled
|
||||
#define is_fscache_enabled(path) (0)
|
||||
#endif
|
||||
@ -1075,6 +1083,14 @@ static inline int is_missing_file_error(int errno_)
|
||||
#define flush_fscache() /* noop */
|
||||
#endif
|
||||
|
||||
#ifndef getcache_fscache
|
||||
#define getcache_fscache() (NULL) /* noop */
|
||||
#endif
|
||||
|
||||
#ifndef merge_fscache
|
||||
#define merge_fscache(dest) /* noop */
|
||||
#endif
|
||||
|
||||
int cmd_main(int, const char **);
|
||||
|
||||
/*
|
||||
|
||||
10
mem-pool.c
10
mem-pool.c
@ -7,7 +7,9 @@
|
||||
#include "git-compat-util.h"
|
||||
#include "mem-pool.h"
|
||||
#include "gettext.h"
|
||||
#include "trace.h"
|
||||
|
||||
static struct trace_key trace_mem_pool = TRACE_KEY_INIT(MEMPOOL);
|
||||
#define BLOCK_GROWTH_SIZE (1024 * 1024 - sizeof(struct mp_block))
|
||||
|
||||
/*
|
||||
@ -65,12 +67,20 @@ void mem_pool_init(struct mem_pool *pool, size_t initial_size)
|
||||
|
||||
if (initial_size > 0)
|
||||
mem_pool_alloc_block(pool, initial_size, NULL);
|
||||
|
||||
trace_printf_key(&trace_mem_pool,
|
||||
"mem_pool (%p): init (%"PRIuMAX") initial size\n",
|
||||
(void *)pool, (uintmax_t)initial_size);
|
||||
}
|
||||
|
||||
void mem_pool_discard(struct mem_pool *pool, int invalidate_memory)
|
||||
{
|
||||
struct mp_block *block, *block_to_free;
|
||||
|
||||
trace_printf_key(&trace_mem_pool,
|
||||
"mem_pool (%p): discard (%"PRIuMAX") unused\n",
|
||||
(void *)pool,
|
||||
(uintmax_t)(pool->mp_block->end - pool->mp_block->next_free));
|
||||
block = pool->mp_block;
|
||||
while (block)
|
||||
{
|
||||
|
||||
@ -20,6 +20,8 @@
|
||||
#include "trace2.h"
|
||||
#include "config.h"
|
||||
|
||||
static struct fscache *fscache;
|
||||
|
||||
/*
|
||||
* Mostly randomly chosen maximum thread counts: we
|
||||
* cap the parallelism to 20 threads, and we want
|
||||
@ -57,6 +59,7 @@ static void *preload_thread(void *_data)
|
||||
nr = index->cache_nr - p->offset;
|
||||
last_nr = nr;
|
||||
|
||||
enable_fscache(nr);
|
||||
do {
|
||||
struct cache_entry *ce = *cep++;
|
||||
struct stat st;
|
||||
@ -100,6 +103,7 @@ static void *preload_thread(void *_data)
|
||||
pthread_mutex_unlock(&pd->mutex);
|
||||
}
|
||||
cache_def_clear(&cache);
|
||||
merge_fscache(fscache);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@ -118,6 +122,7 @@ void preload_index(struct index_state *index,
|
||||
if (!HAVE_THREADS || !core_preload_index)
|
||||
return;
|
||||
|
||||
fscache = getcache_fscache();
|
||||
threads = index->cache_nr / THREAD_COST;
|
||||
if ((index->cache_nr > 1) && (threads < 2) && git_env_bool("GIT_TEST_PRELOAD_INDEX", 0))
|
||||
threads = 2;
|
||||
@ -141,7 +146,6 @@ void preload_index(struct index_state *index,
|
||||
pthread_mutex_init(&pd.mutex, NULL);
|
||||
}
|
||||
|
||||
enable_fscache(1);
|
||||
for (i = 0; i < threads; i++) {
|
||||
struct thread_data *p = data+i;
|
||||
int err;
|
||||
@ -177,8 +181,6 @@ void preload_index(struct index_state *index,
|
||||
|
||||
trace2_data_intmax("index", NULL, "preload/sum_lstat", t2_sum_lstat);
|
||||
trace2_region_leave("index", "preload", NULL);
|
||||
|
||||
enable_fscache(0);
|
||||
}
|
||||
|
||||
int repo_read_index_preload(struct repository *repo,
|
||||
|
||||
@ -1512,7 +1512,7 @@ int refresh_index(struct index_state *istate, unsigned int flags,
|
||||
typechange_fmt = in_porcelain ? "T\t%s\n" : "%s: needs update\n";
|
||||
added_fmt = in_porcelain ? "A\t%s\n" : "%s: needs update\n";
|
||||
unmerged_fmt = in_porcelain ? "U\t%s\n" : "%s: needs merge\n";
|
||||
enable_fscache(1);
|
||||
enable_fscache(0);
|
||||
/*
|
||||
* Use the multi-threaded preload_index() to refresh most of the
|
||||
* cache entries quickly then in the single threaded loop below,
|
||||
@ -1607,7 +1607,7 @@ int refresh_index(struct index_state *istate, unsigned int flags,
|
||||
display_progress(progress, istate->cache_nr);
|
||||
stop_progress(&progress);
|
||||
trace_performance_leave("refresh index");
|
||||
enable_fscache(0);
|
||||
disable_fscache();
|
||||
return has_errors;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user