From b02c7646f4cc812a99ceb1264f95427d2d7996f8 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 14 Nov 2023 09:58:38 +0100 Subject: [PATCH 1/4] t5510: ensure that the packed-refs file needs locking One of the tests in t5510 asserts that a `git fetch --prune` detects failures to prune branches. This is done by locking the packed-refs file, which would then later lead to a locking issue when Git tries to rewrite the file to prune the branches from it. Interestingly though, we do not pack the about-to-be-pruned branch into the packed-refs file, so it never even contained that branch in the first place. While this is good enough right now because the pruning will always lock the file regardless of whether it contains the branch or not, this is a mere implementation detail. In fact, we're about to rewrite branch deletions to make use of the ref transaction interface, which knows to skip rewrites of the packed-refs file in the case where it does not contain the branches in the first place, and this will break the test. Prepare the test for that change by packing the refs before trying to prune them. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t5510-fetch.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index dcadd56d3a..79592a3b0a 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -169,6 +169,7 @@ test_expect_success REFFILES 'fetch --prune fails to delete branches' ' git clone . prune-fail && cd prune-fail && git update-ref refs/remotes/origin/extrabranch main && + git pack-refs --all && : this will prevent --prune from locking packed-refs for deleting refs, but adding loose refs still succeeds && >.git/packed-refs.new && From e85e5dd78aea907702970ae121b0b4e29b3ea90b Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 14 Nov 2023 09:58:42 +0100 Subject: [PATCH 2/4] refs/files: use transactions to delete references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the `files_delete_refs()` callback function of the files backend we implement deletion of references. This is done in two steps: 1. We lock the packed-refs file and delete all references from it in a single transaction. 2. We delete all loose references via separate calls to `refs_delete_ref()`. These steps essentially duplicate the logic around locking and deletion order that we already have in the transactional interfaces, where we do know to lock and evict references from the packed-refs file. Despite the fact that we duplicate the logic, it's also less efficient than if we used a single generic transaction: - The transactional interface knows to skip locking of the packed refs in case they don't contain any of the refs which are about to be deleted. - We end up creating N+1 separate reference transactions, one for the packed-refs file and N for the individual loose references. Refactor the code to instead delete references via a single transaction. As we don't assert the expected old object ID this is equivalent to the previous behaviour, and we already do the same in the packed-refs backend. Despite the fact that the result is simpler to reason about, this change also results in improved performance. The following benchmarks have been executed in linux.git: ``` $ hyperfine -n '{rev}, packed={packed} refcount={refcount}' \ -L packed true,false -L refcount 1,1000 -L rev master,pks-ref-store-generic-delete-refs \ --setup 'git -C /home/pks/Development/git switch --detach {rev} && make -C /home/pks/Development/git -j17' \ --prepare 'printf "create refs/heads/new-branch-%d HEAD\n" $(seq {refcount}) | git -C /home/pks/Reproduction/linux.git update-ref --stdin && if test {packed} = true; then git pack-refs --all; fi' \ --warmup=10 \ '/home/pks/Development/git/bin-wrappers/git -C /home/pks/Reproduction/linux.git branch -d new-branch-{1..{refcount}}' Benchmark 1: master packed=true refcount=1 Time (mean ± σ): 7.8 ms ± 1.6 ms [User: 3.4 ms, System: 4.4 ms] Range (min … max): 5.5 ms … 11.0 ms 120 runs Benchmark 2: master packed=false refcount=1 Time (mean ± σ): 7.0 ms ± 1.1 ms [User: 3.2 ms, System: 3.8 ms] Range (min … max): 5.7 ms … 9.8 ms 180 runs Benchmark 3: master packed=true refcount=1000 Time (mean ± σ): 283.8 ms ± 5.2 ms [User: 45.7 ms, System: 231.5 ms] Range (min … max): 276.7 ms … 291.6 ms 10 runs Benchmark 4: master packed=false refcount=1000 Time (mean ± σ): 284.4 ms ± 5.3 ms [User: 44.2 ms, System: 233.6 ms] Range (min … max): 277.1 ms … 293.3 ms 10 runs Benchmark 5: generic-delete-refs packed=true refcount=1 Time (mean ± σ): 6.2 ms ± 1.8 ms [User: 2.3 ms, System: 3.9 ms] Range (min … max): 4.1 ms … 12.2 ms 142 runs Benchmark 6: generic-delete-refs packed=false refcount=1 Time (mean ± σ): 7.1 ms ± 1.4 ms [User: 2.8 ms, System: 4.3 ms] Range (min … max): 4.2 ms … 10.8 ms 157 runs Benchmark 7: generic-delete-refs packed=true refcount=1000 Time (mean ± σ): 198.9 ms ± 1.7 ms [User: 29.5 ms, System: 165.7 ms] Range (min … max): 196.1 ms … 201.4 ms 10 runs Benchmark 8: generic-delete-refs packed=false refcount=1000 Time (mean ± σ): 199.7 ms ± 7.8 ms [User: 32.2 ms, System: 163.2 ms] Range (min … max): 193.8 ms … 220.7 ms 10 runs Summary generic-delete-refs packed=true refcount=1 ran 1.14 ± 0.37 times faster than master packed=false refcount=1 1.15 ± 0.40 times faster than generic-delete-refs packed=false refcount=1 1.26 ± 0.44 times faster than master packed=true refcount=1 32.24 ± 9.17 times faster than generic-delete-refs packed=true refcount=1000 32.36 ± 9.29 times faster than generic-delete-refs packed=false refcount=1000 46.00 ± 13.10 times faster than master packed=true refcount=1000 46.10 ± 13.13 times faster than master packed=false refcount=1000 ``` Especially in the case where we have many references we can see a clear performance speedup of nearly 30%. This is in contrast to the stated objecive in a27dcf89b68 (refs: make delete_refs() virtual, 2016-09-04), where the virtual `delete_refs()` function was introduced with the intent to speed things up rather than making things slower. So it seems like we have outlived the need for a virtual function. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs/files-backend.c | 70 +++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/refs/files-backend.c b/refs/files-backend.c index db5c0c7a72..778d3a96ba 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -1268,49 +1268,51 @@ static int files_pack_refs(struct ref_store *ref_store, static int files_delete_refs(struct ref_store *ref_store, const char *msg, struct string_list *refnames, unsigned int flags) { - struct files_ref_store *refs = - files_downcast(ref_store, REF_STORE_WRITE, "delete_refs"); + struct ref_transaction *transaction; struct strbuf err = STRBUF_INIT; - int i, result = 0; + struct string_list_item *item; + int ret = 0, failures = 0; if (!refnames->nr) return 0; - if (packed_refs_lock(refs->packed_ref_store, 0, &err)) - goto error; - - if (refs_delete_refs(refs->packed_ref_store, msg, refnames, flags)) { - packed_refs_unlock(refs->packed_ref_store); - goto error; - } - - packed_refs_unlock(refs->packed_ref_store); - - for (i = 0; i < refnames->nr; i++) { - const char *refname = refnames->items[i].string; - - if (refs_delete_ref(&refs->base, msg, refname, NULL, flags)) - result |= error(_("could not remove reference %s"), refname); - } - - strbuf_release(&err); - return result; - -error: /* - * If we failed to rewrite the packed-refs file, then it is - * unsafe to try to remove loose refs, because doing so might - * expose an obsolete packed value for a reference that might - * even point at an object that has been garbage collected. + * Since we don't check the references' old_oids, the + * individual updates can't fail, so we can pack all of the + * updates into a single transaction. */ - if (refnames->nr == 1) - error(_("could not delete reference %s: %s"), - refnames->items[0].string, err.buf); - else - error(_("could not delete references: %s"), err.buf); + transaction = ref_store_transaction_begin(ref_store, &err); + if (!transaction) { + ret = error("%s", err.buf); + goto out; + } + for_each_string_list_item(item, refnames) { + ret = ref_transaction_delete(transaction, item->string, + NULL, flags, msg, &err); + if (ret) { + warning(_("could not delete reference %s: %s"), + item->string, err.buf); + strbuf_reset(&err); + failures = 1; + } + } + + ret = ref_transaction_commit(transaction, &err); + if (ret) { + if (refnames->nr == 1) + error(_("could not delete reference %s: %s"), + refnames->items[0].string, err.buf); + else + error(_("could not delete references: %s"), err.buf); + } + +out: + if (!ret && failures) + ret = -1; + ref_transaction_free(transaction); strbuf_release(&err); - return -1; + return ret; } /* From d6f8e7298254d8291724f9f57648b3ab5f8d3770 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 14 Nov 2023 09:58:46 +0100 Subject: [PATCH 3/4] refs: deduplicate code to delete references Both the files and the packed-refs reference backends now use the same generic transactions-based code to delete references. Let's pull these implementations up into `refs_delete_refs()` to deduplicate the code. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs.c | 48 ++++++++++++++++++++++++++++++++++++++++--- refs/files-backend.c | 46 +---------------------------------------- refs/packed-backend.c | 45 +--------------------------------------- 3 files changed, 47 insertions(+), 92 deletions(-) diff --git a/refs.c b/refs.c index fcae5dddc6..16bfa21df7 100644 --- a/refs.c +++ b/refs.c @@ -2599,13 +2599,55 @@ void ref_transaction_for_each_queued_update(struct ref_transaction *transaction, int refs_delete_refs(struct ref_store *refs, const char *logmsg, struct string_list *refnames, unsigned int flags) { + struct ref_transaction *transaction; + struct strbuf err = STRBUF_INIT; + struct string_list_item *item; + int ret = 0, failures = 0; char *msg; - int retval; + + if (!refnames->nr) + return 0; msg = normalize_reflog_message(logmsg); - retval = refs->be->delete_refs(refs, msg, refnames, flags); + + /* + * Since we don't check the references' old_oids, the + * individual updates can't fail, so we can pack all of the + * updates into a single transaction. + */ + transaction = ref_store_transaction_begin(refs, &err); + if (!transaction) { + ret = error("%s", err.buf); + goto out; + } + + for_each_string_list_item(item, refnames) { + ret = ref_transaction_delete(transaction, item->string, + NULL, flags, msg, &err); + if (ret) { + warning(_("could not delete reference %s: %s"), + item->string, err.buf); + strbuf_reset(&err); + failures = 1; + } + } + + ret = ref_transaction_commit(transaction, &err); + if (ret) { + if (refnames->nr == 1) + error(_("could not delete reference %s: %s"), + refnames->items[0].string, err.buf); + else + error(_("could not delete references: %s"), err.buf); + } + +out: + if (!ret && failures) + ret = -1; + ref_transaction_free(transaction); + strbuf_release(&err); free(msg); - return retval; + return ret; } int delete_refs(const char *msg, struct string_list *refnames, diff --git a/refs/files-backend.c b/refs/files-backend.c index 778d3a96ba..8d28810e67 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -1268,51 +1268,7 @@ static int files_pack_refs(struct ref_store *ref_store, static int files_delete_refs(struct ref_store *ref_store, const char *msg, struct string_list *refnames, unsigned int flags) { - struct ref_transaction *transaction; - struct strbuf err = STRBUF_INIT; - struct string_list_item *item; - int ret = 0, failures = 0; - - if (!refnames->nr) - return 0; - - /* - * Since we don't check the references' old_oids, the - * individual updates can't fail, so we can pack all of the - * updates into a single transaction. - */ - transaction = ref_store_transaction_begin(ref_store, &err); - if (!transaction) { - ret = error("%s", err.buf); - goto out; - } - - for_each_string_list_item(item, refnames) { - ret = ref_transaction_delete(transaction, item->string, - NULL, flags, msg, &err); - if (ret) { - warning(_("could not delete reference %s: %s"), - item->string, err.buf); - strbuf_reset(&err); - failures = 1; - } - } - - ret = ref_transaction_commit(transaction, &err); - if (ret) { - if (refnames->nr == 1) - error(_("could not delete reference %s: %s"), - refnames->items[0].string, err.buf); - else - error(_("could not delete references: %s"), err.buf); - } - -out: - if (!ret && failures) - ret = -1; - ref_transaction_free(transaction); - strbuf_release(&err); - return ret; + return refs_delete_refs(ref_store, msg, refnames, flags); } /* diff --git a/refs/packed-backend.c b/refs/packed-backend.c index 59c78d7941..1589577005 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -1691,50 +1691,7 @@ static int packed_initial_transaction_commit(struct ref_store *ref_store UNUSED, static int packed_delete_refs(struct ref_store *ref_store, const char *msg, struct string_list *refnames, unsigned int flags) { - struct packed_ref_store *refs = - packed_downcast(ref_store, REF_STORE_WRITE, "delete_refs"); - struct strbuf err = STRBUF_INIT; - struct ref_transaction *transaction; - struct string_list_item *item; - int ret; - - (void)refs; /* We need the check above, but don't use the variable */ - - if (!refnames->nr) - return 0; - - /* - * Since we don't check the references' old_oids, the - * individual updates can't fail, so we can pack all of the - * updates into a single transaction. - */ - - transaction = ref_store_transaction_begin(ref_store, &err); - if (!transaction) - return -1; - - for_each_string_list_item(item, refnames) { - if (ref_transaction_delete(transaction, item->string, NULL, - flags, msg, &err)) { - warning(_("could not delete reference %s: %s"), - item->string, err.buf); - strbuf_reset(&err); - } - } - - ret = ref_transaction_commit(transaction, &err); - - if (ret) { - if (refnames->nr == 1) - error(_("could not delete reference %s: %s"), - refnames->items[0].string, err.buf); - else - error(_("could not delete references: %s"), err.buf); - } - - ref_transaction_free(transaction); - strbuf_release(&err); - return ret; + return refs_delete_refs(ref_store, msg, refnames, flags); } static int packed_pack_refs(struct ref_store *ref_store UNUSED, From 29a186917b58bf67e14667a7a7641f5dddcc8589 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Tue, 14 Nov 2023 09:58:50 +0100 Subject: [PATCH 4/4] refs: remove `delete_refs` callback from backends Now that `refs_delete_refs` is implemented in a generic way via the ref transaction interfaces there are no callers left that invoke the `delete_refs` callback anymore. Remove it from all of our backends. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs/debug.c | 15 --------------- refs/files-backend.c | 7 ------- refs/packed-backend.c | 7 ------- refs/refs-internal.h | 3 --- 4 files changed, 32 deletions(-) diff --git a/refs/debug.c b/refs/debug.c index b7ffc4ce67..83b7a0ba65 100644 --- a/refs/debug.c +++ b/refs/debug.c @@ -143,20 +143,6 @@ static int debug_create_symref(struct ref_store *ref_store, return res; } -static int debug_delete_refs(struct ref_store *ref_store, const char *msg, - struct string_list *refnames, unsigned int flags) -{ - struct debug_ref_store *drefs = (struct debug_ref_store *)ref_store; - int res = - drefs->refs->be->delete_refs(drefs->refs, msg, refnames, flags); - int i; - trace_printf_key(&trace_refs, "delete_refs {\n"); - for (i = 0; i < refnames->nr; i++) - trace_printf_key(&trace_refs, "%s\n", refnames->items[i].string); - trace_printf_key(&trace_refs, "}: %d\n", res); - return res; -} - static int debug_rename_ref(struct ref_store *ref_store, const char *oldref, const char *newref, const char *logmsg) { @@ -458,7 +444,6 @@ struct ref_storage_be refs_be_debug = { .pack_refs = debug_pack_refs, .create_symref = debug_create_symref, - .delete_refs = debug_delete_refs, .rename_ref = debug_rename_ref, .copy_ref = debug_copy_ref, diff --git a/refs/files-backend.c b/refs/files-backend.c index 8d28810e67..ad8b1d143f 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -1265,12 +1265,6 @@ static int files_pack_refs(struct ref_store *ref_store, return 0; } -static int files_delete_refs(struct ref_store *ref_store, const char *msg, - struct string_list *refnames, unsigned int flags) -{ - return refs_delete_refs(ref_store, msg, refnames, flags); -} - /* * People using contrib's git-new-workdir have .git/logs/refs -> * /some/other/path/.git/logs/refs, and that may live on another device. @@ -3258,7 +3252,6 @@ struct ref_storage_be refs_be_files = { .pack_refs = files_pack_refs, .create_symref = files_create_symref, - .delete_refs = files_delete_refs, .rename_ref = files_rename_ref, .copy_ref = files_copy_ref, diff --git a/refs/packed-backend.c b/refs/packed-backend.c index 1589577005..b9fa097a29 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -1688,12 +1688,6 @@ static int packed_initial_transaction_commit(struct ref_store *ref_store UNUSED, return ref_transaction_commit(transaction, err); } -static int packed_delete_refs(struct ref_store *ref_store, const char *msg, - struct string_list *refnames, unsigned int flags) -{ - return refs_delete_refs(ref_store, msg, refnames, flags); -} - static int packed_pack_refs(struct ref_store *ref_store UNUSED, struct pack_refs_opts *pack_opts UNUSED) { @@ -1722,7 +1716,6 @@ struct ref_storage_be refs_be_packed = { .pack_refs = packed_pack_refs, .create_symref = NULL, - .delete_refs = packed_delete_refs, .rename_ref = NULL, .copy_ref = NULL, diff --git a/refs/refs-internal.h b/refs/refs-internal.h index 9db8aec4da..4af83bf9a5 100644 --- a/refs/refs-internal.h +++ b/refs/refs-internal.h @@ -553,8 +553,6 @@ typedef int create_symref_fn(struct ref_store *ref_store, const char *ref_target, const char *refs_heads_master, const char *logmsg); -typedef int delete_refs_fn(struct ref_store *ref_store, const char *msg, - struct string_list *refnames, unsigned int flags); typedef int rename_ref_fn(struct ref_store *ref_store, const char *oldref, const char *newref, const char *logmsg); @@ -677,7 +675,6 @@ struct ref_storage_be { pack_refs_fn *pack_refs; create_symref_fn *create_symref; - delete_refs_fn *delete_refs; rename_ref_fn *rename_ref; copy_ref_fn *copy_ref;