Skip to content

Commit a097511

Browse files
committed
mingw: reuse known directory prefixes for phantom symlinks
Building index keys repeatedly resolves parent directories already canonicalized during directory notifications. Reuse those prefixes to avoid redundant filesystem probes. Target registration may skip full resolution only for an exact cached parent and a final child that `lstat()` reports missing. Invalidate cached prefixes before removals or renames and again after success, including rename's internal directory-removal fallback. An atomic dirty flag lets mutations invalidate the cache without taking the tree lock. Advance the generation at the next locked lookup so an in-flight resolution cannot stamp an old path with a newer generation. In a totally unscientific test, the cache cut checkout time by roughly 20% versus the simple `cbtree` implementation, putting it in the same ballpark as an alternative implementation using a trie. The fixture had about 50,000 symlinks, with 25,000 resolving through one directory symlink. Assisted-by: GPT-6 Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
1 parent a4ae38b commit a097511

2 files changed

Lines changed: 187 additions & 32 deletions

File tree

‎compat/mingw.c‎

Lines changed: 155 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@ struct phantom_symlink_info {
460460
struct phantom_symlink_list {
461461
struct cb_node node;
462462
struct phantom_symlink_info *list;
463+
struct strbuf directory;
464+
uint64_t directory_generation;
463465
char key[FLEX_ARRAY];
464466
};
465467

@@ -468,9 +470,86 @@ static struct cb_tree phantom_symlinks = {
468470
.key_offset = offsetof(struct phantom_symlink_list, key)
469471
};
470472
static CRITICAL_SECTION phantom_symlinks_cs;
473+
static uint64_t phantom_directory_generation;
474+
static volatile LONG phantom_directories_invalid;
471475

472476
static wchar_t *normalize_ntpath(wchar_t *wbuf);
473477

478+
static void invalidate_phantom_directories(void)
479+
{
480+
InterlockedExchange(&phantom_directories_invalid, 1);
481+
}
482+
483+
static struct phantom_symlink_list *phantom_symlink_list(const struct strbuf *key)
484+
{
485+
struct phantom_symlink_list *list;
486+
struct cb_node *existing;
487+
488+
FLEX_ALLOC_MEM(list, key, key->buf, key->len);
489+
strbuf_init(&list->directory, 0);
490+
existing = cb_insert(&phantom_symlinks, &list->node, key->len + 1);
491+
if (existing) {
492+
free(list);
493+
list = container_of(existing,
494+
struct phantom_symlink_list, node);
495+
}
496+
return list;
497+
}
498+
499+
static int full_path(struct strbuf *path, const wchar_t *name)
500+
{
501+
wchar_t full[MAX_LONG_PATH];
502+
int len = GetFullPathNameW(name, ARRAY_SIZE(full), full, NULL);
503+
504+
if (!len || len >= ARRAY_SIZE(full)) {
505+
errno = len ? ENAMETOOLONG : err_win_to_posix(GetLastError());
506+
return -1;
507+
}
508+
strbuf_reset(path);
509+
strbuf_grow(path, 3 * len);
510+
len = xwcstoutf(path->buf, normalize_ntpath(full), path->alloc);
511+
if (len < 0)
512+
die_errno("could not encode path '%ls'", name);
513+
strbuf_setlen(path, len);
514+
return 0;
515+
}
516+
517+
static int append_cached_directory(struct cb_node *node, void *data)
518+
{
519+
struct phantom_symlink_list *list =
520+
container_of(node, struct phantom_symlink_list, node);
521+
522+
if (list->directory_generation != phantom_directory_generation)
523+
return 0;
524+
strbuf_addbuf(data, &list->directory);
525+
return 1;
526+
}
527+
528+
/* Cache keys preserve spelling; they cannot collide with absolute paths. */
529+
static int append_from_cached_parent(struct strbuf *resolved,
530+
const struct strbuf *path)
531+
{
532+
struct strbuf key = STRBUF_INIT;
533+
size_t parent_len;
534+
int found;
535+
536+
/* Advance generations here, not during an in-flight cache fill. */
537+
if (InterlockedExchange(&phantom_directories_invalid, 0))
538+
phantom_directory_generation++;
539+
540+
strbuf_addbuf(&key, path);
541+
strbuf_strip_file_from_path(&key);
542+
parent_len = key.len;
543+
strbuf_insertstr(&key, 0, "dir:");
544+
found = cb_each(&phantom_symlinks, (const uint8_t *)key.buf,
545+
key.len + 1, append_cached_directory, resolved);
546+
if (found)
547+
strbuf_add(resolved, path->buf + parent_len,
548+
path->len - parent_len);
549+
strbuf_release(&key);
550+
return found;
551+
}
552+
474553
static void path_to_key(struct strbuf *path, int resolve)
475554
{
476555
struct strbuf resolved = STRBUF_INIT;
@@ -488,38 +567,64 @@ static void path_to_key(struct strbuf *path, int resolve)
488567
strbuf_release(&resolved);
489568
}
490569

570+
static void cache_directory(const struct strbuf *name,
571+
const struct strbuf *resolved)
572+
{
573+
struct strbuf key = STRBUF_INIT;
574+
struct phantom_symlink_list *list;
575+
576+
strbuf_addstr(&key, "dir:");
577+
strbuf_addbuf(&key, name);
578+
strbuf_complete(&key, '/');
579+
list = phantom_symlink_list(&key);
580+
strbuf_reset(&list->directory);
581+
strbuf_addbuf(&list->directory, resolved);
582+
strbuf_complete(&list->directory, '/');
583+
list->directory_generation = phantom_directory_generation;
584+
strbuf_release(&key);
585+
}
586+
491587
static int remember_phantom_symlink(struct phantom_symlink_info *psi,
492588
const wchar_t *wlink)
493589
{
494-
struct strbuf path = STRBUF_INIT;
590+
struct strbuf path = STRBUF_INIT, full = STRBUF_INIT;
495591
struct phantom_symlink_list *list;
496-
struct cb_node *existing;
592+
struct stat st;
497593
wchar_t relative[MAX_LONG_PATH];
498594
char target[MAX_LONG_PATH * 3];
499595
const wchar_t *wtarget = make_relative_to(psi->wtarget, wlink,
500596
relative, ARRAY_SIZE(relative));
597+
int ret = -1;
501598

502599
if (!wtarget)
503600
return -1;
504-
if (wtarget != relative)
505-
wcscpy(relative, wtarget);
506-
if (xwcstoutf(target, normalize_ntpath(relative), sizeof(target)) < 0)
507-
return error_errno("could not encode symlink target '%ls'",
508-
wtarget);
509-
strbuf_addstr(&path, target);
510-
path_to_key(&path, 1);
511-
512-
FLEX_ALLOC_MEM(list, key, path.buf, path.len);
513-
existing = cb_insert(&phantom_symlinks, &list->node, path.len + 1);
514-
if (existing) {
515-
free(list);
516-
list = container_of(existing,
517-
struct phantom_symlink_list, node);
601+
if (!full_path(&full, wtarget) && full.len &&
602+
!is_dir_sep(full.buf[full.len - 1]) &&
603+
append_from_cached_parent(&path, &full) &&
604+
lstat(full.buf, &st) < 0 && errno == ENOENT) {
605+
path_to_key(&path, 0);
606+
} else {
607+
if (wtarget != relative)
608+
wcscpy(relative, wtarget);
609+
if (xwcstoutf(target, normalize_ntpath(relative),
610+
sizeof(target)) < 0) {
611+
error_errno("could not encode symlink target '%ls'",
612+
wtarget);
613+
goto out;
614+
}
615+
strbuf_reset(&path);
616+
strbuf_addstr(&path, target);
617+
path_to_key(&path, 1);
518618
}
619+
620+
list = phantom_symlink_list(&path);
519621
psi->next = list->list;
520622
list->list = psi;
623+
ret = 0;
624+
out:
625+
strbuf_release(&full);
521626
strbuf_release(&path);
522-
return 0;
627+
return ret;
523628
}
524629

525630
static int collect_phantom_symlinks(struct cb_node *node, void *data)
@@ -543,25 +648,31 @@ static void process_phantom_symlinks(const wchar_t *name, int recursive)
543648
{
544649
struct strbuf path = STRBUF_INIT, resolved = STRBUF_INIT;
545650
struct phantom_symlink_info *pending = NULL;
546-
wchar_t wpath[MAX_LONG_PATH];
547-
int len;
548651

549652
EnterCriticalSection(&phantom_symlinks_cs);
550653
if (!phantom_symlinks.root)
551654
goto out;
552-
len = GetFullPathNameW(name, ARRAY_SIZE(wpath), wpath, NULL);
553-
if (!len || len >= ARRAY_SIZE(wpath)) {
554-
errno = len ? ENAMETOOLONG : err_win_to_posix(GetLastError());
655+
if (full_path(&path, name)) {
555656
error_errno("could not get full path of '%ls'", name);
556657
goto out;
557658
}
558-
strbuf_grow(&path, 3 * len);
559-
len = xwcstoutf(path.buf, normalize_ntpath(wpath), path.alloc);
560-
if (len < 0)
561-
die_errno("could not encode path '%ls'", wpath);
562-
strbuf_setlen(&path, len);
563-
if (!recursive && mingw_strbuf_realpath(&resolved, path.buf))
564-
path_to_key(&resolved, 0);
659+
if (!recursive) {
660+
strbuf_strip_suffix(&path, "/");
661+
if (!append_from_cached_parent(&resolved, &path) &&
662+
mingw_strbuf_realpath(&resolved, path.buf) &&
663+
!strbuf_cmp(&path, &resolved)) {
664+
struct strbuf parent = STRBUF_INIT;
665+
666+
strbuf_addbuf(&parent, &path);
667+
strbuf_strip_file_from_path(&parent);
668+
cache_directory(&parent, &parent);
669+
strbuf_release(&parent);
670+
}
671+
if (resolved.len) {
672+
cache_directory(&path, &resolved);
673+
path_to_key(&resolved, 0);
674+
}
675+
}
565676
path_to_key(&path, 0);
566677
cb_each(&phantom_symlinks, (const uint8_t *)path.buf,
567678
path.len + !recursive, collect_phantom_symlinks, &pending);
@@ -704,6 +815,7 @@ int mingw_unlink(const char *pathname, int handle_in_use_error)
704815
if (xutftowcs_long_path(wpathname, pathname) < 0)
705816
return -1;
706817

818+
invalidate_phantom_directories();
707819
if (use_legacy_delete < 0)
708820
use_legacy_delete = git_env_bool("GIT_TEST_LEGACY_DELETE", 0);
709821

@@ -747,6 +859,7 @@ int mingw_unlink(const char *pathname, int handle_in_use_error)
747859

748860
success:
749861
ret = 0;
862+
invalidate_phantom_directories();
750863
if (target.len && xutftowcs_long_path(wpathname, target.buf) >= 0)
751864
process_phantom_symlinks(wpathname, 1);
752865
out:
@@ -804,9 +917,11 @@ int mingw_rmdir(const char *pathname)
804917
if (xutftowcs_long_path(wpathname, pathname) < 0)
805918
return -1;
806919

920+
invalidate_phantom_directories();
807921
do {
808922
if (!_wrmdir(wpathname)) {
809923
invalidate_lstat_cache();
924+
invalidate_phantom_directories();
810925
return 0;
811926
}
812927
if (!is_file_in_use_error(GetLastError()))
@@ -3139,6 +3254,7 @@ int mingw_rename(const char *pold, const char *pnew)
31393254
if (wpnew_len < 0)
31403255
return -1;
31413256

3257+
invalidate_phantom_directories();
31423258
repeat:
31433259
if (supports_file_rename_info_ex) {
31443260
/*
@@ -3191,8 +3307,10 @@ int mingw_rename(const char *pold, const char *pnew)
31913307
&rename_info, sizeof(rename_info));
31923308
gle = GetLastError();
31933309
CloseHandle(old_handle);
3194-
if (success)
3310+
if (success) {
3311+
invalidate_phantom_directories();
31953312
return 0;
3313+
}
31963314

31973315
/*
31983316
* When we see ERROR_INVALID_PARAMETER we can assume that the
@@ -3215,8 +3333,10 @@ int mingw_rename(const char *pold, const char *pnew)
32153333
*/
32163334
} else {
32173335
if (MoveFileExW(wpold, wpnew,
3218-
MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED))
3336+
MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED)) {
3337+
invalidate_phantom_directories();
32193338
return 0;
3339+
}
32203340
gle = GetLastError();
32213341
}
32223342

@@ -3232,6 +3352,7 @@ int mingw_rename(const char *pold, const char *pnew)
32323352
if (MoveFileExW(wpold, wpnew,
32333353
MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED)) {
32343354
SetFileAttributesW(wpnew, attrsold);
3355+
invalidate_phantom_directories();
32353356
return 0;
32363357
}
32373358
gle = GetLastError();
@@ -3256,8 +3377,10 @@ int mingw_rename(const char *pold, const char *pnew)
32563377
if (attrsold == INVALID_FILE_ATTRIBUTES ||
32573378
!(attrsold & FILE_ATTRIBUTE_DIRECTORY))
32583379
errno = EISDIR;
3259-
else if (!_wrmdir(wpnew))
3380+
else if (!_wrmdir(wpnew)) {
3381+
invalidate_phantom_directories();
32603382
goto repeat;
3383+
}
32613384
return -1;
32623385
}
32633386
if ((attrs & FILE_ATTRIBUTE_READONLY) &&

‎t/t2040-checkout-symlink-attr.sh‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,36 @@ test_expect_success MINGW,SYMLINKS 'traverse a file symlink' '
5858
)
5959
'
6060

61+
test_expect_success MINGW,SYMLINKS 'replace a pending prefix symlink' '
62+
test_create_repo retarget &&
63+
(
64+
cd retarget &&
65+
cmd.exe //c "mklink /d pivot old" &&
66+
cache_symlink pivot leading &&
67+
cache_symlink leading/sub nested &&
68+
cache_symlink new pivot &&
69+
cache_symlink missing new/sub/leaf &&
70+
test_write_lines leading nested pivot new/sub/leaf |
71+
git checkout-index -f --stdin &&
72+
cmd.exe //c dir . >dir-listing &&
73+
grep "<SYMLINKD>.*nested " dir-listing &&
74+
test "$(readlink nested)" = leading/sub
75+
)
76+
'
77+
78+
test_expect_success MINGW,SYMLINKS 'resolve a dot-dot prefix target' '
79+
test_create_repo relative-prefix &&
80+
(
81+
cd relative-prefix &&
82+
cache_symlink ../realdir links/leading &&
83+
cache_symlink links/leading/sub nested &&
84+
cache_symlink missing realdir/sub/leaf &&
85+
test_write_lines links/leading nested realdir/sub/leaf |
86+
git checkout-index --stdin &&
87+
cmd.exe //c dir . >dir-listing &&
88+
grep "<SYMLINKD>.*nested " dir-listing &&
89+
test "$(readlink links/leading)" = ../realdir
90+
)
91+
'
92+
6193
test_done

0 commit comments

Comments
 (0)