refs: skip collision checks in initial transactions

Reference transactions use `refs_verify_refname_available()` to check
for colliding references. This check consists of two parts:

  - Checks for whether multiple ref updates in the same transaction
    conflict with each other.

  - Checks for whether existing refs conflict with any refs part of the
    transaction.

While we generally cannot avoid the first check, the second check is
superfluous in cases where the transaction is an initial one in an
otherwise empty ref store. The check results in multiple ref reads as
well as the creation of a ref iterator for every ref we're checking,
which adds up quite fast when performing the check for many refs.

Introduce a new flag that allows us to skip this check and wire it up in
such that the backends pass it when running an initial transaction. This
leads to significant speedups when migrating ref storage backends. From
"files" to "reftable":

    Benchmark 1: migrate files:reftable (refcount = 100000, revision = HEAD~)
      Time (mean ± σ):     472.4 ms ±   6.7 ms    [User: 175.9 ms, System: 285.2 ms]
      Range (min … max):   463.5 ms … 483.2 ms    10 runs

    Benchmark 2: migrate files:reftable (refcount = 100000, revision = HEAD)
      Time (mean ± σ):      86.1 ms ±   1.9 ms    [User: 67.9 ms, System: 16.0 ms]
      Range (min … max):    82.9 ms …  90.9 ms    29 runs

    Summary
      migrate files:reftable (refcount = 100000, revision = HEAD) ran
        5.48 ± 0.15 times faster than migrate files:reftable (refcount = 100000, revision = HEAD~)

And from "reftable" to "files":

    Benchmark 1: migrate reftable:files (refcount = 100000, revision = HEAD~)
      Time (mean ± σ):     452.7 ms ±   3.4 ms    [User: 209.9 ms, System: 235.4 ms]
      Range (min … max):   445.9 ms … 457.5 ms    10 runs

    Benchmark 2: migrate reftable:files (refcount = 100000, revision = HEAD)
      Time (mean ± σ):      95.2 ms ±   2.2 ms    [User: 73.6 ms, System: 20.6 ms]
      Range (min … max):    91.7 ms … 100.8 ms    28 runs

    Summary
      migrate reftable:files (refcount = 100000, revision = HEAD) ran
        4.76 ± 0.11 times faster than migrate reftable:files (refcount = 100000, revision = HEAD~)

Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Patrick Steinhardt
2024-11-20 08:51:35 +01:00
committed by Junio C Hamano
parent 00bd6c3e46
commit e4929cdf79
5 changed files with 36 additions and 27 deletions

11
refs.c
View File

@ -2324,6 +2324,7 @@ int refs_verify_refname_available(struct ref_store *refs,
const char *refname, const char *refname,
const struct string_list *extras, const struct string_list *extras,
const struct string_list *skip, const struct string_list *skip,
int initial_transaction,
struct strbuf *err) struct strbuf *err)
{ {
const char *slash; const char *slash;
@ -2332,8 +2333,6 @@ int refs_verify_refname_available(struct ref_store *refs,
struct strbuf referent = STRBUF_INIT; struct strbuf referent = STRBUF_INIT;
struct object_id oid; struct object_id oid;
unsigned int type; unsigned int type;
struct ref_iterator *iter;
int ok;
int ret = -1; int ret = -1;
/* /*
@ -2363,7 +2362,8 @@ int refs_verify_refname_available(struct ref_store *refs,
if (skip && string_list_has_string(skip, dirname.buf)) if (skip && string_list_has_string(skip, dirname.buf))
continue; continue;
if (!refs_read_raw_ref(refs, dirname.buf, &oid, &referent, if (!initial_transaction &&
!refs_read_raw_ref(refs, dirname.buf, &oid, &referent,
&type, &ignore_errno)) { &type, &ignore_errno)) {
strbuf_addf(err, _("'%s' exists; cannot create '%s'"), strbuf_addf(err, _("'%s' exists; cannot create '%s'"),
dirname.buf, refname); dirname.buf, refname);
@ -2388,6 +2388,10 @@ int refs_verify_refname_available(struct ref_store *refs,
strbuf_addstr(&dirname, refname + dirname.len); strbuf_addstr(&dirname, refname + dirname.len);
strbuf_addch(&dirname, '/'); strbuf_addch(&dirname, '/');
if (!initial_transaction) {
struct ref_iterator *iter;
int ok;
iter = refs_ref_iterator_begin(refs, dirname.buf, NULL, 0, iter = refs_ref_iterator_begin(refs, dirname.buf, NULL, 0,
DO_FOR_EACH_INCLUDE_BROKEN); DO_FOR_EACH_INCLUDE_BROKEN);
while ((ok = ref_iterator_advance(iter)) == ITER_OK) { while ((ok = ref_iterator_advance(iter)) == ITER_OK) {
@ -2403,6 +2407,7 @@ int refs_verify_refname_available(struct ref_store *refs,
if (ok != ITER_DONE) if (ok != ITER_DONE)
BUG("error while iterating over references"); BUG("error while iterating over references");
}
extra_refname = find_descendant_ref(dirname.buf, extras, skip); extra_refname = find_descendant_ref(dirname.buf, extras, skip);
if (extra_refname) if (extra_refname)

5
refs.h
View File

@ -101,13 +101,16 @@ int refs_read_symbolic_ref(struct ref_store *ref_store, const char *refname,
* both "foo" and with "foo/bar/baz" but not with "foo/bar" or * both "foo" and with "foo/bar/baz" but not with "foo/bar" or
* "foo/barbados". * "foo/barbados".
* *
* If `initial_transaction` is truish, then all collision checks with
* preexisting refs are skipped.
*
* extras and skip must be sorted. * extras and skip must be sorted.
*/ */
int refs_verify_refname_available(struct ref_store *refs, int refs_verify_refname_available(struct ref_store *refs,
const char *refname, const char *refname,
const struct string_list *extras, const struct string_list *extras,
const struct string_list *skip, const struct string_list *skip,
int initial_transaction,
struct strbuf *err); struct strbuf *err);
int refs_ref_exists(struct ref_store *refs, const char *refname); int refs_ref_exists(struct ref_store *refs, const char *refname);

View File

@ -706,7 +706,7 @@ retry:
* reason to expect this error to be transitory. * reason to expect this error to be transitory.
*/ */
if (refs_verify_refname_available(&refs->base, refname, if (refs_verify_refname_available(&refs->base, refname,
extras, NULL, err)) { extras, NULL, 0, err)) {
if (mustexist) { if (mustexist) {
/* /*
* To the user the relevant error is * To the user the relevant error is
@ -813,7 +813,7 @@ retry:
REMOVE_DIR_EMPTY_ONLY)) { REMOVE_DIR_EMPTY_ONLY)) {
if (refs_verify_refname_available( if (refs_verify_refname_available(
&refs->base, refname, &refs->base, refname,
extras, NULL, err)) { extras, NULL, 0, err)) {
/* /*
* The error message set by * The error message set by
* verify_refname_available() is OK. * verify_refname_available() is OK.
@ -850,7 +850,7 @@ retry:
*/ */
if (refs_verify_refname_available( if (refs_verify_refname_available(
refs->packed_ref_store, refname, refs->packed_ref_store, refname,
extras, NULL, err)) { extras, NULL, 0, err)) {
ret = TRANSACTION_NAME_CONFLICT; ret = TRANSACTION_NAME_CONFLICT;
goto error_return; goto error_return;
} }
@ -1159,7 +1159,7 @@ static struct ref_lock *lock_ref_oid_basic(struct files_ref_store *refs,
*/ */
if (is_null_oid(&lock->old_oid) && if (is_null_oid(&lock->old_oid) &&
refs_verify_refname_available(refs->packed_ref_store, refname, refs_verify_refname_available(refs->packed_ref_store, refname,
NULL, NULL, err)) NULL, NULL, 0, err))
goto error_return; goto error_return;
lock->ref_name = xstrdup(refname); lock->ref_name = xstrdup(refname);
@ -1538,7 +1538,7 @@ static int refs_rename_ref_available(struct ref_store *refs,
string_list_insert(&skip, old_refname); string_list_insert(&skip, old_refname);
ok = !refs_verify_refname_available(refs, new_refname, ok = !refs_verify_refname_available(refs, new_refname,
NULL, &skip, &err); NULL, &skip, 0, &err);
if (!ok) if (!ok)
error("%s", err.buf); error("%s", err.buf);
@ -3043,8 +3043,7 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,
BUG("initial ref transaction with old_sha1 set"); BUG("initial ref transaction with old_sha1 set");
if (refs_verify_refname_available(&refs->base, update->refname, if (refs_verify_refname_available(&refs->base, update->refname,
&affected_refnames, NULL, &affected_refnames, NULL, 1, err)) {
err)) {
ret = TRANSACTION_NAME_CONFLICT; ret = TRANSACTION_NAME_CONFLICT;
goto cleanup; goto cleanup;
} }

View File

@ -1097,7 +1097,9 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
* at a later point. * at a later point.
*/ */
ret = refs_verify_refname_available(ref_store, u->refname, ret = refs_verify_refname_available(ref_store, u->refname,
&affected_refnames, NULL, err); &affected_refnames, NULL,
transaction->flags & REF_TRANSACTION_FLAG_INITIAL,
err);
if (ret < 0) if (ret < 0)
goto done; goto done;
@ -1584,7 +1586,7 @@ static int write_copy_table(struct reftable_writer *writer, void *cb_data)
if (arg->delete_old) if (arg->delete_old)
string_list_insert(&skip, arg->oldname); string_list_insert(&skip, arg->oldname);
ret = refs_verify_refname_available(&arg->refs->base, arg->newname, ret = refs_verify_refname_available(&arg->refs->base, arg->newname,
NULL, &skip, &errbuf); NULL, &skip, 0, &errbuf);
if (ret < 0) { if (ret < 0) {
error("%s", errbuf.buf); error("%s", errbuf.buf);
goto done; goto done;

View File

@ -199,7 +199,7 @@ static int cmd_verify_ref(struct ref_store *refs, const char **argv)
struct strbuf err = STRBUF_INIT; struct strbuf err = STRBUF_INIT;
int ret; int ret;
ret = refs_verify_refname_available(refs, refname, NULL, NULL, &err); ret = refs_verify_refname_available(refs, refname, NULL, NULL, 0, &err);
if (err.len) if (err.len)
puts(err.buf); puts(err.buf);
return ret; return ret;