From: Johannes Schindelin <Johannes.Schindelin@gmx.de>
To: Patrick Steinhardt <ps@pks.im>
Cc: Johannes Schindelin via GitGitGadget <gitgitgadget@gmail.com>,
git@vger.kernel.org
Subject: Re: [PATCH] packfile: fix perf regression with many packs
Date: Thu, 13 Aug 2026 11:20:11 +0200 (CEST) [thread overview]
Message-ID: <b4860540-6114-2a7b-e266-d1fc2f0041b9@gmx.de> (raw)
In-Reply-To: <an1zz02GNqDu-0Oz@pks.im>
[-- Attachment #1: Type: text/plain, Size: 10715 bytes --]
Hi Patrick,
On Thu, 13 Aug 2026, Patrick Steinhardt wrote:
> On Wed, Aug 12, 2026 at 07:11:09PM +0000, Johannes Schindelin via GitGitGadget wrote:
> > From: Johannes Schindelin <johannes.schindelin@gmx.de>
> >
> > Since 589127caa730 (packfile: move list of packs into the packfile
> > store, 2025-10-30), there is a performance regression when many
> > packfiles need to be loaded: `packfile_store_add_pack()` now calls
> > `packfile_list_remove_internal()` to detect whether the packfile was
> > _already_ in the list, if if so, move it to the end of the list. This
>
> Nit: s/if if/and if/
Thanks, will fix, along with dropping the claim that the CI clone was
fixed by this patch.
>
> > function linearly scans the existing list before every insertion. Newly
> > loading N packs therefore has complexity O(N²).
> >
> > In one reported use case (https://github.com/microsoft/git/issues/970),
> > N equals 37,815 and caused a slow-down of a simple `git rev-parse
> > --short HEAD` (which is regularly executed as part of `GIT_PS1`) from
> > 0.4s to 4.5s. In another, heavily exercised CI scenario, clone times
> > increased from under 2 minutes to over half an hour.
>
> Wow, 38k packfiles is a lot.
Yes.
> > Let's fix this by establishing a fast path for known-new packfiles.
> >
> > The keen reader will note that there is currently only a single,
> > "known-new" caller of the `packfile_list_append()` function, and wonder
> > why not simply remove this check whether the packfile already exists in
> > the list? Originally, when above-mentioned commit introduced that logic,
> > there was a second caller in `prepare_midx()`, which would have required
> > that check, but that caller was removed in 6aff1f25a046 (packfile:
> > always add packfiles to MRU when adding a pack, 2025-10-30). Still, the
> > function is declared in a header file, and to avoid any problems with
> > in-flight or downstream callers, it is safer to extend the signature to
> > be explicit whether or not to skip that check.
>
> Quite conservative, but fair enough.
>
> > diff --git a/packfile-list.c b/packfile-list.c
> > index 01fb913abf..1379ab3a4f 100644
> > --- a/packfile-list.c
> > +++ b/packfile-list.c
> > @@ -57,11 +57,12 @@ void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack)
> > list->tail = entry;
> > }
> >
> > -void packfile_list_append(struct packfile_list *list, struct packed_git *pack)
> > +void packfile_list_append(struct packfile_list *list, struct packed_git *pack,
> > + int is_new)
> > {
> > struct packfile_list_entry *entry;
> >
> > - entry = packfile_list_remove_internal(list, pack);
> > + entry = is_new ? NULL : packfile_list_remove_internal(list, pack);
> > if (!entry) {
> > entry = xmalloc(sizeof(*entry));
> > entry->pack = pack;
>
> I wonder whether we should slightly reformulate this and rename `is_new`
> to `accept_duplicates`. Because ultimately, that is what we're doing
> now: instead of ensuring that the packfile is unique in the list, we
> just don't care and just append the entry to the list.
Hmm. I don't quite agree, we're _not_ accepting duplicates. We know that
those packfiles _cannot_ be duplicates.
> An alternative would be to use a hashmap here that tracks the packs that
> have already been added. It has the advantage that it also covers the
> `prepend()` operation and that callers don't have to be aware of this
> mechanism at all. Furthermore, moving preexisting entries to the back or
> front could become O(logn) if the list was doubly-linked. We do this
> operation quite often to re-sort entries in the list when looking up
> objects.
Indeed, that was my initial reaction, too. I was well on my way to start
writing a hashmap-based fix when the AI assistant pointed out that no
duplicates could possibly exist yet.
> Overall though I'm not quite sure whether the added complexity would be
> worth it, see below patch.
Wow, you got a lot further than I did! And yes, I agree that we do not
(yet?) need to deal with the added complexity.
Ciao,
Johannes
>
> Thanks!
>
> Patrick
>
> diff --git a/http-push.c b/http-push.c
> index 94a1fac9ab..52b00e7c95 100644
> --- a/http-push.c
> +++ b/http-push.c
> @@ -1729,6 +1729,7 @@ int cmd_main(int argc, const char **argv)
> const char *gitdir;
>
> CALLOC_ARRAY(repo, 1);
> + packfile_list_init(&repo->packs);
>
> argv++;
> for (i = 1; i < argc; i++, argv++) {
> @@ -1992,6 +1993,7 @@ int cmd_main(int argc, const char **argv)
> cleanup:
> if (info_ref_lock)
> unlock_remote(info_ref_lock);
> + packfile_list_clear(&repo->packs);
> free(repo->url);
> free(repo);
>
> diff --git a/http-walker.c b/http-walker.c
> index b58a3b2a92..541437e52d 100644
> --- a/http-walker.c
> +++ b/http-walker.c
> @@ -325,6 +325,7 @@ static void process_alternates_response(void *callback_data)
> warning("adding alternate object store: %s",
> target.buf);
> CALLOC_ARRAY(newalt, 1);
> + packfile_list_init(&newalt->packs);
> newalt->base = strbuf_detach(&target, NULL);
>
> while (tail->next != NULL)
> @@ -609,6 +610,7 @@ struct walker *get_http_walker(const char *url)
> struct walker *walker = xmalloc(sizeof(struct walker));
>
> CALLOC_ARRAY(data->alt, 1);
> + packfile_list_init(&data->alt->packs);
> data->alt->base = xstrdup(url);
> for (s = data->alt->base + strlen(data->alt->base) - 1; *s == '/'; --s)
> *s = 0;
> diff --git a/odb/source-packed.c b/odb/source-packed.c
> index 0890704e76..082c2494cb 100644
> --- a/odb/source-packed.c
> +++ b/odb/source-packed.c
> @@ -835,6 +835,7 @@ struct odb_source_packed *odb_source_packed_new(struct object_database *odb,
>
> CALLOC_ARRAY(packed, 1);
> odb_source_init(&packed->base, odb, ODB_SOURCE_PACKED, path, local);
> + packfile_list_init(&packed->packs);
> strmap_init(&packed->packs_by_path);
>
> packed->base.free = odb_source_packed_free;
> diff --git a/packfile-list.c b/packfile-list.c
> index 01fb913abf..d3c4843d8d 100644
> --- a/packfile-list.c
> +++ b/packfile-list.c
> @@ -2,6 +2,28 @@
> #include "packfile.h"
> #include "packfile-list.h"
>
> +static unsigned int packfile_list_entry_hash(struct packfile_list_entry *e)
> +{
> + return memhash(&e->pack, sizeof(e->pack));
> +}
> +
> +static int packfile_list_entry_cmp(const void *data UNUSED,
> + const struct hashmap_entry *h1,
> + const struct hashmap_entry *h2,
> + const void *keydata UNUSED)
> +{
> + const struct packfile_list_entry *e1, *e2;
> + e1 = container_of(h1, const struct packfile_list_entry, ent);
> + e2 = container_of(h2, const struct packfile_list_entry, ent);
> + return e1->pack != e2->pack;
> +}
> +
> +void packfile_list_init(struct packfile_list *list)
> +{
> + memset(list, 0, sizeof(*list));
> + hashmap_init(&list->seen, packfile_list_entry_cmp, NULL, 0);
> +}
> +
> void packfile_list_clear(struct packfile_list *list)
> {
> struct packfile_list_entry *e, *next;
> @@ -12,6 +34,20 @@ void packfile_list_clear(struct packfile_list *list)
> }
>
> list->head = list->tail = NULL;
> +
> + hashmap_clear(&list->seen);
> +}
> +
> +static struct packfile_list_entry *packfile_list_lookup(struct packfile_list *list,
> + struct packed_git *pack)
> +{
> + struct packfile_list_entry key = { .pack = pack };
> + struct hashmap_entry *ent;
> +
> + hashmap_entry_init(&key.ent, packfile_list_entry_hash(&key));
> + ent = hashmap_get(&list->seen, &key.ent, NULL);
> +
> + return ent ? container_of(ent, struct packfile_list_entry, ent) : NULL;
> }
>
> static struct packfile_list_entry *packfile_list_remove_internal(struct packfile_list *list,
> @@ -38,20 +74,33 @@ static struct packfile_list_entry *packfile_list_remove_internal(struct packfile
>
> void packfile_list_remove(struct packfile_list *list, struct packed_git *pack)
> {
> - free(packfile_list_remove_internal(list, pack));
> + struct packfile_list_entry key = { .pack = pack };
> +
> + hashmap_entry_init(&key.ent, packfile_list_entry_hash(&key));
> + if (hashmap_remove(&list->seen, &key.ent, NULL)) {
> + struct packfile_list_entry *e = packfile_list_remove_internal(list, pack);
> + if (!e)
> + BUG("corrupt packfile list");
> + free(e);
> + }
> }
>
> void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack)
> {
> struct packfile_list_entry *entry;
>
> - entry = packfile_list_remove_internal(list, pack);
> - if (!entry) {
> + if (packfile_list_lookup(list, pack)) {
> + entry = packfile_list_remove_internal(list, pack);
> + if (!entry)
> + BUG("corrupt packfile list");
> + } else {
> entry = xmalloc(sizeof(*entry));
> entry->pack = pack;
> + hashmap_entry_init(&entry->ent, packfile_list_entry_hash(entry));
> + hashmap_add(&list->seen, &entry->ent);
> }
> - entry->next = list->head;
>
> + entry->next = list->head;
> list->head = entry;
> if (!list->tail)
> list->tail = entry;
> @@ -61,13 +110,18 @@ void packfile_list_append(struct packfile_list *list, struct packed_git *pack)
> {
> struct packfile_list_entry *entry;
>
> - entry = packfile_list_remove_internal(list, pack);
> - if (!entry) {
> + if (packfile_list_lookup(list, pack)) {
> + entry = packfile_list_remove_internal(list, pack);
> + if (!entry)
> + BUG("corrupt packfile list");
> + } else {
> entry = xmalloc(sizeof(*entry));
> entry->pack = pack;
> + hashmap_entry_init(&entry->ent, packfile_list_entry_hash(entry));
> + hashmap_add(&list->seen, &entry->ent);
> }
> - entry->next = NULL;
>
> + entry->next = NULL;
> if (list->tail) {
> list->tail->next = entry;
> list->tail = entry;
> diff --git a/packfile-list.h b/packfile-list.h
> index 1b05e2aa36..bfb7017852 100644
> --- a/packfile-list.h
> +++ b/packfile-list.h
> @@ -1,17 +1,22 @@
> #ifndef PACKFILE_LIST_H
> #define PACKFILE_LIST_H
>
> +#include "hashmap.h"
> +
> struct object_id;
>
> struct packfile_list {
> struct packfile_list_entry *head, *tail;
> + struct hashmap seen;
> };
>
> struct packfile_list_entry {
> + struct hashmap_entry ent;
> struct packfile_list_entry *next;
> struct packed_git *pack;
> };
>
> +void packfile_list_init(struct packfile_list *list);
> void packfile_list_clear(struct packfile_list *list);
> void packfile_list_remove(struct packfile_list *list, struct packed_git *pack);
> void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack);
>
next prev parent reply other threads:[~2026-08-13 9:20 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-12 19:11 [PATCH] packfile: fix perf regression with many packs Johannes Schindelin via GitGitGadget
2026-08-12 19:51 ` Junio C Hamano
2026-08-12 21:29 ` Jeff King
2026-08-13 7:35 ` Patrick Steinhardt
2026-08-13 8:25 ` Johannes Schindelin
2026-08-13 16:10 ` Jeff King
2026-08-13 8:26 ` Johannes Schindelin
2026-08-12 22:29 ` Ben Knoble
2026-08-13 9:04 ` Johannes Schindelin
2026-08-13 11:18 ` Ben Knoble
2026-08-13 7:35 ` Patrick Steinhardt
2026-08-13 9:20 ` Johannes Schindelin [this message]
2026-08-13 10:01 ` Patrick Steinhardt
2026-08-13 10:42 ` [PATCH] packfile: fix perf regression with many packsy Johannes Schindelin
2026-08-13 11:12 ` Patrick Steinhardt
2026-08-13 13:52 ` [PATCH] packfile: fix perf regression with many packs Junio C Hamano
2026-08-13 16:15 ` Jeff King
2026-08-13 14:56 ` [PATCH v2] " Johannes Schindelin via GitGitGadget
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=b4860540-6114-2a7b-e266-d1fc2f0041b9@gmx.de \
--to=johannes.schindelin@gmx.de \
--cc=git@vger.kernel.org \
--cc=gitgitgadget@gmail.com \
--cc=ps@pks.im \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox