Git development
 help / color / mirror / Atom feed
* [PATCH] pack-objects: prefetch in the order objects are checked
@ 2026-09-03 12:55 Aleksei Sviridkin
  0 siblings, 0 replies; only message in thread
From: Aleksei Sviridkin @ 2026-09-03 12:55 UTC (permalink / raw)
  To: git; +Cc: Aleksei Sviridkin

get_object_details() sorts a pointer array by pack and offset and
hands each object's position in that array down to prefetch_to_pack(),
which reads it as a position in to_pack.objects. That second array is
in the order the traversal added entries, so the batch of objects to
fetch starts at an unrelated entry.

An object missing from a partial clone sits in no pack, and the offset
sort places such entries ahead of packed ones. The index is small
enough that the batch still covers every missing object in a freshly
cloned repository. Loose objects held locally sort by object name
among the missing ones, and enough of them move the index past entries
that are themselves missing. Those never enter the batch, end up with
no recorded type, and are fetched one at a time as the pack is
written. Packing a list of 2 missing and 16 locally present loose
objects takes two fetches rather than one.

Index the sorted array in prefetch_to_pack() so the batch holds what
the caller has not reached yet.

This was introduced by e00549aa9b (pack-objects: prefetch objects to
be packed, 2020-07-20).

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---

Notes:
    Measured on a synthetic repository of 5000 commits, each adding one
    file, cloned with --filter=blob:none over file:// so that all 5000
    blobs are absent.  Fetch invocations counted from GIT_TRACE2_EVENT
    child_start records, objects per fetch read from the --pack_header
    argument index-pack is given.
    
      input to pack-objects              before        after
      fresh blobless clone, --revs       1 fetch/5000  1 fetch/5000
      8 absent blobs, then 400 loose     8 fetches/1   1 fetch/8
    
    The first row is the ordinary case and does not move.
    pack_offset_sort compares the in_pack pointer before anything else,
    and an object that is not present has none, so every absent entry
    lands ahead of every packed entry.  On a fresh clone the first absent
    object is therefore at sorted index 0, and reading 0 as a
    traversal-order index selects the whole array by accident.
    
    The second row is the shape the fix is for.  Loose objects have no
    in_pack pointer either and sort by object name among the absent ones,
    which moves the first absent object to a nonzero sorted index.  The
    scan then starts that far into the traversal-order array.  In this
    input it collects nothing at all, promisor_remote_get_direct()
    returns early on an empty list, type -1 is recorded, and each object
    is fetched by itself while the pack is written.
    
    Alternatives considered:
    
      Drop the start index and scan all of to_pack.objects.  Obviously
      correct, and it removes a parameter rather than adding one.
      Rejected because it repeats the lookups for entries the caller has
      already checked, object_index_start of them on every call.
    
      Pass the unsorted index instead, entry - to_pack.objects.  One line
      at the call site and no new parameter.  Rejected because it only
      guarantees that the triggering object is in its own batch.  The scan
      would still cover an arbitrary suffix of the traversal order and
      leave out absent objects the caller has not reached.
    
    Left alone on purpose:
    
      prefetch_to_pack() keeps its opening brace on the declaration
      line.  CodingGuidelines says nothing about function braces, but
      .clang-format carries BreakBeforeBraces: Linux, so this is a rule
      rather than a habit.  It arrived with the commit named above and
      is one of 9 such definitions under builtin/, 2 of them in this
      file, the other being is_not_in_promisor_pack().  This patch
      rewrites that declaration, so moving the brace would cost one
      character.  Left out because it is not what the patch is about,
      and it is the same edit wherever it lands.
    
      check_object() takes entry, sorted_by_offset and object_index, and
      entry == sorted_by_offset[object_index] holds without being
      written down.  Deriving entry in the callee would make the
      mismatch this patch fixes impossible to express again.  Kept as it
      is because a function that checks one object reads better taking
      that object, and the file has one caller.
    
    Noticed nearby, not touched: pack_offset_sort() orders entries by
    comparing struct packed_git pointers that do not point into one
    array, which C99 6.5.8p5 leaves undefined.  The second paragraph of
    the message rests on that comparison sorting NULL first.  It holds on
    every platform git runs on and predates this patch by years.
    
    The test asserts one fetch round for a packing request of 2 absent and
    16 loose objects, and counts two without the code change, under both
    SHA-1 and SHA-256.  Which objects sort where depends on the hash, so
    the test checks its own precondition, that at least one loose object
    sorts before the first wanted one, instead of assuming it.  A hash
    change turns the test into a failure rather than into one that passes
    for the wrong reason.

 builtin/pack-objects.c | 13 +++++++-----
 t/t5300-pack-object.sh | 45 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 53 insertions(+), 5 deletions(-)

diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index 27048bbb4d..448ddb99eb 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -2227,12 +2227,13 @@ static int can_reuse_delta(const struct object_id *base_oid,
 	return 0;
 }
 
-static void prefetch_to_pack(uint32_t object_index_start) {
+static void prefetch_to_pack(struct object_entry **sorted_by_offset,
+			     uint32_t object_index_start) {
 	struct oid_array to_fetch = OID_ARRAY_INIT;
 	uint32_t i;
 
 	for (i = object_index_start; i < to_pack.nr_objects; i++) {
-		struct object_entry *entry = to_pack.objects + i;
+		struct object_entry *entry = sorted_by_offset[i];
 
 		if (!odb_read_object_info_extended(the_repository->objects,
 						   &entry->idx.oid,
@@ -2246,7 +2247,9 @@ static void prefetch_to_pack(uint32_t object_index_start) {
 	oid_array_clear(&to_fetch);
 }
 
-static void check_object(struct object_entry *entry, uint32_t object_index)
+static void check_object(struct object_entry *entry,
+			 struct object_entry **sorted_by_offset,
+			 uint32_t object_index)
 {
 	size_t canonical_size;
 	enum object_type type;
@@ -2389,7 +2392,7 @@ static void check_object(struct object_entry *entry, uint32_t object_index)
 	if (odb_read_object_info_extended(the_repository->objects, &entry->idx.oid, &oi,
 					  OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_LOOKUP_REPLACE) < 0) {
 		if (repo_has_promisor_remote(the_repository)) {
-			prefetch_to_pack(object_index);
+			prefetch_to_pack(sorted_by_offset, object_index);
 			if (odb_read_object_info_extended(the_repository->objects, &entry->idx.oid, &oi,
 							  OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_LOOKUP_REPLACE) < 0)
 				type = -1;
@@ -2619,7 +2622,7 @@ static void get_object_details(void)
 
 	for (i = 0; i < to_pack.nr_objects; i++) {
 		struct object_entry *entry = sorted_by_offset[i];
-		check_object(entry, i);
+		check_object(entry, sorted_by_offset, i);
 		if (entry->type_valid &&
 		    oe_size_greater_than(&to_pack, entry,
 					 repo_settings_get_big_file_threshold(the_repository)))
diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh
index 73445782e7..d94e3d0630 100755
--- a/t/t5300-pack-object.sh
+++ b/t/t5300-pack-object.sh
@@ -650,6 +650,51 @@ test_expect_success 'prefetch objects' '
 	test_line_count = 1 donelines
 '
 
+test_expect_success 'prefetch objects that sort after locally present ones' '
+	test_when_finished "rm -rf batch_server batch_client" &&
+
+	git init batch_server &&
+	test_config -C batch_server uploadpack.allowanysha1inwant 1 &&
+	test_config -C batch_server uploadpack.allowfilter 1 &&
+	test_config -C batch_server protocol.version 2 &&
+
+	for i in $(test_seq 1 8)
+	do
+		echo "content $i" >batch_server/file$i || return 1
+	done &&
+	git -C batch_server add . &&
+	git -C batch_server commit -m initial &&
+
+	git clone --filter=blob:none --no-checkout \
+		"file://$(pwd)/batch_server" batch_client &&
+	test_config -C batch_client protocol.version 2 &&
+
+	git -C batch_client rev-list --objects --all --missing=print >objects &&
+	sed -n "s/^?//p" objects | sort >absent &&
+	tail -n 2 absent >wanted &&
+
+	>loose_names &&
+	for i in $(test_seq 1 16)
+	do
+		echo "loose $i" |
+		git -C batch_client hash-object -w --stdin >>loose_names ||
+			return 1
+	done &&
+	sort loose_names >loose &&
+
+	# Objects that are in no pack are visited first, in object name
+	# order, so at least one locally present object has to be visited
+	# before the first wanted one for this to test anything.
+	awk -v limit="$(head -n 1 wanted)" "\$0 \"\" < limit \"\"" loose >earlier &&
+	test_file_not_empty earlier &&
+
+	cat wanted loose >to_pack &&
+	GIT_TRACE_PACKET=$(pwd)/trace_batch \
+		git -C batch_client pack-objects --stdout <to_pack >/dev/null &&
+	grep "fetch> done" trace_batch >donelines_batch &&
+	test_line_count = 1 donelines_batch
+'
+
 for hash in sha1 sha256
 do
 	test_expect_success "verify-pack with $hash packfile" '

base-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc
-- 
2.55.0


^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-09-03 12:55 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-03 12:55 [PATCH] pack-objects: prefetch in the order objects are checked Aleksei Sviridkin

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox